diff --git a/.github/workflows/docker-snapshot-release.yml b/.github/workflows/docker-snapshot-release.yml new file mode 100644 index 000000000..f6d5f0798 --- /dev/null +++ b/.github/workflows/docker-snapshot-release.yml @@ -0,0 +1,141 @@ +name: Build and Push Docker Images on PR Merge + +on: + push: + branches: + - main + paths-ignore: + - '.github/ISSUE_TEMPLATE/**' + - '.github/CODE_OF_CONDUCT.md' + - '.github/CODING_CONVENTIONS.md' + - '.github/CONTRIBUTING.md' + - '.github/dependabot.yml' + - '.github/pull_request_template.md' + - '.github/SECURITY.md' + - 'docs/**' + - 'examples/**' + - 'README.md' + - '.gitattributes' + - '.gitignore' + - 'LICENSE' + - 'NOTICE' + +env: + DOCKER_NAMESPACE: eclipsebasyx + +jobs: + build-and-push-prerelease: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: aas-environment + path: basyx.aasenvironment/basyx.aasenvironment.component + - name: aas-repository + path: basyx.aasrepository/basyx.aasrepository.component + - name: submodel-repository + path: basyx.submodelrepository/basyx.submodelrepository.component + - name: conceptdescription-repository + path: basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository.component + - name: aas-discovery + path: basyx.aasdiscoveryservice/basyx.aasdiscoveryservice.component + - name: aasxfileserver + path: basyx.aasxfileserver/basyx.aasxfileserver.component + - name: aas-registry-kafka-mem + path: basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/main/docker + - name: aas-registry-kafka-mongodb + path: basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/main/docker + - name: aas-registry-log-mem + path: basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/main/docker + - name: aas-registry-log-mongodb + path: basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/main/docker + - name: submodel-registry-kafka-mem + path: basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/main/docker + - name: submodel-registry-kafka-mongodb + path: basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/main/docker + - name: submodel-registry-log-mem + 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 + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: linux/amd64,linux/arm64,linux/arm/v7 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Build all modules first + run: mvn clean install -DskipTests + + # Build the project + # For registry modules, we activate the dockerbuild profile and specify the module with --pl + - name: Build BaSyx + run: | + if [[ "${{ matrix.name }}" == *"registry"* ]]; then + # Derive the module's artifactId from the path + module_root=$(dirname "$(dirname "$(dirname "${{ matrix.path }}")")") + artifact_id=$(basename "$module_root") + # Run with dockerbuild profile and namespace + mvn clean install -DskipTests -Pdockerbuild "-Ddocker.namespace=${{ env.DOCKER_NAMESPACE }}" --pl "org.eclipse.digitaltwin.basyx:${artifact_id}" + else + echo "Non-registry module - already built in the previous step." + fi + + - name: Prepare Registry JAR for Docker + if: contains(matrix.name, 'registry') + run: | + # Go three levels up from src/main/docker to get the module root + module_root=$(dirname "$(dirname "$(dirname "${{ matrix.path }}")")") + + # Adjust the path to where the dockerbuild profile places the JAR + JAR_FILE=$(ls "$module_root/target/docker/${{ env.DOCKER_NAMESPACE }}/${{ matrix.name }}/2.0.0-SNAPSHOT/build/maven/"*.jar | head -n 1) + if [ -z "$JAR_FILE" ]; then + echo "No repackaged JAR found in $module_root/target/docker/${{ env.DOCKER_NAMESPACE }}/${{ matrix.name }}/2.0.0-SNAPSHOT/build/maven. Check your build." + exit 1 + fi + + # Create the maven directory inside the Docker context and copy the JAR there + mkdir -p "${{ matrix.path }}/maven" + cp "$JAR_FILE" "${{ matrix.path }}/maven/" + + # Extract the final name without .jar extension + FINAL_NAME=$(basename "$JAR_FILE" .jar) + echo "FINAL_ARGS=FINAL_NAME=${FINAL_NAME}" >> $GITHUB_ENV + + - name: No-Op for Non-Registry Modules + if: "!contains(matrix.name, 'registry')" + run: echo "FINAL_ARGS=" >> $GITHUB_ENV + + - name: Build and Push Docker Image + uses: docker/build-push-action@v6 + with: + context: ${{ matrix.path }} + file: ${{ matrix.path }}/Dockerfile + push: true + platforms: linux/amd64,linux/arm64,linux/arm/v7 + tags: | + ${{ env.DOCKER_NAMESPACE }}/${{ matrix.name }}:2.0.0-SNAPSHOT + build-args: ${{ env.FINAL_ARGS }} + + - name: Verify Docker Image + run: | + docker pull ${{ env.DOCKER_NAMESPACE }}/${{ matrix.name }}:2.0.0-SNAPSHOT diff --git a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-backend-inmemory/pom.xml b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-backend-inmemory/pom.xml index 7d7849257..62201b56d 100644 --- a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-backend-inmemory/pom.xml +++ b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-backend-inmemory/pom.xml @@ -19,8 +19,11 @@ org.eclipse.digitaltwin.basyx basyx.aasdiscoveryservice-backend - - + + + org.eclipse.digitaltwin.basyx + basyx.backend.inmemory.core + org.eclipse.digitaltwin.basyx basyx.aasdiscoveryservice-core diff --git a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/backend/inmemory/AasDiscoveryInMemoryBackendProvider.java b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/backend/inmemory/AasDiscoveryInMemoryBackendProvider.java index 46971ba9c..21720e822 100644 --- a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/backend/inmemory/AasDiscoveryInMemoryBackendProvider.java +++ b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/backend/inmemory/AasDiscoveryInMemoryBackendProvider.java @@ -27,6 +27,7 @@ import org.eclipse.digitaltwin.basyx.aasdiscoveryservice.backend.AasDiscoveryBackendProvider; import org.eclipse.digitaltwin.basyx.aasdiscoveryservice.backend.AasDiscoveryDocument; +import org.eclipse.digitaltwin.basyx.common.backend.inmemory.core.InMemoryCrudRepository; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; @@ -41,7 +42,7 @@ @Component public class AasDiscoveryInMemoryBackendProvider implements AasDiscoveryBackendProvider { - private AasDiscoveryInMemoryCrudRepository repository = new AasDiscoveryInMemoryCrudRepository(); + private CrudRepository repository = new InMemoryCrudRepository(AasDiscoveryDocument::getShellIdentifier); @Override public CrudRepository getCrudRepository() { diff --git a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/backend/inmemory/AasDiscoveryInMemoryCrudRepository.java b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/backend/inmemory/AasDiscoveryInMemoryCrudRepository.java deleted file mode 100644 index 7fdf732c7..000000000 --- a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/backend/inmemory/AasDiscoveryInMemoryCrudRepository.java +++ /dev/null @@ -1,138 +0,0 @@ -/******************************************************************************* - * Copyright (C) 2024 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.aasdiscoveryservice.backend.inmemory; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.stream.StreamSupport; - -import org.eclipse.digitaltwin.aas4j.v3.model.SpecificAssetId; -import org.eclipse.digitaltwin.basyx.aasdiscoveryservice.backend.AasDiscoveryDocument; -import org.eclipse.digitaltwin.basyx.aasdiscoveryservice.core.model.AssetLink; -import org.springframework.data.repository.CrudRepository; -import org.springframework.lang.NonNull; - -/** - * In-memory implementation of the {@link CrudRepository} for the AAS Discovery - * - * @author zielstor, fried, mateusmolina - */ -public class AasDiscoveryInMemoryCrudRepository implements CrudRepository { - - private final ConcurrentMap> assetLinks = new ConcurrentHashMap<>(); - private final ConcurrentMap> assetIds = new ConcurrentHashMap<>(); - - @Override - public synchronized @NonNull S save(@NonNull S entity) { - String shellId = entity.getShellIdentifier(); - - this.assetLinks.put(shellId, entity.getAssetLinks()); - this.assetIds.put(shellId, entity.getSpecificAssetIds()); - - return entity; - } - - @Override - public @NonNull Iterable saveAll(@NonNull Iterable entities) { - entities.forEach(this::save); - return entities; - } - - @Override - public @NonNull Optional findById(@NonNull String id) { - return Optional.ofNullable(buildAasDiscoveryDocument(id)); - } - - @Override - public boolean existsById(@NonNull String id) { - return this.assetLinks.containsKey(id); - } - - @Override - public @NonNull Iterable findAll() { - return assetLinks.keySet().stream().map(this::buildAasDiscoveryDocument).toList(); - } - - @Override - public @NonNull Iterable findAllById(@NonNull Iterable ids) { - return StreamSupport.stream(ids.spliterator(), false).map(this::buildAasDiscoveryDocument).toList(); - } - - @Override - public long count() { - return this.assetLinks.size(); - } - - @Override - public synchronized void deleteById(@NonNull String id) { - this.assetLinks.remove(id); - this.assetIds.remove(id); - } - - @Override - public void delete(@NonNull AasDiscoveryDocument entity) { - this.deleteById(entity.getShellIdentifier()); - } - - @Override - public void deleteAllById(@NonNull Iterable ids) { - for (String id : ids) { - this.deleteById(id); - } - } - - @Override - public void deleteAll(@NonNull Iterable entities) { - for (AasDiscoveryDocument entity : entities) { - this.deleteById(entity.getShellIdentifier()); - } - } - - @Override - public synchronized void deleteAll() { - this.assetLinks.clear(); - this.assetIds.clear(); - } - - private synchronized AasDiscoveryDocument buildAasDiscoveryDocument(String shellId) { - Set assetLinksSet = assetLinks.get(shellId); - List assetIdsList = assetIds.get(shellId); - - if (assetIdsList == null) - assetIdsList = new ArrayList<>(); - - if (assetLinksSet == null) - assetLinksSet = new HashSet<>(); - - return new AasDiscoveryDocument(shellId, assetLinksSet, assetIdsList); - } - -} diff --git a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-core/src/test/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/core/AasDiscoveryServiceSuite.java b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-core/src/test/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/core/AasDiscoveryServiceSuite.java index 536f21d7d..7e552bdea 100644 --- a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-core/src/test/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/core/AasDiscoveryServiceSuite.java +++ b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-core/src/test/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/core/AasDiscoveryServiceSuite.java @@ -55,8 +55,6 @@ public abstract class AasDiscoveryServiceSuite { protected abstract AasDiscoveryService getAasDiscoveryService(); - private final PaginationInfo noLimitPaginationInfo = new PaginationInfo(0, ""); - @Test public void getAllAssetAdministrationShellIdsByAssetLink() { AasDiscoveryService discoveryService = getAasDiscoveryService(); @@ -74,7 +72,7 @@ public void getAllAssetAdministrationShellIdsByAssetLink() { new AssetLink("DummyAssetName2", "DummyAsset_2_Value") )); - List actualResult = discoveryService.getAllAssetAdministrationShellIdsByAssetLink(noLimitPaginationInfo, assetIds) + List actualResult = discoveryService.getAllAssetAdministrationShellIdsByAssetLink(PaginationInfo.NO_LIMIT, assetIds) .getResult(); assertEquals(expectedResult.size(), actualResult.size()); diff --git a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-http/src/main/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/http/AasDiscoveryServiceDescriptionConfiguration.java b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-http/src/main/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/http/AasDiscoveryServiceDescriptionConfiguration.java new file mode 100644 index 000000000..6a33d8b2d --- /dev/null +++ b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-http/src/main/java/org/eclipse/digitaltwin/basyx/aasdiscoveryservice/http/AasDiscoveryServiceDescriptionConfiguration.java @@ -0,0 +1,17 @@ +package org.eclipse.digitaltwin.basyx.aasdiscoveryservice.http; + +import java.util.List; +import java.util.TreeSet; + +import org.eclipse.digitaltwin.basyx.http.description.Profile; +import org.eclipse.digitaltwin.basyx.http.description.ProfileDeclaration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AasDiscoveryServiceDescriptionConfiguration { + @Bean + public ProfileDeclaration aasDiscoveryProfiles() { + return () -> new TreeSet<>(List.of(Profile.DISCOVERYSERVICESPECIFICATION_SSP_001)); + } +} \ No newline at end of file diff --git a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice.component/Dockerfile b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice.component/Dockerfile index 1c3e9648d..99b045565 100644 --- a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice.component/Dockerfile +++ b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice.component/Dockerfile @@ -1,4 +1,6 @@ -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY USER nobody WORKDIR /application ARG JAR_FILE=target/*-exec.jar diff --git a/basyx.aasenvironment/Readme.md b/basyx.aasenvironment/Readme.md index 4bfb39070..5e7c4a108 100644 --- a/basyx.aasenvironment/Readme.md +++ b/basyx.aasenvironment/Readme.md @@ -10,7 +10,7 @@ Eclipse BaSyx provides the AAS Environment as off-the-shelf component: It aggregates the AAS Repository, Submodel Repository and ConceptDescription Repository into a single component. For its features and configuration, see the documentation of the respective components. In addition, it supports the following endpoint defined in DotAAS Part 2 V3 - Serialization Interface: -- GenerateSerializationByIds +- GenerateSerializationByIds. For more information about this endpoint please refer to [Swagger API](https://app.swaggerhub.com/apis/Plattform_i40/Entire-API-Collection/V3.0.1#/Serialization%20API/GenerateSerializationByIds) The Aggregated API endpoint documentation is available at: @@ -40,10 +40,19 @@ For examples, see [application.properties](./basyx.aasenvironment.component/src/ ## AAS Environment Upload Endpoint -AAS environments (e.g. XML, JSON, AASX) can be uploaded by a multipart/form-data POST on the `/upload` endpoint. Please note that the following MIME types are expected for the respective file uploads: +AAS environments (e.g. XML, JSON, AASX) can be uploaded by a multipart/form-data POST on the `/upload` endpoint. Please note that the following MIME types as **Accept Header** are expected for the respective file uploads: * AASX: application/asset-administration-shell-package * JSON: application/json * XML: application/xml + +Below is an example curl request: + +``` +curl --location 'http://localhost:8081/upload' \ +--header 'Accept: application/asset-administration-shell-package' \ +--form 'file=@"Sample.aasx"' + +``` The upload follows the same rules as the preconfiguration in terms of handling existing AAS, submodels and concept descriptions. In order for the file to be recognized correctly, please make sure that its MIME type is properly configured. diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManager.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManager.java index f28480ed7..73ae1cbe1 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManager.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManager.java @@ -223,10 +223,7 @@ public void createSubmodelInAas(String aasIdentifier, Submodel submodel) { throw new RegistryHttpRequestException(aasIdentifier, e); } - Reference smRef = AasUtils.toReference(AasUtils.toReference(shell), submodel); - - // TODO See https://github.com/eclipse-aas4j/aas4j/issues/308 - smRef.setReferredSemanticId(submodel.getSemanticId()); + Reference smRef = AasUtils.toReference(AasUtils.toReference(shell), submodel, true); aasRepository.addSubmodelReference(aasIdentifier, smRef); } diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManagerHelper.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManagerHelper.java index d6e0ab8c6..3d8a90ec7 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManagerHelper.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManagerHelper.java @@ -60,13 +60,13 @@ static ObjectMapper buildObjectMapper() { static AasDescriptorFactory buildAasDescriptorFactory(String... aasRepositoryBaseUrls) { AttributeMapper attributeMapper = new AttributeMapper(objectMapper); - return new AasDescriptorFactory(null, List.of(aasRepositoryBaseUrls), attributeMapper); + return new AasDescriptorFactory(List.of(aasRepositoryBaseUrls), attributeMapper); } static SubmodelDescriptorFactory buildSmDescriptorFactory(String... aasRepositoryBaseUrls) { org.eclipse.digitaltwin.basyx.submodelregistry.client.mapper.AttributeMapper attributeMapperSm = new org.eclipse.digitaltwin.basyx.submodelregistry.client.mapper.AttributeMapper( objectMapper); - return new SubmodelDescriptorFactory(null, List.of(aasRepositoryBaseUrls), attributeMapperSm); + return new SubmodelDescriptorFactory(List.of(aasRepositoryBaseUrls), attributeMapperSm); } } diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/AasDescriptorResolver.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/AasDescriptorResolver.java index 6e743a8e1..a010f3df7 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/AasDescriptorResolver.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/AasDescriptorResolver.java @@ -30,17 +30,20 @@ import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; import org.eclipse.digitaltwin.basyx.aasregistry.client.model.Endpoint; +import org.eclipse.digitaltwin.basyx.aasregistry.client.model.ProtocolInformation; import org.eclipse.digitaltwin.basyx.aasservice.client.ConnectedAasService; import org.eclipse.digitaltwin.basyx.client.internal.resolver.DescriptorResolver; /** * Resolves an AasDescriptor into a {@link ConnectedAasService} - * + * * @author mateusmolina, danish * */ public class AasDescriptorResolver implements DescriptorResolver { + static final String SPEC_INTERFACE = "AAS-3.0"; + private final EndpointResolver endpointResolver; /** @@ -66,16 +69,7 @@ public ConnectedAasService resolveDescriptor(AssetAdministrationShellDescriptor public static Optional parseEndpoint(Endpoint endpoint) { try { - if (endpoint == null || endpoint.getProtocolInformation() == null || endpoint.getProtocolInformation() - .getHref() == null) - return Optional.empty(); - - String baseHref = endpoint.getProtocolInformation() - .getHref(); - // TODO not working: String queryString = "?" + endpoint.toUrlQueryString(); - String queryString = ""; - URI uri = new URI(baseHref + queryString); - return Optional.of(uri); + return Optional.ofNullable(endpoint).filter(ep -> ep.getInterface().equals(SPEC_INTERFACE)).map(Endpoint::getProtocolInformation).map(ProtocolInformation::getHref).map(URI::create); } catch (Exception e) { return Optional.empty(); } diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/SubmodelDescriptorResolver.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/SubmodelDescriptorResolver.java index 5ddc10d72..4d7014577 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/SubmodelDescriptorResolver.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/SubmodelDescriptorResolver.java @@ -25,6 +25,7 @@ package org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.ProtocolInformation; import org.eclipse.digitaltwin.basyx.client.internal.resolver.DescriptorResolver; import java.net.URI; import java.util.Optional; @@ -41,6 +42,8 @@ */ public class SubmodelDescriptorResolver implements DescriptorResolver { + static final String SPEC_INTERFACE = "SUBMODEL-3.0"; + private final EndpointResolver endpointResolver; /** @@ -67,16 +70,7 @@ public ConnectedSubmodelService resolveDescriptor(SubmodelDescriptor smDescripto public static Optional parseEndpoint(Endpoint endpoint) { try { - if (endpoint == null || endpoint.getProtocolInformation() == null || endpoint.getProtocolInformation() - .getHref() == null) - return Optional.empty(); - - String baseHref = endpoint.getProtocolInformation() - .getHref(); - // TODO not working: String queryString = "?" + endpoint.toUrlQueryString(); - String queryString = ""; - URI uri = new URI(baseHref + queryString); - return Optional.of(uri); + return Optional.ofNullable(endpoint).filter(ep -> ep.getInterface().equals(SPEC_INTERFACE)).map(Endpoint::getProtocolInformation).map(ProtocolInformation::getHref).map(URI::create); } catch (Exception e) { return Optional.empty(); } diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestConnectedAasManagerMultithreading.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestConnectedAasManagerMultithreading.java new file mode 100644 index 000000000..8ff0ea860 --- /dev/null +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestConnectedAasManagerMultithreading.java @@ -0,0 +1,158 @@ +/******************************************************************************* + * Copyright (C) 2024 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.aasenvironment.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.stream.IntStream; + +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.Key; +import org.eclipse.digitaltwin.aas4j.v3.model.Reference; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultAssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel; +import org.eclipse.digitaltwin.basyx.aasregistry.client.api.RegistryAndDiscoveryInterfaceApi; +import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository; +import org.eclipse.digitaltwin.basyx.aasrepository.client.ConnectedAasRepository; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.api.SubmodelRegistryApi; +import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; +import org.eclipse.digitaltwin.basyx.submodelrepository.client.ConnectedSubmodelRepository; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +public class TestConnectedAasManagerMultithreading { + static final String AAS_REPOSITORY_BASE_PATH = "http://localhost:8081"; + static final String SM_REPOSITORY_BASE_PATH = "http://localhost:8081"; + static final String AAS_REGISTRY_BASE_PATH = "http://localhost:8050"; + static final String SM_REGISTRY_BASE_PATH = "http://localhost:8060"; + static final int N_THREADS = 20; + + static ConfigurableApplicationContext appContext; + static AasRepository aasRepository; + static SubmodelRepository smRepository; + + static ConnectedAasRepository connectedAasRepository; + static ConnectedSubmodelRepository connectedSmRepository; + static RegistryAndDiscoveryInterfaceApi aasRegistryApi; + static SubmodelRegistryApi smRegistryApi; + + static ConnectedAasManager aasManager; + + @BeforeClass + public static void setupRepositories() { + appContext = new SpringApplication(DummyAasEnvironmentComponent.class).run(new String[] {}); + + connectedAasRepository = new ConnectedAasRepository(AAS_REPOSITORY_BASE_PATH); + connectedSmRepository = new ConnectedSubmodelRepository(SM_REPOSITORY_BASE_PATH); + aasRegistryApi = new RegistryAndDiscoveryInterfaceApi(AAS_REGISTRY_BASE_PATH); + smRegistryApi = new SubmodelRegistryApi(SM_REGISTRY_BASE_PATH); + aasManager = new ConnectedAasManager(AAS_REGISTRY_BASE_PATH, AAS_REPOSITORY_BASE_PATH, SM_REGISTRY_BASE_PATH, SM_REPOSITORY_BASE_PATH); + + cleanUpRegistries(); + } + + @After + public void cleanUpComponents() { + cleanUpRegistries(); + } + + @AfterClass + public static void stopContext() { + appContext.close(); + } + + @Test + public void testParallelSubmodelCreation() throws ExecutionException, InterruptedException { + AssetAdministrationShell shell = createShell(); + + ExecutorService executorService = Executors.newFixedThreadPool(N_THREADS); + ConcurrentLinkedDeque createdSubmodelIds = new ConcurrentLinkedDeque<>(); + + List> futures = IntStream.range(0, N_THREADS).mapToObj(i -> executorService.submit(() -> createdSubmodelIds.add(createSubmodel(shell.getId(), i)))).toList(); + + try { + for (int i = 0; i < N_THREADS; i++) { + futures.get(i).get(); + } + } finally { + executorService.shutdown(); + } + + createdSubmodelIds.forEach(submodelId -> assertSubmodelWasCreatedAndRegistered(shell.getId(), submodelId)); + } + + static void assertSubmodelWasCreatedAndRegistered(String shellId, String submodelId) { + assertEquals(submodelId, aasManager.getSubmodelService(submodelId).getSubmodel().getId()); + assertTrue(connectedAasRepository.getSubmodelReferences(shellId, PaginationInfo.NO_LIMIT).getResult().stream().map(Reference::getKeys).flatMap(Collection::stream).map(Key::getValue).anyMatch(submodelId::equals)); + } + + + private static void cleanUpRegistries() { + try { + aasRegistryApi.deleteAllShellDescriptors(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + try { + smRegistryApi.deleteAllSubmodelDescriptors(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + private static AssetAdministrationShell createShell() { + String id = UUID.randomUUID().toString(); + DefaultAssetAdministrationShell shell = new DefaultAssetAdministrationShell.Builder().id(id).build(); + aasManager.createAas(shell); + return aasManager.getAasService(id).getAAS(); + } + + private static String createSubmodel(String aasId, int threadId) { + try { + String id = aasId + "-thread" + threadId; + DefaultSubmodel submodel = new DefaultSubmodel.Builder().id(id).build(); + aasManager.createSubmodelInAas(aasId, submodel); + return id; + } catch (Exception e) { + throw new RuntimeException("Failed at thread " + threadId, e); + } + } + +} diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestFixture.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestFixture.java index 275f54eb2..2d552fdae 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestFixture.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestFixture.java @@ -25,6 +25,8 @@ package org.eclipse.digitaltwin.basyx.aasenvironment.client; +import java.util.LinkedList; + import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; import org.eclipse.digitaltwin.aas4j.v3.model.AssetInformation; import org.eclipse.digitaltwin.aas4j.v3.model.AssetKind; @@ -38,6 +40,7 @@ import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultReference; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel; import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; +import org.eclipse.digitaltwin.basyx.aasregistry.client.model.Endpoint; import org.eclipse.digitaltwin.basyx.aasregistry.main.client.mapper.DummyAasDescriptorFactory; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncoder; import org.eclipse.digitaltwin.basyx.submodelregistry.client.mapper.AttributeMapper; @@ -88,7 +91,16 @@ public AssetInformation buildAasPre1AssetInformation() { } public AssetAdministrationShellDescriptor buildAasPre1Descriptor() { - return DummyAasDescriptorFactory.createDummyDescriptor(AAS_PRE1_ID, AAS_PRE1_IDSHORT, AAS_PRE1_GLOBALASSETID, aasRepositoryBasePath); + return DummyAasDescriptorFactory.createDummyDescriptor(AAS_PRE1_ID, AAS_PRE1_IDSHORT, AAS_PRE1_GLOBALASSETID, null, aasRepositoryBasePath); + } + + public AssetAdministrationShellDescriptor buildAasPre1Descriptor_withMultipleInterfaces() { + LinkedList endpoints = new LinkedList<>(); + + endpoints.add(DummyAasDescriptorFactory.createEndpoint(aasRepositoryBasePath, "AAS-REPOSITORY-3.0")); + endpoints.add(DummyAasDescriptorFactory.createEndpoint(AAS_PRE1_ID, aasRepositoryBasePath, "AAS-3.0")); + + return DummyAasDescriptorFactory.createDummyDescriptor(AAS_PRE1_ID, AAS_PRE1_IDSHORT, AAS_PRE1_GLOBALASSETID, null, endpoints); } public Reference buildSmPre1Ref() { @@ -100,7 +112,16 @@ public Submodel buildSmPre1() { } public SubmodelDescriptor buildSmPre1Descriptor() { - return DummySubmodelDescriptorFactory.createDummyDescriptor(SM_PRE1_ID, SM_PRE1_IDSHORT, smRepositoryBasePath, null); + return DummySubmodelDescriptorFactory.createDummyDescriptor(SM_PRE1_ID, SM_PRE1_IDSHORT, null, null, smRepositoryBasePath); + } + + public SubmodelDescriptor buildSmPre1Descriptor_withMultipleInterfaces() { + LinkedList endpoints = new LinkedList<>(); + + endpoints.add(DummySubmodelDescriptorFactory.createEndpoint(smRepositoryBasePath, "SUBMODEL-REPOSITORY-3.0")); + endpoints.add(DummySubmodelDescriptorFactory.createEndpoint(SM_PRE1_ID, smRepositoryBasePath, "SUBMODEL-3.0")); + + return DummySubmodelDescriptorFactory.createDummyDescriptor(SM_PRE1_ID, SM_PRE1_IDSHORT, null, null, endpoints); } public AssetAdministrationShell buildAasPos1() { @@ -112,11 +133,11 @@ public AssetInformation buildAasPos1AssetInformation() { } public AssetAdministrationShellDescriptor buildAasPos1Descriptor() { - return DummyAasDescriptorFactory.createDummyDescriptor(AAS_POS1_ID, AAS_POS1_IDSHORT, AAS_POS1_GLOBALASSETID, aasRepositoryBasePath); + return DummyAasDescriptorFactory.createDummyDescriptor(AAS_POS1_ID, AAS_POS1_IDSHORT, AAS_POS1_GLOBALASSETID, null, aasRepositoryBasePath); } public SubmodelDescriptor buildSmPos1Descriptor() { - return DummySubmodelDescriptorFactory.createDummyDescriptor(SM_POS1_ID, SM_POS1_IDSHORT, smRepositoryBasePath, new AttributeMapper(ConnectedAasManagerHelper.buildObjectMapper()).mapSemanticId(buildSmPos1SemanticId())); + return DummySubmodelDescriptorFactory.createDummyDescriptor(SM_POS1_ID, SM_POS1_IDSHORT, new AttributeMapper(ConnectedAasManagerHelper.buildObjectMapper()).mapSemanticId(buildSmPos1SemanticId()), null, smRepositoryBasePath); } public Reference buildSmPos1SemanticId() { diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/RegistryDescriptorResolverTest.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/RegistryDescriptorResolverTest.java index e0666db0d..a6ced6534 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/RegistryDescriptorResolverTest.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/RegistryDescriptorResolverTest.java @@ -31,7 +31,6 @@ import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; import org.eclipse.digitaltwin.basyx.aasenvironment.client.DummyAasEnvironmentComponent; import org.eclipse.digitaltwin.basyx.aasenvironment.client.TestFixture; -import org.eclipse.digitaltwin.basyx.aasregistry.client.ApiException; import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository; import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; import org.junit.AfterClass; @@ -52,10 +51,10 @@ public class RegistryDescriptorResolverTest { private static AasRepository aasRepository; private static SubmodelRepository smRepository; - private final static String AAS_REPOSITORY_BASE_PATH = "http://localhost:8081"; - private final static String SM_REPOSITORY_BASE_PATH = "http://localhost:8081"; + private static final String AAS_REPOSITORY_BASE_PATH = "http://localhost:8081"; + private static final String SM_REPOSITORY_BASE_PATH = "http://localhost:8081"; - private final static TestFixture FIXTURE = new TestFixture(AAS_REPOSITORY_BASE_PATH, SM_REPOSITORY_BASE_PATH); + private static final TestFixture FIXTURE = new TestFixture(AAS_REPOSITORY_BASE_PATH, SM_REPOSITORY_BASE_PATH); @BeforeClass public static void initApplication() { @@ -73,7 +72,7 @@ public static void cleanUp() { } @Test - public void resolveAasDescriptor() throws ApiException { + public void resolveAasDescriptor() { AasDescriptorResolver resolver = new AasDescriptorResolver(new EndpointResolver()); AssetAdministrationShell expectedAas = FIXTURE.buildAasPre1(); @@ -84,7 +83,18 @@ public void resolveAasDescriptor() throws ApiException { } @Test - public void resolveSmDescriptor() throws ApiException { + public void resolveAasDescriptor_withMultipleInterfaces() { + AasDescriptorResolver resolver = new AasDescriptorResolver(new EndpointResolver()); + + AssetAdministrationShell expectedAas = FIXTURE.buildAasPre1(); + + AssetAdministrationShell actualAas = resolver.resolveDescriptor(FIXTURE.buildAasPre1Descriptor_withMultipleInterfaces()).getAAS(); + + assertEquals(expectedAas, actualAas); + } + + @Test + public void resolveSmDescriptor() { SubmodelDescriptorResolver resolver = new SubmodelDescriptorResolver(new EndpointResolver()); Submodel expectedSm = FIXTURE.buildSmPre1(); @@ -94,4 +104,14 @@ public void resolveSmDescriptor() throws ApiException { assertEquals(expectedSm, actualSm); } + @Test + public void resolveSmDescriptor_withMultipleInterfaces() { + SubmodelDescriptorResolver resolver = new SubmodelDescriptorResolver(new EndpointResolver()); + + Submodel expectedSm = FIXTURE.buildSmPre1(); + + Submodel actualSm = resolver.resolveDescriptor(FIXTURE.buildSmPre1Descriptor_withMultipleInterfaces()).getSubmodel(); + + assertEquals(expectedSm, actualSm); + } } diff --git a/basyx.aasenvironment/basyx.aasenvironment-core/pom.xml b/basyx.aasenvironment/basyx.aasenvironment-core/pom.xml index 3d8a5b48b..b64d501ce 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-core/pom.xml +++ b/basyx.aasenvironment/basyx.aasenvironment-core/pom.xml @@ -102,7 +102,7 @@ com.google.guava guava - 33.2.1-jre + 33.4.0-jre diff --git a/basyx.aasenvironment/basyx.aasenvironment-core/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/AasEnvironmentLoaderTest.java b/basyx.aasenvironment/basyx.aasenvironment-core/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/AasEnvironmentLoaderTest.java index 391bf22cd..c3fd3001b 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-core/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/AasEnvironmentLoaderTest.java +++ b/basyx.aasenvironment/basyx.aasenvironment-core/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/AasEnvironmentLoaderTest.java @@ -69,8 +69,6 @@ public class AasEnvironmentLoaderTest { protected static final String TEST_ENVIRONMENT_SHELLS_ONLY_JSON = "/org/eclipse/digitaltwin/basyx/aasenvironment/environment_with_shells_only.json"; protected static final String TEST_ENVIRONMENT_SUBMODELS_ONLY_JSON = "/org/eclipse/digitaltwin/basyx/aasenvironment/environment_with_submodels_only.json"; - protected static final PaginationInfo ALL = new PaginationInfo(0, null); - protected AasRepository aasRepository; protected SubmodelRepository submodelRepository; protected ConceptDescriptionRepository conceptDescriptionRepository; @@ -96,9 +94,9 @@ protected void loadRepositories(List pathsToLoad) throws IOException, De public void testWithResourceFile_AllElementsAreDeployed() throws InvalidFormatException, IOException, DeserializationException { loadRepositories(List.of(TEST_ENVIRONMENT_JSON)); - Assert.assertEquals(2, aasRepository.getAllAas(ALL).getResult().size()); - Assert.assertEquals(2, submodelRepository.getAllSubmodels(ALL).getResult().size()); - Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(ALL).getResult().size()); + Assert.assertEquals(2, aasRepository.getAllAas(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, submodelRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult().size()); } @Test @@ -112,9 +110,9 @@ public void testDeployedTwiceNoVersion_AllDeployedButNotOverriden() throws Inval Mockito.verify(submodelRepository, Mockito.times(2)).createSubmodel(Mockito.any()); Mockito.verify(submodelRepository, Mockito.times(0)).updateSubmodel(Mockito.anyString(), Mockito.any()); - Assert.assertEquals(2, aasRepository.getAllAas(ALL).getResult().size()); - Assert.assertEquals(2, submodelRepository.getAllSubmodels(ALL).getResult().size()); - Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(ALL).getResult().size()); + Assert.assertEquals(2, aasRepository.getAllAas(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, submodelRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult().size()); } @Test @@ -128,9 +126,9 @@ public void testDeployedTwiceWithSameVersion_AllDeployedButNotOverriden() throws Mockito.verify(submodelRepository, Mockito.times(2)).createSubmodel(Mockito.any()); Mockito.verify(submodelRepository, Mockito.times(0)).updateSubmodel(Mockito.anyString(), Mockito.any()); - Assert.assertEquals(2, aasRepository.getAllAas(ALL).getResult().size()); - Assert.assertEquals(2, submodelRepository.getAllSubmodels(ALL).getResult().size()); - Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(ALL).getResult().size()); + Assert.assertEquals(2, aasRepository.getAllAas(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, submodelRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult().size()); } @Test @@ -144,9 +142,9 @@ public void testDeployedTwiceNewRevision_ElementsAreOverriden() throws InvalidFo Mockito.verify(submodelRepository, Mockito.times(2)).createSubmodel(Mockito.any()); Mockito.verify(submodelRepository, Mockito.times(1)).updateSubmodel(Mockito.anyString(), Mockito.any()); - Assert.assertEquals(2, aasRepository.getAllAas(ALL).getResult().size()); - Assert.assertEquals(2, submodelRepository.getAllSubmodels(ALL).getResult().size()); - Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(ALL).getResult().size()); + Assert.assertEquals(2, aasRepository.getAllAas(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, submodelRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult().size()); } @Test @@ -168,17 +166,17 @@ public void testWithResourceFile_NoExceptionsWhenReuploadAfterElementsAreRemoved loadRepositoriesWithEnvironment(List.of(TEST_ENVIRONMENT_JSON), envLoader); - Assert.assertEquals(2, aasRepository.getAllAas(ALL).getResult().size()); - Assert.assertEquals(2, submodelRepository.getAllSubmodels(ALL).getResult().size()); - Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(ALL).getResult().size()); + Assert.assertEquals(2, aasRepository.getAllAas(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, submodelRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult().size()); deleteElementsFromRepos(); loadRepositoriesWithEnvironment(List.of(TEST_ENVIRONMENT_JSON), envLoader); - Assert.assertEquals(2, aasRepository.getAllAas(ALL).getResult().size()); - Assert.assertEquals(2, submodelRepository.getAllSubmodels(ALL).getResult().size()); - Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(ALL).getResult().size()); + Assert.assertEquals(2, aasRepository.getAllAas(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, submodelRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult().size()); } @Test @@ -187,9 +185,9 @@ public void testWithResourceFile_ExceptionIsThrownWhenReuploadWithExistingElemen loadRepositoriesWithEnvironment(List.of(TEST_ENVIRONMENT_JSON), envLoader); - Assert.assertEquals(2, aasRepository.getAllAas(ALL).getResult().size()); - Assert.assertEquals(2, submodelRepository.getAllSubmodels(ALL).getResult().size()); - Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(ALL).getResult().size()); + Assert.assertEquals(2, aasRepository.getAllAas(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, submodelRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(2, conceptDescriptionRepository.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult().size()); String expectedMsg = new CollidingIdentifierException("aas1").getMessage(); Assert.assertThrows(expectedMsg, CollidingIdentifierException.class, () -> loadRepositoriesWithEnvironment(List.of(TEST_ENVIRONMENT_JSON), envLoader)); @@ -204,13 +202,13 @@ private void loadRepositoriesWithEnvironment(List pathsToLoad, AasEnviro } private void deleteElementsFromRepos() { - aasRepository.getAllAas(ALL).getResult().stream().forEach(aas -> aasRepository.deleteAas(aas.getId())); - submodelRepository.getAllSubmodels(ALL).getResult().stream().forEach(sm -> submodelRepository.deleteSubmodel(sm.getId())); - conceptDescriptionRepository.getAllConceptDescriptions(ALL).getResult().stream().forEach(cd -> conceptDescriptionRepository.deleteConceptDescription(cd.getId())); + aasRepository.getAllAas(PaginationInfo.NO_LIMIT).getResult().stream().forEach(aas -> aasRepository.deleteAas(aas.getId())); + submodelRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().stream().forEach(sm -> submodelRepository.deleteSubmodel(sm.getId())); + conceptDescriptionRepository.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult().stream().forEach(cd -> conceptDescriptionRepository.deleteConceptDescription(cd.getId())); - Assert.assertEquals(0, aasRepository.getAllAas(ALL).getResult().size()); - Assert.assertEquals(0, submodelRepository.getAllSubmodels(ALL).getResult().size()); - Assert.assertEquals(0, conceptDescriptionRepository.getAllConceptDescriptions(ALL).getResult().size()); + Assert.assertEquals(0, aasRepository.getAllAas(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(0, submodelRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().size()); + Assert.assertEquals(0, conceptDescriptionRepository.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult().size()); } } \ No newline at end of file diff --git a/basyx.aasenvironment/basyx.aasenvironment-core/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/PreconfigurationLoaderTextualResourceTest.java b/basyx.aasenvironment/basyx.aasenvironment-core/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/PreconfigurationLoaderTextualResourceTest.java index 29a8774c4..7cd689cc3 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-core/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/PreconfigurationLoaderTextualResourceTest.java +++ b/basyx.aasenvironment/basyx.aasenvironment-core/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/PreconfigurationLoaderTextualResourceTest.java @@ -31,6 +31,7 @@ import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException; import org.eclipse.digitaltwin.basyx.aasenvironment.base.DefaultAASEnvironment; import org.eclipse.digitaltwin.basyx.aasenvironment.preconfiguration.AasEnvironmentPreconfigurationLoader; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -52,9 +53,9 @@ protected void loadRepositories(List pathsToLoad) throws IOException, In @Test public void testWithEmptyResource_NoElementsAreDeployed() throws InvalidFormatException, IOException, DeserializationException { loadRepositories(List.of()); - Assert.assertTrue(aasRepository.getAllAas(ALL).getResult().isEmpty()); - Assert.assertTrue(submodelRepository.getAllSubmodels(ALL).getResult().isEmpty()); - Assert.assertTrue(conceptDescriptionRepository.getAllConceptDescriptions(ALL).getResult().isEmpty()); + Assert.assertTrue(aasRepository.getAllAas(PaginationInfo.NO_LIMIT).getResult().isEmpty()); + Assert.assertTrue(submodelRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().isEmpty()); + Assert.assertTrue(conceptDescriptionRepository.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult().isEmpty()); Mockito.verify(aasRepository, Mockito.never()).createAas(Mockito.any()); Mockito.verify(aasRepository, Mockito.never()).updateAas(Mockito.anyString(), Mockito.any()); diff --git a/basyx.aasenvironment/basyx.aasenvironment-core/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/TestAASEnvironmentSerialization.java b/basyx.aasenvironment/basyx.aasenvironment-core/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/TestAASEnvironmentSerialization.java index 3734e36ea..0564b0d4a 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-core/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/TestAASEnvironmentSerialization.java +++ b/basyx.aasenvironment/basyx.aasenvironment-core/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/TestAASEnvironmentSerialization.java @@ -74,7 +74,6 @@ public class TestAASEnvironmentSerialization { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, ""); public static final String AAS_TECHNICAL_DATA_ID = "shell001"; public static final String AAS_OPERATIONAL_DATA_ID = "shell002"; public static final String SUBMODEL_TECHNICAL_DATA_ID = "7A7104BDAB57E184"; @@ -266,9 +265,9 @@ private static List retrieveConceptDescriptionIds(Environment aasEnviron } private void validateRepositoriesState() { - assertTrue(aasRepository.getAllAas(NO_LIMIT_PAGINATION_INFO).getResult().containsAll(createDummyShells())); - assertTrue(submodelRepository.getAllSubmodels(NO_LIMIT_PAGINATION_INFO).getResult().containsAll(createDummySubmodels())); - assertTrue(conceptDescriptionRepository.getAllConceptDescriptions(NO_LIMIT_PAGINATION_INFO).getResult().containsAll(createDummyConceptDescriptions())); + assertTrue(aasRepository.getAllAas(PaginationInfo.NO_LIMIT).getResult().containsAll(createDummyShells())); + assertTrue(submodelRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().containsAll(createDummySubmodels())); + assertTrue(conceptDescriptionRepository.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult().containsAll(createDummyConceptDescriptions())); } } diff --git a/basyx.aasenvironment/basyx.aasenvironment-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/feature/authorization/TestAuthorizedAasEnvironmentSerialization.java b/basyx.aasenvironment/basyx.aasenvironment-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/feature/authorization/TestAuthorizedAasEnvironmentSerialization.java index 8f056bd58..7676800a3 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/feature/authorization/TestAuthorizedAasEnvironmentSerialization.java +++ b/basyx.aasenvironment/basyx.aasenvironment-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/feature/authorization/TestAuthorizedAasEnvironmentSerialization.java @@ -70,7 +70,6 @@ */ public class TestAuthorizedAasEnvironmentSerialization { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); private static String authenticaltionServerTokenEndpoint = "http://localhost:9096/realms/BaSyx/protocol/openid-connect/token"; private static String clientId = "basyx-client-api"; private static AccessTokenProvider tokenProvider; @@ -101,9 +100,9 @@ public static void tearDown() { public void reset() throws FileNotFoundException, IOException { configureSecurityContext(); - Collection assetAdministrationShells = aasRepo.getAllAas(NO_LIMIT_PAGINATION_INFO).getResult(); - Collection submodels = submodelRepo.getAllSubmodels(NO_LIMIT_PAGINATION_INFO).getResult(); - Collection conceptDescriptions = conceptDescriptionRepo.getAllConceptDescriptions(NO_LIMIT_PAGINATION_INFO).getResult(); + Collection assetAdministrationShells = aasRepo.getAllAas(PaginationInfo.NO_LIMIT).getResult(); + Collection submodels = submodelRepo.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult(); + Collection conceptDescriptions = conceptDescriptionRepo.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult(); assetAdministrationShells.stream().forEach(aas -> aasRepo.deleteAas(aas.getId())); submodels.stream().forEach(sm -> submodelRepo.deleteSubmodel(sm.getId())); diff --git a/basyx.aasenvironment/basyx.aasenvironment-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/feature/authorization/TestAuthorizedAasEnvironmentUpload.java b/basyx.aasenvironment/basyx.aasenvironment-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/feature/authorization/TestAuthorizedAasEnvironmentUpload.java index 785ae35ad..3b39566dd 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/feature/authorization/TestAuthorizedAasEnvironmentUpload.java +++ b/basyx.aasenvironment/basyx.aasenvironment-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/feature/authorization/TestAuthorizedAasEnvironmentUpload.java @@ -67,7 +67,6 @@ */ public class TestAuthorizedAasEnvironmentUpload { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); private static String authenticaltionServerTokenEndpoint = "http://localhost:9096/realms/BaSyx/protocol/openid-connect/token"; private static String clientId = "basyx-client-api"; private static AccessTokenProvider tokenProvider; @@ -94,9 +93,9 @@ public void reset() throws FileNotFoundException, IOException { configureSecurityContext(); - Collection assetAdministrationShells = aasRepo.getAllAas(NO_LIMIT_PAGINATION_INFO).getResult(); - Collection submodels = submodelRepo.getAllSubmodels(NO_LIMIT_PAGINATION_INFO).getResult(); - Collection conceptDescriptions = conceptDescriptionRepo.getAllConceptDescriptions(NO_LIMIT_PAGINATION_INFO).getResult(); + Collection assetAdministrationShells = aasRepo.getAllAas(PaginationInfo.NO_LIMIT).getResult(); + Collection submodels = submodelRepo.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult(); + Collection conceptDescriptions = conceptDescriptionRepo.getAllConceptDescriptions(PaginationInfo.NO_LIMIT).getResult(); assetAdministrationShells.stream().forEach(aas -> aasRepo.deleteAas(aas.getId())); submodels.stream().forEach(sm -> submodelRepo.deleteSubmodel(sm.getId())); diff --git a/basyx.aasenvironment/basyx.aasenvironment.component/Dockerfile b/basyx.aasenvironment/basyx.aasenvironment.component/Dockerfile index 1c3e9648d..99b045565 100644 --- a/basyx.aasenvironment/basyx.aasenvironment.component/Dockerfile +++ b/basyx.aasenvironment/basyx.aasenvironment.component/Dockerfile @@ -1,4 +1,6 @@ -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY USER nobody WORKDIR /application ARG JAR_FILE=target/*-exec.jar diff --git a/basyx.aasenvironment/basyx.aasenvironment.component/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/component/TestEnvironmentWithRegistryIntegration.java b/basyx.aasenvironment/basyx.aasenvironment.component/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/component/TestEnvironmentWithRegistryIntegration.java new file mode 100644 index 000000000..ca223dbeb --- /dev/null +++ b/basyx.aasenvironment/basyx.aasenvironment.component/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/component/TestEnvironmentWithRegistryIntegration.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * Copyright (C) 2024 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.aasenvironment.component; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException; +import org.eclipse.digitaltwin.basyx.aasenvironment.AasEnvironment; +import org.eclipse.digitaltwin.basyx.aasenvironment.environmentloader.CompleteEnvironment; +import org.eclipse.digitaltwin.basyx.aasenvironment.environmentloader.CompleteEnvironment.EnvironmentType; +import org.eclipse.digitaltwin.basyx.aasregistry.client.ApiException; +import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; +import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository; +import org.eclipse.digitaltwin.basyx.aasrepository.feature.registry.integration.AasRepositoryRegistryLink; +import org.eclipse.digitaltwin.basyx.core.exceptions.RepositoryRegistryLinkException; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; +import org.eclipse.digitaltwin.basyx.submodelrepository.feature.registry.integration.SubmodelRepositoryRegistryLink; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.io.ClassPathResource; + +/** + * + * Test the {@link AasEnvironment} with aas and submodel registry integration + * features enabled + * + * @author mateusmolina + */ +public class TestEnvironmentWithRegistryIntegration { + + static final String ENV_PATH = "testEnvironment.json"; + + static ConfigurableApplicationContext appContext; + + static AasEnvironment aasEnvironment; + static AasRepositoryRegistryLink aasRepositoryRegistryLink; + static SubmodelRepositoryRegistryLink smRepositoryRegistryLink; + static AasRepository aasRepository; + static SubmodelRepository smRepository; + + @BeforeClass + public static void startAASEnvironment() { + appContext = new SpringApplicationBuilder(AasEnvironmentComponent.class).profiles("reginteg").run(new String[] {}); + + aasEnvironment = appContext.getBean(AasEnvironment.class); + aasRepositoryRegistryLink = appContext.getBean(AasRepositoryRegistryLink.class); + smRepositoryRegistryLink = appContext.getBean(SubmodelRepositoryRegistryLink.class); + aasRepository = appContext.getBean(AasRepository.class); + smRepository = appContext.getBean(SubmodelRepository.class); + + assertRepositoriesAreEmpty(); + } + + @AfterClass + public static void stopAASEnvironment() { + appContext.close(); + } + + @AfterClass + public static void clearRegistries() throws Exception { + smRepositoryRegistryLink.getRegistryApi().deleteAllSubmodelDescriptors(); + aasRepositoryRegistryLink.getRegistryApi().deleteAllShellDescriptors(); + } + + @Test + public void whenUploadDescriptorToRegistryFails_thenNoAasOrSmAreAddedToRepository() throws InvalidFormatException, DeserializationException, IOException, ApiException { + // simulate descriptor already being in registry + aasRepositoryRegistryLink.getRegistryApi().postAssetAdministrationShellDescriptor(buildTestAasDescriptor()); + + CompleteEnvironment completeEnvironment = CompleteEnvironment.fromInputStream(getIsFromClasspath(ENV_PATH), EnvironmentType.JSON); + + assertThrows(RepositoryRegistryLinkException.class, () -> aasEnvironment.loadEnvironment(completeEnvironment)); + + assertRepositoriesAreEmpty(); + } + + private static AssetAdministrationShellDescriptor buildTestAasDescriptor() { + AssetAdministrationShellDescriptor descriptor = new AssetAdministrationShellDescriptor(); + descriptor.setId("https://acplt.test/Test_AssetAdministrationShell"); + return descriptor; + } + + private static InputStream getIsFromClasspath(String fileName) throws IOException { + return new ClassPathResource(fileName).getInputStream(); + } + + private static void assertRepositoriesAreEmpty() { + assertTrue(aasRepository.getAllAas(PaginationInfo.NO_LIMIT).getResult().isEmpty()); + assertTrue(smRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().isEmpty()); + } + +} diff --git a/basyx.aasenvironment/basyx.aasenvironment.component/src/test/resources/application-reginteg.properties b/basyx.aasenvironment/basyx.aasenvironment.component/src/test/resources/application-reginteg.properties new file mode 100644 index 000000000..ce166b3ec --- /dev/null +++ b/basyx.aasenvironment/basyx.aasenvironment.component/src/test/resources/application-reginteg.properties @@ -0,0 +1,7 @@ +basyx.aasrepository.feature.registryintegration=http://localhost:8050 +basyx.submodelrepository.feature.registryintegration=http://localhost:8060 + +basyx.externalurl=http://localhost:8080 + +# Override for empty environment +basyx.environment= \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/factory/AasDescriptorFactory.java b/basyx.aasregistry/basyx.aasregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/factory/AasDescriptorFactory.java index c1091086e..1fe0709bb 100644 --- a/basyx.aasregistry/basyx.aasregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/factory/AasDescriptorFactory.java +++ b/basyx.aasregistry/basyx.aasregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/factory/AasDescriptorFactory.java @@ -40,6 +40,7 @@ import org.eclipse.digitaltwin.basyx.aasregistry.client.model.Endpoint; import org.eclipse.digitaltwin.basyx.aasregistry.client.model.ProtocolInformation; import org.eclipse.digitaltwin.basyx.aasregistry.main.client.mapper.AttributeMapper; +import org.eclipse.digitaltwin.basyx.core.RepositoryUrlHelper; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; /** @@ -52,15 +53,12 @@ public class AasDescriptorFactory { private static final String AAS_INTERFACE = "AAS-3.0"; private static final String AAS_REPOSITORY_PATH = "shells"; - private AssetAdministrationShell shell; - private List aasRepositoryURLs; + private final List aasRepositoryURLs; + private static AttributeMapper attributeMapper; - private AttributeMapper attributeMapper; - - public AasDescriptorFactory(AssetAdministrationShell shell, List aasRepositoryBaseURLs, AttributeMapper attributeMapper) { - this.shell = shell; + public AasDescriptorFactory(List aasRepositoryBaseURLs, AttributeMapper attributeMapper) { this.aasRepositoryURLs = createAasRepositoryUrls(aasRepositoryBaseURLs); - this.attributeMapper = attributeMapper; + AasDescriptorFactory.attributeMapper = attributeMapper; } /** @@ -68,7 +66,7 @@ public AasDescriptorFactory(AssetAdministrationShell shell, List aasRepo * * @return the created AssetAdministrationShellDescriptor */ - public AssetAdministrationShellDescriptor create() { + public AssetAdministrationShellDescriptor create(AssetAdministrationShell shell) { AssetAdministrationShellDescriptor descriptor = new AssetAdministrationShellDescriptor(); @@ -76,7 +74,7 @@ public AssetAdministrationShellDescriptor create() { setIdShort(shell.getIdShort(), descriptor); - setEndpointItem(shell.getId(), descriptor); + setEndpointItem(shell.getId(), descriptor, aasRepositoryURLs); setDescription(shell.getDescription(), descriptor); @@ -95,12 +93,7 @@ public AssetAdministrationShellDescriptor create() { return descriptor; } - public AssetAdministrationShellDescriptor create(AssetAdministrationShell shell) { - this.shell = shell; - return create(); - } - - private void setDescription(List descriptions, AssetAdministrationShellDescriptor descriptor) { + private static void setDescription(List descriptions, AssetAdministrationShellDescriptor descriptor) { if (descriptions == null || descriptions.isEmpty()) return; @@ -108,7 +101,7 @@ private void setDescription(List descriptions, AssetAdminist descriptor.setDescription(attributeMapper.mapDescription(descriptions)); } - private void setDisplayName(List displayNames, AssetAdministrationShellDescriptor descriptor) { + private static void setDisplayName(List displayNames, AssetAdministrationShellDescriptor descriptor) { if (displayNames == null || displayNames.isEmpty()) return; @@ -116,7 +109,7 @@ private void setDisplayName(List displayNames, AssetAdminist descriptor.setDisplayName(attributeMapper.mapDisplayName(displayNames)); } - private void setExtensions(List extensions, AssetAdministrationShellDescriptor descriptor) { + private static void setExtensions(List extensions, AssetAdministrationShellDescriptor descriptor) { if (extensions == null || extensions.isEmpty()) return; @@ -124,7 +117,7 @@ private void setExtensions(List extensions, AssetAdministrationShellD descriptor.setExtensions(attributeMapper.mapExtensions(extensions)); } - private void setAdministration(AdministrativeInformation administration, AssetAdministrationShellDescriptor descriptor) { + private static void setAdministration(AdministrativeInformation administration, AssetAdministrationShellDescriptor descriptor) { if (administration == null) return; @@ -132,7 +125,7 @@ private void setAdministration(AdministrativeInformation administration, AssetAd descriptor.setAdministration(attributeMapper.mapAdministration(administration)); } - private void setAssetKind(AssetInformation assetInformation, AssetAdministrationShellDescriptor descriptor) { + private static void setAssetKind(AssetInformation assetInformation, AssetAdministrationShellDescriptor descriptor) { if (assetInformation == null || assetInformation.getAssetKind() == null) return; @@ -140,7 +133,7 @@ private void setAssetKind(AssetInformation assetInformation, AssetAdministration descriptor.setAssetKind(attributeMapper.mapAssetKind(assetInformation.getAssetKind())); } - private void setAssetType(AssetInformation assetInformation, AssetAdministrationShellDescriptor descriptor) { + private static void setAssetType(AssetInformation assetInformation, AssetAdministrationShellDescriptor descriptor) { if (assetInformation == null || assetInformation.getAssetType() == null) return; @@ -148,7 +141,7 @@ private void setAssetType(AssetInformation assetInformation, AssetAdministration descriptor.setAssetType(assetInformation.getAssetType()); } - private void setGlobalAssetId(AssetInformation assetInformation, AssetAdministrationShellDescriptor descriptor) { + private static void setGlobalAssetId(AssetInformation assetInformation, AssetAdministrationShellDescriptor descriptor) { if (assetInformation == null || assetInformation.getGlobalAssetId() == null) return; @@ -156,7 +149,7 @@ private void setGlobalAssetId(AssetInformation assetInformation, AssetAdministra descriptor.setGlobalAssetId(assetInformation.getGlobalAssetId()); } - private void setEndpointItem(String shellId, AssetAdministrationShellDescriptor descriptor) { + private static void setEndpointItem(String shellId, AssetAdministrationShellDescriptor descriptor, List aasRepositoryURLs) { for (String eachUrl : aasRepositoryURLs) { Endpoint endpoint = new Endpoint(); endpoint.setInterface(AAS_INTERFACE); @@ -167,7 +160,7 @@ private void setEndpointItem(String shellId, AssetAdministrationShellDescriptor } } - private ProtocolInformation createProtocolInformation(String shellId, String url) { + private static ProtocolInformation createProtocolInformation(String shellId, String url) { String href = String.format("%s/%s", url, Base64UrlEncodedIdentifier.encodeIdentifier(shellId)); ProtocolInformation protocolInformation = new ProtocolInformation(); @@ -177,15 +170,15 @@ private ProtocolInformation createProtocolInformation(String shellId, String url return protocolInformation; } - private void setIdShort(String idShort, AssetAdministrationShellDescriptor descriptor) { + private static void setIdShort(String idShort, AssetAdministrationShellDescriptor descriptor) { descriptor.setIdShort(idShort); } - private void setId(String shellId, AssetAdministrationShellDescriptor descriptor) { + private static void setId(String shellId, AssetAdministrationShellDescriptor descriptor) { descriptor.setId(shellId); } - private String getProtocol(String endpoint) { + private static String getProtocol(String endpoint) { try { return new URL(endpoint).getProtocol(); } catch (MalformedURLException e) { @@ -193,21 +186,11 @@ private String getProtocol(String endpoint) { } } - private List createAasRepositoryUrls(List aasRepositoryBaseURLs) { + private static List createAasRepositoryUrls(List aasRepositoryBaseURLs) { List toReturn = new ArrayList<>(aasRepositoryBaseURLs.size()); for (String eachUrl : aasRepositoryBaseURLs) { - toReturn.add(createAasRepositoryUrl(eachUrl)); + toReturn.add(RepositoryUrlHelper.createRepositoryUrl(eachUrl, AAS_REPOSITORY_PATH)); } return toReturn; } - - private String createAasRepositoryUrl(String aasRepositoryBaseURL) { - - try { - return new URL(new URL(aasRepositoryBaseURL), AAS_REPOSITORY_PATH).toString(); - } catch (MalformedURLException e) { - throw new RuntimeException("The AAS Repository Base url is malformed.\n" + e.getMessage()); - } - } - } diff --git a/basyx.aasregistry/basyx.aasregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/mapper/AttributeMapper.java b/basyx.aasregistry/basyx.aasregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/mapper/AttributeMapper.java index 2a9a46b7d..eb0a28140 100644 --- a/basyx.aasregistry/basyx.aasregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/mapper/AttributeMapper.java +++ b/basyx.aasregistry/basyx.aasregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/mapper/AttributeMapper.java @@ -134,8 +134,16 @@ public AdministrativeInformation mapAdministration(org.eclipse.digitaltwin.aas4j * @return the mapped assetKind */ public AssetKind mapAssetKind(org.eclipse.digitaltwin.aas4j.v3.model.AssetKind assetKind) { - - return AssetKind.valueOf(AssetKind.class, assetKind.name()); + switch (assetKind) { + case INSTANCE: + return AssetKind.INSTANCE; + case NOT_APPLICABLE: + return AssetKind.NOTAPPLICABLE; + case TYPE: + return AssetKind.TYPE; + default: + throw new IllegalArgumentException("Unknown AssetKind: " + assetKind); + } } } diff --git a/basyx.aasregistry/basyx.aasregistry-client-native/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/mapper/DummyAasDescriptorFactory.java b/basyx.aasregistry/basyx.aasregistry-client-native/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/mapper/DummyAasDescriptorFactory.java index 7441ce29c..ef1f44e2e 100644 --- a/basyx.aasregistry/basyx.aasregistry-client-native/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/mapper/DummyAasDescriptorFactory.java +++ b/basyx.aasregistry/basyx.aasregistry-client-native/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/mapper/DummyAasDescriptorFactory.java @@ -27,11 +27,15 @@ import java.net.MalformedURLException; import java.net.URL; +import java.util.LinkedList; +import java.util.List; +import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AdministrativeInformation; import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetKind; import org.eclipse.digitaltwin.basyx.aasregistry.client.model.Endpoint; import org.eclipse.digitaltwin.basyx.aasregistry.client.model.ProtocolInformation; +import org.eclipse.digitaltwin.basyx.core.RepositoryUrlHelper; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; /** @@ -43,7 +47,7 @@ public class DummyAasDescriptorFactory { private static final String AAS_REPOSITORY_PATH = "/shells"; - public static AssetAdministrationShellDescriptor createDummyDescriptor(String aasId, String idShort, String globalAssetId, String... aasRepoBaseUrls) { + public static AssetAdministrationShellDescriptor createDummyDescriptor(String aasId, String idShort, String globalAssetId, AdministrativeInformation administrativeInformation, List endpoints) { AssetAdministrationShellDescriptor descriptor = new AssetAdministrationShellDescriptor(); @@ -51,23 +55,46 @@ public static AssetAdministrationShellDescriptor createDummyDescriptor(String aa descriptor.setIdShort(idShort); descriptor.setAssetKind(AssetKind.INSTANCE); descriptor.setGlobalAssetId(globalAssetId); + descriptor.setEndpoints(endpoints); + descriptor.setAdministration(administrativeInformation); + + return descriptor; + } + + public static AssetAdministrationShellDescriptor createDummyDescriptor(String aasId, String idShort, String globalAssetId, AdministrativeInformation administrativeInformation, String... aasRepoBaseUrls) { + LinkedList endpoints = new LinkedList<>(); + for (String eachUrl : aasRepoBaseUrls) { - descriptor.addEndpointsItem(createEndpointItem(aasId, eachUrl)); + endpoints.add(createEndpoint(aasId, eachUrl, "AAS-3.0")); } - return descriptor; + + return createDummyDescriptor(aasId, idShort, globalAssetId, administrativeInformation, endpoints); + } + + public static AdministrativeInformation buildAdministrationInformation(String version, String revision, String templateId) { + AdministrativeInformation administrativeInformation = new AdministrativeInformation(); + administrativeInformation.setVersion(version); + administrativeInformation.setRevision(revision); + administrativeInformation.setTemplateId(templateId); + return administrativeInformation; } - private static Endpoint createEndpointItem(String aasId, String aasRepoBaseUrl) { + public static Endpoint createEndpoint(String endpointUrl, String endpointInterface) { Endpoint endpoint = new Endpoint(); - endpoint.setInterface("AAS-3.0"); - endpoint.setProtocolInformation(createProtocolInformation(aasId, aasRepoBaseUrl)); + endpoint.setInterface(endpointInterface); + endpoint.setProtocolInformation(createProtocolInformation(endpointUrl)); return endpoint; } - private static ProtocolInformation createProtocolInformation(String aasId, String aasRepoBaseUrl) { + public static Endpoint createEndpoint(String aasId, String aasRepoBaseUrl, String endpointInterface) { String href = createHref(aasId, aasRepoBaseUrl); + return createEndpoint(href, endpointInterface); + } + + + private static ProtocolInformation createProtocolInformation(String href) { ProtocolInformation protocolInformation = new ProtocolInformation(); protocolInformation.setHref(href); protocolInformation.endpointProtocol(getProtocol(href)); @@ -76,7 +103,7 @@ private static ProtocolInformation createProtocolInformation(String aasId, Strin } private static String createHref(String aasId, String aasRepoBaseUrl) { - return String.format("%s/%s", createAasRepositoryUrl(aasRepoBaseUrl), Base64UrlEncodedIdentifier.encodeIdentifier(aasId)); + return String.format("%s/%s", RepositoryUrlHelper.createRepositoryUrl(aasRepoBaseUrl, AAS_REPOSITORY_PATH), Base64UrlEncodedIdentifier.encodeIdentifier(aasId)); } private static String getProtocol(String endpoint) { @@ -87,13 +114,5 @@ private static String getProtocol(String endpoint) { } } - private static String createAasRepositoryUrl(String aasRepositoryBaseURL) { - - try { - return new URL(new URL(aasRepositoryBaseURL), AAS_REPOSITORY_PATH).toString(); - } catch (MalformedURLException e) { - throw new RuntimeException("The AAS Repository Base url is malformed. " + e.getMessage()); - } - } } diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/TestAuthorizedAasRegistry.java b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/TestAuthorizedAasRegistry.java index 4d8eabf33..31d3f5b02 100644 --- a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/TestAuthorizedAasRegistry.java +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/TestAuthorizedAasRegistry.java @@ -66,7 +66,6 @@ public class TestAuthorizedAasRegistry { private static final String AAS_REGISTRY_PATH = "shell-descriptors"; - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); private static final String AAS_DESCRIPTOR_SIMPLE_2_JSON = "authorization/AasDescriptorSimple_2.json"; private static final String AAS_DESCRIPTOR_SIMPLE_1_JSON = "authorization/AasDescriptorSimple_1.json"; private static final String SPECIFIC_SHELL_ID_2 = "specificAasId-2"; @@ -108,7 +107,7 @@ public void initializeRepositories() throws FileNotFoundException, IOException { public void reset() throws FileNotFoundException, IOException { configureSecurityContext(); - Collection descriptors = storage.getAllAasDescriptors(NO_LIMIT_PAGINATION_INFO, new DescriptorFilter(AssetKind.TYPE, "TestAsset")).getResult(); + Collection descriptors = storage.getAllAasDescriptors(PaginationInfo.NO_LIMIT, new DescriptorFilter(AssetKind.TYPE, "TestAsset")).getResult(); descriptors.forEach(descriptor -> storage.removeAasDescriptor(descriptor.getId())); diff --git a/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/BaseInterfaceTest.java b/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/BaseInterfaceTest.java index 0ade65fce..8e85e1d00 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/BaseInterfaceTest.java +++ b/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/BaseInterfaceTest.java @@ -134,12 +134,12 @@ protected void verifyNoEventSent() { } protected List getAllAasDescriptors() { - return storage.getAllAasDescriptors(new PaginationInfo(null, null), new DescriptorFilter(null, null)).getResult(); + return storage.getAllAasDescriptors(PaginationInfo.NO_LIMIT, new DescriptorFilter(null, null)).getResult(); } protected List getAllAasDescriptorsFiltered(AssetKind kind, String type) { - return storage.getAllAasDescriptors(new PaginationInfo(null, null), new DescriptorFilter(kind, type)).getResult(); + return storage.getAllAasDescriptors(PaginationInfo.NO_LIMIT, new DescriptorFilter(kind, type)).getResult(); } protected CursorResult> getAllAasDescriptorsWithPagination(int limit, String cursor) { @@ -147,7 +147,7 @@ protected CursorResult> getAllAasDescri } protected List getAllSubmodels(String id) { - return storage.getAllSubmodels(id, new PaginationInfo(null, null)).getResult(); + return storage.getAllSubmodels(id, PaginationInfo.NO_LIMIT).getResult(); } protected CursorResult> getAllSubmodelsWithPagination(String aasId, int limit, String cursor) { diff --git a/basyx.aasregistry/basyx.aasregistry-service-kafka-events/pom.xml b/basyx.aasregistry/basyx.aasregistry-service-kafka-events/pom.xml index f475fccbd..6637743f0 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-kafka-events/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-service-kafka-events/pom.xml @@ -17,7 +17,7 @@ jar - 2023.0.3 + 2024.0.0 diff --git a/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/pom.xml b/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/pom.xml index 0e56f534e..5bc78acca 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/pom.xml @@ -16,7 +16,7 @@ jar - 2023.0.3 + 2024.0.0 ${project.basedir}/${openapi.folder.name} ${openapi.folder}/${openapi.mongodb.file.name} ${openapi.folder}/temporary-extensions-result-file.yaml diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/main/docker/Dockerfile b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/main/docker/Dockerfile index 324d4fc49..5fd979847 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/main/docker/Dockerfile +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/main/docker/Dockerfile @@ -1,8 +1,11 @@ -FROM amazoncorretto:17 as builder -COPY maven/${project.build.finalName}.jar ./ -RUN java -Djarmode=layertools -jar ${project.build.finalName}.jar extract +FROM eclipse-temurin:17 as builder +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG FINAL_NAME=${project.build.finalName} +COPY maven/${FINAL_NAME}.jar ./ +RUN java -Djarmode=layertools -jar ${FINAL_NAME}.jar extract -FROM amazoncorretto:17 +FROM eclipse-temurin:17 RUN mkdir /workspace WORKDIR /workspace COPY --from=builder dependencies/ ./ diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/main/docker/Dockerfile b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/main/docker/Dockerfile index b064339b7..1a5c74674 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/main/docker/Dockerfile +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/main/docker/Dockerfile @@ -1,8 +1,13 @@ -FROM amazoncorretto:17 as builder -COPY maven/${project.build.finalName}.jar ./ -RUN java -Djarmode=layertools -jar ${project.build.finalName}.jar extract +FROM eclipse-temurin:17 as builder +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG FINAL_NAME=${project.build.finalName} +COPY maven/${FINAL_NAME}.jar ./ +RUN java -Djarmode=layertools -jar ${FINAL_NAME}.jar extract -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY RUN mkdir /workspace WORKDIR /workspace COPY --from=builder dependencies/ ./ diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/main/docker/Dockerfile b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/main/docker/Dockerfile index cba9fc132..ae897c5af 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/main/docker/Dockerfile +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/main/docker/Dockerfile @@ -1,8 +1,13 @@ -FROM amazoncorretto:17 as builder -COPY maven/${project.build.finalName}.jar ./ -RUN java -Djarmode=layertools -jar ${project.build.finalName}.jar extract +FROM eclipse-temurin:17 as builder +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG FINAL_NAME=${project.build.finalName} +COPY maven/${FINAL_NAME}.jar ./ +RUN java -Djarmode=layertools -jar ${FINAL_NAME}.jar extract -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY RUN mkdir /workspace WORKDIR /workspace COPY --from=builder dependencies/ ./ diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/main/docker/Dockerfile b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/main/docker/Dockerfile index 807bbd7ca..2924f4e1f 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/main/docker/Dockerfile +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/main/docker/Dockerfile @@ -1,8 +1,13 @@ -FROM amazoncorretto:17 as builder -COPY maven/${project.build.finalName}.jar ./ -RUN java -Djarmode=layertools -jar ${project.build.finalName}.jar extract +FROM eclipse-temurin:17 as builder +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG FINAL_NAME=${project.build.finalName} +COPY maven/${FINAL_NAME}.jar ./ +RUN java -Djarmode=layertools -jar ${FINAL_NAME}.jar extract -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY RUN mkdir /workspace WORKDIR /workspace COPY --from=builder dependencies/ ./ diff --git a/basyx.aasregistry/basyx.aasregistry-service/pom.xml b/basyx.aasregistry/basyx.aasregistry-service/pom.xml index 5cc6e735b..f3f08e22f 100644 --- a/basyx.aasregistry/basyx.aasregistry-service/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-service/pom.xml @@ -16,7 +16,7 @@ jar - 2023.0.3 + 2024.0.0 ${project.basedir}/${openapi.folder.name} ${openapi.folder}/${openapi.name} diff --git a/basyx.aasregistry/open-api/patch-base-extensions.yaml b/basyx.aasregistry/open-api/patch-base-extensions.yaml index 37082ee38..a6ca0bb6f 100644 --- a/basyx.aasregistry/open-api/patch-base-extensions.yaml +++ b/basyx.aasregistry/open-api/patch-base-extensions.yaml @@ -92,7 +92,7 @@ value: "^(0|[1-9][0-9]{0,3})$" - op: replace path: /components/schemas/AdministrativeInformation/allOf/1/properties/templateId/pattern - value: "^([\\t\\n\\r \\ud7ff\\ue000-\\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + value: "^([\\x09\\x0a\\x0d\\x20-\\ud7ff\\ue000-\\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" ## EmbeddedDataSpecification - op: replace path: /components/schemas/EmbeddedDataSpecification/properties/dataSpecificationContent/$ref diff --git a/basyx.aasregistry/pom.xml b/basyx.aasregistry/pom.xml index ce4c59025..0aca16197 100644 --- a/basyx.aasregistry/pom.xml +++ b/basyx.aasregistry/pom.xml @@ -16,18 +16,19 @@ 1.18.20.0 + 1.18.30 ${java.version} ${java.version} UTF-8 src/generated - 3.0.42 + 3.0.64 3.0.0 open-api Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0.1_SSP-001-resolved.yaml Plattform_i40-Registry-and-Discovery.yaml - 33.2.1-jre + 33.4.0-jre 3.9.0 3.0-alpha-2 0.9.14 @@ -75,6 +76,13 @@ org.projectlombok lombok-maven-plugin ${lombok.maven-plugin.version} + + + org.projectlombok + lombok + ${lombok.maven-plugin.lombok.version} + + org.apache.maven.plugins @@ -241,6 +249,13 @@ org.projectlombok lombok-maven-plugin + + + org.projectlombok + lombok + ${lombok.maven-plugin.lombok.version} + + generate-sources diff --git a/basyx.aasrepository/basyx.aasrepository-backend-inmemory/pom.xml b/basyx.aasrepository/basyx.aasrepository-backend-inmemory/pom.xml index 3f6b3a9b8..45c8dc476 100644 --- a/basyx.aasrepository/basyx.aasrepository-backend-inmemory/pom.xml +++ b/basyx.aasrepository/basyx.aasrepository-backend-inmemory/pom.xml @@ -22,6 +22,10 @@ org.eclipse.digitaltwin.basyx basyx.aasrepository-backend + + org.eclipse.digitaltwin.basyx + basyx.backend.inmemory.core + org.eclipse.digitaltwin.basyx basyx.aasrepository-core diff --git a/basyx.aasrepository/basyx.aasrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/backend/inmemory/AasInMemoryBackend.java b/basyx.aasrepository/basyx.aasrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/backend/inmemory/AasInMemoryBackend.java deleted file mode 100644 index aeb02fd99..000000000 --- a/basyx.aasrepository/basyx.aasrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/backend/inmemory/AasInMemoryBackend.java +++ /dev/null @@ -1,117 +0,0 @@ -/******************************************************************************* - * Copyright (C) 2023 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.aasrepository.backend.inmemory; - -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; -import org.springframework.data.repository.CrudRepository; -import org.springframework.lang.NonNull; - -/** - * InMemory implementation for the AAS backend - * - * @author mateusmolina - * - */ -public class AasInMemoryBackend implements CrudRepository { - - private final ConcurrentMap inMemoryStore = new ConcurrentHashMap<>(); - - @Override - public @NonNull S save(@NonNull S entity) { - inMemoryStore.put(entity.getId(), entity); - - return entity; - } - - @Override - public @NonNull Iterable saveAll(@NonNull Iterable entities) { - entities.forEach(this::save); - - return entities; - } - - @Override - public @NonNull Optional findById(@NonNull String id) { - return Optional.ofNullable(inMemoryStore.get(id)); - } - - @Override - public boolean existsById(@NonNull String id) { - return inMemoryStore.containsKey(id); - } - - @Override - public @NonNull Iterable findAll() { - return inMemoryStore.values(); - } - - @Override - public @NonNull Iterable findAllById(@NonNull Iterable ids) { - return StreamSupport.stream(ids.spliterator(), false).map(inMemoryStore::get).filter(Objects::nonNull).collect(Collectors.toList()); - } - - @Override - public long count() { - return inMemoryStore.size(); - } - - @Override - public void deleteById(@NonNull String id) { - inMemoryStore.remove(id); - } - - @Override - public void delete(@NonNull AssetAdministrationShell entity) { - inMemoryStore.remove(entity.getId()); - } - - @Override - public void deleteAllById(@NonNull Iterable ids) { - for (String id : ids) - inMemoryStore.remove(id); - } - - @Override - public void deleteAll(@NonNull Iterable entities) { - for (AssetAdministrationShell entity : entities) - inMemoryStore.remove(entity.getId()); - } - - @Override - public void deleteAll() { - inMemoryStore.clear(); - } - - -} - diff --git a/basyx.aasrepository/basyx.aasrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/backend/inmemory/AasInMemoryBackendProvider.java b/basyx.aasrepository/basyx.aasrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/backend/inmemory/AasInMemoryBackendProvider.java index 0a8a4e788..44860711d 100644 --- a/basyx.aasrepository/basyx.aasrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/backend/inmemory/AasInMemoryBackendProvider.java +++ b/basyx.aasrepository/basyx.aasrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/backend/inmemory/AasInMemoryBackendProvider.java @@ -27,6 +27,7 @@ import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; import org.eclipse.digitaltwin.basyx.aasrepository.backend.AasBackendProvider; +import org.eclipse.digitaltwin.basyx.common.backend.inmemory.core.InMemoryCrudRepository; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; @@ -35,7 +36,7 @@ * * InMemory backend provider for the AAS * - * @author mateusmolina + * @author mateusmolina, danish */ @ConditionalOnExpression("'${basyx.backend}'.equals('InMemory')") @Component @@ -43,7 +44,7 @@ public class AasInMemoryBackendProvider implements AasBackendProvider { @Override public CrudRepository getCrudRepository() { - return new AasInMemoryBackend(); + return new InMemoryCrudRepository(AssetAdministrationShell::getId); } } diff --git a/basyx.aasrepository/basyx.aasrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/AasRepositorySuite.java b/basyx.aasrepository/basyx.aasrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/AasRepositorySuite.java index 0020d6ba3..6e50cf903 100644 --- a/basyx.aasrepository/basyx.aasrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/AasRepositorySuite.java +++ b/basyx.aasrepository/basyx.aasrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/AasRepositorySuite.java @@ -51,7 +51,6 @@ * */ public abstract class AasRepositorySuite extends AasServiceSuite { - private final PaginationInfo noLimitPaginationInfo = new PaginationInfo(0, ""); private static final String AAS_EMPTY_ID = " "; private static final String AAS_NULL_ID = null; @@ -132,7 +131,7 @@ public void deleteNonExistingAas() { @Test(expected = ElementDoesNotExistException.class) public void getSubmodelReferencesOfNonExistingAas() { AasRepository aasRepo = getAasRepository(); - aasRepo.getSubmodelReferences("doesNotMatter", noLimitPaginationInfo).getResult(); + aasRepo.getSubmodelReferences("doesNotMatter", PaginationInfo.NO_LIMIT).getResult(); } @Test(expected = ElementDoesNotExistException.class) diff --git a/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/TestAuthorizedAasRepository.java b/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/TestAuthorizedAasRepository.java index a38bbe50c..9d08021b4 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/TestAuthorizedAasRepository.java +++ b/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/TestAuthorizedAasRepository.java @@ -150,6 +150,26 @@ public void getAasWithCorrectRoleAndPermission() throws IOException { assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); } + @Test + public void getAasWithOnlyResourceRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.USER_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificAasAccessURL(SPECIFIC_SHELL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getAasWithBothRealmAndResourceRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.VISITOR_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificAasAccessURL(SPECIFIC_SHELL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + @Test public void getAasWithCorrectRoleAndSpecificAasPermission() throws IOException { DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; @@ -196,6 +216,16 @@ public void createAasWithCorrectRoleAndPermission() throws IOException { deleteElementWithAuthorization(getSpecificAasAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); } + @Test + public void createAasWithBothRealmAndResourceRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.VISITOR_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = createAasOnRepositoryWithAuthorization(getAasJSONString(AAS_SIMPLE_2_JSON), accessToken); + assertEquals(HttpStatus.CREATED.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificAasAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + @Test public void createAasWithInsufficientPermissionRole() throws IOException { String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); diff --git a/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/test/resources/rbac_rules.json b/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/test/resources/rbac_rules.json index 6bf880c8d..4dc81114d 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/test/resources/rbac_rules.json +++ b/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/test/resources/rbac_rules.json @@ -15,6 +15,30 @@ "aasIds": "*" } }, + { + "role": "basyx-user", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId" + } + }, + { + "role": "basyx-user", + "action": "CREATE", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId-2" + } + }, + { + "role": "visitor", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId" + } + }, { "role": "basyx-reader-two", "action": "READ", diff --git a/basyx.aasrepository/basyx.aasrepository-feature-mqtt/Readme.md b/basyx.aasrepository/basyx.aasrepository-feature-mqtt/Readme.md index e632464eb..a3f13cc22 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-mqtt/Readme.md +++ b/basyx.aasrepository/basyx.aasrepository-feature-mqtt/Readme.md @@ -5,4 +5,6 @@ This feature provides hierarchical MQTT eventing for a multitude of events: | ----------- | ----------- | --- | | AAS Created | aas-repository/\$repoId/shells/created| Created AAS JSON | | AAS Updated | aas-repository/\$repoId/shells/updated| Updated AAS JSON| -| AAS Deleted | aas-repository/\$repoId/shells/deleted| Deleted AAS JSON| \ No newline at end of file +| AAS Deleted | aas-repository/\$repoId/shells/deleted| Deleted AAS JSON| +| Submodel Reference Created | aas-repository/\$repoId/shells/submodels/\$submodelId/created| Created AAS JSON | +| Submodel Reference Deleted | aas-repository/\$repoId/shells/submodels/\$submodelId/deleted| Deleted AAS JSON| \ No newline at end of file diff --git a/basyx.aasrepository/basyx.aasrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/mqtt/MqttAasRepository.java b/basyx.aasrepository/basyx.aasrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/mqtt/MqttAasRepository.java index ce2ed9418..48732da4c 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/mqtt/MqttAasRepository.java +++ b/basyx.aasrepository/basyx.aasrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/mqtt/MqttAasRepository.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (C) 2021 the Eclipse BaSyx Authors + * Copyright (C) 2024 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 @@ -107,12 +107,19 @@ public CursorResult> getSubmodelReferences(String aasId, Paginat @Override public void addSubmodelReference(String aasId, Reference submodelReference) { + AssetAdministrationShell shell = decorated.getAas(aasId); decorated.addSubmodelReference(aasId, submodelReference); + + String referenceId = submodelReference.getKeys().get(0).getValue(); + + submodelReferenceCreated(shell, getName(), referenceId); } @Override public void removeSubmodelReference(String aasId, String submodelId) { + AssetAdministrationShell shell = decorated.getAas(aasId); decorated.removeSubmodelReference(aasId, submodelId); + submodelReferenceDeleted(shell, getName(), submodelId); } @Override @@ -136,6 +143,14 @@ private void aasUpdated(AssetAdministrationShell shell, String repoId) { private void aasDeleted(AssetAdministrationShell shell, String repoId) { sendMqttMessage(topicFactory.createDeleteAASTopic(repoId), serializePayload(shell)); } + + private void submodelReferenceCreated(AssetAdministrationShell shell, String repoId, String referenceId) { + sendMqttMessage(topicFactory.createCreateAASSubmodelReferenceTopic(repoId, referenceId), serializePayload(shell)); + } + + private void submodelReferenceDeleted(AssetAdministrationShell shell, String repoId, String referenceId) { + sendMqttMessage(topicFactory.createDeleteAASSubmodelReferenceTopic(repoId, referenceId), serializePayload(shell)); + } private String serializePayload(AssetAdministrationShell shell) { try { diff --git a/basyx.aasrepository/basyx.aasrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/mqtt/MqttAasRepositoryTopicFactory.java b/basyx.aasrepository/basyx.aasrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/mqtt/MqttAasRepositoryTopicFactory.java index f79ce8961..f714d69e6 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/mqtt/MqttAasRepositoryTopicFactory.java +++ b/basyx.aasrepository/basyx.aasrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/mqtt/MqttAasRepositoryTopicFactory.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (C) 2021 the Eclipse BaSyx Authors + * Copyright (C) 2024 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 @@ -37,6 +37,7 @@ public class MqttAasRepositoryTopicFactory extends AbstractMqttTopicFactory { private static final String AASREPOSITORY = "aas-repository"; private static final String SHELLS = "shells"; + private static final String SUBMODELS = "submodels"; private static final String CREATED = "created"; private static final String UPDATED = "updated"; private static final String DELETED = "deleted"; @@ -93,4 +94,40 @@ public String createDeleteAASTopic(String repoId) { .add(DELETED) .toString(); } + + /** + * Creates the hierarchical topic for the submodel reference create event + * + * @param repoId + * @param referenceId + * @return + */ + public String createCreateAASSubmodelReferenceTopic(String repoId, String referenceId) { + return new StringJoiner("/", "", "") + .add(AASREPOSITORY) + .add(repoId) + .add(SHELLS) + .add(SUBMODELS) + .add(referenceId) + .add(CREATED) + .toString(); + } + + /** + * Creates the hierarchical topic for the submodel reference delete event + * + * @param repoId + * @param referenceId + * @return + */ + public String createDeleteAASSubmodelReferenceTopic(String repoId, String referenceId) { + return new StringJoiner("/", "", "") + .add(AASREPOSITORY) + .add(repoId) + .add(SHELLS) + .add(SUBMODELS) + .add(referenceId) + .add(DELETED) + .toString(); + } } diff --git a/basyx.aasrepository/basyx.aasrepository-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/mqtt/TestMqttV2AASAggregatorObserver.java b/basyx.aasrepository/basyx.aasrepository-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/mqtt/TestMqttV2AASAggregatorObserver.java index 785bd0e33..fd61cf76b 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/mqtt/TestMqttV2AASAggregatorObserver.java +++ b/basyx.aasrepository/basyx.aasrepository-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/mqtt/TestMqttV2AASAggregatorObserver.java @@ -1,7 +1,5 @@ -package org.eclipse.digitaltwin.basyx.aasrepository.feature.mqtt; - /******************************************************************************* - * Copyright (C) 2021 the Eclipse BaSyx Authors + * Copyright (C) 2024 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 @@ -24,10 +22,12 @@ * * SPDX-License-Identifier: MIT ******************************************************************************/ +package org.eclipse.digitaltwin.basyx.aasrepository.feature.mqtt; import static org.junit.Assert.assertEquals; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -94,7 +94,7 @@ public static void tearDownClass() { public void createAasEvent() throws DeserializationException { AssetAdministrationShell shell = createAasDummy("createAasEventId"); aasRepository.createAas(shell); - + assertEquals(topicFactory.createCreateAASTopic(aasRepository.getName()), listener.lastTopic); assertEquals(shell, deserializePayload(listener.lastPayload)); } @@ -121,6 +121,29 @@ public void deleteAasEvent() throws DeserializationException { assertEquals(topicFactory.createDeleteAASTopic(aasRepository.getName()), listener.lastTopic); assertEquals(shell, deserializePayload(listener.lastPayload)); } + + @Test + public void addSubmodelReferenceEvent() throws DeserializationException { + AssetAdministrationShell shell = createAasDummy("createAasSubmodelRefEventId"); + aasRepository.createAas(shell); + + Reference submodelReference = DummyAasFactory.createDummyReference(DummyAasFactory.DUMMY_SUBMODEL_ID); + aasRepository.addSubmodelReference(shell.getId(), submodelReference); + + assertEquals(topicFactory.createCreateAASSubmodelReferenceTopic(aasRepository.getName(), DummyAasFactory.DUMMY_SUBMODEL_ID), listener.lastTopic); + assertEquals(shell, deserializePayload(listener.lastPayload)); + } + + @Test + public void removeSubmodelReferenceEvent() throws DeserializationException { + AssetAdministrationShell shell = createAasWithSubmodelReference("removeAasSubmodelRefEventId"); + aasRepository.createAas(shell); + + aasRepository.removeSubmodelReference(shell.getId(), DummyAasFactory.DUMMY_SUBMODEL_ID); + + assertEquals(topicFactory.createDeleteAASSubmodelReferenceTopic(aasRepository.getName(), DummyAasFactory.DUMMY_SUBMODEL_ID), listener.lastTopic); + assertEquals(shell, deserializePayload(listener.lastPayload)); + } private AssetAdministrationShell deserializePayload(String payload) throws DeserializationException { return new JsonDeserializer().read(payload, AssetAdministrationShell.class); @@ -135,6 +158,16 @@ private AssetAdministrationShell createAasDummy(String aasId) { return new DefaultAssetAdministrationShell.Builder().id(aasId) .build(); } + + private AssetAdministrationShell createAasWithSubmodelReference(String aasId) { + Reference submodelReference = DummyAasFactory.createDummyReference(DummyAasFactory.DUMMY_SUBMODEL_ID); + + List submodelReferences = new ArrayList(); + submodelReferences.add(submodelReference); + + return new DefaultAssetAdministrationShell.Builder().id(aasId).submodels(submodelReferences) + .build(); + } private static AasRepository createMqttAasRepository(MqttClient client) { AasRepositoryFactory repoFactory = new SimpleAasRepositoryFactory(new AasInMemoryBackendProvider(), new InMemoryAasServiceFactory(new InMemoryFileRepository())); diff --git a/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/RegistryIntegrationAasRepository.java b/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/RegistryIntegrationAasRepository.java index 17d93879d..f5a8a238c 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/RegistryIntegrationAasRepository.java +++ b/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/RegistryIntegrationAasRepository.java @@ -78,9 +78,19 @@ public AssetAdministrationShell getAas(String shellId) throws ElementDoesNotExis @Override public void createAas(AssetAdministrationShell shell) throws CollidingIdentifierException { + AssetAdministrationShellDescriptor descriptor = new AasDescriptorFactory(aasRepositoryRegistryLink.getAasRepositoryBaseURLs(), attributeMapper).create(shell); + decorated.createAas(shell); - integrateAasWithRegistry(shell, aasRepositoryRegistryLink.getAasRepositoryBaseURLs()); + boolean registrationSuccessful = false; + + try { + registerAas(descriptor); + registrationSuccessful = true; + } finally { + if (!registrationSuccessful) + decorated.deleteAas(shell.getId()); + } } @Override @@ -125,17 +135,15 @@ public AssetInformation getAssetInformation(String shellId) throws ElementDoesNo return decorated.getAssetInformation(shellId); } - private void integrateAasWithRegistry(AssetAdministrationShell shell, List aasRepositoryURLs) { - AssetAdministrationShellDescriptor descriptor = new AasDescriptorFactory(shell, aasRepositoryURLs, attributeMapper).create(); - + private void registerAas(AssetAdministrationShellDescriptor descriptor) { RegistryAndDiscoveryInterfaceApi registryApi = aasRepositoryRegistryLink.getRegistryApi(); try { registryApi.postAssetAdministrationShellDescriptor(descriptor); - logger.info("Shell '{}' has been automatically linked with the Registry", shell.getId()); + logger.info("Shell '{}' has been automatically linked with the Registry", descriptor.getId()); } catch (ApiException e) { - throw new RepositoryRegistryLinkException(shell.getId(), e); + throw new RepositoryRegistryLinkException(descriptor.getId(), e); } } diff --git a/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/AasRepositoryRegistryLinkDescriptorGenerationTest.java b/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/AasRepositoryRegistryLinkDescriptorGenerationTest.java new file mode 100644 index 000000000..8b3739033 --- /dev/null +++ b/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/AasRepositoryRegistryLinkDescriptorGenerationTest.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (C) 2024 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.aasrepository.feature.registry.integration; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultAssetAdministrationShell; +import org.eclipse.digitaltwin.basyx.aasregistry.client.api.RegistryAndDiscoveryInterfaceApi; +import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; +import org.eclipse.digitaltwin.basyx.aasregistry.main.client.factory.AasDescriptorFactory; +import org.eclipse.digitaltwin.basyx.aasregistry.main.client.mapper.AttributeMapper; +import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository; +import org.eclipse.digitaltwin.basyx.aasrepository.backend.SimpleAasRepositoryFactory; +import org.eclipse.digitaltwin.basyx.aasrepository.backend.inmemory.AasInMemoryBackendProvider; +import org.eclipse.digitaltwin.basyx.aasservice.backend.InMemoryAasServiceFactory; +import org.eclipse.digitaltwin.basyx.core.filerepository.InMemoryFileRepository; +import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.Mockito; + +/** + * Test suite for {@link RegistryIntegrationAasRepository} feature + */ +@RunWith(Parameterized.class) +public class AasRepositoryRegistryLinkDescriptorGenerationTest { + private static final String DUMMY_IDSHORT = "ExampleMotor"; + private static final String DUMMY_AAS_ID = "customIdentifier"; + + private static final String BASE_URL = "http://localhost:8081"; + + private RegistryIntegrationAasRepository registryIntegrationAasRepository; + private AasRepository mockedAasRepository; + private AasRepositoryRegistryLink mockedRegistryLink; + private AttributeMapper mockedAttributeMapper; + private RegistryAndDiscoveryInterfaceApi mockedRegistryApi; + + @Before + public void setUp() { + mockedAasRepository = getAasRepository(); + mockedRegistryLink = Mockito.mock(AasRepositoryRegistryLink.class); + mockedAttributeMapper = Mockito.mock(AttributeMapper.class); + mockedRegistryApi = Mockito.mock(RegistryAndDiscoveryInterfaceApi.class); + + Mockito.when(mockedRegistryLink.getRegistryApi()).thenReturn(mockedRegistryApi); + + registryIntegrationAasRepository = new RegistryIntegrationAasRepository(mockedAasRepository, mockedRegistryLink, mockedAttributeMapper); + } + + @Parameterized.Parameter(0) + public String externalUrl; + + @Parameterized.Parameter(1) + public String expectedUrl; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { { BASE_URL + "/context", BASE_URL + "/context/shells/" + Base64UrlEncodedIdentifier.encodeIdentifier(DUMMY_AAS_ID) }, + { BASE_URL, BASE_URL + "/shells/" + Base64UrlEncodedIdentifier.encodeIdentifier(DUMMY_AAS_ID) }, { BASE_URL + "/", BASE_URL + "/shells/" + Base64UrlEncodedIdentifier.encodeIdentifier(DUMMY_AAS_ID) }, + { BASE_URL + "/context/", BASE_URL + "/context/shells/" + Base64UrlEncodedIdentifier.encodeIdentifier(DUMMY_AAS_ID) } }); + } + + @Test + public void testExternalUrl() { + Mockito.when(mockedRegistryLink.getAasRepositoryBaseURLs()).thenReturn(List.of(externalUrl)); + + AssetAdministrationShellDescriptor descriptor = createAndRetrieveDescriptor(createDummyAas()); + String actualUrl = descriptor.getEndpoints().get(0).getProtocolInformation().getHref(); + + assertEquals(expectedUrl, actualUrl); + } + + private AssetAdministrationShellDescriptor createAndRetrieveDescriptor(AssetAdministrationShell shell) { + registryIntegrationAasRepository.createAas(shell); + + AasDescriptorFactory descriptorFactory = new AasDescriptorFactory(mockedRegistryLink.getAasRepositoryBaseURLs(), mockedAttributeMapper); + return descriptorFactory.create(shell); + } + + private AssetAdministrationShell createDummyAas() { + return new DefaultAssetAdministrationShell.Builder() + .id(DUMMY_AAS_ID) + .idShort(DUMMY_IDSHORT) + .build(); + } + + protected AasRepository getAasRepository() { + return new SimpleAasRepositoryFactory(new AasInMemoryBackendProvider(), new InMemoryAasServiceFactory(new InMemoryFileRepository())).create(); + } + +} diff --git a/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/AasRepositoryRegistryLinkTestSuite.java b/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/AasRepositoryRegistryLinkTestSuite.java index 19629dcb7..e1c9d06f9 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/AasRepositoryRegistryLinkTestSuite.java +++ b/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/AasRepositoryRegistryLinkTestSuite.java @@ -40,6 +40,7 @@ import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; import org.eclipse.digitaltwin.basyx.aasregistry.client.model.GetAssetAdministrationShellDescriptorsResult; import org.eclipse.digitaltwin.basyx.aasregistry.main.client.mapper.DummyAasDescriptorFactory; +import org.eclipse.digitaltwin.basyx.core.RepositoryUrlHelper; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; import org.eclipse.digitaltwin.basyx.http.serialization.BaSyxHttpTestUtils; import org.junit.After; @@ -66,7 +67,7 @@ public abstract class AasRepositoryRegistryLinkTestSuite { protected abstract RegistryAndDiscoveryInterfaceApi getAasRegistryApi(); private final AssetAdministrationShellDescriptor DUMMY_DESCRIPTOR = DummyAasDescriptorFactory - .createDummyDescriptor(DUMMY_AAS_ID, DUMMY_IDSHORT, DUMMY_GLOBAL_ASSETID, getAasRepoBaseUrls()); + .createDummyDescriptor(DUMMY_AAS_ID, DUMMY_IDSHORT, DUMMY_GLOBAL_ASSETID, DummyAasDescriptorFactory.buildAdministrationInformation("0", "9", "testTemplateId"), getAasRepoBaseUrls()); @Test public void createAas() throws FileNotFoundException, IOException, ApiException { @@ -129,11 +130,12 @@ private String getAas1JSONString() throws FileNotFoundException, IOException { } private CloseableHttpResponse createAasOnRepo(String aasJsonContent) throws IOException { - return BaSyxHttpTestUtils.executePostOnURL(createAasRepositoryUrl(getFirstAasRepoBaseUrl()), aasJsonContent); + String url = RepositoryUrlHelper.createRepositoryUrl(getFirstAasRepoBaseUrl(), AAS_REPOSITORY_PATH); + return BaSyxHttpTestUtils.executePostOnURL(RepositoryUrlHelper.createRepositoryUrl(getFirstAasRepoBaseUrl(), AAS_REPOSITORY_PATH), aasJsonContent); } private String getSpecificAasAccessURL(String aasId) { - return createAasRepositoryUrl(getFirstAasRepoBaseUrl()) + "/" + return RepositoryUrlHelper.createRepositoryUrl(getFirstAasRepoBaseUrl(), AAS_REPOSITORY_PATH) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(aasId); } @@ -141,13 +143,4 @@ private String getFirstAasRepoBaseUrl() { return getAasRepoBaseUrls()[0]; } - private static String createAasRepositoryUrl(String aasRepositoryBaseURL) { - - try { - return new URL(new URL(aasRepositoryBaseURL), AAS_REPOSITORY_PATH).toString(); - } catch (MalformedURLException e) { - throw new RuntimeException("The AAS Repository Base url is malformed. " + e.getMessage()); - } - } - -} +} \ No newline at end of file diff --git a/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/resources/AasSimple_1.json b/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/resources/AasSimple_1.json index 16e79e653..2ec457d80 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/resources/AasSimple_1.json +++ b/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/resources/AasSimple_1.json @@ -5,5 +5,10 @@ "assetInformation": { "assetKind": "Instance", "globalAssetId": "globalAssetId" + }, + "administration": { + "version": "0", + "revision": "9", + "templateId": "testTemplateId" } } diff --git a/basyx.aasrepository/basyx.aasrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/http/TestAasRepositoryHTTP.java b/basyx.aasrepository/basyx.aasrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/http/TestAasRepositoryHTTP.java index 9591f4566..5e5edea39 100644 --- a/basyx.aasrepository/basyx.aasrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/http/TestAasRepositoryHTTP.java +++ b/basyx.aasrepository/basyx.aasrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/http/TestAasRepositoryHTTP.java @@ -41,7 +41,6 @@ * */ public class TestAasRepositoryHTTP extends AasRepositoryHTTPSuite { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); private static ConfigurableApplicationContext appContext; @BeforeClass @@ -52,7 +51,7 @@ public static void startAasRepo() throws Exception { @Override public void resetRepository() { AasRepository repo = appContext.getBean(AasRepository.class); - repo.getAllAas(NO_LIMIT_PAGINATION_INFO) + repo.getAllAas(PaginationInfo.NO_LIMIT) .getResult() .stream() .map(a -> a.getId()) diff --git a/basyx.aasrepository/basyx.aasrepository.component/Dockerfile b/basyx.aasrepository/basyx.aasrepository.component/Dockerfile index 1c3e9648d..99b045565 100644 --- a/basyx.aasrepository/basyx.aasrepository.component/Dockerfile +++ b/basyx.aasrepository/basyx.aasrepository.component/Dockerfile @@ -1,4 +1,6 @@ -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY USER nobody WORKDIR /application ARG JAR_FILE=target/*-exec.jar diff --git a/basyx.aasservice/basyx.aasservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/backend/InMemoryAasService.java b/basyx.aasservice/basyx.aasservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/backend/InMemoryAasService.java index 563dfc8f4..0fc9b9516 100644 --- a/basyx.aasservice/basyx.aasservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/backend/InMemoryAasService.java +++ b/basyx.aasservice/basyx.aasservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/backend/InMemoryAasService.java @@ -93,16 +93,19 @@ public CursorResult> getSubmodelReferences(PaginationInfo pInfo) @Override public void addSubmodelReference(Reference submodelReference) { - throwExceptionIfReferenceIsAlreadyPresent(submodelReference); - - aas.getSubmodels().add(submodelReference); + List submodelsRefs = aas.getSubmodels(); + synchronized (submodelsRefs) { + throwExceptionIfReferenceIsAlreadyPresent(submodelReference); + submodelsRefs.add(submodelReference); + } } @Override public void removeSubmodelReference(String submodelId) { - Reference specificSubmodelReference = getSubmodelReferenceById(submodelId); - - aas.getSubmodels().remove(specificSubmodelReference); + List submodelsRefs = aas.getSubmodels(); + synchronized (submodelsRefs) { + submodelsRefs.remove(getSubmodelReferenceById(submodelId)); + } } @Override diff --git a/basyx.aasservice/basyx.aasservice-core/src/test/java/org/eclipse/digitaltwin/basyx/aasservice/AasServiceSuite.java b/basyx.aasservice/basyx.aasservice-core/src/test/java/org/eclipse/digitaltwin/basyx/aasservice/AasServiceSuite.java index 0c7e4cbf0..be42e8490 100644 --- a/basyx.aasservice/basyx.aasservice-core/src/test/java/org/eclipse/digitaltwin/basyx/aasservice/AasServiceSuite.java +++ b/basyx.aasservice/basyx.aasservice-core/src/test/java/org/eclipse/digitaltwin/basyx/aasservice/AasServiceSuite.java @@ -64,8 +64,6 @@ */ public abstract class AasServiceSuite { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); - protected abstract AasService getAasService(AssetAdministrationShell shell); protected abstract AasService getAasServiceWithThumbnail() throws IOException; @@ -82,7 +80,7 @@ public void getSubmodelReference() { DummyAssetAdministrationShellFactory.addDummySubmodelReference(expected); AasService aasService = getAasService(expected); - List submodelReferences = aasService.getSubmodelReferences(NO_LIMIT_PAGINATION_INFO).getResult(); + List submodelReferences = aasService.getSubmodelReferences(PaginationInfo.NO_LIMIT).getResult(); Reference submodelReference = getFirstSubmodelReference(submodelReferences); assertEquals(DummyAssetAdministrationShellFactory.submodelReference, submodelReference); } @@ -100,7 +98,7 @@ public void addSubmodelReference() { aasService.addSubmodelReference(ref); - List submodelReferences = aasService.getSubmodelReferences(NO_LIMIT_PAGINATION_INFO).getResult(); + List submodelReferences = aasService.getSubmodelReferences(PaginationInfo.NO_LIMIT).getResult(); Reference submodelReference = getFirstSubmodelReference(submodelReferences); @@ -113,9 +111,9 @@ public void removeSubmodelReference() { DummyAssetAdministrationShellFactory.addDummySubmodelReference(expected); AasService aasService = getAasService(expected); - List submodelReferences = aasService.getSubmodelReferences(NO_LIMIT_PAGINATION_INFO).getResult(); + List submodelReferences = aasService.getSubmodelReferences(PaginationInfo.NO_LIMIT).getResult(); aasService.removeSubmodelReference(DummyAssetAdministrationShellFactory.SUBMODEL_ID); - submodelReferences = aasService.getSubmodelReferences(NO_LIMIT_PAGINATION_INFO).getResult(); + submodelReferences = aasService.getSubmodelReferences(PaginationInfo.NO_LIMIT).getResult(); assertEquals(0, submodelReferences.size()); } diff --git a/basyx.aasservice/basyx.aasservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/feature/mqtt/MqttAasService.java b/basyx.aasservice/basyx.aasservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/feature/mqtt/MqttAasService.java index 17dcdd0e3..cc8909ff3 100644 --- a/basyx.aasservice/basyx.aasservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/feature/mqtt/MqttAasService.java +++ b/basyx.aasservice/basyx.aasservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/feature/mqtt/MqttAasService.java @@ -54,7 +54,6 @@ * */ public class MqttAasService implements AasService { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); private static Logger logger = LoggerFactory.getLogger(MqttAasService.class); private MqttAasServiceTopicFactory topicFactory; @@ -154,7 +153,7 @@ public void removeSubmodelReference(String submodelId) { } private Reference extractSubmodelReferenceById(String submodelId) { - List submodelsReferences = getSubmodelReferences(NO_LIMIT_PAGINATION_INFO).getResult(); + List submodelsReferences = getSubmodelReferences(PaginationInfo.NO_LIMIT).getResult(); return submodelsReferences.stream() .filter(reference -> containsSubmodelId(reference, submodelId)) diff --git a/basyx.aasxfileserver/basyx.aasxfileserver-backend-inmemory/pom.xml b/basyx.aasxfileserver/basyx.aasxfileserver-backend-inmemory/pom.xml index 79e8704d8..f601be551 100644 --- a/basyx.aasxfileserver/basyx.aasxfileserver-backend-inmemory/pom.xml +++ b/basyx.aasxfileserver/basyx.aasxfileserver-backend-inmemory/pom.xml @@ -17,6 +17,10 @@ org.eclipse.digitaltwin.basyx basyx.aasxfileserver-core + + org.eclipse.digitaltwin.basyx + basyx.backend.inmemory.core + org.eclipse.digitaltwin.basyx basyx.aasxfileserver-core diff --git a/basyx.aasxfileserver/basyx.aasxfileserver-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/AASXFileServerInMemoryBackendProvider.java b/basyx.aasxfileserver/basyx.aasxfileserver-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/AASXFileServerInMemoryBackendProvider.java index 68a050e98..b80b64198 100644 --- a/basyx.aasxfileserver/basyx.aasxfileserver-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/AASXFileServerInMemoryBackendProvider.java +++ b/basyx.aasxfileserver/basyx.aasxfileserver-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/AASXFileServerInMemoryBackendProvider.java @@ -27,6 +27,7 @@ import org.eclipse.digitaltwin.basyx.aasxfileserver.backend.AASXFileServerBackendProvider; import org.eclipse.digitaltwin.basyx.aasxfileserver.model.Package; +import org.eclipse.digitaltwin.basyx.common.backend.inmemory.core.InMemoryCrudRepository; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; @@ -36,7 +37,7 @@ @Component public class AASXFileServerInMemoryBackendProvider implements AASXFileServerBackendProvider { - private AASXFileServerInMemoryCrudRepository repository = new AASXFileServerInMemoryCrudRepository(); + private CrudRepository repository = new InMemoryCrudRepository(Package::getPackageId); @Override public CrudRepository getCrudRepository() { diff --git a/basyx.aasxfileserver/basyx.aasxfileserver-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/AASXFileServerInMemoryCrudRepository.java b/basyx.aasxfileserver/basyx.aasxfileserver-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/AASXFileServerInMemoryCrudRepository.java deleted file mode 100644 index f1c61802a..000000000 --- a/basyx.aasxfileserver/basyx.aasxfileserver-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/AASXFileServerInMemoryCrudRepository.java +++ /dev/null @@ -1,110 +0,0 @@ -/******************************************************************************* -* Copyright (C) 2024 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.aasxfileserver; - -import java.util.List; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import org.eclipse.digitaltwin.basyx.aasxfileserver.model.Package; -import org.springframework.data.repository.CrudRepository; -import org.springframework.lang.NonNull; - -public class AASXFileServerInMemoryCrudRepository implements CrudRepository { - - private final ConcurrentMap packageMap = new ConcurrentHashMap<>(); - - @Override - public @NonNull S save(@NonNull S entity) { - packageMap.put(entity.getPackageId(), entity); - - return entity; - } - - @Override - public @NonNull Iterable saveAll(@NonNull Iterable entities) { - entities.forEach(this::save); - - return entities; - } - - @Override - public @NonNull Optional findById(@NonNull String id) { - return Optional.ofNullable(packageMap.get(id)); - } - - @Override - public boolean existsById(@NonNull String id) { - return packageMap.containsKey(id); - } - - @Override - public @NonNull Iterable findAll() { - return packageMap.values(); - } - - @Override - public @NonNull Iterable findAllById(@NonNull Iterable ids) { - List idList = StreamSupport.stream(ids.spliterator(), false).collect(Collectors.toList()); - return packageMap.entrySet().stream().filter(entry -> idList.contains(entry.getKey())).map(Entry::getValue).collect(Collectors.toList()); - } - - @Override - public long count() { - return packageMap.size(); - } - - @Override - public void deleteById(@NonNull String id) { - packageMap.remove(id); - } - - @Override - public void delete(@NonNull Package entity) { - packageMap.remove(entity.getPackageId()); - } - - @Override - public void deleteAllById(@NonNull Iterable ids) { - List idList = StreamSupport.stream(ids.spliterator(), false).collect(Collectors.toList()); - packageMap.keySet().removeAll(idList); - } - - @Override - public void deleteAll(@NonNull Iterable entities) { - List idList = StreamSupport.stream(entities.spliterator(), false).map(Package::getPackageId).collect(Collectors.toList()); - packageMap.keySet().removeAll(idList); - } - - @Override - public void deleteAll() { - packageMap.clear(); - } -} diff --git a/basyx.aasxfileserver/basyx.aasxfileserver-backend/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/backend/CrudAASXFileServer.java b/basyx.aasxfileserver/basyx.aasxfileserver-backend/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/backend/CrudAASXFileServer.java index 599a28c7b..ce5211c52 100644 --- a/basyx.aasxfileserver/basyx.aasxfileserver-backend/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/backend/CrudAASXFileServer.java +++ b/basyx.aasxfileserver/basyx.aasxfileserver-backend/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/backend/CrudAASXFileServer.java @@ -25,7 +25,6 @@ package org.eclipse.digitaltwin.basyx.aasxfileserver.backend; import java.io.InputStream; -import java.util.Collection; import java.util.List; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; @@ -33,7 +32,6 @@ import java.util.stream.StreamSupport; import org.eclipse.digitaltwin.aas4j.v3.model.PackageDescription; -import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultPackageDescription; import org.eclipse.digitaltwin.basyx.aasxfileserver.AASXFileServer; import org.eclipse.digitaltwin.basyx.aasxfileserver.model.Package; diff --git a/basyx.aasxfileserver/basyx.aasxfileserver-core/src/test/java/org/eclipse/digitaltwin/basyx/aasxfileserver/core/AASXFileServerSuite.java b/basyx.aasxfileserver/basyx.aasxfileserver-core/src/test/java/org/eclipse/digitaltwin/basyx/aasxfileserver/core/AASXFileServerSuite.java index e93d552a4..2d0fc77b0 100644 --- a/basyx.aasxfileserver/basyx.aasxfileserver-core/src/test/java/org/eclipse/digitaltwin/basyx/aasxfileserver/core/AASXFileServerSuite.java +++ b/basyx.aasxfileserver/basyx.aasxfileserver-core/src/test/java/org/eclipse/digitaltwin/basyx/aasxfileserver/core/AASXFileServerSuite.java @@ -53,7 +53,6 @@ public abstract class AASXFileServerSuite { protected abstract AASXFileServer getAASXFileServer(); - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0,""); @Test public void getAllAASXPackageIds() { @@ -65,7 +64,7 @@ public void getAllAASXPackageIds() { List expectedPackageDescriptions = Arrays.asList(expectedDescription1, expectedDescription2); - CursorResult> pagedPackageDescriptions = server.getAllAASXPackageIds("",NO_LIMIT_PAGINATION_INFO); + CursorResult> pagedPackageDescriptions = server.getAllAASXPackageIds("", PaginationInfo.NO_LIMIT); List actualPackageDescriptions = pagedPackageDescriptions.getResult(); @@ -82,7 +81,7 @@ public void getAllAASXPackageIdsByShellId() { List expectedPackageDescriptions = Arrays.asList(expectedDescription); - CursorResult> pagedPackageDescriptions = server.getAllAASXPackageIds("AAS_ID_3",NO_LIMIT_PAGINATION_INFO); + CursorResult> pagedPackageDescriptions = server.getAllAASXPackageIds("AAS_ID_3", PaginationInfo.NO_LIMIT); List actualPackageDescriptions = pagedPackageDescriptions.getResult(); assertGetAllAASXPackageIds(expectedPackageDescriptions, actualPackageDescriptions); } @@ -103,7 +102,7 @@ public void getAllAASXPackageIdsEmpty() { String shellId = "testShellId"; AASXFileServer server = getAASXFileServer(); - CursorResult> pagedPackageDescriptions = server.getAllAASXPackageIds(shellId,NO_LIMIT_PAGINATION_INFO); + CursorResult> pagedPackageDescriptions = server.getAllAASXPackageIds(shellId, PaginationInfo.NO_LIMIT); List packageDescriptions = pagedPackageDescriptions.getResult(); assertTrue(packageDescriptions.isEmpty()); } @@ -128,7 +127,7 @@ public void updateExistingAASXByPackageId() throws IOException { expectedPackageDescription.setPackageId("1"); expectedPackageDescription.setItems(DummyAASXFileServerFactory.SECOND_SHELL_IDS); - CursorResult> pagedPackageDescriptions = server.getAllAASXPackageIds("",NO_LIMIT_PAGINATION_INFO); + CursorResult> pagedPackageDescriptions = server.getAllAASXPackageIds("", PaginationInfo.NO_LIMIT); List actualPackageDescription = pagedPackageDescriptions.getResult(); assertUpdatedAASXPackageId(expectedPackageDescription, actualPackageDescription, server); } diff --git a/basyx.aasxfileserver/basyx.aasxfileserver-http/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/http/AASXFileServerDescriptionConfiguration.java b/basyx.aasxfileserver/basyx.aasxfileserver-http/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/http/AASXFileServerDescriptionConfiguration.java new file mode 100644 index 000000000..1d4c88c95 --- /dev/null +++ b/basyx.aasxfileserver/basyx.aasxfileserver-http/src/main/java/org/eclipse/digitaltwin/basyx/aasxfileserver/http/AASXFileServerDescriptionConfiguration.java @@ -0,0 +1,17 @@ +package org.eclipse.digitaltwin.basyx.aasxfileserver.http; + +import java.util.List; +import java.util.TreeSet; + +import org.eclipse.digitaltwin.basyx.http.description.Profile; +import org.eclipse.digitaltwin.basyx.http.description.ProfileDeclaration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AASXFileServerDescriptionConfiguration { + @Bean + public ProfileDeclaration aasxFileServerProfiles() { + return () -> new TreeSet<>(List.of(Profile.AASXFILESERVERSERVICESPECIFICATION_SSP_001)); + } +} \ No newline at end of file diff --git a/basyx.aasxfileserver/basyx.aasxfileserver-http/src/test/java/org/eclipse/digitaltwin/basyx/aasxfileserver/http/TestAASXFileServerHTTP.java b/basyx.aasxfileserver/basyx.aasxfileserver-http/src/test/java/org/eclipse/digitaltwin/basyx/aasxfileserver/http/TestAASXFileServerHTTP.java index 8fb388f8c..33de1702d 100644 --- a/basyx.aasxfileserver/basyx.aasxfileserver-http/src/test/java/org/eclipse/digitaltwin/basyx/aasxfileserver/http/TestAASXFileServerHTTP.java +++ b/basyx.aasxfileserver/basyx.aasxfileserver-http/src/test/java/org/eclipse/digitaltwin/basyx/aasxfileserver/http/TestAASXFileServerHTTP.java @@ -74,7 +74,7 @@ public void setup() { @AfterMethod public void cleanUp() { try { - this.aasxFileServer.getAllAASXPackageIds("", new PaginationInfo(0, "")).getResult().stream().forEach(p -> this.aasxFileServer.deleteAASXByPackageId(p.getPackageId())); + this.aasxFileServer.getAllAASXPackageIds("", PaginationInfo.NO_LIMIT).getResult().stream().forEach(p -> this.aasxFileServer.deleteAASXByPackageId(p.getPackageId())); } catch (Exception ignored) { } @@ -193,11 +193,11 @@ public void testGetAllPackages() throws Exception { } private boolean isPackageNotPresent(String aasId) { - return this.aasxFileServer.getAllAASXPackageIds(aasId, new PaginationInfo(0, "")).getResult().size() == 0; + return this.aasxFileServer.getAllAASXPackageIds(aasId, PaginationInfo.NO_LIMIT).getResult().size() == 0; } private boolean isPackagePresentOneTime(String aasId) { - return this.aasxFileServer.getAllAASXPackageIds(aasId, new PaginationInfo(0, "")).getResult().size() == 1; + return this.aasxFileServer.getAllAASXPackageIds(aasId, PaginationInfo.NO_LIMIT).getResult().size() == 1; } private PackageDescription createAASXPackageOnServer() throws Exception { diff --git a/basyx.aasxfileserver/basyx.aasxfileserver.component/Dockerfile b/basyx.aasxfileserver/basyx.aasxfileserver.component/Dockerfile index 1c3e9648d..99b045565 100644 --- a/basyx.aasxfileserver/basyx.aasxfileserver.component/Dockerfile +++ b/basyx.aasxfileserver/basyx.aasxfileserver.component/Dockerfile @@ -1,4 +1,6 @@ -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY USER nobody WORKDIR /application ARG JAR_FILE=target/*-exec.jar diff --git a/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/SubmodelAuthorizationRbacStorage.java b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/SubmodelAuthorizationRbacStorage.java index fed4005aa..2c8ea1652 100644 --- a/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/SubmodelAuthorizationRbacStorage.java +++ b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/SubmodelAuthorizationRbacStorage.java @@ -91,7 +91,7 @@ public boolean exist(String key) { @Override public Map getRbacRules() { - return smService.getSubmodelElements(new PaginationInfo(0, "")).getResult().stream().map(SubmodelElementCollection.class::cast).map(ruleAdapter::adapt).collect(Collectors.toMap(rbacRule -> createKey(rbacRule), rbacRule -> rbacRule)); + return smService.getSubmodelElements(PaginationInfo.NO_LIMIT).getResult().stream().map(SubmodelElementCollection.class::cast).map(ruleAdapter::adapt).collect(Collectors.toMap(rbacRule -> createKey(rbacRule), rbacRule -> rbacRule)); } private String createKey(RbacRule rbacRule) { diff --git a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/KeycloakRoleProvider.java b/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/KeycloakRoleProvider.java index 5394a509f..6a0a8088c 100644 --- a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/KeycloakRoleProvider.java +++ b/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/KeycloakRoleProvider.java @@ -27,8 +27,10 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.eclipse.digitaltwin.basyx.authorization.CommonAuthorizationProperties; import org.eclipse.digitaltwin.basyx.authorization.SubjectInformation; @@ -48,6 +50,7 @@ public class KeycloakRoleProvider implements RoleProvider { private static final String CLAIM_REALM_ACCESS = "realm_access"; + private static final String CLAIM_RESOURCE_ACCESS = "resource_access"; private static final String CLAIM_ROLES = "roles"; @@ -65,21 +68,38 @@ public List getRoles() { validateJwt(jwt); - Map> realmAccess = jwt.getClaim(CLAIM_REALM_ACCESS); + Map> realmAccess = new HashMap<>(); + Map>> resourceAccess = new HashMap<>(); + + if (jwt.hasClaim(CLAIM_REALM_ACCESS)) + realmAccess = jwt.getClaim(CLAIM_REALM_ACCESS); + + if (jwt.hasClaim(CLAIM_RESOURCE_ACCESS)) + resourceAccess = jwt.getClaim(CLAIM_RESOURCE_ACCESS); - return getRolesFromRealmAccess(realmAccess); + return extractRolesFromClaims(realmAccess, resourceAccess); } - private List getRolesFromRealmAccess(Map> realmAccess) { - if (realmAccess == null || realmAccess.isEmpty()) + private List extractRolesFromClaims(Map> realmAccess, Map>> resourceAccess) { + if (realmAccess.isEmpty() && resourceAccess.isEmpty()) return new ArrayList<>(); - Collection roles = realmAccess.get(CLAIM_ROLES); - - if (roles == null || roles.isEmpty()) - return new ArrayList<>(); - - return new ArrayList<>(roles); + List roles = new ArrayList<>(); + + Collection realmRoles = realmAccess.get(CLAIM_ROLES); + + if (realmRoles != null && !realmRoles.isEmpty()) + roles.addAll(realmRoles); + + for (Map.Entry>> entry : resourceAccess.entrySet()) { + Map> clientRolesMap = entry.getValue(); + Collection clientRoles = clientRolesMap.get(CLAIM_ROLES); + + if (clientRoles != null) + roles.addAll(clientRoles); + } + + return roles.stream().distinct().collect(Collectors.toList()); } private void validateJwt(Jwt jwt) { @@ -96,4 +116,4 @@ private SubjectInformation getSubjectInformation() { return subjectInfo; } -} +} \ No newline at end of file diff --git a/basyx.common/basyx.authorization/src/test/java/org/eclipse/digitaltwin/basyx/authorization/TestKeycloakRoleProvider.java b/basyx.common/basyx.authorization/src/test/java/org/eclipse/digitaltwin/basyx/authorization/TestKeycloakRoleProvider.java new file mode 100644 index 000000000..3b5756232 --- /dev/null +++ b/basyx.common/basyx.authorization/src/test/java/org/eclipse/digitaltwin/basyx/authorization/TestKeycloakRoleProvider.java @@ -0,0 +1,188 @@ +/******************************************************************************* + * Copyright (C) 2024 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.authorization; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.eclipse.digitaltwin.basyx.authorization.rbac.KeycloakRoleProvider; +import org.eclipse.digitaltwin.basyx.core.exceptions.NullSubjectException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.*; + +/** + * Tests the behaviour of {@link KeycloakRoleProvider} + * + * @author danish + */ +public class TestKeycloakRoleProvider { + + @Mock + private SubjectInformationProvider subjectInformationProvider; + + @Mock + private Jwt jwt; + + @InjectMocks + private KeycloakRoleProvider keycloakRoleProvider; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + + @SuppressWarnings("unchecked") + SubjectInformation subjectInfo = mock(SubjectInformation.class); + when(subjectInfo.get()).thenReturn(jwt); + when(subjectInformationProvider.get()).thenReturn(subjectInfo); + } + + @Test + public void getRoles_whenBothRealmAndResourceRolesPresent() { + Map> realmAccess = new HashMap<>(); + realmAccess.put("roles", Arrays.asList("ROLE_USER", "ROLE_ADMIN")); + + Map>> resourceAccess = new HashMap<>(); + resourceAccess.put("client1", new HashMap<>(Collections.singletonMap("roles", Arrays.asList("ROLE_SUPERUSER", "ROLE_ADMIN")))); + resourceAccess.put("client2", new HashMap<>(Collections.singletonMap("roles", Arrays.asList("ROLE_SUPPORT")))); + + when(jwt.hasClaim("realm_access")).thenReturn(true); + when(jwt.hasClaim("resource_access")).thenReturn(true); + when(jwt.getClaim("realm_access")).thenReturn(realmAccess); + when(jwt.getClaim("resource_access")).thenReturn(resourceAccess); + + List roles = keycloakRoleProvider.getRoles(); + + assertEquals(4, roles.size()); + assertTrue(roles.contains("ROLE_USER")); + assertTrue(roles.contains("ROLE_ADMIN")); + assertTrue(roles.contains("ROLE_SUPERUSER")); + assertTrue(roles.contains("ROLE_SUPPORT")); + } + + @Test + public void getRoles_whenOnlyRealmRolesPresent() { + Map> realmAccess = new HashMap<>(); + realmAccess.put("roles", Arrays.asList("ROLE_USER", "ROLE_ADMIN")); + + when(jwt.hasClaim("realm_access")).thenReturn(true); + when(jwt.hasClaim("resource_access")).thenReturn(true); + when(jwt.getClaim("realm_access")).thenReturn(realmAccess); + when(jwt.getClaim("resource_access")).thenReturn(Collections.emptyMap()); + + List roles = keycloakRoleProvider.getRoles(); + + assertEquals(2, roles.size()); + assertTrue(roles.contains("ROLE_USER")); + assertTrue(roles.contains("ROLE_ADMIN")); + } + + @Test + public void getRoles_whenOnlyResourceRolesPresent() { + Map>> resourceAccess = new HashMap<>(); + resourceAccess.put("client1", new HashMap<>(Collections.singletonMap("roles", Arrays.asList("ROLE_SUPERUSER", "ROLE_SUPPORT")))); + + when(jwt.hasClaim("realm_access")).thenReturn(true); + when(jwt.hasClaim("resource_access")).thenReturn(true); + when(jwt.getClaim("realm_access")).thenReturn(Collections.emptyMap()); + when(jwt.getClaim("resource_access")).thenReturn(resourceAccess); + + List roles = keycloakRoleProvider.getRoles(); + + assertEquals(2, roles.size()); + assertTrue(roles.contains("ROLE_SUPERUSER")); + assertTrue(roles.contains("ROLE_SUPPORT")); + } + + @Test + public void getRoles_whenNoRolesPresent() { + when(jwt.hasClaim("realm_access")).thenReturn(true); + when(jwt.hasClaim("resource_access")).thenReturn(true); + when(jwt.getClaim("realm_access")).thenReturn(Collections.emptyMap()); + when(jwt.getClaim("resource_access")).thenReturn(Collections.emptyMap()); + + List roles = keycloakRoleProvider.getRoles(); + + assertTrue(roles.isEmpty()); + } + + @Test(expected = NullSubjectException.class) + public void getRoles_whenJwtIsNull() { + @SuppressWarnings("unchecked") + SubjectInformation subjectInfo = mock(SubjectInformation.class); + when(subjectInfo.get()).thenReturn(null); + when(subjectInformationProvider.get()).thenReturn(subjectInfo); + + keycloakRoleProvider.getRoles(); + } + + @Test + public void getRoles_whenRealmAccessNotPresentButResourceAccessPresent() { + Map>> resourceAccess = new HashMap<>(); + resourceAccess.put("client1", new HashMap<>(Collections.singletonMap("roles", Arrays.asList("ROLE_SUPPORT", "ROLE_USER")))); + + when(jwt.hasClaim("realm_access")).thenReturn(false); + when(jwt.hasClaim("resource_access")).thenReturn(true); + when(jwt.getClaim("resource_access")).thenReturn(resourceAccess); + + List roles = keycloakRoleProvider.getRoles(); + + assertEquals(2, roles.size()); + assertTrue(roles.contains("ROLE_SUPPORT")); + assertTrue(roles.contains("ROLE_USER")); + } + + @Test + public void getRoles_whenResourceAccessNotPresentButRealmAccessPresent() { + Map> realmAccess = new HashMap<>(); + realmAccess.put("roles", Arrays.asList("ROLE_USER", "ROLE_ADMIN")); + + when(jwt.hasClaim("resource_access")).thenReturn(false); + when(jwt.hasClaim("realm_access")).thenReturn(true); + when(jwt.getClaim("realm_access")).thenReturn(realmAccess); + + List roles = keycloakRoleProvider.getRoles(); + + assertEquals(2, roles.size()); + assertTrue(roles.contains("ROLE_USER")); + assertTrue(roles.contains("ROLE_ADMIN")); + } + + @Test + public void getRoles_whenClaimNotPresent() { + when(jwt.hasClaim("realm_access")).thenReturn(false); + when(jwt.hasClaim("resource_access")).thenReturn(false); + + List roles = keycloakRoleProvider.getRoles(); + + assertTrue(roles.isEmpty()); + } +} diff --git a/basyx.common/basyx.backend.inmemory.core/pom.xml b/basyx.common/basyx.backend.inmemory.core/pom.xml new file mode 100644 index 000000000..579e9c321 --- /dev/null +++ b/basyx.common/basyx.backend.inmemory.core/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + org.eclipse.digitaltwin.basyx + basyx.common + ${revision} + + basyx.backend.inmemory.core + BaSyx InMemory Backend core + BaSyx InMemory Backend core + + + + org.eclipse.digitaltwin.basyx + basyx.core + + + org.springframework.data + spring-data-commons + + + org.eclipse.digitaltwin.aas4j + aas4j-dataformat-json + + + \ No newline at end of file diff --git a/basyx.submodelrepository/basyx.submodelrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/SubmodelInMemoryBackend.java b/basyx.common/basyx.backend.inmemory.core/src/main/java/org/eclipse/digitaltwin/basyx/common/backend/inmemory/core/InMemoryCrudRepository.java similarity index 69% rename from basyx.submodelrepository/basyx.submodelrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/SubmodelInMemoryBackend.java rename to basyx.common/basyx.backend.inmemory.core/src/main/java/org/eclipse/digitaltwin/basyx/common/backend/inmemory/core/InMemoryCrudRepository.java index 91afca35d..8c5267266 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/SubmodelInMemoryBackend.java +++ b/basyx.common/basyx.backend.inmemory.core/src/main/java/org/eclipse/digitaltwin/basyx/common/backend/inmemory/core/InMemoryCrudRepository.java @@ -23,45 +23,51 @@ * SPDX-License-Identifier: MIT ******************************************************************************/ -package org.eclipse.digitaltwin.basyx.submodelrepository; +package org.eclipse.digitaltwin.basyx.common.backend.inmemory.core; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; import org.springframework.data.repository.CrudRepository; import org.springframework.lang.NonNull; /** - * InMemory implementation for the Submodel backend - * - * @author mateusmolina, danish + * CrudRepository implementation for InMemory backends * + * @author danish */ -public class SubmodelInMemoryBackend implements CrudRepository { - - private final ConcurrentMap inMemoryStore = new ConcurrentHashMap<>(); +public class InMemoryCrudRepository implements CrudRepository { + + private final ConcurrentMap inMemoryStore = new ConcurrentHashMap<>(); + private Function idGetter; + public InMemoryCrudRepository(Function idGetter) { + this.idGetter = idGetter; + } + @Override - public @NonNull S save(@NonNull S entity) { - inMemoryStore.put(entity.getId(), entity); + public @NonNull S save(@NonNull S entity) { + String id = idGetter.apply(entity); + + inMemoryStore.put(id, entity); return entity; } @Override - public @NonNull Iterable saveAll(@NonNull Iterable entities) { + public @NonNull Iterable saveAll(@NonNull Iterable entities) { entities.forEach(this::save); return entities; } @Override - public @NonNull Optional findById(String id) { + public @NonNull Optional findById(@NonNull String id) { return Optional.ofNullable(inMemoryStore.get(id)); } @@ -71,12 +77,12 @@ public boolean existsById(@NonNull String id) { } @Override - public @NonNull Iterable findAll() { + public @NonNull Iterable findAll() { return inMemoryStore.values(); } @Override - public @NonNull Iterable findAllById(@NonNull Iterable ids) { + public @NonNull Iterable findAllById(@NonNull Iterable ids) { return StreamSupport.stream(ids.spliterator(), false).map(inMemoryStore::get).filter(Objects::nonNull).collect(Collectors.toList()); } @@ -91,8 +97,10 @@ public void deleteById(@NonNull String id) { } @Override - public void delete(@NonNull Submodel entity) { - inMemoryStore.remove(entity.getId()); + public void delete(@NonNull T entity) { + String id = idGetter.apply(entity); + + inMemoryStore.remove(id); } @Override @@ -102,9 +110,10 @@ public void deleteAllById(@NonNull Iterable ids) { } @Override - public void deleteAll(@NonNull Iterable entities) { - for (Submodel entity : entities) - inMemoryStore.remove(entity.getId()); + public void deleteAll(@NonNull Iterable entities) { + + for (T entity : entities) + inMemoryStore.remove(idGetter.apply(entity)); } @Override @@ -112,5 +121,5 @@ public void deleteAll() { inMemoryStore.clear(); } -} +} diff --git a/basyx.common/basyx.client/pom.xml b/basyx.common/basyx.client/pom.xml index b751f6a23..04245d5fd 100644 --- a/basyx.common/basyx.client/pom.xml +++ b/basyx.common/basyx.client/pom.xml @@ -63,7 +63,7 @@ com.nimbusds oauth2-oidc-sdk - 11.12 + 11.20.1 diff --git a/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/RepositoryUrlHelper.java b/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/RepositoryUrlHelper.java new file mode 100644 index 000000000..a046bfcc2 --- /dev/null +++ b/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/RepositoryUrlHelper.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (C) 2024 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.core; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; + +public final class RepositoryUrlHelper { + + private RepositoryUrlHelper() { + } + + public static String createRepositoryUrl(String baseUrl, String additionalPath) { + try { + URI baseUri = new URI(baseUrl); + + String[] basePathSegments = baseUri.getPath().replaceAll("^/|/$", "").split("/"); + String[] additionalPathSegments = additionalPath != null ? additionalPath.replaceAll("^/|/$", "").split("/") : new String[0]; + + StringBuilder fullPath = new StringBuilder(); + for (String segment : basePathSegments) { + if (!segment.isEmpty()) { + fullPath.append("/").append(segment); + } + } + for (String segment : additionalPathSegments) { + if (!segment.isEmpty()) { + fullPath.append("/").append(segment); + } + } + + URI finalUri = new URI(baseUri.getScheme(), null, baseUri.getHost(), baseUri.getPort(), fullPath.toString(), null, null); + + return finalUri.toURL().toString(); + } catch (URISyntaxException | MalformedURLException e) { + throw new RuntimeException("The Base URL or additional path is malformed.\n" + e.getMessage(), e); + } + } + +} diff --git a/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/exceptions/FailedToDeepCopyException.java b/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/exceptions/FailedToDeepCopyException.java new file mode 100644 index 000000000..37636226c --- /dev/null +++ b/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/exceptions/FailedToDeepCopyException.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (C) 2024 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.core.exceptions; + +public class FailedToDeepCopyException extends RuntimeException { + + public FailedToDeepCopyException(String objId, Throwable e) { + super(getMessage(objId), e); + } + + private static String getMessage(String objId) { + return "Failed to deep copy object with id " + objId; + } +} diff --git a/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/pagination/PaginationInfo.java b/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/pagination/PaginationInfo.java index 1b65cce4d..f23939e08 100644 --- a/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/pagination/PaginationInfo.java +++ b/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/pagination/PaginationInfo.java @@ -27,7 +27,7 @@ package org.eclipse.digitaltwin.basyx.core.pagination; public class PaginationInfo { - public static final PaginationInfo NO_LIMIT = new PaginationInfo(0, null); + public static final PaginationInfo NO_LIMIT = new PaginationInfo(null, null); private Integer limit; private String cursor; diff --git a/basyx.common/basyx.core/src/test/java/org/eclipse/digitaltwin/basyx/core/TestRepositoryUrlHelper.java b/basyx.common/basyx.core/src/test/java/org/eclipse/digitaltwin/basyx/core/TestRepositoryUrlHelper.java new file mode 100644 index 000000000..5a7a473a9 --- /dev/null +++ b/basyx.common/basyx.core/src/test/java/org/eclipse/digitaltwin/basyx/core/TestRepositoryUrlHelper.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (C) 2024 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.core; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class TestRepositoryUrlHelper { + + private static final String BASE_URL = "http://localhost:8081"; + private static final String BASE_URL_WITH_CONTEXT = "http://localhost:8081/context"; + private static final String ADDITIONAL_PATH = "shells"; + + private static final String EXPECTED_URL = BASE_URL + "/" + ADDITIONAL_PATH; + private static final String EXPECTED_URL_WITH_CONTEXT = BASE_URL_WITH_CONTEXT + "/" + ADDITIONAL_PATH; + + @Test + public void testUrlWithTrailingSlashAndPathNameWithNoLeadingSlash() { + assertEquals(EXPECTED_URL, RepositoryUrlHelper.createRepositoryUrl(BASE_URL + "/", ADDITIONAL_PATH)); + } + + @Test + public void testUrlWithoutTrailingSlashAndPathNameWithNoLeadingSlash() { + assertEquals(EXPECTED_URL, RepositoryUrlHelper.createRepositoryUrl(BASE_URL, ADDITIONAL_PATH)); + } + + @Test + public void testUrlWithTrailingSlashAndPathNameWithLeadingSlash() { + assertEquals(EXPECTED_URL, RepositoryUrlHelper.createRepositoryUrl(BASE_URL + "/", "/" + ADDITIONAL_PATH)); + } + + @Test + public void testUrlWithoutTrailingSlashAndPathNameWithLeadingSlash() { + assertEquals(EXPECTED_URL, RepositoryUrlHelper.createRepositoryUrl(BASE_URL, "/" + ADDITIONAL_PATH)); + } + + @Test + public void testUrlWithContextAndTrailingSlashAndPathNameWithLeadingSlash() { + assertEquals(EXPECTED_URL_WITH_CONTEXT, + RepositoryUrlHelper.createRepositoryUrl(BASE_URL_WITH_CONTEXT + "/", "/" + ADDITIONAL_PATH)); + } + + @Test + public void testUrlWithContextAndNoTrailingSlashAndPathNameWithLeadingSlash() { + assertEquals(EXPECTED_URL_WITH_CONTEXT, + RepositoryUrlHelper.createRepositoryUrl(BASE_URL_WITH_CONTEXT, "/" + ADDITIONAL_PATH)); + } + + @Test + public void testUrlWithContextAndTrailingSlashAndPathNameWithNoLeadingSlash() { + assertEquals(EXPECTED_URL_WITH_CONTEXT, + RepositoryUrlHelper.createRepositoryUrl(BASE_URL_WITH_CONTEXT + "/", ADDITIONAL_PATH)); + } + + @Test + public void testUrlWithContextAndNoTrailingSlashAndPathNameWithNoLeadingSlash() { + assertEquals(EXPECTED_URL_WITH_CONTEXT, + RepositoryUrlHelper.createRepositoryUrl(BASE_URL_WITH_CONTEXT, ADDITIONAL_PATH)); + } +} diff --git a/basyx.common/basyx.core/src/test/java/org/eclipse/digitaltwin/basyx/core/pagination/PaginationSupportTest.java b/basyx.common/basyx.core/src/test/java/org/eclipse/digitaltwin/basyx/core/pagination/PaginationSupportTest.java index 8d1464cde..a3a15bc81 100644 --- a/basyx.common/basyx.core/src/test/java/org/eclipse/digitaltwin/basyx/core/pagination/PaginationSupportTest.java +++ b/basyx.common/basyx.core/src/test/java/org/eclipse/digitaltwin/basyx/core/pagination/PaginationSupportTest.java @@ -39,7 +39,7 @@ public void testPaginationNoLimit() { @Test public void testNoLimit() { PaginationSupport support = getPaginationSupport(); - CursorResult> cursorResult = support.getPaged(new PaginationInfo(null, null)); + CursorResult> cursorResult = support.getPaged(PaginationInfo.NO_LIMIT); Assert.assertEquals(null, cursorResult.getCursor()); Assert.assertArrayEquals(new String[] { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" }, cursorResult.getResult().toArray(String[]::new)); } diff --git a/basyx.common/basyx.mqttcore/src/main/java/org/eclipse/digitaltwin/basyx/common/mqttcore/serializer/SubmodelElementSerializer.java b/basyx.common/basyx.mqttcore/src/main/java/org/eclipse/digitaltwin/basyx/common/mqttcore/serializer/SubmodelElementSerializer.java index f2395f0f5..771fd9c43 100644 --- a/basyx.common/basyx.mqttcore/src/main/java/org/eclipse/digitaltwin/basyx/common/mqttcore/serializer/SubmodelElementSerializer.java +++ b/basyx.common/basyx.mqttcore/src/main/java/org/eclipse/digitaltwin/basyx/common/mqttcore/serializer/SubmodelElementSerializer.java @@ -25,6 +25,8 @@ package org.eclipse.digitaltwin.basyx.common.mqttcore.serializer; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException; @@ -72,6 +74,34 @@ public static String serializeSubmodelElement(SubmodelElement submodelElement) { throw new RuntimeException(e); } } + + /** + * Serializer to create a JSON String for the given submodel elements. + * + * @param submodelElement + * @return serialized list of submodelElements as JSON String + */ + public static String serializeSubmodelElements(List submodelElements) { + try { + List updatedSubmodelElements = new ArrayList<>(); + + for(int i = 0; i < submodelElements.size(); i++) { + SubmodelElement elem = submodelElements.get(i); + SubmodelElement localElement; + if (shouldSendEmptyValueEvent(elem)) { + localElement = getSubmodelElementWithoutValue(elem); + } else { + localElement = elem; + } + + updatedSubmodelElements.add(localElement); + } + + return new JsonSerializer().writeList(updatedSubmodelElements); + } catch (SerializationException | DeserializationException e) { + throw new RuntimeException(e); + } + } /** * Generator to create a copy of a submodelElement without its value. diff --git a/basyx.common/pom.xml b/basyx.common/pom.xml index 9ac32b4b3..fe6807eaa 100644 --- a/basyx.common/pom.xml +++ b/basyx.common/pom.xml @@ -20,6 +20,7 @@ basyx.mongocore basyx.authorization basyx.client + basyx.backend.inmemory.core basyx.filerepository-backend basyx.filerepository-backend-inmemory basyx.filerepository-backend-mongodb diff --git a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-backend-inmemory/pom.xml b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-backend-inmemory/pom.xml index 3d9e795dd..12471a489 100644 --- a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-backend-inmemory/pom.xml +++ b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-backend-inmemory/pom.xml @@ -20,6 +20,10 @@ org.eclipse.digitaltwin.basyx basyx.conceptdescriptionrepository-backend + + + org.eclipse.digitaltwin.basyx + basyx.backend.inmemory.core org.eclipse.digitaltwin.basyx diff --git a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/ConceptDescriptionInMemoryBackend.java b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/ConceptDescriptionInMemoryBackend.java deleted file mode 100644 index 38d8afa27..000000000 --- a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/ConceptDescriptionInMemoryBackend.java +++ /dev/null @@ -1,116 +0,0 @@ -/******************************************************************************* - * Copyright (C) 2024 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.conceptdescriptionrepository; - -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import org.eclipse.digitaltwin.aas4j.v3.model.ConceptDescription; -import org.springframework.data.repository.CrudRepository; -import org.springframework.lang.NonNull; - -/** - * InMemory implementation for the {@link ConceptDescription} backend - * - * @author danish, mateusmolina - * - */ -public class ConceptDescriptionInMemoryBackend implements CrudRepository { - - private final ConcurrentMap inMemoryStore = new ConcurrentHashMap<>(); - - @Override - public @NonNull S save(@NonNull S entity) { - inMemoryStore.put(entity.getId(), entity); - - return entity; - } - - @Override - public @NonNull Iterable saveAll(@NonNull Iterable entities) { - entities.forEach(this::save); - - return entities; - } - - @Override - public @NonNull Optional findById(@NonNull String id) { - return Optional.ofNullable(inMemoryStore.get(id)); - } - - @Override - public boolean existsById(@NonNull String id) { - return inMemoryStore.containsKey(id); - } - - @Override - public @NonNull Iterable findAll() { - return inMemoryStore.values(); - } - - @Override - public @NonNull Iterable findAllById(@NonNull Iterable ids) { - return StreamSupport.stream(ids.spliterator(), false).map(inMemoryStore::get).filter(Objects::nonNull).collect(Collectors.toList()); - } - - @Override - public long count() { - return inMemoryStore.size(); - } - - @Override - public void deleteById(@NonNull String id) { - inMemoryStore.remove(id); - } - - @Override - public void delete(@NonNull ConceptDescription entity) { - inMemoryStore.remove(entity.getId()); - } - - @Override - public void deleteAllById(@NonNull Iterable ids) { - for (String id : ids) - inMemoryStore.remove(id); - } - - @Override - public void deleteAll(@NonNull Iterable entities) { - for (ConceptDescription entity : entities) - inMemoryStore.remove(entity.getId()); - } - - @Override - public void deleteAll() { - inMemoryStore.clear(); - } - -} - diff --git a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/ConceptDescriptionInMemoryBackendProvider.java b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/ConceptDescriptionInMemoryBackendProvider.java index b2dae7e65..e5775d9e7 100644 --- a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/ConceptDescriptionInMemoryBackendProvider.java +++ b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/ConceptDescriptionInMemoryBackendProvider.java @@ -27,6 +27,7 @@ import org.eclipse.digitaltwin.aas4j.v3.model.ConceptDescription; import org.eclipse.digitaltwin.basyx.aasrepository.backend.ConceptDescriptionBackendProvider; +import org.eclipse.digitaltwin.basyx.common.backend.inmemory.core.InMemoryCrudRepository; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; @@ -42,7 +43,7 @@ public class ConceptDescriptionInMemoryBackendProvider implements ConceptDescrip @Override public CrudRepository getCrudRepository() { - return new ConceptDescriptionInMemoryBackend(); + return new InMemoryCrudRepository(ConceptDescription::getId); } } diff --git a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/core/ConceptDescriptionRepositorySuite.java b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/core/ConceptDescriptionRepositorySuite.java index 1ebf9fe72..3fda24520 100644 --- a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/core/ConceptDescriptionRepositorySuite.java +++ b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/core/ConceptDescriptionRepositorySuite.java @@ -61,8 +61,6 @@ public abstract class ConceptDescriptionRepositorySuite { protected abstract ConceptDescriptionRepository getConceptDescriptionRepository(Collection conceptDescriptions); - private final PaginationInfo noLimitPaginationInfo = new PaginationInfo(0, ""); - private static final String EMPTY_ID = " "; private static final String NULL_ID = null; @@ -71,7 +69,7 @@ public void getAllConceptDescriptionsPreconfigured() { Collection expectedConceptDescriptions = DummyConceptDescriptionFactory.getConceptDescriptions(); ConceptDescriptionRepository repo = getConceptDescriptionRepository(expectedConceptDescriptions); - Collection actualConceptDescriptions = repo.getAllConceptDescriptions(noLimitPaginationInfo) + Collection actualConceptDescriptions = repo.getAllConceptDescriptions(PaginationInfo.NO_LIMIT) .getResult(); assertEquals(4, actualConceptDescriptions.size()); @@ -84,7 +82,7 @@ public void getAllConceptDescriptionsWithIdShortPreconfigured() { Collection expectedDescriptions = Arrays.asList(DummyConceptDescriptionFactory.createBasicConceptDescription()); ConceptDescriptionRepository repo = getConceptDescriptionRepository(allConceptDescriptions); - Collection actualConceptDescriptions = repo.getAllConceptDescriptionsByIdShort("BasicConceptDescription", noLimitPaginationInfo) + Collection actualConceptDescriptions = repo.getAllConceptDescriptionsByIdShort("BasicConceptDescription", PaginationInfo.NO_LIMIT) .getResult(); assertEquals(1, actualConceptDescriptions.size()); @@ -103,7 +101,7 @@ public void getAllConceptDescriptionsWithIsCaseOfPreconfigured() { Collection expectedDescriptions = Arrays.asList(DummyConceptDescriptionFactory.createConceptDescription(), DummyConceptDescriptionFactory.createBasicConceptDescriptionWithDataSpecification()); ConceptDescriptionRepository repo = getConceptDescriptionRepository(allConceptDescriptions); - Collection actualConceptDescriptions = repo.getAllConceptDescriptionsByIsCaseOf(reference, noLimitPaginationInfo) + Collection actualConceptDescriptions = repo.getAllConceptDescriptionsByIsCaseOf(reference, PaginationInfo.NO_LIMIT) .getResult(); assertEquals(2, actualConceptDescriptions.size()); @@ -122,7 +120,7 @@ public void getAllConceptDescriptionsWithDataSpecPreconfigured() { Collection expectedDescriptions = Arrays.asList(DummyConceptDescriptionFactory.createBasicConceptDescriptionWithDataSpecification()); ConceptDescriptionRepository repo = getConceptDescriptionRepository(allConceptDescriptions); - Collection actualConceptDescriptions = repo.getAllConceptDescriptionsByDataSpecificationReference(reference, noLimitPaginationInfo) + Collection actualConceptDescriptions = repo.getAllConceptDescriptionsByDataSpecificationReference(reference, PaginationInfo.NO_LIMIT) .getResult(); assertEquals(1, actualConceptDescriptions.size()); @@ -132,7 +130,7 @@ public void getAllConceptDescriptionsWithDataSpecPreconfigured() { @Test public void getAllConceptDescriptionsEmpty() { ConceptDescriptionRepository repo = getConceptDescriptionRepository(); - Collection conceptDescriptions = repo.getAllConceptDescriptions(noLimitPaginationInfo) + Collection conceptDescriptions = repo.getAllConceptDescriptions(PaginationInfo.NO_LIMIT) .getResult(); assertIsEmpty(conceptDescriptions); diff --git a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/http/TestConceptDescriptionRepositoryHTTP.java b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/http/TestConceptDescriptionRepositoryHTTP.java index 3517c3349..c7289d7b9 100644 --- a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/http/TestConceptDescriptionRepositoryHTTP.java +++ b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/conceptdescriptionrepository/http/TestConceptDescriptionRepositoryHTTP.java @@ -45,7 +45,6 @@ * */ public class TestConceptDescriptionRepositoryHTTP extends ConceptDescriptionRepositoryHTTPSuite { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); private static ConfigurableApplicationContext appContext; @BeforeClass @@ -61,7 +60,7 @@ public void resetRepository() { } private void resetRepoToDefaultConceptDescriptions(ConceptDescriptionRepository repo, Collection conceptDescriptions) { - repo.getAllConceptDescriptions(NO_LIMIT_PAGINATION_INFO) + repo.getAllConceptDescriptions(PaginationInfo.NO_LIMIT) .getResult() .stream() .map(s -> s.getId()) diff --git a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository.component/Dockerfile b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository.component/Dockerfile index 850188a4c..a628311c2 100644 --- a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository.component/Dockerfile +++ b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository.component/Dockerfile @@ -1,4 +1,6 @@ -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY USER nobody WORKDIR /application ARG JAR_FILE=target/*-exec.jar diff --git a/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/factory/SubmodelDescriptorFactory.java b/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/factory/SubmodelDescriptorFactory.java index 7ba46b67d..a7376be7c 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/factory/SubmodelDescriptorFactory.java +++ b/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/factory/SubmodelDescriptorFactory.java @@ -36,6 +36,7 @@ import org.eclipse.digitaltwin.aas4j.v3.model.LangStringTextType; import org.eclipse.digitaltwin.aas4j.v3.model.Reference; import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.basyx.core.RepositoryUrlHelper; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; import org.eclipse.digitaltwin.basyx.submodelregistry.client.mapper.AttributeMapper; import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.Endpoint; @@ -52,15 +53,12 @@ public class SubmodelDescriptorFactory { private static final String SUBMODEL_INTERFACE = "SUBMODEL-3.0"; private static final String SUBMODEL_REPOSITORY_PATH = "submodels"; - private Submodel submodel; - private List submodelRepositoryURLs; + private final List submodelRepositoryURLs; + private static AttributeMapper attributeMapper; - private AttributeMapper attributeMapper; - - public SubmodelDescriptorFactory(Submodel submodel, List submodelRepositoryBaseURLs, AttributeMapper attributeMapper) { - this.submodel = submodel; + public SubmodelDescriptorFactory(List submodelRepositoryBaseURLs, AttributeMapper attributeMapper) { this.submodelRepositoryURLs = createSubmodelRepositoryUrls(submodelRepositoryBaseURLs); - this.attributeMapper = attributeMapper; + SubmodelDescriptorFactory.attributeMapper = attributeMapper; } /** @@ -68,7 +66,7 @@ public SubmodelDescriptorFactory(Submodel submodel, List submodelReposit * * @return the created {@link SubmodelDescriptor} */ - public SubmodelDescriptor create() { + public SubmodelDescriptor create(Submodel submodel) { SubmodelDescriptor descriptor = new SubmodelDescriptor(); @@ -76,7 +74,7 @@ public SubmodelDescriptor create() { setIdShort(submodel.getIdShort(), descriptor); - setEndpointItem(submodel.getId(), descriptor); + setEndpointItem(submodel.getId(), descriptor, submodelRepositoryURLs); setDescription(submodel.getDescription(), descriptor); @@ -93,12 +91,7 @@ public SubmodelDescriptor create() { return descriptor; } - public SubmodelDescriptor create(Submodel submodel) { - this.submodel = submodel; - return create(); - } - - private void setDescription(List descriptions, SubmodelDescriptor descriptor) { + private static void setDescription(List descriptions, SubmodelDescriptor descriptor) { if (descriptions == null || descriptions.isEmpty()) return; @@ -106,7 +99,7 @@ private void setDescription(List descriptions, SubmodelDescr descriptor.setDescription(attributeMapper.mapDescription(descriptions)); } - private void setDisplayName(List displayNames, SubmodelDescriptor descriptor) { + private static void setDisplayName(List displayNames, SubmodelDescriptor descriptor) { if (displayNames == null || displayNames.isEmpty()) return; @@ -114,7 +107,7 @@ private void setDisplayName(List displayNames, SubmodelDescr descriptor.setDisplayName(attributeMapper.mapDisplayName(displayNames)); } - private void setExtensions(List extensions, SubmodelDescriptor descriptor) { + private static void setExtensions(List extensions, SubmodelDescriptor descriptor) { if (extensions == null || extensions.isEmpty()) return; @@ -122,7 +115,7 @@ private void setExtensions(List extensions, SubmodelDescriptor descri descriptor.setExtensions(attributeMapper.mapExtensions(extensions)); } - private void setAdministration(AdministrativeInformation administration, SubmodelDescriptor descriptor) { + private static void setAdministration(AdministrativeInformation administration, SubmodelDescriptor descriptor) { if (administration == null) return; @@ -130,7 +123,7 @@ private void setAdministration(AdministrativeInformation administration, Submode descriptor.setAdministration(attributeMapper.mapAdministration(administration)); } - private void setSemanticId(Reference reference, SubmodelDescriptor descriptor) { + private static void setSemanticId(Reference reference, SubmodelDescriptor descriptor) { if (reference == null) return; @@ -138,7 +131,7 @@ private void setSemanticId(Reference reference, SubmodelDescriptor descriptor) { descriptor.setSemanticId(attributeMapper.mapSemanticId(reference)); } - private void setSupplementalSemanticId(List supplementalSemanticIds, SubmodelDescriptor descriptor) { + private static void setSupplementalSemanticId(List supplementalSemanticIds, SubmodelDescriptor descriptor) { if (supplementalSemanticIds == null || supplementalSemanticIds.isEmpty()) return; @@ -146,7 +139,7 @@ private void setSupplementalSemanticId(List supplementalSemanticIds, descriptor.setSupplementalSemanticId(attributeMapper.mapSupplementalSemanticId(supplementalSemanticIds)); } - private void setEndpointItem(String shellId, SubmodelDescriptor descriptor) { + private static void setEndpointItem(String shellId, SubmodelDescriptor descriptor, List submodelRepositoryURLs) { for (String eachUrl : submodelRepositoryURLs) { Endpoint endpoint = new Endpoint(); @@ -158,7 +151,7 @@ private void setEndpointItem(String shellId, SubmodelDescriptor descriptor) { } } - private ProtocolInformation createProtocolInformation(String shellId, String url) { + private static ProtocolInformation createProtocolInformation(String shellId, String url) { String href = String.format("%s/%s", url, Base64UrlEncodedIdentifier.encodeIdentifier(shellId)); ProtocolInformation protocolInformation = new ProtocolInformation(); @@ -168,15 +161,15 @@ private ProtocolInformation createProtocolInformation(String shellId, String url return protocolInformation; } - private void setIdShort(String idShort, SubmodelDescriptor descriptor) { + private static void setIdShort(String idShort, SubmodelDescriptor descriptor) { descriptor.setIdShort(idShort); } - private void setId(String shellId, SubmodelDescriptor descriptor) { + private static void setId(String shellId, SubmodelDescriptor descriptor) { descriptor.setId(shellId); } - private String getProtocol(String endpoint) { + private static String getProtocol(String endpoint) { try { return new URL(endpoint).getProtocol(); } catch (MalformedURLException e) { @@ -184,21 +177,11 @@ private String getProtocol(String endpoint) { } } - private List createSubmodelRepositoryUrls(List submodelRepositoryBaseURLs) { + private static List createSubmodelRepositoryUrls(List submodelRepositoryBaseURLs) { List toReturn = new ArrayList<>(submodelRepositoryBaseURLs.size()); for (String eachUrl : submodelRepositoryBaseURLs) { - toReturn.add(createSubmodelRepositoryUrl(eachUrl)); + toReturn.add(RepositoryUrlHelper.createRepositoryUrl(eachUrl, SUBMODEL_REPOSITORY_PATH)); } return toReturn; } - - private String createSubmodelRepositoryUrl(String submodelRepositoryBaseURL) { - - try { - return new URL(new URL(submodelRepositoryBaseURL), SUBMODEL_REPOSITORY_PATH).toString(); - } catch (MalformedURLException e) { - throw new RuntimeException("The Submodel Repository Base url is malformed.\n" + e.getMessage()); - } - } - } diff --git a/basyx.submodelregistry/basyx.submodelregistry-client-native/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/mapper/DummySubmodelDescriptorFactory.java b/basyx.submodelregistry/basyx.submodelregistry-client-native/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/mapper/DummySubmodelDescriptorFactory.java index d6755f00e..91bad3903 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-client-native/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/mapper/DummySubmodelDescriptorFactory.java +++ b/basyx.submodelregistry/basyx.submodelregistry-client-native/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/mapper/DummySubmodelDescriptorFactory.java @@ -28,8 +28,12 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import org.eclipse.digitaltwin.basyx.core.RepositoryUrlHelper; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.AdministrativeInformation; import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.Endpoint; import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.Key; import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.KeyTypes; @@ -47,37 +51,50 @@ public class DummySubmodelDescriptorFactory { private static final String SUBMODEL_REPOSITORY_PATH = "/submodels"; - public static SubmodelDescriptor createDummyDescriptor(String smId, String idShort, String smRepoBaseUrl, Reference semanticId) { - return createDummyDescriptor(smId, idShort, new String[] {smRepoBaseUrl}, semanticId); - } - - public static SubmodelDescriptor createDummyDescriptor(String smId, String idShort, String[] smRepoBaseUrls, Reference semanticId) { + public static SubmodelDescriptor createDummyDescriptor(String smId, String idShort, Reference semanticId, AdministrativeInformation administrativeInformation, List endpoints) { SubmodelDescriptor descriptor = new SubmodelDescriptor(); descriptor.setId(smId); descriptor.setIdShort(idShort); descriptor.setSemanticId(semanticId); + descriptor.setEndpoints(endpoints); + descriptor.administration(administrativeInformation); + + return descriptor; + } + + public static SubmodelDescriptor createDummyDescriptor(String smId, String idShort, Reference semanticId, AdministrativeInformation administrativeInformation, String... smRepoBaseUrls) { + LinkedList endpoints = new LinkedList<>(); for (String eachUrl : smRepoBaseUrls) { - descriptor.addEndpointsItem(createEndpointItem(smId, eachUrl)); + endpoints.add(createEndpoint(smId, eachUrl, "SUBMODEL-3.0")); } - return descriptor; + + return createDummyDescriptor(smId, idShort, semanticId, administrativeInformation, endpoints); } public static Reference createSemanticId() { return new Reference().keys(Arrays.asList(new Key().type(KeyTypes.GLOBALREFERENCE).value("0173-1#01-AFZ615#016"))).type(ReferenceTypes.EXTERNALREFERENCE); } - private static Endpoint createEndpointItem(String smId, String smRepoBaseUrl) { + public static Endpoint createEndpoint(String endpointUrl, String endpointInterface) { Endpoint endpoint = new Endpoint(); - endpoint.setInterface("SUBMODEL-3.0"); - endpoint.setProtocolInformation(createProtocolInformation(smId, smRepoBaseUrl)); + endpoint.setInterface(endpointInterface); + endpoint.setProtocolInformation(createProtocolInformation(endpointUrl)); return endpoint; } - private static ProtocolInformation createProtocolInformation(String smId, String smRepoBaseUrl) { + public static Endpoint createEndpoint(String smId, String smRepoBaseUrl, String endpointInterface) { String href = createHref(smId, smRepoBaseUrl); + return createEndpoint(href, endpointInterface); + } + + public static AdministrativeInformation buildAdministrationInformation(String version, String revision, String templateId) { + return new AdministrativeInformation().version(version).revision(revision).templateId(templateId); + } + + private static ProtocolInformation createProtocolInformation(String href) { ProtocolInformation protocolInformation = new ProtocolInformation(); protocolInformation.setHref(href); protocolInformation.endpointProtocol(getProtocol(href)); @@ -86,7 +103,7 @@ private static ProtocolInformation createProtocolInformation(String smId, String } private static String createHref(String smId, String smRepoBaseUrl) { - return String.format("%s/%s", createSubmodelRepositoryUrl(smRepoBaseUrl), Base64UrlEncodedIdentifier.encodeIdentifier(smId)); + return String.format("%s/%s", RepositoryUrlHelper.createRepositoryUrl(smRepoBaseUrl, SUBMODEL_REPOSITORY_PATH), Base64UrlEncodedIdentifier.encodeIdentifier(smId)); } private static String getProtocol(String endpoint) { @@ -96,13 +113,4 @@ private static String getProtocol(String endpoint) { throw new RuntimeException(); } } - - private static String createSubmodelRepositoryUrl(String smRepositoryBaseURL) { - - try { - return new URL(new URL(smRepositoryBaseURL), SUBMODEL_REPOSITORY_PATH).toString(); - } catch (MalformedURLException e) { - throw new RuntimeException("The Submodel Repository Base url is malformed.\n " + e.getMessage()); - } - } } diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/regression/feature/authorization/TestAuthorizedSubmodelRegistry.java b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/regression/feature/authorization/TestAuthorizedSubmodelRegistry.java index 37f359a32..af8568883 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/regression/feature/authorization/TestAuthorizedSubmodelRegistry.java +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/regression/feature/authorization/TestAuthorizedSubmodelRegistry.java @@ -66,7 +66,6 @@ */ public class TestAuthorizedSubmodelRegistry { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); private static final String SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON = "authorization/SubmodelDescriptorSimple_2.json"; private static final String SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON = "authorization/SubmodelDescriptorSimple_1.json"; private static final String SPECIFIC_SUBMODEL_ID_2 = "specificSubmodelId-2"; @@ -107,7 +106,7 @@ public void initializeRepositories() throws IOException { public void reset() throws IOException { configureSecurityContext(); - Collection descriptors = storage.getAllSubmodelDescriptors(NO_LIMIT_PAGINATION_INFO).getResult(); + Collection descriptors = storage.getAllSubmodelDescriptors(PaginationInfo.NO_LIMIT).getResult(); descriptors.forEach(descriptor -> storage.removeSubmodelDescriptor(descriptor.getId())); diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/BaseInterfaceTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/BaseInterfaceTest.java index 4bb7ce5d3..e9d999838 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/BaseInterfaceTest.java +++ b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/BaseInterfaceTest.java @@ -107,7 +107,7 @@ protected void verifyNoEventSent() { } protected List getAllSubmodels() { - return storage.getAllSubmodelDescriptors(new PaginationInfo(null, null)).getResult(); + return storage.getAllSubmodelDescriptors(PaginationInfo.NO_LIMIT).getResult(); } protected CursorResult> getAllSubmodelsWithPagination(int limit, String cursor) { diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-kafka-events/pom.xml b/basyx.submodelregistry/basyx.submodelregistry-service-kafka-events/pom.xml index f4662b89a..f3164a9dd 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-kafka-events/pom.xml +++ b/basyx.submodelregistry/basyx.submodelregistry-service-kafka-events/pom.xml @@ -17,7 +17,7 @@ jar - 2023.0.3 + 2024.0.0 diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/pom.xml b/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/pom.xml index 70f6c5d62..80b6ebe1e 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/pom.xml +++ b/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/pom.xml @@ -17,7 +17,7 @@ jar - 2023.0.3 + 2024.0.0 ${project.basedir}/${openapi.folder.name} ${openapi.folder}/${openapi.mongodb.file.name} ${openapi.folder}/temporary-extensions-result-file.yaml diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/main/docker/Dockerfile b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/main/docker/Dockerfile index 324d4fc49..f994cd2ad 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/main/docker/Dockerfile +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/main/docker/Dockerfile @@ -1,8 +1,13 @@ -FROM amazoncorretto:17 as builder -COPY maven/${project.build.finalName}.jar ./ -RUN java -Djarmode=layertools -jar ${project.build.finalName}.jar extract +FROM eclipse-temurin:17 as builder +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG FINAL_NAME=${project.build.finalName} +COPY maven/${FINAL_NAME}.jar ./ +RUN java -Djarmode=layertools -jar ${FINAL_NAME}.jar extract -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY RUN mkdir /workspace WORKDIR /workspace COPY --from=builder dependencies/ ./ diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/main/docker/Dockerfile b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/main/docker/Dockerfile index b064339b7..1a5c74674 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/main/docker/Dockerfile +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/main/docker/Dockerfile @@ -1,8 +1,13 @@ -FROM amazoncorretto:17 as builder -COPY maven/${project.build.finalName}.jar ./ -RUN java -Djarmode=layertools -jar ${project.build.finalName}.jar extract +FROM eclipse-temurin:17 as builder +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG FINAL_NAME=${project.build.finalName} +COPY maven/${FINAL_NAME}.jar ./ +RUN java -Djarmode=layertools -jar ${FINAL_NAME}.jar extract -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY RUN mkdir /workspace WORKDIR /workspace COPY --from=builder dependencies/ ./ diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/main/docker/Dockerfile b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/main/docker/Dockerfile index cba9fc132..ae897c5af 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/main/docker/Dockerfile +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/main/docker/Dockerfile @@ -1,8 +1,13 @@ -FROM amazoncorretto:17 as builder -COPY maven/${project.build.finalName}.jar ./ -RUN java -Djarmode=layertools -jar ${project.build.finalName}.jar extract +FROM eclipse-temurin:17 as builder +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG FINAL_NAME=${project.build.finalName} +COPY maven/${FINAL_NAME}.jar ./ +RUN java -Djarmode=layertools -jar ${FINAL_NAME}.jar extract -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY RUN mkdir /workspace WORKDIR /workspace COPY --from=builder dependencies/ ./ diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/main/docker/Dockerfile b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/main/docker/Dockerfile index 807bbd7ca..2924f4e1f 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/main/docker/Dockerfile +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/main/docker/Dockerfile @@ -1,8 +1,13 @@ -FROM amazoncorretto:17 as builder -COPY maven/${project.build.finalName}.jar ./ -RUN java -Djarmode=layertools -jar ${project.build.finalName}.jar extract +FROM eclipse-temurin:17 as builder +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG FINAL_NAME=${project.build.finalName} +COPY maven/${FINAL_NAME}.jar ./ +RUN java -Djarmode=layertools -jar ${FINAL_NAME}.jar extract -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY RUN mkdir /workspace WORKDIR /workspace COPY --from=builder dependencies/ ./ diff --git a/basyx.submodelregistry/basyx.submodelregistry-service/pom.xml b/basyx.submodelregistry/basyx.submodelregistry-service/pom.xml index 020c00c5e..83c9b89d0 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service/pom.xml +++ b/basyx.submodelregistry/basyx.submodelregistry-service/pom.xml @@ -16,7 +16,7 @@ jar - 2023.0.3 + 2024.0.0 ${project.basedir}/${openapi.folder.name} ${openapi.folder}/${openapi.name} diff --git a/basyx.submodelregistry/open-api/patch-base-extensions.yaml b/basyx.submodelregistry/open-api/patch-base-extensions.yaml index 9bf76df17..dab515fe5 100644 --- a/basyx.submodelregistry/open-api/patch-base-extensions.yaml +++ b/basyx.submodelregistry/open-api/patch-base-extensions.yaml @@ -97,7 +97,7 @@ value: "^(0|[1-9][0-9]{0,3})$" - op: replace path: /components/schemas/AdministrativeInformation/allOf/1/properties/templateId/pattern - value: "^([\\t\\n\\r \\ud7ff\\ue000-\\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" + value: "^([\\x09\\x0a\\x0d\\x20-\\ud7ff\\ue000-\\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$" ## EmbeddedDataSpecification - op: replace path: /components/schemas/EmbeddedDataSpecification/properties/dataSpecificationContent/$ref diff --git a/basyx.submodelregistry/pom.xml b/basyx.submodelregistry/pom.xml index 9dd36e6d1..aebd14a8e 100644 --- a/basyx.submodelregistry/pom.xml +++ b/basyx.submodelregistry/pom.xml @@ -16,16 +16,17 @@ 1.18.20.0 + 1.18.30 ${java.version} ${java.version} src/generated - 3.0.42 + 3.0.64 3.0.0 open-api Plattform_i40-SubmodelRegistryServiceSpecification-V3.0.1_SSP-001-resolved.yaml Plattform_i40-SubmodelRegistry-and-Discovery.yaml - 33.2.1-jre + 33.4.0-jre 3.6.0 3.0-alpha-2 0.9.14 @@ -71,6 +72,13 @@ org.projectlombok lombok-maven-plugin ${lombok.maven-plugin.version} + + + org.projectlombok + lombok + ${lombok.maven-plugin.lombok.version} + + io.swagger.codegen.v3 @@ -222,6 +230,13 @@ org.projectlombok lombok-maven-plugin + + + org.projectlombok + lombok + ${lombok.maven-plugin.lombok.version} + + generate-sources diff --git a/basyx.submodelrepository/basyx.submodelrepository-backend-inmemory/pom.xml b/basyx.submodelrepository/basyx.submodelrepository-backend-inmemory/pom.xml index 11d97c975..1edfa7be2 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-backend-inmemory/pom.xml +++ b/basyx.submodelrepository/basyx.submodelrepository-backend-inmemory/pom.xml @@ -24,6 +24,10 @@ org.eclipse.digitaltwin.basyx basyx.submodelservice-core + + + org.eclipse.digitaltwin.basyx + basyx.backend.inmemory.core org.eclipse.digitaltwin.basyx diff --git a/basyx.submodelrepository/basyx.submodelrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/SubmodelInMemoryBackendProvider.java b/basyx.submodelrepository/basyx.submodelrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/SubmodelInMemoryBackendProvider.java index 79b69eb2a..bf65b85dc 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/SubmodelInMemoryBackendProvider.java +++ b/basyx.submodelrepository/basyx.submodelrepository-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/SubmodelInMemoryBackendProvider.java @@ -26,6 +26,7 @@ package org.eclipse.digitaltwin.basyx.submodelrepository; import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.basyx.common.backend.inmemory.core.InMemoryCrudRepository; import org.eclipse.digitaltwin.basyx.submodelrepository.backend.SubmodelBackendProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.data.repository.CrudRepository; @@ -43,7 +44,7 @@ public class SubmodelInMemoryBackendProvider implements SubmodelBackendProvider @Override public CrudRepository getCrudRepository() { - return new SubmodelInMemoryBackend(); + return new InMemoryCrudRepository(Submodel::getId); } } diff --git a/basyx.submodelrepository/basyx.submodelrepository-backend-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/TestMongoDBSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-backend-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/TestMongoDBSubmodelRepository.java index 5746eecb0..68f2d4474 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-backend-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/TestMongoDBSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-backend-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/TestMongoDBSubmodelRepository.java @@ -29,12 +29,12 @@ import java.util.Collection; import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; -import org.eclipse.digitaltwin.basyx.InvokableOperation; import org.eclipse.digitaltwin.basyx.common.mongocore.BasyxMongoMappingContext; import org.eclipse.digitaltwin.basyx.common.mongocore.MongoDBUtilities; import org.eclipse.digitaltwin.basyx.core.exceptions.NotInvokableException; import org.eclipse.digitaltwin.basyx.core.filerepository.FileRepository; import org.eclipse.digitaltwin.basyx.core.filerepository.MongoDBFileRepository; +import org.eclipse.digitaltwin.basyx.operation.InvokableOperation; import org.eclipse.digitaltwin.basyx.submodelrepository.backend.SimpleSubmodelRepositoryFactory; import org.eclipse.digitaltwin.basyx.submodelrepository.backend.SubmodelBackendProvider; import org.eclipse.digitaltwin.basyx.submodelrepository.core.SubmodelRepositorySuite; 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 b1f80f289..5f6f91ed8 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 @@ -26,6 +26,7 @@ package org.eclipse.digitaltwin.basyx.submodelrepository.backend; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -34,13 +35,11 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException; -import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException; -import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonDeserializer; -import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer; import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable; +import org.eclipse.digitaltwin.aas4j.v3.model.Reference; import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.basyx.client.internal.ApiException; import org.eclipse.digitaltwin.basyx.core.exceptions.CollidingIdentifierException; import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException; import org.eclipse.digitaltwin.basyx.core.exceptions.ElementNotAFileException; @@ -50,6 +49,8 @@ import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult; import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; import org.eclipse.digitaltwin.basyx.core.pagination.PaginationSupport; +import org.eclipse.digitaltwin.basyx.http.Base64UrlEncoder; +import org.eclipse.digitaltwin.basyx.serialization.SubmodelMetadataUtil; import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService; import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelServiceFactory; @@ -58,6 +59,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.repository.CrudRepository; +import org.springframework.http.HttpStatus; /** * Default Implementation for the {@link SubmodelRepository} based on Spring @@ -69,7 +71,6 @@ public class CrudSubmodelRepository implements SubmodelRepository { private Logger logger = LoggerFactory.getLogger(CrudSubmodelRepository.class); - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); private CrudRepository submodelBackend; private SubmodelServiceFactory submodelServiceFactory; @@ -120,6 +121,27 @@ public CursorResult> getAllSubmodels(PaginationInfo pInfo) { return paginationSupport.getPaged(pInfo); } + @Override + public CursorResult> getAllSubmodels(String semanticId, PaginationInfo pInfo) { + Iterable iterable = submodelBackend.findAll(); + List submodels = StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList()); + + List filteredSubmodels = submodels.stream() + .filter((submodel) -> { + return submodel.getSemanticId() != null && + submodel.getSemanticId().getKeys().stream().filter((key) -> { + return key.getValue().equals(semanticId); + }).findAny().isPresent(); + }) + .collect(Collectors.toList()); + + TreeMap submodelMap = filteredSubmodels.stream().collect(Collectors.toMap(Submodel::getId, submodel -> submodel, (a, b) -> a, TreeMap::new)); + + PaginationSupport paginationSupport = new PaginationSupport<>(submodelMap, Submodel::getId); + + return paginationSupport.getPaged(pInfo); + } + @Override public Submodel getSubmodel(String submodelId) throws ElementDoesNotExistException { return submodelBackend.findById(submodelId).orElseThrow(() -> new ElementDoesNotExistException(submodelId)); @@ -222,7 +244,7 @@ public OperationVariable[] invokeOperation(String submodelId, String idShortPath @Override public SubmodelValueOnly getSubmodelByIdValueOnly(String submodelId) throws ElementDoesNotExistException { - return new SubmodelValueOnly(getSubmodelElements(submodelId, NO_LIMIT_PAGINATION_INFO).getResult()); + return new SubmodelValueOnly(getSubmodelElements(submodelId, PaginationInfo.NO_LIMIT).getResult()); } @Override @@ -230,7 +252,7 @@ public Submodel getSubmodelByIdMetadata(String submodelId) throws ElementDoesNot Submodel submodel = getSubmodel(submodelId); - return getSubmodelDeepCopy(submodel); + return SubmodelMetadataUtil.extractMetadata(submodel); } @Override @@ -278,23 +300,6 @@ private void throwIfMissingId(Collection submodels) { submodels.stream().map(Submodel::getId).forEach(this::throwIfSubmodelIdEmptyOrNull); } - private Submodel getSubmodelDeepCopy(Submodel submodel) { - - try { - String submodelAsJSON = new JsonSerializer().write(submodel); - - Submodel submodelDeepCopy = new JsonDeserializer().read(submodelAsJSON, Submodel.class); - - submodelDeepCopy.setSubmodelElements(null); - - return submodelDeepCopy; - } catch (DeserializationException e) { - throw new RuntimeException("Unable to deserialize the Submodel", e); - } catch (SerializationException e) { - throw new RuntimeException("Unable to serialize the Submodel", e); - } - } - private SubmodelService getSubmodelServiceOrThrow(String submodelId) { Submodel submodel = submodelBackend.findById(submodelId).orElseThrow(() -> new ElementDoesNotExistException(submodelId)); 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 0fc9c551b..376eaf2e7 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 @@ -155,6 +155,21 @@ public CursorResult> getAllSubmodels(PaginationInfo pInfo) { } + @Override + public CursorResult> getAllSubmodels(String semanticId, PaginationInfo pInfo) { + try { + String encodedCursor = pInfo.getCursor() == null ? null : Base64UrlEncoder.encode(pInfo.getCursor()); + String encodedSemanticId = Base64UrlEncoder.encode(semanticId); + return repoApi.getAllSubmodels(encodedSemanticId, null, pInfo.getLimit(), encodedCursor, null, null); + } catch (ApiException e) { + if (e.getCode() == HttpStatus.INTERNAL_SERVER_ERROR.value()) { + return new CursorResult<>("", new ArrayList<>()); + } else { + throw e; + } + } + } + @Override public void updateSubmodelElement(String submodelId, String idShortPath, SubmodelElement submodelElement) throws ElementDoesNotExistException { getConnectedSubmodelService(submodelId).updateSubmodelElement(idShortPath, submodelElement); @@ -202,12 +217,16 @@ public OperationVariable[] invokeOperation(String submodelId, String idShortPath @Override public SubmodelValueOnly getSubmodelByIdValueOnly(String submodelId) throws ElementDoesNotExistException { - return new SubmodelValueOnly(getSubmodelByIdMetadata(submodelId).getSubmodelElements()); + return new SubmodelValueOnly(getSubmodel(submodelId).getSubmodelElements()); } @Override public Submodel getSubmodelByIdMetadata(String submodelId) throws ElementDoesNotExistException { - return repoApi.getSubmodelById(submodelId, null, null); + try { + return repoApi.getSubmodelByIdMetadata(submodelId, null); + } catch (ApiException e) { + throw mapExceptionSubmodelAccess(submodelId, e); + } } @Override diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/internal/SubmodelRepositoryApi.java b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/internal/SubmodelRepositoryApi.java index 5ed4fb1a8..c82cadd29 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/internal/SubmodelRepositoryApi.java +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/internal/SubmodelRepositoryApi.java @@ -32,6 +32,7 @@ import java.net.http.HttpResponse; import java.time.Duration; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.StringJoiner; import java.util.function.Consumer; @@ -238,6 +239,123 @@ private HttpRequest.Builder getSubmodelByIdRequestBuilder(String submodelIdentif return localVarRequestBuilder; } + /** + * Returns the metadata attributes of a specific Submodel + * + * @param submodelIdentifier + * The Submodel’s unique id (UTF8-BASE64-URL-encoded) (required) + * @param level + * Determines the structural depth of the respective resource content + * (optional, default to deep) + * @return SubmodelMetadata + * @throws ApiException + * if fails to make API call + */ + public Submodel getSubmodelByIdMetadata(String submodelIdentifier, String level) throws ApiException { + + ApiResponse localVarResponse = getSubmodelByIdMetadataWithHttpInfo(submodelIdentifier, level); + return localVarResponse.getData(); + } + + /** + * Returns the metadata attributes of a specific Submodel + * + * @param submodelIdentifier + * The Submodel’s unique id (UTF8-BASE64-URL-encoded) (required) + * @param level + * Determines the structural depth of the respective resource content + * (optional, default to deep) + * @return ApiResponse<SubmodelMetadata> + * @throws ApiException + * if fails to make API call + */ + public ApiResponse getSubmodelByIdMetadataWithHttpInfo(String submodelIdentifier, String level) throws ApiException { + String submodelIdentifierAsBytes = ApiClient.base64UrlEncode(submodelIdentifier); + return getSubmodelByIdMetadataWithHttpInfoNoUrlEncoding(submodelIdentifierAsBytes, level); + + } + + /** + * Returns the metadata attributes of a specific Submodel + * + * @param submodelIdentifier + * The Submodel’s unique id (UTF8-BASE64-URL-encoded) (required) + * @param level + * Determines the structural depth of the respective resource content + * (optional, default to deep) + * @return ApiResponse<SubmodelMetadata> + * @throws ApiException + * if fails to make API call + */ + public ApiResponse getSubmodelByIdMetadataWithHttpInfoNoUrlEncoding(String submodelIdentifier, String level) throws ApiException { + HttpRequest.Builder localVarRequestBuilder = getSubmodelByIdMetadataRequestBuilder(submodelIdentifier, level); + try { + HttpResponse localVarResponse = memberVarHttpClient.send(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofInputStream()); + if (memberVarResponseInterceptor != null) { + memberVarResponseInterceptor.accept(localVarResponse); + } + try { + if (localVarResponse.statusCode() / 100 != 2) { + throw getApiException("getSubmodelByIdMetadata", localVarResponse); + } + Submodel deserializedSubmodel = localVarResponse.body() == null ? null : memberVarObjectMapper.readValue(localVarResponse.body(), new TypeReference() { + }); + + if (deserializedSubmodel != null && deserializedSubmodel.getSubmodelElements() != null && deserializedSubmodel.getSubmodelElements().isEmpty()) + deserializedSubmodel.setSubmodelElements(null); + + return new ApiResponse<>(localVarResponse.statusCode(), localVarResponse.headers().map(), deserializedSubmodel); + } finally { + } + } catch (IOException e) { + throw new ApiException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ApiException(e); + } + } + + private HttpRequest.Builder getSubmodelByIdMetadataRequestBuilder(String submodelIdentifier, String level) throws ApiException { + // verify the required parameter 'submodelIdentifier' is set + if (submodelIdentifier == null) { + throw new ApiException(400, "Missing the required parameter 'submodelIdentifier' when calling getSubmodelByIdMetadata"); + } + + HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + + String localVarPath = "/submodels/{submodelIdentifier}/$metadata".replace("{submodelIdentifier}", ApiClient.urlEncode(submodelIdentifier.toString())); + + List localVarQueryParams = new ArrayList<>(); + StringJoiner localVarQueryStringJoiner = new StringJoiner("&"); + String localVarQueryParameterBaseName; + localVarQueryParameterBaseName = "level"; + localVarQueryParams.addAll(ApiClient.parameterToPairs("level", level)); + + if (!localVarQueryParams.isEmpty() || localVarQueryStringJoiner.length() != 0) { + StringJoiner queryJoiner = new StringJoiner("&"); + localVarQueryParams.forEach(p -> queryJoiner.add(p.getName() + '=' + p.getValue())); + if (localVarQueryStringJoiner.length() != 0) { + queryJoiner.add(localVarQueryStringJoiner.toString()); + } + localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath + '?' + queryJoiner.toString())); + } else { + localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); + } + + localVarRequestBuilder.header("Accept", "application/json"); + + addAuthorizationHeaderIfAuthIsEnabled(localVarRequestBuilder); + + localVarRequestBuilder.method("GET", HttpRequest.BodyPublishers.noBody()); + if (memberVarReadTimeout != null) { + localVarRequestBuilder.timeout(memberVarReadTimeout); + } + if (memberVarInterceptor != null) { + memberVarInterceptor.accept(localVarRequestBuilder); + } + return localVarRequestBuilder; + } + /** * Creates a new Submodel * diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/TestConnectedSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/TestConnectedSubmodelRepository.java index 9946f1c8e..8809b86f9 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/TestConnectedSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/TestConnectedSubmodelRepository.java @@ -51,7 +51,6 @@ */ public class TestConnectedSubmodelRepository extends SubmodelRepositorySuite { private static ConfigurableApplicationContext appContext; - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); @BeforeClass public static void startAASRepo() throws Exception { @@ -61,7 +60,7 @@ public static void startAASRepo() throws Exception { @After public void removeSubmodelFromRepo() { SubmodelRepository repo = appContext.getBean(SubmodelRepository.class); - repo.getAllSubmodels(NO_LIMIT_PAGINATION_INFO).getResult().stream().map(s -> s.getId()).forEach(repo::deleteSubmodel); + repo.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().stream().map(s -> s.getId()).forEach(repo::deleteSubmodel); } @AfterClass 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 d34139fa9..0a5df3ec7 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 @@ -38,6 +38,7 @@ import org.eclipse.digitaltwin.basyx.core.exceptions.MissingIdentifierException; import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult; import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelElementValue; import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelValueOnly; @@ -55,7 +56,14 @@ public interface SubmodelRepository { * @return a list of all found Submodels */ public CursorResult> getAllSubmodels(PaginationInfo pInfo); - + + /** + * Retrieves all Submodels from the repository filtered by the Semantic ID + * + * @return a list of all found Submodels with common Semantic ID + */ + public CursorResult> getAllSubmodels(String semanticId, PaginationInfo pInfo); + /** * Retrieves the Submodel with the specific id * diff --git a/basyx.submodelrepository/basyx.submodelrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/core/SubmodelRepositorySuite.java b/basyx.submodelrepository/basyx.submodelrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/core/SubmodelRepositorySuite.java index 77b7a99d3..e094bfdb8 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/core/SubmodelRepositorySuite.java +++ b/basyx.submodelrepository/basyx.submodelrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/core/SubmodelRepositorySuite.java @@ -26,7 +26,6 @@ package org.eclipse.digitaltwin.basyx.submodelrepository.core; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -34,7 +33,6 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Set; import org.eclipse.digitaltwin.aas4j.v3.model.DataTypeDefXsd; import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable; @@ -50,6 +48,7 @@ import org.eclipse.digitaltwin.basyx.core.exceptions.NotInvokableException; import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult; import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.serialization.SubmodelMetadataUtil; import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; import org.eclipse.digitaltwin.basyx.submodelservice.DummySubmodelFactory; import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService; @@ -69,7 +68,6 @@ * */ public abstract class SubmodelRepositorySuite extends SubmodelServiceSuite { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(null, null); private static final String EMPTY_ID = " "; private static final String NULL_ID = null; private static final String ID = "testId"; @@ -87,20 +85,30 @@ public void getAllSubmodelsPreconfigured() { Collection expectedSubmodels = DummySubmodelFactory.getSubmodels(); SubmodelRepository repo = getSubmodelRepository(expectedSubmodels); - Collection submodels = repo.getAllSubmodels(NO_LIMIT_PAGINATION_INFO).getResult(); + Collection submodels = repo.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult(); + + assertSubmodelsAreContained(expectedSubmodels, submodels); + } + + @Test + public void getAllSubmodelsBySemanticIDPreconfigured() { + Collection expectedSubmodels = DummySubmodelFactory.getSubmodelsBySemanticid(DummySubmodelFactory.SUBMODEL_TECHNICAL_DATA_SEMANTIC_ID); + + SubmodelRepository repo = getSubmodelRepository(expectedSubmodels); + Collection submodels = repo.getAllSubmodels(DummySubmodelFactory.SUBMODEL_TECHNICAL_DATA_SEMANTIC_ID, PaginationInfo.NO_LIMIT).getResult(); assertSubmodelsAreContained(expectedSubmodels, submodels); } private void assertSubmodelsAreContained(Collection expectedSubmodels, Collection submodels) { - assertEquals(3, submodels.size()); + assertEquals(expectedSubmodels.size(), submodels.size()); assertTrue(submodels.containsAll(expectedSubmodels)); } @Test public void getAllSubmodelsEmpty() { SubmodelRepository repo = getSubmodelRepository(); - Collection submodels = repo.getAllSubmodels(NO_LIMIT_PAGINATION_INFO).getResult(); + Collection submodels = repo.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult(); assertIsEmpty(submodels); } @@ -214,7 +222,7 @@ public void deleteNonExistingSubmodel() { @Test(expected = ElementDoesNotExistException.class) public void getSubmodelElementsOfNonExistingSubmodel() { SubmodelRepository repo = getSubmodelRepositoryWithDummySubmodels(); - repo.getSubmodelElements("notExisting", NO_LIMIT_PAGINATION_INFO).getResult(); + repo.getSubmodelElements("notExisting", PaginationInfo.NO_LIMIT).getResult(); } @Test(expected = ElementDoesNotExistException.class) @@ -273,7 +281,7 @@ public void getSubmodelByIdMetadata() throws JsonProcessingException { Submodel expectedSubmodel = buildDummySubmodelWithNoSmElement(ID); expectedSubmodel.setSubmodelElements(null); repo.createSubmodel(expectedSubmodel); - + Submodel retrievedSubmodelMetadata = repo.getSubmodelByIdMetadata(ID); retrievedSubmodelMetadata.setSubmodelElements(null); @@ -340,6 +348,44 @@ public void invokeNonOperation() { submodelRepo.invokeOperation(DummySubmodelFactory.SUBMODEL_TECHNICAL_DATA_ID, SubmodelServiceHelper.SUBMODEL_TECHNICAL_DATA_ANNOTATED_RELATIONSHIP_ELEMENT_ID_SHORT, new OperationVariable[0]); } + + @Test + public void getSubmodelByIdValueOnlyExistingSubmodel() { + SubmodelRepository repo = getSubmodelRepositoryWithDummySubmodels(); + + String submodelId = DummySubmodelFactory.SUBMODEL_OPERATIONAL_DATA_ID; + Submodel expectedSubmodel = DummySubmodelFactory.createOperationalDataSubmodel(); + SubmodelValueOnly expectedValueOnly = new SubmodelValueOnly(expectedSubmodel.getSubmodelElements()); + + SubmodelValueOnly valueOnly = repo.getSubmodelByIdValueOnly(submodelId); + + assertEquals(expectedValueOnly.getIdShort(), valueOnly.getIdShort()); + assertEquals(expectedValueOnly.getValuesOnlyMap(), valueOnly.getValuesOnlyMap()); + } + + @Test(expected = ElementDoesNotExistException.class) + public void getSubmodelByIdValueOnlyNonExistingSubmodel() { + SubmodelRepository repo = getSubmodelRepositoryWithDummySubmodels(); + repo.getSubmodelByIdValueOnly("nonExistingSubmodelId"); + } + + @Test + public void getSubmodelByIdMetadataExistingSubmodel() { + SubmodelRepository repo = getSubmodelRepositoryWithDummySubmodels(); + + Submodel expectedMetadata = SubmodelMetadataUtil.extractMetadata(DummySubmodelFactory.createOperationalDataSubmodel()); + String submodelId = expectedMetadata.getId(); + + Submodel metadata = repo.getSubmodelByIdMetadata(submodelId); + + assertEquals(expectedMetadata, metadata); + } + + @Test(expected = ElementDoesNotExistException.class) + public void getSubmodelByIdMetadataNonExistingSubmodel() { + SubmodelRepository repo = getSubmodelRepositoryWithDummySubmodels(); + repo.getSubmodelByIdMetadata("nonExistingSubmodelId"); + } private Submodel buildDummySubmodel(String id) { return new DefaultSubmodel.Builder().id(id).submodelElements(new DefaultProperty.Builder().idShort("prop").value("testValue").valueType(DataTypeDefXsd.STRING).build()).build(); 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 ea17a075a..b952820cc 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 @@ -95,6 +95,15 @@ public CursorResult> getAllSubmodels(PaginationInfo pInfo) { return paginationSupport.getPaged(pInfo); } + + @Override + public CursorResult> getAllSubmodels(String semanticId, PaginationInfo pInfo) { + boolean isAuthorized = permissionResolver.hasPermission(Action.READ, new SubmodelTargetInformation(getIdAsList(ALL_ALLOWED_WILDCARD), getIdAsList(ALL_ALLOWED_WILDCARD))); + + throwExceptionIfInsufficientPermission(isAuthorized); + + return decorated.getAllSubmodels(semanticId, pInfo); + } @Override public Submodel getSubmodel(String submodelId) throws ElementDoesNotExistException { diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/TestAuthorizedSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/TestAuthorizedSubmodelRepository.java index 81226a58b..085eb545a 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/TestAuthorizedSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/TestAuthorizedSubmodelRepository.java @@ -46,6 +46,7 @@ import org.eclipse.digitaltwin.basyx.authorization.DummyCredentialStore; import org.eclipse.digitaltwin.basyx.authorization.jwt.JwtTokenDecoder; import org.eclipse.digitaltwin.basyx.authorization.jwt.PublicKeyUtils; +import org.eclipse.digitaltwin.basyx.core.RepositoryUrlHelper; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; import org.eclipse.digitaltwin.basyx.http.serialization.BaSyxHttpTestUtils; import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; @@ -200,7 +201,7 @@ public void getSubmodelWithCorrectRoleAndSpecificSubmodelElementPermission() thr public void createSubmodelWithCorrectRoleAndPermission() throws IOException { String accessToken = getAccessToken(DummyCredentialStore.BASYX_CREATOR_CREDENTIAL); - CloseableHttpResponse retrievalResponse = createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), accessToken); + CloseableHttpResponse retrievalResponse = createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), accessToken); assertEquals(HttpStatus.CREATED.value(), retrievalResponse.getCode()); deleteElementWithAuthorization(getSpecificSubmodelAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); @@ -210,13 +211,13 @@ public void createSubmodelWithCorrectRoleAndPermission() throws IOException { public void createSubmodelWithInsufficientPermissionRole() throws IOException { String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); - CloseableHttpResponse retrievalResponse = createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), accessToken); + CloseableHttpResponse retrievalResponse = createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), accessToken); assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); } @Test public void createSubmodelWithNoAuthorization() throws IOException { - CloseableHttpResponse retrievalResponse = createElementOnRepositoryWithNoAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON)); + CloseableHttpResponse retrievalResponse = createElementOnRepositoryWithNoAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON)); assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); } @@ -262,7 +263,7 @@ public void updateSubmodelWithNoAuthorization() throws IOException { @Test public void deleteSubmodelWithCorrectRoleAndPermission() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_CREDENTIAL); @@ -276,7 +277,7 @@ public void deleteSubmodelWithCorrectRoleAndPermission() throws IOException { @Test public void deleteSubmodelWithCorrectRoleAndSpecificSubmodelPermission() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); @@ -424,7 +425,7 @@ public void getSubmodelElementValueWithCorrectRoleAndPermission() throws IOExcep DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_CREDENTIAL; String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); - + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelElementValueAccessURL(SPECIFIC_SUBMODEL_ID, SUBMODEL_ELEMENT_IDSHORT_PATH), accessToken); assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); } @@ -627,7 +628,7 @@ public void updateSubmodelElementWithNoAuthorization() throws IOException { @Test public void deleteSubmodelElementWithCorrectRoleAndPermission() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); @@ -641,7 +642,7 @@ public void deleteSubmodelElementWithCorrectRoleAndPermission() throws IOExcepti @Test public void deleteSubmodelElementWithCorrectRoleAndSpecificSMEPermission() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); String accessToken = getAccessToken(DummyCredentialStore.BASYX_SME_UPDATER_THREE_CREDENTIAL); @@ -655,7 +656,7 @@ public void deleteSubmodelElementWithCorrectRoleAndSpecificSMEPermission() throw @Test public void deleteSubmodelElementWithCorrectRoleAndUnauthorizedSpecificSME() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); String accessToken = getAccessToken(DummyCredentialStore.BASYX_SME_UPDATER_THREE_CREDENTIAL); @@ -669,7 +670,7 @@ public void deleteSubmodelElementWithCorrectRoleAndUnauthorizedSpecificSME() thr @Test public void deleteSubmodelElementWithInsufficientPermissionRole() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); String accessToken = getAccessToken(DummyCredentialStore.BASYX_CREATOR_CREDENTIAL); @@ -683,7 +684,7 @@ public void deleteSubmodelElementWithInsufficientPermissionRole() throws IOExcep @Test public void deleteSubmodelElementWithNoAuthorization() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSpecificSubmodelElementAccessURL(SPECIFIC_SUBMODEL_ID_2, SUBMODEL_ELEMENT_IDSHORT_PATH_2)); @@ -851,7 +852,7 @@ public void getSubmodelByMetadataWithNoAuthorization() throws IOException { @Test public void getFileWithCorrectRoleAndPermission() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); java.io.File file = ResourceUtils.getFile("classpath:" + FILE_NAME); @@ -869,7 +870,7 @@ public void getFileWithCorrectRoleAndPermission() throws IOException { @Test public void getFileWithAuthorizedSpecificSubmodelAndSpecificSME() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); java.io.File file = ResourceUtils.getFile("classpath:" + FILE_NAME); @@ -887,7 +888,7 @@ public void getFileWithAuthorizedSpecificSubmodelAndSpecificSME() throws IOExcep @Test public void getFileWithInsufficientPermissionRole() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); java.io.File file = ResourceUtils.getFile("classpath:" + FILE_NAME); @@ -905,7 +906,7 @@ public void getFileWithInsufficientPermissionRole() throws IOException { @Test public void getFileWithUnauthorizedSpecificSME() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); DummyCredential dummyCredential = DummyCredentialStore.BASYX_FILE_SME_READER_CREDENTIAL; @@ -919,7 +920,7 @@ public void getFileWithUnauthorizedSpecificSME() throws IOException { @Test public void getFileWithNoAuthorization() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); CloseableHttpResponse retrievalResponse = getElementWithNoAuthorization(getSMEFileDownloadURL(SPECIFIC_SUBMODEL_ID_2, FILE_SUBMODEL_ELEMENT_IDSHORT_PATH)); assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); @@ -929,7 +930,7 @@ public void getFileWithNoAuthorization() throws IOException { @Test public void setFileWithCorrectRoleAndPermission() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); java.io.File file = ResourceUtils.getFile("classpath:" + FILE_NAME); @@ -945,7 +946,7 @@ public void setFileWithCorrectRoleAndPermission() throws IOException { @Test public void setFileWithAuthorizedSpecificSubmodelAndSpecificSME() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); java.io.File file = ResourceUtils.getFile("classpath:" + FILE_NAME); @@ -961,7 +962,7 @@ public void setFileWithAuthorizedSpecificSubmodelAndSpecificSME() throws IOExcep @Test public void setFileWithInsufficientPermissionRole() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); java.io.File file = ResourceUtils.getFile("classpath:" + FILE_NAME); @@ -977,7 +978,7 @@ public void setFileWithInsufficientPermissionRole() throws IOException { @Test public void setFileWithUnauthorizedSpecificSME() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); java.io.File file = ResourceUtils.getFile("classpath:" + FILE_NAME); @@ -993,7 +994,7 @@ public void setFileWithUnauthorizedSpecificSME() throws IOException { @Test public void setFileWithNoAuthorization() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); java.io.File file = ResourceUtils.getFile("classpath:" + FILE_NAME); @@ -1005,7 +1006,7 @@ public void setFileWithNoAuthorization() throws IOException { @Test public void deleteFileWithCorrectRoleAndPermission() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); java.io.File file = ResourceUtils.getFile("classpath:" + FILE_NAME); @@ -1023,7 +1024,7 @@ public void deleteFileWithCorrectRoleAndPermission() throws IOException { @Test public void deleteFileWithAuthorizedSpecificSubmodelAndSpecificSME() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); java.io.File file = ResourceUtils.getFile("classpath:" + FILE_NAME); @@ -1041,7 +1042,7 @@ public void deleteFileWithAuthorizedSpecificSubmodelAndSpecificSME() throws IOEx @Test public void deleteFileWithInsufficientPermissionRole() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); java.io.File file = ResourceUtils.getFile("classpath:" + FILE_NAME); @@ -1059,7 +1060,7 @@ public void deleteFileWithInsufficientPermissionRole() throws IOException { @Test public void deleteFileWithUnauthorizedSpecificSME() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); java.io.File file = ResourceUtils.getFile("classpath:" + FILE_NAME); @@ -1077,7 +1078,7 @@ public void deleteFileWithUnauthorizedSpecificSME() throws IOException { @Test public void deleteFileWithNoAuthorization() throws IOException { - createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + createElementOnRepositoryWithAuthorization(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), getStringFromFile(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSMEFileDownloadURL(SPECIFIC_SUBMODEL_ID_2, FILE_SUBMODEL_ELEMENT_IDSHORT_PATH)); assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); @@ -1185,7 +1186,7 @@ private String getAccessToken(DummyCredential dummyCredential) { } private CloseableHttpResponse getAllSubmodelsWithAuthorization(String accessToken) throws IOException { - return BaSyxHttpTestUtils.executeAuthorizedGetOnURL(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), accessToken); + return BaSyxHttpTestUtils.executeAuthorizedGetOnURL(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH), accessToken); } private CloseableHttpResponse getAllSubmodelElementsWithAuthorization(String submodelId, String accessToken) throws IOException { @@ -1193,7 +1194,7 @@ private CloseableHttpResponse getAllSubmodelElementsWithAuthorization(String sub } private CloseableHttpResponse getAllSubmodelsNoAuthorization() throws IOException { - return BaSyxHttpTestUtils.executeGetOnURL(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl)); + return BaSyxHttpTestUtils.executeGetOnURL(RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH)); } private CloseableHttpResponse getSpecificSubmodelElementNoAuthorization(String submodelId, String submodelElementIdShortPath) throws IOException { @@ -1217,7 +1218,7 @@ private CloseableHttpResponse getElementWithNoAuthorization(String url) throws I } private String getSpecificSubmodelAccessURL(String submodelId) { - return createSubmodelRepositoryUrl(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl)) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId); + return RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId); } private String getSpecificSubmodelValueOnlyAccessURL(String submodelId) { @@ -1229,7 +1230,7 @@ private String getSpecificSubmodelMetadataAccessURL(String submodelId) { } private String getSpecificSubmodelElementAccessURL(String submodelId, String submodelElementIdShortPath) { - return createSubmodelRepositoryUrl(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl)) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId) + "/submodel-elements/" + submodelElementIdShortPath; + return RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId) + "/submodel-elements/" + submodelElementIdShortPath; } private String getSpecificSubmodelElementValueAccessURL(String submodelId, String submodelElementIdShortPath) { @@ -1257,7 +1258,7 @@ private CloseableHttpResponse requestOperationInvocationNoAuthorization(String u } protected String getAllSubmodelElementsAccessURL(String submodelId) { - return createSubmodelRepositoryUrl(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl)) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId) + "/submodel-elements"; + return RepositoryUrlHelper.createRepositoryUrl(submodelRepositoryBaseUrl, SUBMODEL_REPOSITORY_PATH) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId) + "/submodel-elements"; } private static CloseableHttpResponse createElementOnRepositoryWithAuthorization(String url, String submodelJsonContent, String accessToken) throws IOException { @@ -1302,15 +1303,6 @@ private static String getAdminAccessToken() { return tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); } - private static String createSubmodelRepositoryUrl(String submodelRepositoryBaseURL) { - - try { - return new URL(new URL(submodelRepositoryBaseURL), SUBMODEL_REPOSITORY_PATH).toString(); - } catch (MalformedURLException e) { - throw new RuntimeException("The Submodel Repository Base url is malformed. " + e.getMessage()); - } - } - private String getJSONValueAsString(String fileName) throws FileNotFoundException, IOException { return BaSyxHttpTestUtils.readJSONStringFromClasspath(fileName); } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/Readme.md b/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/Readme.md index 628ebfdf5..fd810ae9c 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/Readme.md +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/Readme.md @@ -9,5 +9,8 @@ This feature provides hierarchical MQTT eventing for a multitude of events: | SubmodelElement Created | sm-repository/$repoId/submodels/$submodelIdBase64URLEncoded/submodelElements/$idShortPath/created | Created SubmodelElement JSON | | SubmodelElement Updated | sm-repository/$repoId/submodels/$submodelIdBase64URLEncoded/submodelElements/$idShortPath/updated | Updated SubmodelElement JSON | | SubmodelElement Deleted | sm-repository/$repoId/submodels/$submodelIdBase64URLEncoded/submodelElements/$idShortPath/deleted | Deleted SubmodelElement JSON | +| SubmodelElements Patched | sm-repository/$repoId/submodels/$submodelIdBase64URLEncoded/submodelElements/patched | Patched SubmodelElements JSON | +| FileValue Updated | sm-repository/$repoId/submodels/$submodelIdBase64URLEncoded/submodelElements/$idShortPath/attachment/updated | Updated SubmodelElement JSON | +| FileValue Deleted | sm-repository/$repoId/submodels/$submodelIdBase64URLEncoded/submodelElements/$idShortPath/attachment/deleted | Deleted SubmodelElement JSON | Per default, the SubmodelElement topic payloads include the SubmodelElement's value. If this is not desired, the SubmodelElement can be annotated with a Qualifier of type *emptyValueUpdateEvent* and value *true* 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 c5b0a5f8f..9b4e661be 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 @@ -1,5 +1,31 @@ +/******************************************************************************* + * Copyright (C) 2024 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.submodelrepository.feature.mqtt; +import java.io.File; import java.io.InputStream; import java.util.List; @@ -45,6 +71,11 @@ public MqttSubmodelRepository(SubmodelRepository decorated, IMqttClient mqttClie public CursorResult> getAllSubmodels(PaginationInfo pInfo) { return decorated.getAllSubmodels(pInfo); } + + @Override + public CursorResult> getAllSubmodels(String semanticId, PaginationInfo pInfo) { + return decorated.getAllSubmodels(semanticId, pInfo); + } @Override public Submodel getSubmodel(String submodelId) throws ElementDoesNotExistException { @@ -121,6 +152,12 @@ public void deleteSubmodelElement(String submodelId, String idShortPath) throws submodelElementDeleted(submodelElement, getName(), submodelId, idShortPath); } + @Override + public void patchSubmodelElements(String submodelId, List submodelElementList) { + decorated.patchSubmodelElements(submodelId, submodelElementList); + submodelElementsPatched(submodelElementList, getName(), submodelId); + } + @Override public String getName() { return decorated.getName(); @@ -136,6 +173,35 @@ public Submodel getSubmodelByIdMetadata(String submodelId) { return decorated.getSubmodelByIdMetadata(submodelId); } + @Override + public OperationVariable[] invokeOperation(String submodelId, String idShortPath, OperationVariable[] input) throws ElementDoesNotExistException { + return decorated.invokeOperation(submodelId, idShortPath, input); + } + + @Override + public java.io.File getFileByPathSubmodel(String submodelId, String idShortPath) { + return decorated.getFileByPathSubmodel(submodelId, idShortPath); + } + + @Override + public void deleteFileValue(String identifier, String idShortPath) { + SubmodelElement submodelElement = decorated.getSubmodelElement(identifier, idShortPath); + decorated.deleteFileValue(identifier, idShortPath); + fileValueDeleted(submodelElement, getName(), identifier, idShortPath); + } + + @Override + public void setFileValue(String submodelId, String idShortPath, String fileName, InputStream inputStream){ + decorated.setFileValue(submodelId, idShortPath, fileName, inputStream); + SubmodelElement submodelElement = decorated.getSubmodelElement(submodelId, idShortPath); + fileValueUpdated(submodelElement, getName(), submodelId, idShortPath); + } + + @Override + public InputStream getFileByFilePath(String submodelId, String filePath) { + return decorated.getFileByFilePath(submodelId, filePath); + } + private void submodelCreated(Submodel submodel, String repoId) { sendMqttMessage(topicFactory.createCreateSubmodelTopic(repoId), SubmodelSerializer.serializeSubmodel(submodel)); } @@ -159,6 +225,18 @@ private void submodelElementUpdated(SubmodelElement submodelElement, String repo private void submodelElementDeleted(SubmodelElement submodelElement, String repoId, String submodelId, String submodelElementId) { sendMqttMessage(topicFactory.createDeleteSubmodelElementTopic(repoId, submodelId, submodelElementId), SubmodelElementSerializer.serializeSubmodelElement(submodelElement)); } + + private void submodelElementsPatched(List submodelElements, String repoId, String submodelId) { + sendMqttMessage(topicFactory.createPatchSubmodelElementsTopic(repoId, submodelId), SubmodelElementSerializer.serializeSubmodelElements(submodelElements)); + } + + private void fileValueDeleted(SubmodelElement submodelElement, String repoId, String submodelId, String submodelElementId) { + sendMqttMessage(topicFactory.createDeleteFileValueTopic(repoId, submodelId, submodelElementId), SubmodelElementSerializer.serializeSubmodelElement(submodelElement)); + } + + private void fileValueUpdated(SubmodelElement submodelElement, String repoId, String submodelId, String submodelElementId) { + sendMqttMessage(topicFactory.createUpdateFileValueTopic(repoId, submodelId, submodelElementId), SubmodelElementSerializer.serializeSubmodelElement(submodelElement)); + } /** * Sends MQTT message to connected broker @@ -189,37 +267,4 @@ private MqttMessage createMqttMessage(String payload) { } } - @Override - public OperationVariable[] invokeOperation(String submodelId, String idShortPath, OperationVariable[] input) throws ElementDoesNotExistException { - return decorated.invokeOperation(submodelId, idShortPath, input); - } - - @Override - public java.io.File getFileByPathSubmodel(String submodelId, String idShortPath) { - return decorated.getFileByPathSubmodel(submodelId, idShortPath); - } - - @Override - public void deleteFileValue(String identifier, String idShortPath) { - // TODO: Eventing - decorated.deleteFileValue(identifier, idShortPath); - } - - @Override - public void setFileValue(String submodelId, String idShortPath, String fileName, InputStream inputStream){ - // TODO: Eventing - decorated.setFileValue(submodelId, idShortPath, fileName, inputStream); - } - - @Override - public void patchSubmodelElements(String submodelId, List submodelElementList) { - // TODO: Eventing - decorated.patchSubmodelElements(submodelId, submodelElementList); - } - - @Override - public InputStream getFileByFilePath(String submodelId, String filePath) { - return decorated.getFileByFilePath(submodelId, filePath); - } - } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttSubmodelRepositoryTopicFactory.java b/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttSubmodelRepositoryTopicFactory.java index ea4d29263..14ab1f0e5 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttSubmodelRepositoryTopicFactory.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttSubmodelRepositoryTopicFactory.java @@ -41,7 +41,9 @@ public class MqttSubmodelRepositoryTopicFactory extends AbstractMqttTopicFactory private static final String CREATED = "created"; private static final String UPDATED = "updated"; private static final String DELETED = "deleted"; + private static final String PATCHED = "patched"; private static final String SUBMODELELEMENTS = "submodelElements"; + private static final String ATTACHMENT = "attachment"; /** * @param encoder @@ -104,4 +106,34 @@ public String createUpdateSubmodelElementTopic(String repoId, String submodelId, public String createDeleteSubmodelElementTopic(String repoId, String submodelId, String submodelElementId) { return new StringJoiner("/", "", "").add(SUBMODELREPOSITORY).add(repoId).add(SUBMODELS).add(encodeId(submodelId)).add(SUBMODELELEMENTS).add(submodelElementId).add(DELETED).toString(); } + + /** + * Creates the hierarchical topic for the patch event of submodelElements + * + * @param repoId + * @param submodelId + */ + public String createPatchSubmodelElementsTopic(String repoId, String submodelId) { + return new StringJoiner("/", "", "").add(SUBMODELREPOSITORY).add(repoId).add(SUBMODELS).add(encodeId(submodelId)).add(SUBMODELELEMENTS).add(PATCHED).toString(); + } + + /** + * Creates the hierarchical topic for the delete event of a file of a file element + * + * @param repoId + * + */ + public String createDeleteFileValueTopic(String repoId, String submodelId, String submodelElementId) { + return new StringJoiner("/", "", "").add(SUBMODELREPOSITORY).add(repoId).add(SUBMODELS).add(encodeId(submodelId)).add(SUBMODELELEMENTS).add(submodelElementId).add(ATTACHMENT).add(DELETED).toString(); + } + + /** + * Creates the hierarchical topic for the update event of a file of a file element + * + * @param repoId + * + */ + public String createUpdateFileValueTopic(String repoId, String submodelId, String submodelElementId) { + return new StringJoiner("/", "", "").add(SUBMODELREPOSITORY).add(repoId).add(SUBMODELS).add(encodeId(submodelId)).add(SUBMODELELEMENTS).add(submodelElementId).add(ATTACHMENT).add(UPDATED).toString(); + } } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/TestMqttSubmodelObserver.java b/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/TestMqttSubmodelObserver.java index 1198e9ab1..2afc466d0 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/TestMqttSubmodelObserver.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/TestMqttSubmodelObserver.java @@ -28,21 +28,30 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException; import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonDeserializer; +import org.eclipse.digitaltwin.aas4j.v3.model.File; import org.eclipse.digitaltwin.aas4j.v3.model.Property; import org.eclipse.digitaltwin.aas4j.v3.model.Qualifier; import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultFile; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultQualifier; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel; import org.eclipse.digitaltwin.basyx.common.mqttcore.encoding.Base64URLEncoder; import org.eclipse.digitaltwin.basyx.common.mqttcore.serializer.SubmodelElementSerializer; +import org.eclipse.digitaltwin.basyx.core.exceptions.FileHandlingException; +import org.eclipse.digitaltwin.basyx.core.filerepository.FileMetadata; +import org.eclipse.digitaltwin.basyx.core.filerepository.FileRepository; import org.eclipse.digitaltwin.basyx.core.filerepository.InMemoryFileRepository; import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelInMemoryBackendProvider; import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; @@ -58,6 +67,9 @@ import org.junit.BeforeClass; import org.junit.Test; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; + import io.moquette.broker.Server; import io.moquette.broker.config.ClasspathResourceLoader; import io.moquette.broker.config.IConfig; @@ -74,7 +86,13 @@ public class TestMqttSubmodelObserver { private static MqttSubmodelRepositoryTopicFactory topicFactory = new MqttSubmodelRepositoryTopicFactory(new Base64URLEncoder()); private static SubmodelRepository submodelRepository; + + private static JsonDeserializer deserializer = new JsonDeserializer(); + private static final String FILE_SUBMODEL_ELEMENT_NAME = "testFile.txt"; + private static final String FILE_SUBMODEL_ELEMENT_CONTENT = "This is a text file."; + private static String SAVED_FILE_PATH = ""; + @BeforeClass public static void setUpClass() throws MqttException, IOException { mqttBroker = startBroker(); @@ -135,10 +153,10 @@ public void createSubmodelElementEvent() throws DeserializationException { @Test public void updateSubmodelElementEvent() throws DeserializationException { - Submodel submodel = createSubmodelDummy("updateSubmodelForElementEventId"); + Submodel submodel = createSubmodelDummyWithSubmodelElement("updateSubmodelForElementEventId", "updateSubmodelElementEventId"); submodelRepository.createSubmodel(submodel); - SubmodelElement submodelElement = createSubmodelElementDummy("updateSubmodelElementEventId"); - submodelRepository.createSubmodelElement(submodel.getId(), submodelElement); + SubmodelElement submodelElement = submodel.getSubmodelElements().get(0); + SubmodelElementValue value = new PropertyValue("updatedValue"); submodelRepository.setSubmodelElementValue(submodel.getId(), submodelElement.getIdShort(), value); @@ -148,10 +166,11 @@ public void updateSubmodelElementEvent() throws DeserializationException { @Test public void deleteSubmodelElementEvent() throws DeserializationException { - Submodel submodel = createSubmodelDummy("deleteSubmodelForElementEventId"); + Submodel submodel = createSubmodelDummyWithSubmodelElement("deleteSubmodelForElementEventId", "deleteSubmodelElementEventId"); submodelRepository.createSubmodel(submodel); - SubmodelElement submodelElement = createSubmodelElementDummy("deleteSubmodelElementEventId"); - submodelRepository.createSubmodelElement(submodel.getId(), submodelElement); + + SubmodelElement submodelElement = submodel.getSubmodelElements().get(0); + submodelRepository.deleteSubmodelElement(submodel.getId(), submodelElement.getIdShort()); assertEquals(topicFactory.createDeleteSubmodelElementTopic(submodelRepository.getName(), submodel.getId(), submodelElement.getIdShort()), listener.lastTopic); @@ -175,6 +194,50 @@ public void createSubmodelElementWithoutValueEvent() throws DeserializationExcep assertEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload)); } + @Test + public void patchSubmodelElementsEvent() throws DeserializationException, JsonMappingException, JsonProcessingException { + Submodel submodel = createSubmodelDummyWithSubmodelElements("patchSubmodelForElementEventId"); + submodelRepository.createSubmodel(submodel); + + List submodelElements = submodel.getSubmodelElements(); + + for (int i = 0; i < submodelElements.size(); i++) { + SubmodelElement submodelElement = submodelElements.get(i); + submodelElement.setIdShort("patchedSubmodelElementId_" + i); + } + + submodelRepository.patchSubmodelElements(submodel.getId(), submodelElements); + + assertEquals(topicFactory.createPatchSubmodelElementsTopic(submodelRepository.getName(), submodel.getId()), listener.lastTopic); + assertEquals(submodelElements, deserializeSubmodelElementsListPayload(listener.lastPayload)); + } + + @Test + public void setFileValueEvent() throws DeserializationException, IOException { + Submodel submodel = createSubmodelDummyWithFileSubmodelElement("setSubmodelFileValueEventId", "setFileValueSubmodelElementEventId"); + submodelRepository.createSubmodel(submodel); + + File submodelElement = (File) submodel.getSubmodelElements().get(0); + + submodelRepository.setFileValue(submodel.getId(), submodelElement.getIdShort(), FILE_SUBMODEL_ELEMENT_NAME, getInputStreamOfDummyFile(FILE_SUBMODEL_ELEMENT_CONTENT)); + + assertEquals(topicFactory.createUpdateFileValueTopic(submodelRepository.getName(), submodel.getId(), submodelElement.getIdShort()), listener.lastTopic); + assertEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload)); + } + + @Test + public void deleteFileValueEvent() throws DeserializationException, IOException { + Submodel submodel = createSubmodelDummyWithFileSubmodelElement("deleteSubmodelFileValueEventId", "deleteFileValueSubmodelElementEventId"); + submodelRepository.createSubmodel(submodel); + + File submodelElement = (File) submodel.getSubmodelElements().get(0); + + submodelRepository.deleteFileValue(submodel.getId(), submodelElement.getIdShort()); + + assertEquals(topicFactory.createDeleteFileValueTopic(submodelRepository.getName(), submodel.getId(), submodelElement.getIdShort()), listener.lastTopic); + assertEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload)); + } + private List createNoValueQualifierList() { Qualifier emptyValueQualifier = new DefaultQualifier.Builder().type(SubmodelElementSerializer.EMPTYVALUEUPDATE_TYPE).value("true").build(); return Arrays.asList(emptyValueQualifier); @@ -187,17 +250,67 @@ private Submodel deserializeSubmodelPayload(String payload) throws Deserializati private SubmodelElement deserializeSubmodelElementPayload(String payload) throws DeserializationException { return new JsonDeserializer().read(payload, SubmodelElement.class); } + + private List deserializeSubmodelElementsListPayload(String payload) throws DeserializationException, JsonMappingException, JsonProcessingException { + return deserializer.readList(payload, SubmodelElement.class); + } private Submodel createSubmodelDummy(String submodelId) { return new DefaultSubmodel.Builder().id(submodelId).build(); } + + private Submodel createSubmodelDummyWithSubmodelElement(String submodelId, String submodelElementId) { + List submodelElements = new ArrayList<>(); + + submodelElements.add(createSubmodelElementDummy(submodelElementId)); + + return new DefaultSubmodel.Builder().id(submodelId).submodelElements(submodelElements).build(); + } + + private Submodel createSubmodelDummyWithFileSubmodelElement(String submodelId, String submodelElementId) { + List submodelElements = new ArrayList<>(); + + submodelElements.add(createFileSubmodelElement(submodelElementId)); + + return new DefaultSubmodel.Builder().id(submodelId).submodelElements(submodelElements).build(); + } + + private Submodel createSubmodelDummyWithSubmodelElements(String submodelId) { + List submodelElements = createSubmodelElementsListDummy(2); + + return new DefaultSubmodel.Builder().id(submodelId).submodelElements(submodelElements).build(); + } private SubmodelElement createSubmodelElementDummy(String submodelElementId) { + Property defaultProp = new DefaultProperty.Builder().idShort(submodelElementId).value("defaultValue").build(); + return new DefaultProperty.Builder().idShort(submodelElementId).value("defaultValue").build(); } + + public File createFileSubmodelElement(String submodelElementId) { + return new DefaultFile.Builder().idShort(submodelElementId).value(SAVED_FILE_PATH).contentType("text/plain").build(); + } + + private static InputStream getInputStreamOfDummyFile(String fileContent) throws FileNotFoundException, IOException { + return new ByteArrayInputStream(fileContent.getBytes()); + } + + private List createSubmodelElementsListDummy(int count) { + List submodelElements = new ArrayList(); + + for (int i = 0; i < count; i++) { + submodelElements.add(createSubmodelElementDummy("submodelElementId_" + i)); + } + + return submodelElements; + } - private static SubmodelRepository createMqttSubmodelRepository(MqttClient client) { - SubmodelRepositoryFactory repoFactory = new SimpleSubmodelRepositoryFactory(new SubmodelInMemoryBackendProvider(), new InMemorySubmodelServiceFactory(new InMemoryFileRepository())); + private static SubmodelRepository createMqttSubmodelRepository(MqttClient client) throws FileHandlingException, FileNotFoundException, IOException { + FileRepository fileRepository = new InMemoryFileRepository(); + + SAVED_FILE_PATH = fileRepository.save(new FileMetadata(FILE_SUBMODEL_ELEMENT_NAME, "", getInputStreamOfDummyFile(FILE_SUBMODEL_ELEMENT_CONTENT))); + + SubmodelRepositoryFactory repoFactory = new SimpleSubmodelRepositoryFactory(new SubmodelInMemoryBackendProvider(), new InMemorySubmodelServiceFactory(fileRepository)); return new MqttSubmodelRepositoryFactory(repoFactory, client, new MqttSubmodelRepositoryTopicFactory(new Base64URLEncoder())).create(); } 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 6404475cb..457fa6c19 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 @@ -65,6 +65,11 @@ public OperationDelegationSubmodelRepository(SubmodelRepository decorated, Opera public CursorResult> getAllSubmodels(PaginationInfo pInfo) { return decorated.getAllSubmodels(pInfo); } + + @Override + public CursorResult> getAllSubmodels(String semanticId, PaginationInfo pInfo) { + return decorated.getAllSubmodels(semanticId, pInfo); + } @Override public Submodel getSubmodel(String submodelId) throws ElementDoesNotExistException { diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/TestOperationDelegationFeature.java b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/TestOperationDelegationFeature.java index 1aa094112..7f6800b3c 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/TestOperationDelegationFeature.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/TestOperationDelegationFeature.java @@ -42,13 +42,13 @@ import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultQualifier; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel; -import org.eclipse.digitaltwin.basyx.InvokableOperation; import org.eclipse.digitaltwin.basyx.core.exceptions.OperationDelegationException; import org.eclipse.digitaltwin.basyx.core.filerepository.InMemoryFileRepository; import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; import org.eclipse.digitaltwin.basyx.http.Aas4JHTTPSerializationExtension; import org.eclipse.digitaltwin.basyx.http.BaSyxHTTPConfiguration; import org.eclipse.digitaltwin.basyx.http.SerializationExtension; +import org.eclipse.digitaltwin.basyx.operation.InvokableOperation; import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelInMemoryBackendProvider; import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepositoryFactory; @@ -75,7 +75,6 @@ */ public class TestOperationDelegationFeature { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); private static SubmodelRepository submodelRepository; private static HTTPMockServer httpMockServer = new HTTPMockServer(2020); private static WebClient webClient; @@ -104,7 +103,7 @@ public void reset() { if (submodelRepository == null) return; - submodelRepository.getAllSubmodels(NO_LIMIT_PAGINATION_INFO).getResult().stream().forEach(sm -> submodelRepository.deleteSubmodel(sm.getId())); + submodelRepository.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().stream().forEach(sm -> submodelRepository.deleteSubmodel(sm.getId())); } @Test 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 071c0e8e1..ead8f6708 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 @@ -73,6 +73,11 @@ public RegistryIntegrationSubmodelRepository(SubmodelRepository decorated, Submo public CursorResult> getAllSubmodels(PaginationInfo paginationInfo) { return decorated.getAllSubmodels(paginationInfo); } + + @Override + public CursorResult> getAllSubmodels(String semanticId, PaginationInfo paginationInfo) { + return decorated.getAllSubmodels(semanticId, paginationInfo); + } @Override public Submodel getSubmodel(String submodelId) throws ElementDoesNotExistException { @@ -86,9 +91,19 @@ public void updateSubmodel(String submodelId, Submodel submodel) throws ElementD @Override public void createSubmodel(Submodel submodel) throws CollidingIdentifierException { + SubmodelDescriptor descriptor = new SubmodelDescriptorFactory(submodelRepositoryRegistryLink.getSubmodelRepositoryBaseURLs(), attributeMapper).create(submodel); + decorated.createSubmodel(submodel); - integrateSubmodelWithRegistry(submodel, submodelRepositoryRegistryLink.getSubmodelRepositoryBaseURLs()); + boolean registrationSuccessful = false; + + try { + registerSubmodel(descriptor); + registrationSuccessful = true; + } finally { + if (!registrationSuccessful) + decorated.deleteSubmodel(submodel.getId()); + } } @Override @@ -168,17 +183,15 @@ public void deleteFileValue(String submodelId, String idShortPath) throws Elemen decorated.deleteFileValue(submodelId, idShortPath); } - private void integrateSubmodelWithRegistry(Submodel submodel, List submodelRepositoryURLs) { - SubmodelDescriptor descriptor = new SubmodelDescriptorFactory(submodel, submodelRepositoryURLs, attributeMapper).create(); - + private void registerSubmodel(SubmodelDescriptor descriptor) { SubmodelRegistryApi registryApi = submodelRepositoryRegistryLink.getRegistryApi(); try { registryApi.postSubmodelDescriptor(descriptor); - logger.info("Submodel '{}' has been automatically linked with the Registry", submodel.getId()); + logger.info("Submodel '{}' has been automatically linked with the Registry", descriptor.getId()); } catch (ApiException e) { - throw new RepositoryRegistryLinkException(submodel.getId(), e); + throw new RepositoryRegistryLinkException(descriptor.getId(), e); } } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/SubmodelRepositoryRegistryLinkDescriptorGenerationTest.java b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/SubmodelRepositoryRegistryLinkDescriptorGenerationTest.java new file mode 100644 index 000000000..f220de890 --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/SubmodelRepositoryRegistryLinkDescriptorGenerationTest.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright (C) 2024 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.submodelrepository.feature.registry.integration; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel; +import org.eclipse.digitaltwin.basyx.client.internal.ApiException; +import org.eclipse.digitaltwin.basyx.core.filerepository.InMemoryFileRepository; +import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.api.SubmodelRegistryApi; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.factory.SubmodelDescriptorFactory; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.mapper.AttributeMapper; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.SubmodelDescriptor; +import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelInMemoryBackendProvider; +import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; +import org.eclipse.digitaltwin.basyx.submodelrepository.backend.SimpleSubmodelRepositoryFactory; +import org.eclipse.digitaltwin.basyx.submodelservice.InMemorySubmodelServiceFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.Mockito; + +/** + * Test suite for {@link RegistryIntegrationAasRepository} feature + */ +@RunWith(Parameterized.class) +public class SubmodelRepositoryRegistryLinkDescriptorGenerationTest { + private static final String DUMMY_SUBMODEL_IDSHORT = "TechnicalData"; + private static final String DUMMY_SUBMODEL_ID = "7A7104BDAB57E184"; + + private static final String BASE_URL = "http://localhost:8081"; + + private RegistryIntegrationSubmodelRepository registryIntegrationSubmodelRepository; + private SubmodelRepository mockedSubmodelRepository; + private SubmodelRepositoryRegistryLink mockedRegistryLink; + private AttributeMapper mockedAttributeMapper; + private SubmodelRegistryApi mockedRegistryApi; + + @Before + public void setUp() { + mockedSubmodelRepository = getSubmodelRepository(); + mockedRegistryLink = Mockito.mock(SubmodelRepositoryRegistryLink.class); + mockedAttributeMapper = Mockito.mock(AttributeMapper.class); + mockedRegistryApi = Mockito.mock(SubmodelRegistryApi.class); + + Mockito.when(mockedRegistryLink.getRegistryApi()).thenReturn(mockedRegistryApi); + + registryIntegrationSubmodelRepository = new RegistryIntegrationSubmodelRepository(mockedSubmodelRepository, mockedRegistryLink, mockedAttributeMapper); + } + + @Parameterized.Parameter(0) + public String externalUrl; + + @Parameterized.Parameter(1) + public String expectedUrl; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { { BASE_URL + "/context", BASE_URL + "/context/submodels/" + Base64UrlEncodedIdentifier.encodeIdentifier(DUMMY_SUBMODEL_ID) }, + { BASE_URL, BASE_URL + "/submodels/" + Base64UrlEncodedIdentifier.encodeIdentifier(DUMMY_SUBMODEL_ID) }, { BASE_URL + "/", BASE_URL + "/submodels/" + Base64UrlEncodedIdentifier.encodeIdentifier(DUMMY_SUBMODEL_ID) }, + { BASE_URL + "/context/", BASE_URL + "/context/submodels/" + Base64UrlEncodedIdentifier.encodeIdentifier(DUMMY_SUBMODEL_ID) } }); + } + + @Test + public void testExternalUrl() throws ApiException { + Mockito.when(mockedRegistryLink.getSubmodelRepositoryBaseURLs()).thenReturn(List.of(externalUrl)); + + SubmodelDescriptor descriptor = createAndRetrieveDescriptor(createDummySubmodel()); + String actualUrl = descriptor.getEndpoints().get(0).getProtocolInformation().getHref(); + + assertEquals(expectedUrl, actualUrl); + } + + private SubmodelDescriptor createAndRetrieveDescriptor(Submodel submodel) throws ApiException { + registryIntegrationSubmodelRepository.createSubmodel(submodel); + + SubmodelDescriptorFactory descriptorFactory = new SubmodelDescriptorFactory(mockedRegistryLink.getSubmodelRepositoryBaseURLs(), mockedAttributeMapper); + return descriptorFactory.create(submodel); + } + + private Submodel createDummySubmodel() { + return new DefaultSubmodel.Builder().id(DUMMY_SUBMODEL_ID).idShort(DUMMY_SUBMODEL_IDSHORT).build(); + } + + protected SubmodelRepository getSubmodelRepository() { + return new SimpleSubmodelRepositoryFactory(new SubmodelInMemoryBackendProvider(), new InMemorySubmodelServiceFactory(new InMemoryFileRepository())).create(); + } + +} diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/SubmodelRepositoryRegistryLinkTestSuite.java b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/SubmodelRepositoryRegistryLinkTestSuite.java index 2c02ef48f..ad7c08255 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/SubmodelRepositoryRegistryLinkTestSuite.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/SubmodelRepositoryRegistryLinkTestSuite.java @@ -30,12 +30,11 @@ import java.io.FileNotFoundException; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; import java.util.List; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.ParseException; +import org.eclipse.digitaltwin.basyx.core.RepositoryUrlHelper; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; import org.eclipse.digitaltwin.basyx.http.serialization.BaSyxHttpTestUtils; import org.eclipse.digitaltwin.basyx.submodelregistry.client.ApiException; @@ -54,9 +53,8 @@ * @author danish */ public abstract class SubmodelRepositoryRegistryLinkTestSuite { + private static final String SUBMODEL_REPOSITORY_PATH = "submodels"; - private static final String SUBMODEL_REPOSITORY_PATH = "/submodels"; - private static final String DUMMY_SUBMODEL_IDSHORT = "TechnicalData"; private static final String DUMMY_SUBMODEL_ID = "7A7104BDAB57E184"; @@ -64,7 +62,8 @@ public abstract class SubmodelRepositoryRegistryLinkTestSuite { protected abstract String getSubmodelRegistryUrl(); protected abstract SubmodelRegistryApi getSubmodelRegistryApi(); - private final SubmodelDescriptor DUMMY_DESCRIPTOR = DummySubmodelDescriptorFactory.createDummyDescriptor(DUMMY_SUBMODEL_ID, DUMMY_SUBMODEL_IDSHORT, getSubmodelRepoBaseUrls(), DummySubmodelDescriptorFactory.createSemanticId()); + private final SubmodelDescriptor DUMMY_DESCRIPTOR = DummySubmodelDescriptorFactory.createDummyDescriptor(DUMMY_SUBMODEL_ID, DUMMY_SUBMODEL_IDSHORT, DummySubmodelDescriptorFactory.createSemanticId(), + DummySubmodelDescriptorFactory.buildAdministrationInformation("0", "9", "testTemplateId"), getSubmodelRepoBaseUrls()); @Test public void createSubmodel() throws FileNotFoundException, IOException, ApiException { @@ -150,7 +149,7 @@ private String getExpectedSubmodel() throws FileNotFoundException, IOException { } private CloseableHttpResponse createSubmodelOnRepo(String submodelJsonContent) throws IOException { - return BaSyxHttpTestUtils.executePostOnURL(createSubmodelRepositoryUrl(getFirstSubmodeRepoBaseUrl()), submodelJsonContent); + return BaSyxHttpTestUtils.executePostOnURL(RepositoryUrlHelper.createRepositoryUrl(getFirstSubmodeRepoBaseUrl(), SUBMODEL_REPOSITORY_PATH), submodelJsonContent); } private String getFirstSubmodeRepoBaseUrl() { @@ -164,16 +163,6 @@ private CloseableHttpResponse createSubmodelElementOnRepo(String submodelElement } private String getSpecificSubmodelAccessURL(String submodelId) { - return createSubmodelRepositoryUrl(getFirstSubmodeRepoBaseUrl()) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId); - } - - private static String createSubmodelRepositoryUrl(String smRepositoryBaseURL) { - - try { - return new URL(new URL(smRepositoryBaseURL), SUBMODEL_REPOSITORY_PATH).toString(); - } catch (MalformedURLException e) { - throw new RuntimeException("The Submodel Repository Base url is malformed.\n " + e.getMessage()); - } + return RepositoryUrlHelper.createRepositoryUrl(getFirstSubmodeRepoBaseUrl(), SUBMODEL_REPOSITORY_PATH) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId); } - } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/resources/ExpectedSubmodel.json b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/resources/ExpectedSubmodel.json index 791e0f14c..4af941bcf 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/resources/ExpectedSubmodel.json +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/resources/ExpectedSubmodel.json @@ -12,6 +12,11 @@ }, "id": "7A7104BDAB57E184", "idShort": "TechnicalData", + "administration": { + "version": "0", + "revision": "9", + "templateId": "testTemplateId" + }, "submodelElements": [ { "modelType": "Property", diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/resources/SingleSubmodel.json b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/resources/SingleSubmodel.json index 3391b2db6..fc7845bc4 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/resources/SingleSubmodel.json +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/resources/SingleSubmodel.json @@ -3,6 +3,11 @@ "id": "7A7104BDAB57E184", "idShort": "TechnicalData", "kind": "Instance", + "administration": { + "version": "0", + "revision": "9", + "templateId": "testTemplateId" + }, "submodelElements": [ { "modelType": "Property", 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 c6039bdb5..5a910a9ea 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 @@ -44,9 +44,12 @@ import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifierSize; +import org.eclipse.digitaltwin.basyx.http.Base64UrlEncoder; import org.eclipse.digitaltwin.basyx.http.pagination.Base64UrlEncodedCursor; import org.eclipse.digitaltwin.basyx.http.pagination.PagedResult; import org.eclipse.digitaltwin.basyx.http.pagination.PagedResultPagingMetadata; +import org.eclipse.digitaltwin.basyx.operation.OperationRequestExecutor; +import org.eclipse.digitaltwin.basyx.operation.Invokable; import org.eclipse.digitaltwin.basyx.pagination.GetSubmodelElementsResult; import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; import org.eclipse.digitaltwin.basyx.submodelrepository.http.pagination.GetSubmodelsResult; @@ -55,6 +58,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; @@ -115,7 +119,12 @@ public ResponseEntity getAllSubmodels(@Base64UrlEncodedIdentifierSi PaginationInfo pInfo = new PaginationInfo(limit, decodedCursor); - CursorResult> cursorResult = repository.getAllSubmodels(pInfo); + CursorResult> cursorResult; + if (semanticId != null) { + cursorResult = repository.getAllSubmodels(semanticId.getIdentifier(), pInfo); + } else { + cursorResult = repository.getAllSubmodels(pInfo); + } GetSubmodelsResult paginatedSubmodel = new GetSubmodelsResult(); @@ -214,7 +223,11 @@ public ResponseEntity getSubmodelByIdMetadata(Base64UrlEncodedIdentifi public ResponseEntity getFileByPath(Base64UrlEncodedIdentifier submodelIdentifier, String idShortPath) { Resource resource = new FileSystemResource(repository.getFileByPathSubmodel(submodelIdentifier.getIdentifier(), idShortPath)); - return new ResponseEntity(resource, HttpStatus.OK); + String fileName = resource.getFilename(); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"") + .body(resource); } @Override @@ -272,28 +285,12 @@ private ResponseEntity handleSubmodelElementValueNormalGetReque @Override public ResponseEntity invokeOperationSubmodelRepo(Base64UrlEncodedIdentifier submodelIdentifier, String idShortPath, @Valid OperationRequest body, @Valid Boolean async) { - List inVars = new ArrayList<>(); - inVars.addAll(body.getInputArguments()); - inVars.addAll(body.getInoutputArguments()); - List result = Arrays.asList(repository.invokeOperation(submodelIdentifier.getIdentifier(), idShortPath, inVars.toArray(new OperationVariable[0]))); + // TODO: #566 Add async operation execution support to + // SubmodelRepositoryController - List outVars = new ArrayList<>(result); - List inoutputVars = new ArrayList<>(); - - if (!body.getInoutputArguments().isEmpty()) { - List inoutputVarsIdShorts = body.getInoutputArguments().stream().map(OperationVariable::getValue).map(SubmodelElement::getIdShort).toList(); - - inoutputVars = result.stream().filter(opVar -> inoutputVarsIdShorts.contains(opVar.getValue().getIdShort())).toList(); - - outVars.removeAll(inoutputVars); - } - - return ResponseEntity.ok(createOperationResult(outVars, inoutputVars)); - } - - private OperationResult createOperationResult(List outputVars, List inoutputVars) { - return new DefaultOperationResult.Builder().outputArguments(outputVars).inoutputArguments(inoutputVars).build(); + Invokable invokable = inArgs -> repository.invokeOperation(submodelIdentifier.getIdentifier(), idShortPath, inArgs); + return ResponseEntity.ok(OperationRequestExecutor.executeOperationRequestSynchronously(invokable, body)); } private String getEncodedCursorFromCursorResult(CursorResult cursorResult) { diff --git a/basyx.submodelrepository/basyx.submodelrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/SubmodelRepositorySubmodelHTTPTestSuite.java b/basyx.submodelrepository/basyx.submodelrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/SubmodelRepositorySubmodelHTTPTestSuite.java index 4bc035603..9d33255e9 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/SubmodelRepositorySubmodelHTTPTestSuite.java +++ b/basyx.submodelrepository/basyx.submodelrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/SubmodelRepositorySubmodelHTTPTestSuite.java @@ -27,6 +27,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import java.io.FileNotFoundException; import java.io.IOException; @@ -42,6 +43,7 @@ import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.ProtocolException; @@ -95,6 +97,18 @@ public void getAllSubmodelsPreconfigured() throws IOException, ParseException { BaSyxHttpTestUtils.assertSameJSONContent(expectedSubmodelsJSON,getJSONWithoutCursorInfo(submodelsJSON)); } + + @Test + public void getAllSubmodelsBySemanticIDPreconfigured() throws IOException, ParseException { + String semanticId = Base64UrlEncodedIdentifier.encodeIdentifier(DummySubmodelFactory.SUBMODEL_TECHNICAL_DATA_SEMANTIC_ID); + String url = getURL() + "?semanticId=" + semanticId; + + String submodelsJSON = BaSyxSubmodelHttpTestUtils.requestAllSubmodels(url); + + String expectedSubmodelsJSON = getJSONValueAsString("MultipleSubmodelsWithSameSemanticId.json"); + + BaSyxHttpTestUtils.assertSameJSONContent(expectedSubmodelsJSON,getJSONWithoutCursorInfo(submodelsJSON)); + } @Test public void getSpecificSubmodel() throws ParseException, IOException { @@ -355,6 +369,7 @@ public void deleteFileFromNotExistElement() throws FileNotFoundException, Unsupp @Test public void getFile() throws FileNotFoundException, IOException, ParseException { String fileName = DummySubmodelFactory.FILE_NAME; + String expectedFileName = Base64UrlEncodedIdentifier.encodeIdentifier(DummySubmodelFactory.SUBMODEL_FOR_FILE_TEST) + "-" + DummySubmodelFactory.SUBMODEL_ELEMENT_FILE_ID_SHORT + "-" + fileName; byte[] expectedFile = readBytesFromClasspath(fileName); @@ -363,6 +378,14 @@ public void getFile() throws FileNotFoundException, IOException, ParseException CloseableHttpResponse response = BaSyxHttpTestUtils.executeGetOnURL(createSMEFileGetURL(DummySubmodelFactory.SUBMODEL_FOR_FILE_TEST, DummySubmodelFactory.SUBMODEL_ELEMENT_FILE_ID_SHORT)); assertEquals(HttpStatus.OK.value(), response.getCode()); + Header contentDispositionHeader = response.getFirstHeader("Content-Disposition"); + assertNotNull(contentDispositionHeader); + + String contentDisposition = contentDispositionHeader.getValue(); + String actualFileName = extractFileNameFromContentDisposition(contentDisposition); + + assertEquals(expectedFileName, actualFileName); + byte[] actualFile = EntityUtils.toByteArray(response.getEntity()); response.close(); @@ -384,6 +407,31 @@ public void getFileFromNotExistElement() throws FileNotFoundException, Unsupport assertEquals(HttpStatus.NOT_FOUND.value(), response.getCode()); } + @Test + public void getSubmodelByIdValueOnly() throws IOException, ParseException { + String submodelIdentifier = Base64UrlEncodedIdentifier.encodeIdentifier(DummySubmodelFactory.createTechnicalDataSubmodel().getId()); + String url = getURL() + "/" + submodelIdentifier + "/$value"; + + String expectedSubmodelJSON = getJSONValueAsString("SingleSubmodelValueOnly.json"); + + CloseableHttpResponse response = BaSyxHttpTestUtils.executeGetOnURL(url); + assertEquals(HttpStatus.OK.value(), response.getCode()); + + String actualSubmodelJSON = BaSyxHttpTestUtils.getResponseAsString(response); + + BaSyxHttpTestUtils.assertSameJSONContent(expectedSubmodelJSON, actualSubmodelJSON); + } + + private String extractFileNameFromContentDisposition(String contentDisposition) { + for (String part : contentDisposition.split(";")) { + part = part.trim(); + if (part.startsWith("filename")) { + return part.split("=")[1].replace("\"", "").trim(); + } + } + return null; + } + private String getJSONWithoutCursorInfo(String response) throws JsonMappingException, JsonProcessingException { return BaSyxHttpTestUtils.removeCursorFromJSON(response); } diff --git a/basyx.submodelrepository/basyx.submodelrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/TestSubmodelRepositorySubmodelElementsHTTP.java b/basyx.submodelrepository/basyx.submodelrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/TestSubmodelRepositorySubmodelElementsHTTP.java index 9fff6f6de..ce18a4a70 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/TestSubmodelRepositorySubmodelElementsHTTP.java +++ b/basyx.submodelrepository/basyx.submodelrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/TestSubmodelRepositorySubmodelElementsHTTP.java @@ -43,7 +43,6 @@ * */ public class TestSubmodelRepositorySubmodelElementsHTTP extends SubmodelServiceSubmodelElementsTestSuiteHTTP { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); private static ConfigurableApplicationContext appContext; @BeforeClass @@ -60,7 +59,7 @@ public void createSubmodelOnRepo() { @After public void removeSubmodelFromRepo() { SubmodelRepository repo = appContext.getBean(SubmodelRepository.class); - repo.getAllSubmodels(NO_LIMIT_PAGINATION_INFO).getResult().stream().map(s -> s.getId()).forEach(repo::deleteSubmodel); + repo.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().stream().map(s -> s.getId()).forEach(repo::deleteSubmodel); } @AfterClass diff --git a/basyx.submodelrepository/basyx.submodelrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/TestSubmodelRepositorySubmodelHTTP.java b/basyx.submodelrepository/basyx.submodelrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/TestSubmodelRepositorySubmodelHTTP.java index 57132baec..f050e82de 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/TestSubmodelRepositorySubmodelHTTP.java +++ b/basyx.submodelrepository/basyx.submodelrepository-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/TestSubmodelRepositorySubmodelHTTP.java @@ -42,7 +42,6 @@ * */ public class TestSubmodelRepositorySubmodelHTTP extends SubmodelRepositorySubmodelHTTPTestSuite { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); private static ConfigurableApplicationContext appContext; @BeforeClass @@ -53,7 +52,7 @@ public static void startAASRepo() throws Exception { @Override public void resetRepository() { SubmodelRepository repo = appContext.getBean(SubmodelRepository.class); - repo.getAllSubmodels(NO_LIMIT_PAGINATION_INFO).getResult().stream().map(s -> s.getId()).forEach(repo::deleteSubmodel); + repo.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().stream().map(s -> s.getId()).forEach(repo::deleteSubmodel); } @Override diff --git a/basyx.submodelrepository/basyx.submodelrepository-http/src/test/resources/MultipleSubmodelsWithSameSemanticId.json b/basyx.submodelrepository/basyx.submodelrepository-http/src/test/resources/MultipleSubmodelsWithSameSemanticId.json new file mode 100644 index 000000000..e41361c39 --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-http/src/test/resources/MultipleSubmodelsWithSameSemanticId.json @@ -0,0 +1,454 @@ +{ + "paging_metadata": {}, + "result": [ + { + "modelType": "Submodel", + "id": "7A7104BDAB57E184", + "kind":"Instance", + "idShort": "TechnicalData", + "submodelElements": [ + { + "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": "MultiLanguageProperty", + "category": "PARAM", + "idShort": "MultiLanguage", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + }, + "value": [ + { + "language": "en", + "text": "Hello" + }, + { + "language": "de", + "text": "Hallo" + } + ] + }, + { + "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": "Entity", + "entityType": "CoManagedEntity", + "specificAssetIds": [ + { + "name": "specificAssetIdName", + "value": "specificValue" + } + ], + "statements": [ + { + "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" + } + } + ], + "category": "Entity", + "idShort": "EntityData", + "globalAssetId": "globalAssetID", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "ReferenceElement", + "category": "PARAMETER", + "idShort": "ReferenceElement", + "value": { + "keys": [ + { + "type": "DataElement", + "value": "DataElement" + } + ], + "type": "ModelReference" + }, + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "RelationshipElement", + "category": "PARAMETER", + "idShort": "RelationshipElement", + "first": { + "keys": [ + { + "type": "DataElement", + "value": "DataElement" + } + ], + "type": "ModelReference" + }, + "second": { + "keys": [ + { + "type": "BasicEventElement", + "value": "BasicEventElement" + } + ], + "type": "ExternalReference" + }, + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "AnnotatedRelationshipElement", + "category": "PARAMETER", + "idShort": "AnnotatedRelationshipElement", + "first": { + "keys": [ + { + "type": "DataElement", + "value": "DataElement" + } + ], + "type": "ModelReference" + }, + "second": { + "keys": [ + { + "type": "BasicEventElement", + "value": "BasicEventElement" + } + ], + "type": "ExternalReference" + }, + "annotations": [ + { + "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" + } + } + ], + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Blob", + "contentType": "application/xml", + "value": "VGVzdCBjb250ZW50IG9mIFhNTCBmaWxl", + "category": "PARAMETER", + "idShort": "BlobData", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "SubmodelElementCollection", + "category": "PARAMETER", + "idShort": "SubmodelElementCollection", + "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": "MaxRotationSpeed", + "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": "SubmodelElementList", + "category": "PARAMETER", + "idShort": "SubmodelElementList", + "orderRelevant": true, + "value": [ + { + "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": "Property", + "value": "5000", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "MaxRotationSpeed", + "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": "Operation", + "idShort": "square", + "inputVariables": [ + { + "value": { + "modelType": "Property", + "valueType": "xs:int", + "idShort": "input" + } + } + ], + "outputVariables": [ + { + "value": { + "modelType": "Property", + "valueType": "xs:int", + "idShort": "result" + } + } + ] + } + ], + "semanticId": { + "keys": [ + { + "type": "GlobalReference", + "value": "0173-1#01-AFZ615#016" + } + ], + "type": "ExternalReference" + } + }, + { + "id": "8A6344BDAB57E184", + "idShort": "FileTests", + "modelType": "Submodel", + "kind":"Instance", + "semanticId": { + "keys": [{ + "type": "GlobalReference", + "value": "0173-1#01-AFZ615#016" + } + ], + "type": "ExternalReference" + }, + "submodelElements": [{ + "category": "PARAMETER", + "contentType": "application/json", + "idShort": "FileData", + "modelType": "File", + "semanticId": { + "keys": [{ + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + }, + "value": "BaSyx-Logo.png" + }, { + "category": "PARAMETER", + "idShort": "NonFileParameter", + "modelType": "Property", + "semanticId": { + "keys": [{ + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + }, + "value": "5000", + "valueType": "xs:integer" + } + ] + } + ] +} \ No newline at end of file diff --git a/basyx.submodelrepository/basyx.submodelrepository-http/src/test/resources/SingleSubmodelValueOnly.json b/basyx.submodelrepository/basyx.submodelrepository-http/src/test/resources/SingleSubmodelValueOnly.json new file mode 100644 index 000000000..89ca82af2 --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-http/src/test/resources/SingleSubmodelValueOnly.json @@ -0,0 +1,117 @@ +{ + "SubmodelElementList": [ + { + "min": 200, + "max": 300 + }, + "5000" + ], + "EntityData": { + "statements": [ + { + "MaxRotationSpeed": "5000" + }, + { + "RotationSpeedRange": { + "min": 200, + "max": 300 + } + } + ], + "entityType": "CoManagedEntity", + "globalAssetId": "globalAssetID", + "specificAssetIds": [ + { + "specificAssetIdName": "specificValue" + } + ] + }, + "RelationshipElement": { + "first": { + "type": "ModelReference", + "keys": [ + { + "type": "DataElement", + "value": "DataElement" + } + ] + }, + "second": { + "type": "ExternalReference", + "keys": [ + { + "type": "BasicEventElement", + "value": "BasicEventElement" + } + ] + } + }, + "ReferenceElement": { + "type": "ModelReference", + "keys": [ + { + "type": "DataElement", + "value": "DataElement" + } + ] + }, + "BlobData": { + "contentType": "application/xml", + "value": "Test content of XML file" + }, + "SubmodelElementCollection": { + "MaxRotationSpeed": "5000", + "FileData": { + "contentType": "application/json", + "value": "testFile.json" + } + }, + "MultiLanguage": [ + { + "en": "Hello" + }, + { + "de": "Hallo" + } + ], + "AnnotatedRelationshipElement": { + "first": { + "type": "ModelReference", + "keys": [ + { + "type": "DataElement", + "value": "DataElement" + } + ] + }, + "second": { + "type": "ExternalReference", + "keys": [ + { + "type": "BasicEventElement", + "value": "BasicEventElement" + } + ] + }, + "annotation": [ + { + "MaxRotationSpeed": "5000" + }, + { + "RotationSpeedRange": { + "min": 200, + "max": 300 + } + } + ] + }, + "MaxRotationSpeed": "5000", + "RotationSpeedRange": { + "min": 200, + "max": 300 + }, + "FileData": { + "contentType": "application/json", + "value": "testFile.json" + } +} \ No newline at end of file diff --git a/basyx.submodelrepository/basyx.submodelrepository.component/Dockerfile b/basyx.submodelrepository/basyx.submodelrepository.component/Dockerfile index 1c3e9648d..99b045565 100644 --- a/basyx.submodelrepository/basyx.submodelrepository.component/Dockerfile +++ b/basyx.submodelrepository/basyx.submodelrepository.component/Dockerfile @@ -1,4 +1,6 @@ -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY USER nobody WORKDIR /application ARG JAR_FILE=target/*-exec.jar 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 ab232f2ab..3c37f673f 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 @@ -39,7 +39,6 @@ import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList; -import org.eclipse.digitaltwin.basyx.InvokableOperation; import org.eclipse.digitaltwin.basyx.core.exceptions.CollidingIdentifierException; import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException; import org.eclipse.digitaltwin.basyx.core.exceptions.ElementNotAFileException; @@ -52,6 +51,7 @@ import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; import org.eclipse.digitaltwin.basyx.core.pagination.PaginationSupport; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; +import org.eclipse.digitaltwin.basyx.operation.InvokableOperation; import org.eclipse.digitaltwin.basyx.submodelservice.pathparsing.HierarchicalSubmodelElementParser; import org.eclipse.digitaltwin.basyx.submodelservice.pathparsing.SubmodelElementIdShortHelper; import org.eclipse.digitaltwin.basyx.submodelservice.value.FileBlobValue; @@ -73,6 +73,8 @@ public class InMemorySubmodelService implements SubmodelService { private final FileRepository fileRepository; + private final Object submodelLock = new Object(); + /** * Creates the InMemory SubmodelService containing the passed Submodel * @@ -115,20 +117,22 @@ public SubmodelElementValue getSubmodelElementValue(String idShort) throws Eleme @SuppressWarnings("unchecked") @Override public void setSubmodelElementValue(String idShort, SubmodelElementValue value) throws ElementDoesNotExistException { - SubmodelElementValueMapperFactory submodelElementValueFactory = new SubmodelElementValueMapperFactory(); + synchronized (submodelLock) { + SubmodelElementValueMapperFactory submodelElementValueFactory = new SubmodelElementValueMapperFactory(); - ValueMapper valueMapper = submodelElementValueFactory.create(getSubmodelElement(idShort)); + ValueMapper valueMapper = submodelElementValueFactory.create(getSubmodelElement(idShort)); - valueMapper.setValue(value); + valueMapper.setValue(value); + } } @Override public void createSubmodelElement(SubmodelElement submodelElement) throws CollidingIdentifierException { - throwIfSubmodelElementExists(submodelElement.getIdShort()); - - List smElements = submodel.getSubmodelElements(); - smElements.add(submodelElement); - submodel.setSubmodelElements(smElements); + synchronized (submodelLock) { + List smElements = submodel.getSubmodelElements(); + throwIfSubmodelElementExists(submodelElement.getIdShort()); + smElements.add(submodelElement); + } } private void throwIfSubmodelElementExists(String submodelElementId) { @@ -142,46 +146,51 @@ private void throwIfSubmodelElementExists(String submodelElementId) { @Override public void createSubmodelElement(String idShortPath, SubmodelElement submodelElement) throws ElementDoesNotExistException, CollidingIdentifierException { - throwIfSubmodelElementExists(getFullIdShortPath(idShortPath, submodelElement.getIdShort())); - - SubmodelElement parentSme = parser.getSubmodelElementFromIdShortPath(idShortPath); - if (parentSme instanceof SubmodelElementList) { - SubmodelElementList list = (SubmodelElementList) parentSme; - List submodelElements = list.getValue(); - submodelElements.add(submodelElement); - list.setValue(submodelElements); - return; - } - if (parentSme instanceof SubmodelElementCollection) { - SubmodelElementCollection collection = (SubmodelElementCollection) parentSme; - List submodelElements = collection.getValue(); - submodelElements.add(submodelElement); - collection.setValue(submodelElements); - return; + synchronized (submodelLock) { + throwIfSubmodelElementExists(getFullIdShortPath(idShortPath, submodelElement.getIdShort())); + + SubmodelElement parentSme = parser.getSubmodelElementFromIdShortPath(idShortPath); + if (parentSme instanceof SubmodelElementList) { + SubmodelElementList list = (SubmodelElementList) parentSme; + List submodelElements = list.getValue(); + submodelElements.add(submodelElement); + list.setValue(submodelElements); + return; + } + if (parentSme instanceof SubmodelElementCollection) { + SubmodelElementCollection collection = (SubmodelElementCollection) parentSme; + List submodelElements = collection.getValue(); + submodelElements.add(submodelElement); + collection.setValue(submodelElements); + } } } @Override public void updateSubmodelElement(String idShortPath, SubmodelElement submodelElement) { - deleteSubmodelElement(idShortPath); + synchronized (submodelLock) { + deleteSubmodelElement(idShortPath); - String idShortPathParentSME = parser.getIdShortPathOfParentElement(idShortPath); - if (idShortPath.equals(idShortPathParentSME)) { - createSubmodelElement(submodelElement); - return; + String idShortPathParentSME = parser.getIdShortPathOfParentElement(idShortPath); + if (idShortPath.equals(idShortPathParentSME)) { + createSubmodelElement(submodelElement); + return; + } + createSubmodelElement(idShortPathParentSME, submodelElement); } - createSubmodelElement(idShortPathParentSME, submodelElement); } @Override public void deleteSubmodelElement(String idShortPath) throws ElementDoesNotExistException { - deleteAssociatedFileIfAny(idShortPath); + synchronized (submodelLock) { + deleteAssociatedFileIfAny(idShortPath); - if (!helper.isNestedIdShortPath(idShortPath)) { - deleteFlatSubmodelElement(idShortPath); - return; + if (!helper.isNestedIdShortPath(idShortPath)) { + deleteFlatSubmodelElement(idShortPath); + return; + } + deleteNestedSubmodelElement(idShortPath); } - deleteNestedSubmodelElement(idShortPath); } private void deleteNestedSubmodelElement(String idShortPath) { @@ -259,44 +268,48 @@ public java.io.File getFileByPath(String idShortPath) throws ElementDoesNotExist @Override public void setFileValue(String idShortPath, String fileName, InputStream inputStream) throws ElementDoesNotExistException, ElementNotAFileException { - SubmodelElement submodelElement = getSubmodelElement(idShortPath); + synchronized (submodelLock) { + SubmodelElement submodelElement = getSubmodelElement(idShortPath); - throwIfSmElementIsNotAFile(submodelElement); + throwIfSmElementIsNotAFile(submodelElement); - File fileSmElement = (File) submodelElement; + File fileSmElement = (File) submodelElement; - if (fileRepository.exists(fileSmElement.getValue())) - fileRepository.delete(fileSmElement.getValue()); + if (fileRepository.exists(fileSmElement.getValue())) + fileRepository.delete(fileSmElement.getValue()); - String uniqueFileName = createUniqueFileName(idShortPath, fileName); + String uniqueFileName = createUniqueFileName(idShortPath, fileName); - FileMetadata fileMetadata = new FileMetadata(uniqueFileName, fileSmElement.getContentType(), inputStream); - - if(fileRepository.exists(fileMetadata.getFileName())) - fileRepository.delete(fileMetadata.getFileName()); + FileMetadata fileMetadata = new FileMetadata(uniqueFileName, fileSmElement.getContentType(), inputStream); - String filePath = fileRepository.save(fileMetadata); + if (fileRepository.exists(fileMetadata.getFileName())) + fileRepository.delete(fileMetadata.getFileName()); - FileBlobValue fileValue = new FileBlobValue(fileSmElement.getContentType(), filePath); + String filePath = fileRepository.save(fileMetadata); - setSubmodelElementValue(idShortPath, fileValue); + FileBlobValue fileValue = new FileBlobValue(fileSmElement.getContentType(), filePath); + + setSubmodelElementValue(idShortPath, fileValue); + } } @Override public void deleteFileValue(String idShortPath) throws ElementDoesNotExistException, ElementNotAFileException, FileDoesNotExistException { - SubmodelElement submodelElement = getSubmodelElement(idShortPath); + synchronized (submodelLock) { + SubmodelElement submodelElement = getSubmodelElement(idShortPath); - throwIfSmElementIsNotAFile(submodelElement); + throwIfSmElementIsNotAFile(submodelElement); - File fileSubmodelElement = (File) submodelElement; - String filePath = fileSubmodelElement.getValue(); + File fileSubmodelElement = (File) submodelElement; + String filePath = fileSubmodelElement.getValue(); - fileRepository.delete(filePath); + fileRepository.delete(filePath); - FileBlobValue fileValue = new FileBlobValue(" ", " "); + FileBlobValue fileValue = new FileBlobValue(" ", " "); - setSubmodelElementValue(idShortPath, fileValue); + setSubmodelElementValue(idShortPath, fileValue); + } } @Override diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestConnectedSubmodelService.java b/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestConnectedSubmodelService.java index a6cda20f4..a285c0e54 100644 --- a/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestConnectedSubmodelService.java +++ b/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestConnectedSubmodelService.java @@ -27,6 +27,7 @@ package org.eclipse.digitaltwin.basyx.submodelservice.client; import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; import org.eclipse.digitaltwin.basyx.submodelrepository.http.DummySubmodelRepositoryComponent; @@ -63,7 +64,7 @@ public static void shutdownSubmodelService() { @After public void removeSubmodelFromRepo() { SubmodelRepository repo = appContext.getBean(SubmodelRepository.class); - repo.getAllSubmodels(NO_LIMIT_PAGINATION_INFO).getResult().stream().map(s -> s.getId()).forEach(repo::deleteSubmodel); + repo.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().stream().map(s -> s.getId()).forEach(repo::deleteSubmodel); } @Override diff --git a/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/operation/Invokable.java b/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/operation/Invokable.java new file mode 100644 index 000000000..6336abb30 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/operation/Invokable.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (C) 2024 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.operation; + +import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable; + +/** + * Interface for invokable operations + * + * @author mateusmolina + */ +@FunctionalInterface +public interface Invokable { + OperationVariable[] invoke(OperationVariable[] arguments); +} \ No newline at end of file diff --git a/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/InvokableOperation.java b/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/operation/InvokableOperation.java similarity index 82% rename from basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/InvokableOperation.java rename to basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/operation/InvokableOperation.java index b78fc65db..d662beecf 100644 --- a/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/InvokableOperation.java +++ b/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/operation/InvokableOperation.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (C) 2023 the Eclipse BaSyx Authors + * Copyright (C) 2024 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 @@ -24,9 +24,7 @@ ******************************************************************************/ -package org.eclipse.digitaltwin.basyx; - -import java.util.function.Function; +package org.eclipse.digitaltwin.basyx.operation; import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable; import org.eclipse.digitaltwin.aas4j.v3.model.builder.OperationBuilder; @@ -38,8 +36,8 @@ * @author schnicke * */ -public class InvokableOperation extends DefaultOperation { - private Function invokable; +public class InvokableOperation extends DefaultOperation implements Invokable { + private Invokable invokable; /** * Invokes the operation with the passed arguments @@ -47,8 +45,9 @@ public class InvokableOperation extends DefaultOperation { * @param arguments * @return */ + @Override public OperationVariable[] invoke(OperationVariable[] arguments) { - return invokable.apply(arguments); + return invokable.invoke(arguments); } /** @@ -56,7 +55,7 @@ public OperationVariable[] invoke(OperationVariable[] arguments) { * * @param invokable */ - public void setInvokable(Function invokable) { + public void setInvokable(Invokable invokable) { this.invokable = invokable; } @@ -72,7 +71,7 @@ protected InvokableOperation newBuildingInstance() { return new InvokableOperation(); } - public Builder invokable(Function invokable) { + public Builder invokable(Invokable invokable) { getBuildingInstance().setInvokable(invokable); return getSelf(); } diff --git a/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/operation/OperationRequestExecutor.java b/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/operation/OperationRequestExecutor.java new file mode 100644 index 000000000..60dcf1e27 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/operation/OperationRequestExecutor.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (C) 2024 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.operation; + +import java.util.ArrayList; +import java.util.Arrays; +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.OperationVariable; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperationResult; + +/** + * Executes an Operation Request + * + * @author mateusmolina + */ +public class OperationRequestExecutor { + + private OperationRequestExecutor() { + } + + public static OperationResult executeOperationRequestSynchronously(Invokable invokable, OperationRequest request) { + List inVars = new ArrayList<>(); + inVars.addAll(request.getInputArguments()); + inVars.addAll(request.getInoutputArguments()); + + List result = Arrays.asList(invokable.invoke(inVars.toArray(new OperationVariable[0]))); + + List outVars = new ArrayList<>(result); + List inoutputVars = new ArrayList<>(); + + if (!request.getInoutputArguments().isEmpty()) { + List inoutputVarsIdShorts = request.getInoutputArguments().stream().map(OperationVariable::getValue).map(SubmodelElement::getIdShort).toList(); + + inoutputVars = result.stream().filter(opVar -> inoutputVarsIdShorts.contains(opVar.getValue().getIdShort())).toList(); + + outVars.removeAll(inoutputVars); + } + + return createSuccessOperationResult(outVars, inoutputVars); + } + + private static OperationResult createSuccessOperationResult(List outputVars, List inoutputVars) { + return new DefaultOperationResult.Builder().success(true).outputArguments(outputVars).inoutputArguments(inoutputVars).build(); + } + +} diff --git a/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/serialization/SubmodelMetadataUtil.java b/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/serialization/SubmodelMetadataUtil.java new file mode 100644 index 000000000..11a21f8fb --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/serialization/SubmodelMetadataUtil.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (C) 2024 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.serialization; + +import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException; +import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.SerializationException; +import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonDeserializer; +import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonSerializer; +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.basyx.core.exceptions.FailedToDeepCopyException; + +public final class SubmodelMetadataUtil { + + private SubmodelMetadataUtil() { + } + + /** + * Returns a new submodel that only contains the metadata of the given submodel. + * + * @param submodel + * @return A new submodel with the metadata of the given submodel. + */ + public static Submodel extractMetadata(Submodel submodel) { + Submodel submodelDeepCopy = deepCopy(submodel); + + submodelDeepCopy.setSubmodelElements(null); + + return submodelDeepCopy; + } + + /** + * Deep copy a submodel. + * + * @param submodel + * @return A deep copy of the submodel. + * + * @throws FailedToDeepCopyException + * + */ + private static Submodel deepCopy(Submodel submodel) { + try { + String submodelAsJSON = new JsonSerializer().write(submodel); + + return new JsonDeserializer().read(submodelAsJSON, Submodel.class); + + } catch (DeserializationException | SerializationException e) { + throw new FailedToDeepCopyException(submodel.getId(), e); + } + } +} diff --git a/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/value/PropertyValue.java b/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/value/PropertyValue.java index 8f707422c..46df7b6b6 100644 --- a/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/value/PropertyValue.java +++ b/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/value/PropertyValue.java @@ -47,4 +47,12 @@ public PropertyValue(String value) { public String getValue() { return value; } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + PropertyValue that = (PropertyValue) obj; + return value.equals(that.value); + } } diff --git a/basyx.submodelservice/basyx.submodelservice-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/DummySubmodelFactory.java b/basyx.submodelservice/basyx.submodelservice-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/DummySubmodelFactory.java index f8696fc9f..a6086000d 100644 --- a/basyx.submodelservice/basyx.submodelservice-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/DummySubmodelFactory.java +++ b/basyx.submodelservice/basyx.submodelservice-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/DummySubmodelFactory.java @@ -110,6 +110,19 @@ public class DummySubmodelFactory { public static Collection getSubmodels() { return Lists.newArrayList(createTechnicalDataSubmodel(), createOperationalDataSubmodel(), createSimpleDataSubmodel()); } + + public static Collection getSubmodelsBySemanticid(String semanticId) { + Collection submodels = Lists.newArrayList(createTechnicalDataSubmodel(), createOperationalDataSubmodel(), createSimpleDataSubmodel()); + + return submodels.stream() + .filter((submodel) -> { + return submodel.getSemanticId() != null && + submodel.getSemanticId().getKeys().stream().filter((key) -> { + return key.getValue().equals(semanticId); + }).findAny().isPresent(); + }) + .collect(Collectors.toList()); + } public static Submodel createSubmodelWithAllSubmodelElements() { List submodelElements = getAllSubmodelElementsList(); diff --git a/basyx.submodelservice/basyx.submodelservice-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/SubmodelServiceHelper.java b/basyx.submodelservice/basyx.submodelservice-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/SubmodelServiceHelper.java index 8c6ef3f31..7fe1161e4 100644 --- a/basyx.submodelservice/basyx.submodelservice-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/SubmodelServiceHelper.java +++ b/basyx.submodelservice/basyx.submodelservice-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/SubmodelServiceHelper.java @@ -67,7 +67,7 @@ import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSpecificAssetId; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList; -import org.eclipse.digitaltwin.basyx.InvokableOperation; +import org.eclipse.digitaltwin.basyx.operation.InvokableOperation; import com.google.common.collect.Lists; diff --git a/basyx.submodelservice/basyx.submodelservice-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/SubmodelServiceSuite.java b/basyx.submodelservice/basyx.submodelservice-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/SubmodelServiceSuite.java index e3f1e43b6..94820e893 100644 --- a/basyx.submodelservice/basyx.submodelservice-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/SubmodelServiceSuite.java +++ b/basyx.submodelservice/basyx.submodelservice-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/SubmodelServiceSuite.java @@ -78,8 +78,6 @@ * */ public abstract class SubmodelServiceSuite { - protected static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(null, null); - protected abstract SubmodelService getSubmodelService(Submodel submodel); /** @@ -107,7 +105,7 @@ public void getSubmodelElements() { Submodel technicalData = DummySubmodelFactory.createTechnicalDataSubmodel(); SubmodelService smService = getSubmodelService(technicalData); - assertTrue(technicalData.getSubmodelElements().containsAll(smService.getSubmodelElements(NO_LIMIT_PAGINATION_INFO).getResult())); + assertTrue(technicalData.getSubmodelElements().containsAll(smService.getSubmodelElements(PaginationInfo.NO_LIMIT).getResult())); } @Test 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 7248c6904..e95df7c9d 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 @@ -27,16 +27,12 @@ import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; 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.OperationVariable; import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; -import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperationResult; import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException; import org.eclipse.digitaltwin.basyx.core.exceptions.ElementNotAFileException; import org.eclipse.digitaltwin.basyx.core.exceptions.FileDoesNotExistException; @@ -45,7 +41,10 @@ import org.eclipse.digitaltwin.basyx.http.pagination.Base64UrlEncodedCursor; import org.eclipse.digitaltwin.basyx.http.pagination.PagedResult; import org.eclipse.digitaltwin.basyx.http.pagination.PagedResultPagingMetadata; +import org.eclipse.digitaltwin.basyx.operation.OperationRequestExecutor; +import org.eclipse.digitaltwin.basyx.operation.Invokable; import org.eclipse.digitaltwin.basyx.pagination.GetSubmodelElementsResult; +import org.eclipse.digitaltwin.basyx.serialization.SubmodelMetadataUtil; import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService; import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelElementValue; import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelValueOnly; @@ -70,7 +69,6 @@ @RestController public class SubmodelServiceHTTPApiController implements SubmodelServiceHTTPApi { - private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); private SubmodelService service; @Autowired @@ -165,9 +163,8 @@ public ResponseEntity getSubmodelMetadata(@Parameter(in = ParameterIn. "core" }, defaultValue = "deep")) @Valid @RequestParam(value = "level", required = false, defaultValue = "deep") String level) { Submodel submodel = service.getSubmodel(); - submodel.setSubmodelElements(null); - return new ResponseEntity(submodel, HttpStatus.OK); + return new ResponseEntity<>(SubmodelMetadataUtil.extractMetadata(submodel), HttpStatus.OK); } @Override @@ -177,7 +174,7 @@ public ResponseEntity getSubmodelValueOnly( @Parameter(in = ParameterIn.QUERY, description = "Determines to which extent the resource is being serialized", schema = @Schema(allowableValues = { "withBlobValue", "withoutBlobValue" }, defaultValue = "withoutBlobValue")) @Valid @RequestParam(value = "extent", required = false, defaultValue = "withoutBlobValue") String extent) { - SubmodelValueOnly result = new SubmodelValueOnly(service.getSubmodelElements(NO_LIMIT_PAGINATION_INFO).getResult()); + SubmodelValueOnly result = new SubmodelValueOnly(service.getSubmodelElements(PaginationInfo.NO_LIMIT).getResult()); return new ResponseEntity(result, HttpStatus.OK); } @@ -237,30 +234,9 @@ public ResponseEntity patchSubmodelValueOnly(@Parameter(in = ParameterIn.D public 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) { - List inVars = new ArrayList<>(); - inVars.addAll(body.getInputArguments()); - inVars.addAll(body.getInoutputArguments()); - - List result = Arrays.asList(service.invokeOperation(idShortPath, inVars.toArray(new OperationVariable[0]))); - - List outVars = new ArrayList<>(result); - List inoutputVars = new ArrayList<>(); - - if (!body.getInoutputArguments().isEmpty()) { - List inoutputVarsIdShorts = body.getInoutputArguments().stream().map(OperationVariable::getValue).map(SubmodelElement::getIdShort).toList(); - - inoutputVars = result.stream().filter(opVar -> inoutputVarsIdShorts.contains(opVar.getValue().getIdShort())).toList(); - outVars.removeAll(inoutputVars); - } - - return ResponseEntity.ok(createOperationResult(outVars, inoutputVars)); - } - - private OperationResult createOperationResult(List outputVars, List inoutputVars) { - return new DefaultOperationResult.Builder() - .outputArguments(outputVars).inoutputArguments(inoutputVars) - .build(); + Invokable invokable = inArgs -> service.invokeOperation(idShortPath, inArgs); + return ResponseEntity.ok(OperationRequestExecutor.executeOperationRequestSynchronously(invokable, body)); } private String getEncodedCursorFromCursorResult(CursorResult cursorResult) { @@ -321,5 +297,4 @@ private void closeInputStream(InputStream fileInputstream) { e.printStackTrace(); } } - } diff --git a/basyx.submodelservice/basyx.submodelservice-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceSubmodelTestSuiteHTTP.java b/basyx.submodelservice/basyx.submodelservice-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceSubmodelTestSuiteHTTP.java index e75db2f7f..d07c164d6 100644 --- a/basyx.submodelservice/basyx.submodelservice-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceSubmodelTestSuiteHTTP.java +++ b/basyx.submodelservice/basyx.submodelservice-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceSubmodelTestSuiteHTTP.java @@ -25,6 +25,8 @@ package org.eclipse.digitaltwin.basyx.submodelservice.http; +import static org.junit.Assert.assertEquals; + import java.io.FileNotFoundException; import java.io.IOException; @@ -32,6 +34,7 @@ import org.apache.hc.core5.http.ParseException; import org.eclipse.digitaltwin.basyx.http.serialization.BaSyxHttpTestUtils; import org.junit.Test; +import org.springframework.http.HttpStatus; /** * Base testsuite for all Submodel Service HTTP tests related to the Submodel @@ -63,6 +66,19 @@ public void getSubmodelMetadata() throws IOException, ParseException { BaSyxHttpTestUtils.assertSameJSONContent(expected, submodelMetadata); } + @Test + public void getSubmodelMetadata_preservesSubmodel() throws IOException, ParseException { + + String submodelBefore = BaSyxHttpTestUtils.getResponseAsString(BaSyxHttpTestUtils.executeGetOnURL(getURL())); + + CloseableHttpResponse response = BaSyxHttpTestUtils.executeGetOnURL(getSubmodelMetadataURL()); + assertEquals(HttpStatus.OK.value(), response.getCode()); + + String submodelAfter = BaSyxHttpTestUtils.getResponseAsString(BaSyxHttpTestUtils.executeGetOnURL(getURL())); + + BaSyxHttpTestUtils.assertSameJSONContent(submodelBefore, submodelAfter); + } + private String getSubmodelMetadataURL() { return getURL() + "/$metadata"; } diff --git a/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/operation/result-inout.json b/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/operation/result-inout.json index 3aa563302..19b9d9bdc 100644 --- a/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/operation/result-inout.json +++ b/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/operation/result-inout.json @@ -1,4 +1,5 @@ { + "success": true, "inoutputArguments": [ { "value": { diff --git a/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/operation/result.json b/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/operation/result.json index 7a48f8c6a..20cf503ca 100644 --- a/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/operation/result.json +++ b/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/operation/result.json @@ -1,4 +1,5 @@ { + "success": true, "outputArguments": [ { "value": { diff --git a/basyx.submodelservice/basyx.submodelservice.example/Dockerfile b/basyx.submodelservice/basyx.submodelservice.example/Dockerfile index 1c3e9648d..99b045565 100644 --- a/basyx.submodelservice/basyx.submodelservice.example/Dockerfile +++ b/basyx.submodelservice/basyx.submodelservice.example/Dockerfile @@ -1,4 +1,6 @@ -FROM amazoncorretto:17 +FROM eclipse-temurin:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY USER nobody WORKDIR /application ARG JAR_FILE=target/*-exec.jar diff --git a/basyx.submodelservice/basyx.submodelservice.example/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/example/ExampleSubmodelFactory.java b/basyx.submodelservice/basyx.submodelservice.example/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/example/ExampleSubmodelFactory.java index 7002dbc5c..ac0c07261 100644 --- a/basyx.submodelservice/basyx.submodelservice.example/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/example/ExampleSubmodelFactory.java +++ b/basyx.submodelservice/basyx.submodelservice.example/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/example/ExampleSubmodelFactory.java @@ -46,7 +46,7 @@ import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultReference; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel; -import org.eclipse.digitaltwin.basyx.InvokableOperation; +import org.eclipse.digitaltwin.basyx.operation.InvokableOperation; import com.google.common.collect.Lists; diff --git a/ci/keycloak/Dockerfile b/ci/keycloak/Dockerfile index 54bc49c28..8f4ff346f 100644 --- a/ci/keycloak/Dockerfile +++ b/ci/keycloak/Dockerfile @@ -1,5 +1,6 @@ FROM quay.io/keycloak/keycloak:22.0.0 - +ARG HTTP_PROXY +ARG HTTPS_PROXY # Make the realm configuration available for import COPY /realm/BaSyx-realm.json /opt/keycloak_import/ diff --git a/ci/keycloak/Dockerfile.keycloak b/ci/keycloak/Dockerfile.keycloak index f7f0af270..e9eaa153f 100644 --- a/ci/keycloak/Dockerfile.keycloak +++ b/ci/keycloak/Dockerfile.keycloak @@ -1,5 +1,7 @@ # syntax=docker/dockerfile:1 FROM maven:3-eclipse-temurin-17-alpine as build +ARG HTTP_PROXY +ARG HTTPS_PROXY WORKDIR /workspace COPY ./initializer/pom.xml /workspace/pom.xml COPY ./initializer/src /workspace/src @@ -7,6 +9,8 @@ COPY ./realm/BaSyx-realm.json /workspace/BaSyx-realm.json RUN mvn install FROM keycloak/keycloak:24.0.4 +ARG HTTP_PROXY +ARG HTTPS_PROXY COPY --from=build /workspace/target/org.eclipse.digitaltwin.basyx.v3.clients-keycloak-issuer-initializer.jar /opt/keycloak/providers/issuer-initializer.jar COPY --from=build /workspace/BaSyx-realm.json /opt/keycloak/data/import/BaSyx-realm.json diff --git a/ci/keycloak/realm/BaSyx-realm.json b/ci/keycloak/realm/BaSyx-realm.json index b893decf7..2b28f4c58 100644 --- a/ci/keycloak/realm/BaSyx-realm.json +++ b/ci/keycloak/realm/BaSyx-realm.json @@ -511,14 +511,14 @@ "attributes" : { } } ], "basyx-client-api" : [ { - "id" : "2dd4b9b1-748f-43f3-b62b-048c92ae79d1", + "id" : "2dd4b9b1-748f-43f3-b62b-048c92ae79d1", "name" : "basyx-creator", "description" : "", "composite" : false, "clientRole" : true, "containerId" : "3fb3e5e5-dbd8-4d51-b964-746c5b2181a4", "attributes" : { } - }, { + }, { "id" : "ba077409-1b5d-4fc8-b20e-10389507fb75", "name" : "basyx-admin", "description" : "", @@ -621,7 +621,7 @@ "clientRole" : true, "containerId" : "049e1323-6efb-4543-bc52-566cd292732a", "attributes" : { } - } ], + } ], "basyx-demo" : [ ], "workstation-1" : [ { "id" : "914a18c6-4f14-418f-99e0-bfdcf604ac01", @@ -629,7 +629,7 @@ "composite" : false, "clientRole" : true, "containerId" : "96031210-9e6c-4252-a22e-e81a47e30d65", - "attributes" : { } + "attributes" : { } } ] } }, @@ -985,9 +985,6 @@ "disableableCredentialTypes" : [ ], "requiredActions" : [ ], "realmRoles" : [ "basyx-reader", "default-roles-basyx" ], - "clientRoles" : { - "basyx-client-api" : [ "basyx-user" ] - }, "notBefore" : 0, "groups" : [ ] }, { @@ -1387,9 +1384,12 @@ "disableableCredentialTypes" : [ ], "requiredActions" : [ ], "realmRoles" : [ "visitor", "default-roles-basyx" ], + "clientRoles" : { + "basyx-client-api" : [ "basyx-user" ] + }, "notBefore" : 0, "groups" : [ ] - }, { + }, { "id" : "a19abcac-34d5-46bb-a604-b07dc234e80f", "createdTimestamp" : 1715582034760, "username" : "service-account-workstation-1", @@ -1400,7 +1400,7 @@ "credentials" : [ ], "disableableCredentialTypes" : [ ], "requiredActions" : [ ], - "realmRoles" : [ "basyx-reader", "basyx-deleter", "basyx-updater", "basyx-creator", "default-roles-basyx", "admin" ], + "realmRoles" : [ "basyx-reader", "basyx-deleter", "basyx-updater", "admin", "basyx-creator", "default-roles-basyx" ], "clientRoles" : { "workstation-1" : [ "uma_protection" ] }, @@ -1428,7 +1428,7 @@ "requiredActions" : [ ], "realmRoles" : [ "default-roles-basyx" ], "notBefore" : 0, - "groups" : [ "/BaSyxGroup" ] + "groups" : [ "/BaSyxGroup" ] } ], "scopeMappings" : [ { "clientScope" : "offline_access", @@ -1575,7 +1575,7 @@ "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { - "id" : "615f29de-5f3d-4384-8b71-7351ff2c2a32", + "id" : "615f29de-5f3d-4384-8b71-7351ff2c2a32", "clientId" : "basyx-demo", "name" : "", "description" : "", @@ -1601,9 +1601,10 @@ "protocol" : "openid-connect", "attributes" : { "oidc.ciba.grant.enabled" : "false", - "oauth2.device.authorization.grant.enabled" : "false", "client.secret.creation.time" : "1716897911", "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", "backchannel.logout.revoke.offline.tokens" : "false" }, "authenticationFlowBindingOverrides" : { }, @@ -1611,7 +1612,7 @@ "nodeReRegistrationTimeout" : -1, "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { + }, { "id" : "45f21c3d-4e85-466f-984f-d7bd47392453", "clientId" : "broker", "name" : "${client_broker}", @@ -1713,7 +1714,7 @@ } ], "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { + }, { "id" : "96031210-9e6c-4252-a22e-e81a47e30d65", "clientId" : "workstation-1", "name" : "Workstation 1", @@ -1741,9 +1742,10 @@ "protocol" : "openid-connect", "attributes" : { "oidc.ciba.grant.enabled" : "false", - "oauth2.device.authorization.grant.enabled" : "false", "client.secret.creation.time" : "1715582034", "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", "backchannel.logout.revoke.offline.tokens" : "false" }, "authenticationFlowBindingOverrides" : { }, @@ -1757,6 +1759,7 @@ "consentRequired" : false, "config" : { "user.session.note" : "clientAddress", + "userinfo.token.claim" : "true", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "clientAddress", @@ -1770,6 +1773,7 @@ "consentRequired" : false, "config" : { "user.session.note" : "clientHost", + "userinfo.token.claim" : "true", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "clientHost", @@ -1783,6 +1787,7 @@ "consentRequired" : false, "config" : { "user.session.note" : "client_id", + "userinfo.token.claim" : "true", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "client_id", @@ -1790,7 +1795,15 @@ } } ], "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], + "authorizationSettings" : { + "allowRemoteResourceManagement" : true, + "policyEnforcementMode" : "ENFORCING", + "resources" : [ ], + "policies" : [ ], + "scopes" : [ ], + "decisionStrategy" : "UNANIMOUS" + } } ], "clientScopes" : [ { "id" : "e0f355da-f9ff-4104-b305-043b0188747b", @@ -2288,7 +2301,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper" ] } }, { "id" : "7256d195-1e91-4f63-a9c4-6bef95243a92", @@ -2325,7 +2338,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "saml-user-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper" ] } }, { "id" : "face2c9e-4d23-44e2-9a09-74e1d8448bd3", diff --git a/docs/Roadmap.md b/docs/Roadmap.md index f828e087e..f706ef5c5 100644 --- a/docs/Roadmap.md +++ b/docs/Roadmap.md @@ -2,17 +2,22 @@ Here, a list of features, components and general updates that are planned in the future is described. Please note that this list is not exhaustive and repriorization might happen. ## BaSyx AAS Core -- Implementation of AAS API Spec as well as upcoming features (continous) +- Implementation of AAS API Spec as well as upcoming features (continuous) - AAS Client SDK (continuous) -- AASX File Server with MongoDB and InMemory Persistency (Q2/2024) -- Dynamic integration of features/backends in AAS Infrastructure OTS components (Q2/2024) -- Dynamic Configuration of RBAC Security based on Submodels (Q2/2024) -- Code Generation based on AASX files (Client, ...) (Q3/2024) +- Dynamic integration of features/backends in AAS Infrastructure OTS components (Q4/2024) +- ABAC support for all BaSyx components (Q1/2025) +- Code Generation based on AASX files (Client, ...) (Q1/2025) +- Gateway component for distribution of AASs and submodels across distributed registries and repositories (Q1/2025) + - Registry Integration will be moved to this component + - AAS Preconfiguration will move to this component + - AAS Discovery Integration will be implemented as part of this component +- Support for the Eclipse Dataspace Connector (Q2/2025) +- Benchmarking tool for AAS API components (Q2/2025) ## BaSyx AAS Integration Components ### DataBridge For more details, see [basyx-databridge](https://github.com/eclipse-basyx/basyx-databridge) -- Bi-directional Data Exchange, e.g., with OPC UA (Q2/2024) -- Support for Asset Interface Description Submodel and Asset Interface Mapping Configuration Submodel (Q2/2024) -- GUI-based Configuration (Q2/2024) -- Integration of more complex data transformation, e.g., data aggregation of historic data (Q2/2024) +- Support for Asset Interface Description Submodel and Asset Interface Mapping Configuration Submodel (Q4/2024) +- GUI-based Configuration (Q1/2025) +- RBAC support for the DataBridge (Q1/2025) +- Integration of more complex data transformations, e.g., data aggregation of historic data (Q2/2025) diff --git a/examples/BaSyxClient/.gitignore b/examples/BaSyxClient/.gitignore new file mode 100644 index 000000000..9669b63ae --- /dev/null +++ b/examples/BaSyxClient/.gitignore @@ -0,0 +1 @@ +ingest/ \ No newline at end of file diff --git a/examples/BaSyxClient/README.md b/examples/BaSyxClient/README.md new file mode 100644 index 000000000..e4357b33d --- /dev/null +++ b/examples/BaSyxClient/README.md @@ -0,0 +1,56 @@ +# BaSyx Client Example + +```mermaid +sequenceDiagram + participant ERP as Legacy ERP + participant Client as BaSyx Client + participant Basyx as BaSyx Infrastructure + + loop Every $gen_interval seconds + ERP->>Client: New Inventory CSV Available + Client->>Client: Read & Parse CSV Data + Client->>Basyx: Update the Warehouse with the current Motors + Client->>Basyx: Update Motor AAS Submodels + alt New Motor Added + Client->>Basyx: Create/Register Motor AAS Submodels + end + alt Motor Sold + Client->>Basyx: Mark Motor as Sold + Client->>Basyx: Remove Motor from the Warehouse + end + end +``` + +This use case demonstrates an integration between a **legacy ERP system** and the **BaSyx** infrastructure. The ERP system, simulated by a Python application, periodically exports warehouse inventory data in the form of CSV files. These CSV exports contain key details about motors in stock, under maintenance, or sold. + +The **BaSyx Client** serves as an adapter application that interfaces between the legacy ERP and BaSyx's registry and repository servers. The client uses the **BaSyx Java Client libraries** to perform the following operations: + +- **Ingest Inventory Data:** The BaSyx Client monitors the CSV exports from the ERP. When a new CSV file is generated, the client reads and parses the inventory data. +- **Create and Register Motors:** The client creates and registers the **AASs** and **submodels** in the **BaSyx infrastructure** +- **Update Motors:** The client updates the Motors' **submodels** in the **BaSyx infrastructure**, reflecting their current status (in stock, under maintenance, sold). +- **Update Warehouse:** The client updates the Warehouse **AAS** with the motors currently stored in it. +- **Lifecycle Management:** Motors marked as "Sold" remain in the system but are no longer updated or maintained. Their status is reflected in the AAS submodels. + +## Running the example + +To run the example containers, you need to have Docker installed on your device. + +1. Open a terminal in this folder +2. Run the following command to start the BaSyx containers: + +```sh +docker-compose up -d +``` + +### Endpoints + +- AAS Environment: [http://localhost:8081](http://localhost:8081) +- AAS Registry: [http://localhost:8082](http://localhost:8082) +- Submodel Registry: [http://localhost:8083](http://localhost:8083) +- AAS Web GUI: [http://localhost:3000](http://localhost:3000) + +### How to use + +1. Open the AAS Web GUI at [http://localhost:3000](http://localhost:3000) +2. Observe motors being added and updated based on incoming exports from the `legacy-erp` service (visible in the folder ./ingest). +3. In the Warehouse AAS you will find an overview of the motors currently stored \ No newline at end of file diff --git a/examples/BaSyxClient/basyx-client/Dockerfile b/examples/BaSyxClient/basyx-client/Dockerfile new file mode 100644 index 000000000..4dff8d0a3 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/Dockerfile @@ -0,0 +1,14 @@ +FROM maven:3.8.1-openjdk-17 AS build +ARG HTTP_PROXY +ARG HTTPS_PROXY +WORKDIR /app +COPY pom.xml . +COPY src src +RUN mvn clean package -DskipTests + +FROM openjdk:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY +WORKDIR /app +COPY --from=build /app/target/*-jar-with-dependencies.jar app.jar +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/examples/BaSyxClient/basyx-client/HELP.md b/examples/BaSyxClient/basyx-client/HELP.md new file mode 100644 index 000000000..6ed8cfcd8 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/HELP.md @@ -0,0 +1,19 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.2.4/maven-plugin/reference/html/) +* [Create an OCI image](https://docs.spring.io/spring-boot/docs/3.2.4/maven-plugin/reference/html/#build-image) +* [Spring Web](https://docs.spring.io/spring-boot/docs/3.2.4/reference/htmlsingle/index.html#web) +* [Spring Boot Actuator](https://docs.spring.io/spring-boot/docs/3.2.4/reference/htmlsingle/index.html#actuator) + +### Guides +The following guides illustrate how to use some features concretely: + +* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) +* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) +* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) +* [Building a RESTful Web Service with Spring Boot Actuator](https://spring.io/guides/gs/actuator-service/) + diff --git a/examples/BaSyxClient/basyx-client/mvnw b/examples/BaSyxClient/basyx-client/mvnw new file mode 100644 index 000000000..66df28542 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/examples/BaSyxClient/basyx-client/mvnw.cmd b/examples/BaSyxClient/basyx-client/mvnw.cmd new file mode 100644 index 000000000..95ba6f54a --- /dev/null +++ b/examples/BaSyxClient/basyx-client/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/examples/BaSyxClient/basyx-client/pom.xml b/examples/BaSyxClient/basyx-client/pom.xml new file mode 100644 index 000000000..ffbc422fe --- /dev/null +++ b/examples/BaSyxClient/basyx-client/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + org.eclipse.digitaltwin.basyx + example-basyx-client + 0.0.1-SNAPSHOT + examples.basyxclient + BaSyx Client Example + + 17 + 17 + + + + + org.eclipse.digitaltwin.basyx + basyx.aasenvironment-client + 2.0.0-milestone-04 + + + + com.opencsv + opencsv + 5.9 + + + + + + + maven-assembly-plugin + + + package + + single + + + + + + jar-with-dependencies + + + + + org.eclipse.digitaltwin.basyx.examples.basyxclient.Application + + + + + + + + \ No newline at end of file diff --git a/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/Application.java b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/Application.java new file mode 100644 index 000000000..0d3f06281 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/Application.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (C) 2024 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.examples.basyxclient; + +import java.nio.file.Path; +public class Application { + + static final Path DIR_TO_WATCH = Path.of("/ingest"); + public static void main(String[] args) { + new Thread(DirectoryWatcherFactory.build(DIR_TO_WATCH)).start(); + } + +} diff --git a/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/DirectoryWatcherFactory.java b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/DirectoryWatcherFactory.java new file mode 100644 index 000000000..618449bb3 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/DirectoryWatcherFactory.java @@ -0,0 +1,45 @@ +package org.eclipse.digitaltwin.basyx.examples.basyxclient; + +import java.nio.file.Path; +import java.util.List; + +import org.eclipse.digitaltwin.basyx.aasenvironment.client.ConnectedAasManager; +import org.eclipse.digitaltwin.basyx.examples.basyxclient.configuration.BasyxSettings; +import org.eclipse.digitaltwin.basyx.examples.basyxclient.processing.BaSyxSyncService; +import org.eclipse.digitaltwin.basyx.examples.basyxclient.processing.BaSyxWarehouseService; +import org.eclipse.digitaltwin.basyx.examples.basyxclient.processing.DirectoryWatcher; +import org.eclipse.digitaltwin.basyx.examples.basyxclient.processing.EntryProcessor; + +public final class DirectoryWatcherFactory { + + private DirectoryWatcherFactory() { + } + + public static DirectoryWatcher build(Path pathToWatch) { + BasyxSettings settings = buildBasyxSettingsFromEnv(); + ConnectedAasManager connectedAasManager = buildConnectedAasManager(settings); + List entryProcessors = buildEntryProcessors(connectedAasManager); + + return new DirectoryWatcher(entryProcessors, pathToWatch); + } + + static BasyxSettings buildBasyxSettingsFromEnv() { + String aasRegistryBaseUrl = System.getenv("AAS_REGISTRY_BASE_URL"); + String aasRepositoryBaseUrl = System.getenv("AAS_REPOSITORY_BASE_URL"); + String submodelRegistryBaseUrl = System.getenv("SUBMODEL_REGISTRY_BASE_URL"); + String submodelRepositoryBaseUrl = System.getenv("SUBMODEL_REPOSITORY_BASE_URL"); + + return new BasyxSettings(aasRepositoryBaseUrl, submodelRepositoryBaseUrl, aasRegistryBaseUrl, submodelRegistryBaseUrl); + } + + static ConnectedAasManager buildConnectedAasManager(BasyxSettings settings) { + return new ConnectedAasManager(settings.aasRegistryBaseUrl(), settings.aasRepositoryBaseUrl(), settings.submodelRegistryBaseUrl(), settings.submodelRepositoryBaseUrl()); + } + + static List buildEntryProcessors(ConnectedAasManager connectedAasManager) { + BaSyxSyncService syncService = new BaSyxSyncService(connectedAasManager); + BaSyxWarehouseService warehouseService = new BaSyxWarehouseService(connectedAasManager); + + return List.of(syncService, warehouseService); + } +} diff --git a/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/configuration/BasyxSettings.java b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/configuration/BasyxSettings.java new file mode 100644 index 000000000..ba4f26cb3 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/configuration/BasyxSettings.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (C) 2024 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.examples.basyxclient.configuration; + +public record BasyxSettings(String aasRepositoryBaseUrl, String submodelRepositoryBaseUrl, String aasRegistryBaseUrl, String submodelRegistryBaseUrl) { +} \ No newline at end of file diff --git a/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/model/MotorEntry.java b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/model/MotorEntry.java new file mode 100644 index 000000000..ff7a6dd4c --- /dev/null +++ b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/model/MotorEntry.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * Copyright (C) 2024 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.examples.basyxclient.model; + +import java.time.LocalDateTime; + +import org.eclipse.digitaltwin.basyx.examples.basyxclient.utils.LocalDateTimeConverter; + +import com.opencsv.bean.CsvBindByName; +import com.opencsv.bean.CsvCustomBindByName; + +public class MotorEntry { + @CsvBindByName + private String motorId; + + @CsvBindByName + private String motorType; + + @CsvBindByName + private String manufacturer; + + @CsvCustomBindByName(converter = LocalDateTimeConverter.class) + private LocalDateTime purchaseDate; + + @CsvBindByName + private String location; + + @CsvCustomBindByName(converter = LocalDateTimeConverter.class) + private LocalDateTime lastMaintenance; + + @CsvCustomBindByName(converter = LocalDateTimeConverter.class) + private LocalDateTime maintenanceSchedule; + + @CsvCustomBindByName(converter = LocalDateTimeConverter.class) + private LocalDateTime warrantyPeriod; + + @CsvBindByName + private String status; + + @CsvCustomBindByName(converter = LocalDateTimeConverter.class) + private LocalDateTime dateSold; + + public String getMotorId() { + return motorId; + } + + public void setMotorId(String motorId) { + this.motorId = motorId; + } + + public String getMotorType() { + return motorType; + } + + public void setMotorType(String motorType) { + this.motorType = motorType; + } + + public String getManufacturer() { + return manufacturer; + } + + public void setManufacturer(String manufacturer) { + this.manufacturer = manufacturer; + } + + public LocalDateTime getPurchaseDate() { + return purchaseDate; + } + + public void setPurchaseDate(LocalDateTime purchaseDate) { + this.purchaseDate = purchaseDate; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public LocalDateTime getLastMaintenance() { + return lastMaintenance; + } + + public void setLastMaintenance(LocalDateTime lastMaintenance) { + this.lastMaintenance = lastMaintenance; + } + + public LocalDateTime getMaintenanceSchedule() { + return maintenanceSchedule; + } + + public void setMaintenanceSchedule(LocalDateTime maintenanceSchedule) { + this.maintenanceSchedule = maintenanceSchedule; + } + + public LocalDateTime getWarrantyPeriod() { + return warrantyPeriod; + } + + public void setWarrantyPeriod(LocalDateTime warrantyPeriod) { + this.warrantyPeriod = warrantyPeriod; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public LocalDateTime getDateSold() { + return dateSold; + } + + public void setDateSold(LocalDateTime dateSold) { + this.dateSold = dateSold; + } +} diff --git a/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/BaSyxSyncService.java b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/BaSyxSyncService.java new file mode 100644 index 000000000..b4b4b58f1 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/BaSyxSyncService.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (C) 2024 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.examples.basyxclient.processing; + +import java.util.List; + +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.ConnectedAasManager; +import org.eclipse.digitaltwin.basyx.aasservice.AasService; +import org.eclipse.digitaltwin.basyx.examples.basyxclient.model.MotorEntry; +import org.eclipse.digitaltwin.basyx.submodelservice.client.ConnectedSubmodelService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BaSyxSyncService implements EntryProcessor { + + private final Logger logger = LoggerFactory.getLogger(BaSyxSyncService.class); + + private final ConnectedAasManager connectedAasManager; + + public BaSyxSyncService(ConnectedAasManager connectedAasManager) { + this.connectedAasManager = connectedAasManager; + } + + @Override + public void process(List entries) { + entries.forEach(this::syncWithBaSyx); + } + + public void syncWithBaSyx(MotorEntry motorEntry) { + if (!entryAlreadyPushed(motorEntry)) + pushEntryAasAndSubmodels(motorEntry); + + updateSubmodelsBasedOnEntry(motorEntry); + } + + public AasService pushEntryAasAndSubmodels(MotorEntry entry) { + logger.info("Pushing entry: {}", entry.getMotorId()); + + AssetAdministrationShell aas = MotorAasBuilder.fromEntry(entry); + connectedAasManager.createAas(aas); + + MotorAasBuilder.buildSubmodelsFromEntry(entry).forEach(sm -> connectedAasManager.createSubmodelInAas(aas.getId(), sm)); + + return connectedAasManager.getAasService(aas.getId()); + } + + public boolean entryAlreadyPushed(MotorEntry entry) { + try { + connectedAasManager.getAasService(MotorAasBuilder.buildIdFromEntry(entry)); + return true; + } catch (Exception e) { + return false; + } + } + + public void updateSubmodelsBasedOnEntry(MotorEntry motorEntry) { + logger.debug("Updating submodels for entry: {}", motorEntry.getMotorId()); + + List toUpdateSms = MotorAasBuilder.buildSubmodelsFromEntry(motorEntry); + toUpdateSms.forEach(sm -> { + ConnectedSubmodelService service = connectedAasManager.getSubmodelService(sm.getId()); + sm.getSubmodelElements().forEach(se -> service.updateSubmodelElement(se.getIdShort(), se)); + }); + } + +} diff --git a/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/BaSyxWarehouseService.java b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/BaSyxWarehouseService.java new file mode 100644 index 000000000..9baad52c0 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/BaSyxWarehouseService.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * Copyright (C) 2024 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.examples.basyxclient.processing; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.KeyTypes; +import org.eclipse.digitaltwin.aas4j.v3.model.ReferenceElement; +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultKey; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultReference; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultReferenceElement; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.ConnectedAasManager; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.examples.basyxclient.model.MotorEntry; +import org.eclipse.digitaltwin.basyx.submodelservice.client.ConnectedSubmodelService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BaSyxWarehouseService implements EntryProcessor { + + private final Logger logger = LoggerFactory.getLogger(BaSyxWarehouseService.class); + + private final ConnectedAasManager connectedAasManager; + + // managing only one warehouse + private static final int WAREHOUSE_NUM = 0; + + private ConnectedSubmodelService overviewSubmodelService; + + public BaSyxWarehouseService(ConnectedAasManager connectedAasManager) { + this.connectedAasManager = connectedAasManager; + } + + @Override + public void process(List entries) { + if (!isWarehouldAlreadyPushed(WAREHOUSE_NUM)) { + logger.info("No Warehouse found yet. Pushing a new warehouse to BaSyx..."); + pushWarehouseToBaSyx(); + } + + if (overviewSubmodelService == null) + return; + + entries.forEach(this::updateEntryAisle); + } + + public void pushWarehouseToBaSyx() { + + AssetAdministrationShell warehouseAas = WarehouseAasBuilder.build(WAREHOUSE_NUM); + connectedAasManager.createAas(warehouseAas); + + Submodel overviewSm = WarehouseAasBuilder.buildOverviewSm(WAREHOUSE_NUM); + connectedAasManager.createSubmodelInAas(warehouseAas.getId(), overviewSm); + + overviewSubmodelService = connectedAasManager.getSubmodelService(overviewSm.getId()); + } + + public void updateEntryAisle(MotorEntry entry) { + String actualAisleIdShort = entry.getLocation(); + String motorId = MotorAasBuilder.buildIdFromEntry(entry); + + if (!isInWarehouse(actualAisleIdShort)) { + removeMotorFromWarehouseIfPresent(motorId); + return; + } + + SubmodelElementCollection aisle = (SubmodelElementCollection) overviewSubmodelService.getSubmodelElement(actualAisleIdShort); + List motorRefs = getMotorRefs(aisle); + + if (!isNewMotor(motorRefs, motorId)) + return; + + List newMotorRefs = addMotorToAisle(motorRefs, motorId); + List newSE = newMotorRefs.stream().map(SubmodelElement.class::cast).toList(); + + aisle.setValue(newSE); + overviewSubmodelService.updateSubmodelElement(actualAisleIdShort, aisle); + + logger.info("Motor {} added to warehouse.", motorId); + } + + private void removeMotorFromWarehouseIfPresent(String motorId) { + overviewSubmodelService.getSubmodelElements(new PaginationInfo(100, null)).getResult().stream().forEach(aisle -> removeMotorFromAisleIfPresent((SubmodelElementCollection) aisle, motorId)); + } + + private void removeMotorFromAisleIfPresent(SubmodelElementCollection aisle, String motorId) { + List motorRefs = getMotorRefs(aisle); + Optional foundReference = motorRefs.stream().filter(refEl -> refEl.getIdShort().equals(motorId)).findFirst(); + + if (!foundReference.isPresent()) + return; + + List newMotorRefs = new ArrayList<>(motorRefs); + newMotorRefs.remove(foundReference.get()); + List newRefs = newMotorRefs.stream().map(SubmodelElement.class::cast).toList(); + aisle.setValue(newRefs); + overviewSubmodelService.updateSubmodelElement(aisle.getIdShort(), aisle); + + logger.info("Motor {} removed from {}.", motorId, aisle.getIdShort()); + } + + private List getMotorRefs(SubmodelElement aisle) { + SubmodelElementCollection aisleCol = (SubmodelElementCollection) aisle; + return aisleCol.getValue().stream().map(ReferenceElement.class::cast).toList(); + } + + private boolean isNewMotor(List motorRefs, String motorId) { + return motorRefs.stream().map(ReferenceElement::getIdShort).noneMatch(motorId::equals); + } + + private List addMotorToAisle(List motorRefs, String motorId) { + DefaultReference ref = new DefaultReference.Builder().keys(new DefaultKey.Builder().type(KeyTypes.ASSET_ADMINISTRATION_SHELL).value(motorId).build()).build(); + List newList = new ArrayList<>(motorRefs); + newList.add(new DefaultReferenceElement.Builder().idShort(motorId).value(ref).build()); + return newList; + } + + private boolean isInWarehouse(String actualAisle) { + return actualAisle.startsWith("Aisle"); + } + + private boolean isWarehouldAlreadyPushed(int warehouseId) { + try { + connectedAasManager.getAasService(WarehouseAasBuilder.buildId(warehouseId)); + return true; + } catch (Exception e) { + return false; + } + } +} diff --git a/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/DirectoryWatcher.java b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/DirectoryWatcher.java new file mode 100644 index 000000000..dd98695e4 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/DirectoryWatcher.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (C) 2024 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.examples.basyxclient.processing; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.List; + +import org.eclipse.digitaltwin.basyx.examples.basyxclient.model.MotorEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.opencsv.bean.CsvToBeanBuilder; + +public class DirectoryWatcher implements Runnable { + + private final Logger logger = LoggerFactory.getLogger(DirectoryWatcher.class); + + private final List entryProcessors; + + private final Path dirToWatch; + + public DirectoryWatcher(List entryProcessors, Path dirToWatch) { + this.entryProcessors = entryProcessors; + this.dirToWatch = dirToWatch; + } + + @Override + public void run() { + try { + watchDirectory(dirToWatch); + } catch (IOException | InterruptedException e) { + logger.error("Error watching directory", e); + Thread.currentThread().interrupt(); + } + } + + public void watchDirectory(Path dirToWatch) throws IOException, InterruptedException { + WatchService watchService = FileSystems.getDefault().newWatchService(); + dirToWatch.register(watchService, StandardWatchEventKinds.ENTRY_CREATE); + + logger.info("Watching directory: {}", dirToWatch); + + while (true) { + WatchKey key = watchService.take(); + + for (WatchEvent event : key.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + + if (kind == StandardWatchEventKinds.ENTRY_CREATE) { + @SuppressWarnings("unchecked") + WatchEvent ev = (WatchEvent) event; + Path newFilePath = dirToWatch.resolve(ev.context()); + + logger.info("New file detected: {}", newFilePath); + + try { + List entries = parseCsvFile(newFilePath); + processAllEntries(entries); + } catch (Exception e) { + logger.error("Error parsing file: {}", newFilePath, e); + } + } + } + boolean valid = key.reset(); + if (!valid) { + break; + } + } + } + + private List parseCsvFile(Path filePath) throws IllegalStateException, FileNotFoundException { + return new CsvToBeanBuilder(new FileReader(filePath.toString())).withType(MotorEntry.class).build().parse(); + } + + private void processAllEntries(List entries) { + for (EntryProcessor entryProcessor : entryProcessors) { + try { + logger.info("Applying processor: {}", entryProcessor.getClass().getSimpleName()); + entryProcessor.process(entries); + } catch (Exception e) { + logger.error("Processor {} failed", entryProcessor.getClass().getSimpleName(), e); + } + } + } + + +} diff --git a/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/EntryProcessor.java b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/EntryProcessor.java new file mode 100644 index 000000000..05c9a5af7 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/EntryProcessor.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (C) 2024 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.examples.basyxclient.processing; + +import java.util.List; + +import org.eclipse.digitaltwin.basyx.examples.basyxclient.model.MotorEntry; +public interface EntryProcessor { + void process(List entries); +} diff --git a/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/MotorAasBuilder.java b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/MotorAasBuilder.java new file mode 100644 index 000000000..968bdde12 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/MotorAasBuilder.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright (C) 2024 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.examples.basyxclient.processing; + +import java.util.List; + +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.KeyTypes; +import org.eclipse.digitaltwin.aas4j.v3.model.ReferenceTypes; +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultAssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultKey; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultReference; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection; +import org.eclipse.digitaltwin.basyx.examples.basyxclient.model.MotorEntry; + +public final class MotorAasBuilder { + public static final String ID_PREFIX = "http://example.com/motors/"; + + private MotorAasBuilder() { + } + + public static AssetAdministrationShell fromEntry(MotorEntry entry) { + return new DefaultAssetAdministrationShell.Builder().id(buildIdFromEntry(entry)).idShort(entry.getMotorId()).build(); + } + + public static String buildIdFromEntry(MotorEntry entry) { + return ID_PREFIX + entry.getMotorId(); + } + + public static List buildSubmodelsFromEntry(MotorEntry entry) { + return List.of(buildNameplateSmFromEntry(entry), buildMaintenanceDataSmFromEntry(entry), buildStatusSmFromEntry(entry)); + } + + private static Submodel buildMaintenanceDataSmFromEntry(MotorEntry entry) { + String smId = buildIdFromEntry(entry) + "/maintenanceData"; + + List seList = List.of(buildProperty("MaintenanceSchedule", toStringOrEmptyIfNull(entry.getMaintenanceSchedule())), buildProperty("LastMaintenance", toStringOrEmptyIfNull(entry.getLastMaintenance())), + buildProperty("WarrantyPeriod", toStringOrEmptyIfNull(entry.getWarrantyPeriod()))); + + return new DefaultSubmodel.Builder().id(smId).idShort("MaintenanceData").submodelElements(seList).build(); + } + + private static Submodel buildStatusSmFromEntry(MotorEntry entry) { + String smId = buildIdFromEntry(entry) + "/status"; + + List seList = List.of(buildProperty("Status", entry.getStatus()), buildProperty("DateSold", toStringOrEmptyIfNull(entry.getDateSold()))); + + return new DefaultSubmodel.Builder().id(smId).idShort("Status").submodelElements(seList).build(); + } + + private static Submodel buildNameplateSmFromEntry(MotorEntry entry) { + String smId = buildIdFromEntry(entry) + "/nameplate"; + DefaultKey key = new DefaultKey.Builder().type(KeyTypes.CONCEPT_DESCRIPTION).value("https://admin-shell.io/zvei/nameplate/2/0/Nameplate").build(); + DefaultReference ref = new DefaultReference.Builder().type(ReferenceTypes.EXTERNAL_REFERENCE).keys(key).build(); + + SubmodelElement assetSpecificProperties = new DefaultSubmodelElementCollection.Builder().idShort("AssetSpecificProperties").value(buildProperty("MotorType", entry.getMotorType())).build(); + + List seList = List.of(buildProperty("ManufacturerName", entry.getManufacturer()), buildProperty("DateOfManufacture", toStringOrEmptyIfNull(entry.getPurchaseDate())), + buildProperty("SerialNumber", entry.getMotorId()), assetSpecificProperties); + + return new DefaultSubmodel.Builder().id(smId).idShort("Nameplate").semanticId(ref).submodelElements(seList).build(); + } + + private static SubmodelElement buildProperty(String idShort, String value) { + return new DefaultProperty.Builder().idShort(idShort).value(value).build(); + } + + private static String toStringOrEmptyIfNull(Object o) { + return o == null ? "" : o.toString(); + } +} diff --git a/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/WarehouseAasBuilder.java b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/WarehouseAasBuilder.java new file mode 100644 index 000000000..391ed8156 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/processing/WarehouseAasBuilder.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (C) 2024 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.examples.basyxclient.processing; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultAssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection; + +public final class WarehouseAasBuilder { + public static final String ID_PREFIX = "http://example.com/warehouse/"; + + private WarehouseAasBuilder() { + } + + public static AssetAdministrationShell build(int warehouseNum) { + return new DefaultAssetAdministrationShell.Builder().id(buildId(warehouseNum)).idShort("Warehouse").build(); + } + + public static Submodel buildOverviewSm(int warehouseNum) { + String smId = buildId(warehouseNum) + "/overview"; + List aisles = IntStream.range(1, 11).mapToObj(WarehouseAasBuilder::buildAisle).collect(Collectors.toList()); + + return new DefaultSubmodel.Builder().id(smId).idShort("Overview").submodelElements(aisles).build(); + } + + public static String buildId(int warehouseNum) { + return ID_PREFIX + warehouseNum; + } + + private static SubmodelElementCollection buildAisle(int aisleNum) { + String aisleIdShort = "Aisle_" + aisleNum; + return new DefaultSubmodelElementCollection.Builder().idShort(aisleIdShort).build(); + } + +} diff --git a/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/utils/LocalDateTimeConverter.java b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/utils/LocalDateTimeConverter.java new file mode 100644 index 000000000..6792db66c --- /dev/null +++ b/examples/BaSyxClient/basyx-client/src/main/java/org/eclipse/digitaltwin/basyx/examples/basyxclient/utils/LocalDateTimeConverter.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (C) 2024 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.examples.basyxclient.utils; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import com.opencsv.bean.AbstractBeanField; + +public class LocalDateTimeConverter extends AbstractBeanField { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + + @Override + protected LocalDateTime convert(String value) { + if (value == null || value.trim().isEmpty() || value.equalsIgnoreCase("N/A")) + return null; + + try { + return LocalDateTime.parse(value, DATE_TIME_FORMATTER); + } catch (DateTimeParseException e) { + return null; + } + } +} diff --git a/examples/BaSyxClient/basyx-client/src/main/resources/application.properties b/examples/BaSyxClient/basyx-client/src/main/resources/application.properties new file mode 100644 index 000000000..aea68afc9 --- /dev/null +++ b/examples/BaSyxClient/basyx-client/src/main/resources/application.properties @@ -0,0 +1,2 @@ +server.port=8087 +spring.application.name=examples.basyxclient diff --git a/examples/BaSyxClient/config/aas-env.properties b/examples/BaSyxClient/config/aas-env.properties new file mode 100644 index 000000000..6f76083f4 --- /dev/null +++ b/examples/BaSyxClient/config/aas-env.properties @@ -0,0 +1,5 @@ +server.port=8081 +basyx.backend=InMemory +basyx.cors.allowed-origins=* +basyx.cors.allowed-methods=GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD +basyx.externalurl=http://localhost:8081 diff --git a/examples/BaSyxClient/config/aas-registry.yml b/examples/BaSyxClient/config/aas-registry.yml new file mode 100644 index 000000000..4fac8db3a --- /dev/null +++ b/examples/BaSyxClient/config/aas-registry.yml @@ -0,0 +1,4 @@ +basyx: + cors: + allowed-origins: '*' + allowed-methods: GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD diff --git a/examples/BaSyxClient/config/basyx-client.env b/examples/BaSyxClient/config/basyx-client.env new file mode 100644 index 000000000..1225b9b38 --- /dev/null +++ b/examples/BaSyxClient/config/basyx-client.env @@ -0,0 +1,4 @@ +AAS_REGISTRY_BASE_URL=http://localhost:8082 +SUBMODEL_REGISTRY_BASE_URL=http://localhost:8083 +AAS_REPOSITORY_BASE_URL=http://localhost:8081 +SUBMODEL_REPOSITORY_BASE_URL=http://localhost:8081 \ No newline at end of file diff --git a/examples/BaSyxClient/config/erp.env b/examples/BaSyxClient/config/erp.env new file mode 100644 index 000000000..5e4b725fa --- /dev/null +++ b/examples/BaSyxClient/config/erp.env @@ -0,0 +1 @@ +ERP_GEN_INTERVAL=10 \ No newline at end of file diff --git a/examples/BaSyxClient/config/sm-registry.yml b/examples/BaSyxClient/config/sm-registry.yml new file mode 100644 index 000000000..4fac8db3a --- /dev/null +++ b/examples/BaSyxClient/config/sm-registry.yml @@ -0,0 +1,4 @@ +basyx: + cors: + allowed-origins: '*' + allowed-methods: GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD diff --git a/examples/BaSyxClient/docker-compose.yml b/examples/BaSyxClient/docker-compose.yml new file mode 100644 index 000000000..5fcf076c9 --- /dev/null +++ b/examples/BaSyxClient/docker-compose.yml @@ -0,0 +1,69 @@ +version: '3' +services: + aas-env: + image: eclipsebasyx/aas-environment:2.0.0-milestone-04 + container_name: aas-env + volumes: + - ./config/aas-env.properties:/application/application.properties + ports: + - '8081:8081' + depends_on: + aas-registry: + condition: service_healthy + sm-registry: + condition: service_healthy + + aas-registry: + image: eclipsebasyx/aas-registry-log-mem:2.0.0-milestone-04 + container_name: aas-registry + ports: + - '8082:8080' + volumes: + - ./config/aas-registry.yml:/workspace/config/application.yml + + sm-registry: + image: eclipsebasyx/submodel-registry-log-mem:2.0.0-milestone-04 + container_name: sm-registry + ports: + - '8083:8080' + volumes: + - ./config/sm-registry.yml:/workspace/config/application.yml + + aas-web-ui: + image: eclipsebasyx/aas-gui:SNAPSHOT + container_name: aas-ui + ports: + - '3000:3000' + environment: + AAS_REGISTRY_PATH: http://localhost:8082/shell-descriptors + SUBMODEL_REGISTRY_PATH: http://localhost:8083/submodel-descriptors + AAS_REPO_PATH: http://localhost:8081/shells + SUBMODEL_REPO_PATH: http://localhost:8081/submodels + CD_REPO_PATH: http://localhost:8081/concept-descriptions + depends_on: + aas-env: + condition: service_healthy + + basyx-client: + build: + context: ./basyx-client + dockerfile: Dockerfile + container_name: basyx-client + env_file: ./config/basyx-client.env + volumes: + - ./ingest:/ingest + network_mode: host + depends_on: + aas-env: + condition: service_healthy + + legacy-erp: + build: + context: ./legacy-erp + dockerfile: Dockerfile + env_file: ./config/erp.env + volumes: + - ./ingest:/export + depends_on: + basyx-client: + condition: service_started \ No newline at end of file diff --git a/examples/BaSyxClient/legacy-erp/Dockerfile b/examples/BaSyxClient/legacy-erp/Dockerfile new file mode 100644 index 000000000..6e1e5529a --- /dev/null +++ b/examples/BaSyxClient/legacy-erp/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.10-slim +ARG HTTP_PROXY +ARG HTTPS_PROXY +WORKDIR /app + +COPY . /app + +RUN chmod +x inventory_generator.py + +CMD ["python", "./inventory_generator.py"] diff --git a/examples/BaSyxClient/legacy-erp/inventory_generator.py b/examples/BaSyxClient/legacy-erp/inventory_generator.py new file mode 100644 index 000000000..c2e7cbab1 --- /dev/null +++ b/examples/BaSyxClient/legacy-erp/inventory_generator.py @@ -0,0 +1,107 @@ +import os +import csv +import random +import time +from datetime import datetime, timedelta + +export_path = os.getenv("ERP_INTERNAL_EXPORT_PATH", "/export") +gen_interval = int(os.getenv("ERP_GEN_INTERVAL", 10)) +max_number_of_motors = int(os.getenv("ERP_MAX_NUMBER_OF_MOTORS", 30)) + +if not os.path.exists(export_path): + os.makedirs(export_path) + +manufacturers = ["MotorCorp", "PowerMotors", "ElectroTech", "MechWorks"] + +motor_data = [] + +STATUS_IN_STOCK = "In Stock" +STATUS_UNDER_MAINTENANCE = "Under Maintenance" +STATUS_SOLD = "Sold" +TIMESTAMP_FMT = "%Y-%m-%dT%H:%M:%S" + +def do_with_chance(action, probability, *args): + if random.random() < probability: + action(*args) + + +def create_motor(): + motor_id = f"MTR{random.randint(1000, 9999)}" + motor = { + "MotorID": motor_id, + "MotorType": random.choice(["AC Motor", "DC Motor"]), + "Manufacturer": random.choice(manufacturers), + "PurchaseDate": datetime.now().strftime(TIMESTAMP_FMT), + "Location": f"Aisle_{random.randint(1, 10)}", + "LastMaintenance": "N/A", + "MaintenanceSchedule": (datetime.now() + timedelta(days=random.randint(90, 180))).strftime(TIMESTAMP_FMT), + "WarrantyPeriod": (datetime.now() + timedelta(days=365*random.randint(2, 5))).strftime(TIMESTAMP_FMT), + "Status": STATUS_IN_STOCK, + "DateSold": "" + } + return motor + +def receive_new_motor(): + new_motor = create_motor() + motor_data.append(new_motor) + print(f"New motor received: {new_motor['MotorID']}") + +def sell_motor(motor): + motor["Location"] = "N/A" + motor["DateSold"] = datetime.now().strftime(TIMESTAMP_FMT) + motor["Status"] = STATUS_SOLD + print(f"Motor sold: {motor['MotorID']}") + +def send_motor_to_maintenance(motor): + motor["Status"] = "Under Maintenance" + motor["LastMaintenance"] = datetime.now().strftime(TIMESTAMP_FMT) + next_maintenance = datetime.now() + timedelta(days=180) + motor["MaintenanceSchedule"] = next_maintenance.strftime(TIMESTAMP_FMT) + print(f"Motor {motor['MotorID']} is now under maintenance") + +def remove_motor_from_maintenance(motor): + motor["Status"] = STATUS_IN_STOCK + print(f"Motor {motor['MotorID']} is now back in stock after maintenance") + +def manage_motor_lifecycle(motor): + if motor["Status"] == STATUS_IN_STOCK: + action = random.choice(["sell", "maintenance", "none", "none"]) + if action == "sell": + sell_motor(motor) + elif action == "maintenance": + send_motor_to_maintenance(motor) + elif motor["Status"] == STATUS_UNDER_MAINTENANCE: + do_with_chance(remove_motor_from_maintenance, 0.3, motor) + +def generate_inventory_csv(): + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + file_name = f"inventory_{timestamp}.csv" + file_path = os.path.join(export_path, file_name) + + with open(file_path, mode='w', newline='') as file: + writer = csv.DictWriter(file, fieldnames=motor_data[0].keys()) + writer.writeheader() + writer.writerows(motor_data) + + print(f"Generated new inventory CSV: {file_name}") + +def start_generating(): + # Receive a few initial motors + for _ in range(3): + receive_new_motor() + + while True: + # Simulate motor lifecycle events for each motor + for motor in motor_data: + manage_motor_lifecycle(motor) + + # Occasionally receive new motors + if(len(motor_data) < max_number_of_motors): + do_with_chance(receive_new_motor, 0.5) + + generate_inventory_csv() + + time.sleep(gen_interval) + +if __name__ == "__main__": + start_generating() diff --git a/examples/BaSyxDatabridge/README.md b/examples/BaSyxDatabridge/README.md new file mode 100644 index 000000000..b73db0236 --- /dev/null +++ b/examples/BaSyxDatabridge/README.md @@ -0,0 +1,42 @@ +# BaSyx Databridge Example Setup + +This example showcases the usage of the BaSyx Databridge. The BaSyx Databridge is a service that allows the exchange of data between assets and the AAS. +Here, an example MQTT client for an environmental sensor is used to send data via the BaSyx Databridge to an AAS located in the BaSyx AAS Environment. + +## How to run the BaSyx Databridge Example + +1. Open a terminal in this folder +2. Run the following command to start the BaSyx containers: + +```bash +docker-compose up -d +``` + +> To run the example containers, you need to have Docker installed on your device. + +## View the working Example + +To see the working example, open the [BaSyx AAS Web UI](http://localhost:3000) and navigate to the `SensorExampleAAS`. You can see the data coming from the MQTT client in the `SensorData` submodel. +To see updates in real-time, active the `Auto-Sync` feature in the AAS Web UI (see top right corner of the UI). + +## Where to find the configuration + +### BaSyx Databridge + +The configuration for the BaSyx Databridge can be found in the `databridge` folder. There you can find the `routes.config` file, which defines the routes from the assets data source to the AAS data sink. +The data source configuration can be found in the `mqttconsumer.json` file, and the data sink configuration can be found in the `aasserver.json` file. +There are also data transformers defined for extracting the data from the MQTT message. +For more details on how to configure the BaSyx Databridge, please refer to the [BaSyx Databridge documentation](https://wiki.basyx.org/en/latest/content/user_documentation/basyx_components/databridge/index.html). + +### MQTT Client + +The configuration for the MQTT client can be found in the `mqtt-publisher` folder. It is a small Python script that publishes data to the MQTT broker. + +### MQTT Broker + +The MQTT brokers configuration can be found in the `mosquitto` folder. The configuration is defined in the `config/mosquitto.conf` file. + +### BaSyx Components + +The configuration for the BaSyx components can be found in the `basyx` folder. +The AAS used in this example is located in the `aas` folder. diff --git a/examples/BaSyxDatabridge/aas/SensorExampleComplete.aasx b/examples/BaSyxDatabridge/aas/SensorExampleComplete.aasx new file mode 100644 index 000000000..33e68fe50 Binary files /dev/null and b/examples/BaSyxDatabridge/aas/SensorExampleComplete.aasx differ diff --git a/examples/BaSyxDatabridge/basyx/aas-env.properties b/examples/BaSyxDatabridge/basyx/aas-env.properties new file mode 100644 index 000000000..3d3d08d9d --- /dev/null +++ b/examples/BaSyxDatabridge/basyx/aas-env.properties @@ -0,0 +1,8 @@ +server.port=8081 +basyx.backend=InMemory +basyx.environment=file:aas +basyx.cors.allowed-origins=* +basyx.cors.allowed-methods=GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD +basyx.aasrepository.feature.registryintegration=http://aas-registry:8080 +basyx.submodelrepository.feature.registryintegration=http://sm-registry:8080 +basyx.externalurl=http://localhost:8081 diff --git a/examples/BaSyxDatabridge/basyx/aas-registry.yml b/examples/BaSyxDatabridge/basyx/aas-registry.yml new file mode 100644 index 000000000..4fac8db3a --- /dev/null +++ b/examples/BaSyxDatabridge/basyx/aas-registry.yml @@ -0,0 +1,4 @@ +basyx: + cors: + allowed-origins: '*' + allowed-methods: GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD diff --git a/examples/BaSyxDatabridge/basyx/sm-registry.yml b/examples/BaSyxDatabridge/basyx/sm-registry.yml new file mode 100644 index 000000000..4fac8db3a --- /dev/null +++ b/examples/BaSyxDatabridge/basyx/sm-registry.yml @@ -0,0 +1,4 @@ +basyx: + cors: + allowed-origins: '*' + allowed-methods: GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD diff --git a/examples/BaSyxDatabridge/databridge/AirQualityTransformer.jsonata b/examples/BaSyxDatabridge/databridge/AirQualityTransformer.jsonata new file mode 100644 index 000000000..af93b795f --- /dev/null +++ b/examples/BaSyxDatabridge/databridge/AirQualityTransformer.jsonata @@ -0,0 +1 @@ +airQuality \ No newline at end of file diff --git a/examples/BaSyxDatabridge/databridge/HumidityTransformer.jsonata b/examples/BaSyxDatabridge/databridge/HumidityTransformer.jsonata new file mode 100644 index 000000000..6506bbd58 --- /dev/null +++ b/examples/BaSyxDatabridge/databridge/HumidityTransformer.jsonata @@ -0,0 +1 @@ +humidity \ No newline at end of file diff --git a/examples/BaSyxDatabridge/databridge/TemperatureTransformer.jsonata b/examples/BaSyxDatabridge/databridge/TemperatureTransformer.jsonata new file mode 100644 index 000000000..4d81bb0f8 --- /dev/null +++ b/examples/BaSyxDatabridge/databridge/TemperatureTransformer.jsonata @@ -0,0 +1 @@ +temperature \ No newline at end of file diff --git a/examples/BaSyxDatabridge/databridge/aasserver.json b/examples/BaSyxDatabridge/databridge/aasserver.json new file mode 100644 index 000000000..2d6509135 --- /dev/null +++ b/examples/BaSyxDatabridge/databridge/aasserver.json @@ -0,0 +1,20 @@ +[ + { + "uniqueId": "TemperatureAAS", + "submodelEndpoint": "http://host.docker.internal:8081/submodels/YjdlNTA2NzgtZGY3Mi00ODAxLWI3YzAtNzI2NTNkMmU0NzI0", + "idShortPath": "TemperatureValue", + "api": "DotAAS-V3" + }, + { + "uniqueId": "HumidityAAS", + "submodelEndpoint": "http://host.docker.internal:8081/submodels/YjdlNTA2NzgtZGY3Mi00ODAxLWI3YzAtNzI2NTNkMmU0NzI0", + "idShortPath": "HumidityValue", + "api": "DotAAS-V3" + }, + { + "uniqueId": "AirQualityAAS", + "submodelEndpoint": "http://host.docker.internal:8081/submodels/YjdlNTA2NzgtZGY3Mi00ODAxLWI3YzAtNzI2NTNkMmU0NzI0", + "idShortPath": "AirQualityValue", + "api": "DotAAS-V3" + } +] diff --git a/examples/BaSyxDatabridge/databridge/jsonatatransformer.json b/examples/BaSyxDatabridge/databridge/jsonatatransformer.json new file mode 100644 index 000000000..691253839 --- /dev/null +++ b/examples/BaSyxDatabridge/databridge/jsonatatransformer.json @@ -0,0 +1,20 @@ +[ + { + "uniqueId": "TemperatureTransformer", + "queryPath": "TemperatureTransformer.jsonata", + "inputType": "JsonString", + "outputType": "JsonString" + }, + { + "uniqueId": "HumidityTransformer", + "queryPath": "HumidityTransformer.jsonata", + "inputType": "JsonString", + "outputType": "JsonString" + }, + { + "uniqueId": "AirQualityTransformer", + "queryPath": "AirQualityTransformer.jsonata", + "inputType": "JsonString", + "outputType": "JsonString" + } +] \ No newline at end of file diff --git a/examples/BaSyxDatabridge/databridge/mqttconsumer.json b/examples/BaSyxDatabridge/databridge/mqttconsumer.json new file mode 100644 index 000000000..5a6f7c890 --- /dev/null +++ b/examples/BaSyxDatabridge/databridge/mqttconsumer.json @@ -0,0 +1,8 @@ +[ + { + "uniqueId": "EnvironmentalSensorMQTT", + "serverUrl": "mosquitto", + "serverPort": 1883, + "topic": "EnvironmentalSensor/CombinedData" + } +] diff --git a/examples/BaSyxDatabridge/databridge/routes.json b/examples/BaSyxDatabridge/databridge/routes.json new file mode 100644 index 000000000..887c84db5 --- /dev/null +++ b/examples/BaSyxDatabridge/databridge/routes.json @@ -0,0 +1,22 @@ +[ + { + "datasource": "EnvironmentalSensorMQTT", + "transformers": [ + "TemperatureTransformer", + "HumidityTransformer", + "AirQualityTransformer" + ], + "datasinks": [ + "TemperatureAAS", + "HumidityAAS", + "AirQualityAAS" + ], + "datasinkMappingConfiguration": + { + "TemperatureAAS": ["TemperatureTransformer"], + "HumidityAAS": ["HumidityTransformer"], + "AirQualityAAS": ["AirQualityTransformer"] + }, + "trigger": "event" + } +] diff --git a/examples/BaSyxDatabridge/docker-compose.yml b/examples/BaSyxDatabridge/docker-compose.yml new file mode 100644 index 000000000..e04089f52 --- /dev/null +++ b/examples/BaSyxDatabridge/docker-compose.yml @@ -0,0 +1,89 @@ +services: + # AAS Environment + aas-env: + image: eclipsebasyx/aas-environment:2.0.0-SNAPSHOT + container_name: aas-env + volumes: + - ./aas:/application/aas + - ./basyx/aas-env.properties:/application/application.properties + ports: + - '8081:8081' + restart: always + depends_on: + aas-registry: + condition: service_healthy + sm-registry: + condition: service_healthy + + # AAS Registry + aas-registry: + image: eclipsebasyx/aas-registry-log-mem:2.0.0-SNAPSHOT + container_name: aas-registry + ports: + - '8082:8080' + volumes: + - ./basyx/aas-registry.yml:/workspace/config/application.yml + restart: always + + # Submodel Registry + sm-registry: + image: eclipsebasyx/submodel-registry-log-mem:2.0.0-SNAPSHOT + container_name: sm-registry + ports: + - '8083:8080' + volumes: + - ./basyx/sm-registry.yml:/workspace/config/application.yml + restart: always + + # AAS Web UI + aas-web-ui: + image: eclipsebasyx/aas-gui:SNAPSHOT + container_name: aas-ui + ports: + - '3000:3000' + environment: + AAS_REGISTRY_PATH: http://localhost:8082/shell-descriptors + SUBMODEL_REGISTRY_PATH: http://localhost:8083/submodel-descriptors + AAS_REPO_PATH: http://localhost:8081/shells + SUBMODEL_REPO_PATH: http://localhost:8081/submodels + CD_REPO_PATH: http://localhost:8081/concept-descriptions + restart: always + depends_on: + aas-env: + condition: service_healthy + + # MQTT Broker + mosquitto: + image: eclipse-mosquitto:2.0.15 + container_name: mosquitto + ports: + - 1883:1883 + volumes: + - ./mosquitto:/mosquitto + healthcheck: + test: ["CMD-SHELL", mosquitto_sub -p 1883 -t 'topic' -C 1 -E -i probe -W 3] + interval: 5s + retries: 3 + start_period: 1s + timeout: 10s + restart: always + + # MQTT Publisher (for testing) + mqtt-publisher: + build: ./mqtt-publisher + container_name: mqtt-publisher + depends_on: + - mosquitto + restart: always + + # DataBridge + databridge: + image: eclipsebasyx/databridge:1.0.0-SNAPSHOT + container_name: databridge + volumes: + - "./databridge:/usr/share/config" + depends_on: + - mosquitto + - aas-env + restart: always + diff --git a/examples/BaSyxDatabridge/mosquitto/config/mosquitto.conf b/examples/BaSyxDatabridge/mosquitto/config/mosquitto.conf new file mode 100644 index 000000000..d05bdebdb --- /dev/null +++ b/examples/BaSyxDatabridge/mosquitto/config/mosquitto.conf @@ -0,0 +1,23 @@ +# Default listener port for unencrypted connections (usually 1883) +listener 1883 0.0.0.0 + +# Path to the directory where persistence information is stored. +# Remove or comment out to disable persistence. +persistence true +persistence_location /mosquitto/data/ + +# Log settings +log_dest file /mosquitto/log/mosquitto.log +log_type all + +# Connection settings +# Setting for maximum concurrent connections. -1 means unlimited. +max_connections -1 + +# Security settings +# Uncomment and set these for enabling username-password authentication. +#allow_anonymous false +#password_file /mosquitto/config/mosquitto_passwd + +# Other settings like SSL/TLS, ACLs, etc., can also be configured as needed. +allow_anonymous true diff --git a/examples/BaSyxDatabridge/mqtt-publisher/Dockerfile b/examples/BaSyxDatabridge/mqtt-publisher/Dockerfile new file mode 100644 index 000000000..1217cd26f --- /dev/null +++ b/examples/BaSyxDatabridge/mqtt-publisher/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.8-slim +ARG HTTP_PROXY +ARG HTTPS_PROXY +RUN pip install paho-mqtt + +COPY publisher.py /publisher.py + +CMD ["python", "/publisher.py"] \ No newline at end of file diff --git a/examples/BaSyxDatabridge/mqtt-publisher/publisher.py b/examples/BaSyxDatabridge/mqtt-publisher/publisher.py new file mode 100644 index 000000000..9a56afbd5 --- /dev/null +++ b/examples/BaSyxDatabridge/mqtt-publisher/publisher.py @@ -0,0 +1,47 @@ +import time +import random +import json +import paho.mqtt.client as mqtt + +# MQTT settings +broker_address = "mosquitto" +port = 1883 +base_topic = "EnvironmentalSensor/" + +# Connect to MQTT Broker +client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, "Client") +client.connect(broker_address, port) + +# Start the network loop in a separate thread +client.loop_start() + +try: + + while True: + # Generate different types of dynamic data + temperatureValue = random.uniform(0, 30) + humidityValue = random.uniform(0.0, 100.0) + airQualityValue = random.uniform(0.0, 1000.0) + + # Create a dictionary with all values + data = { + "temperature": temperatureValue, + "humidity": humidityValue, + "airQuality": airQualityValue + } + + # Convert the dictionary to a JSON string + json_data = json.dumps(data) + + # Publish dynamic data to respective subtopics + client.publish(base_topic + "Temperature", temperatureValue) + client.publish(base_topic + "Humidity", humidityValue) + client.publish(base_topic + "AirQuality", airQualityValue) + client.publish(base_topic + "CombinedData", json_data) + + # Wait for a short period before publishing the next set of values + time.sleep(1) +except KeyboardInterrupt: + # Stop the network loop if the script is interrupted + client.loop_stop() + client.disconnect() diff --git a/examples/BaSyxMinimal/docker-compose.yml b/examples/BaSyxMinimal/docker-compose.yml index ffa899d73..844ed1498 100644 --- a/examples/BaSyxMinimal/docker-compose.yml +++ b/examples/BaSyxMinimal/docker-compose.yml @@ -80,7 +80,7 @@ services: condition: service_healthy aas-web-ui_v2: - image: eclipsebasyx/aas-gui:v2-240801 + image: eclipsebasyx/aas-gui:SNAPSHOT container_name: aas-web-ui_v2 ports: - "3000:3000" diff --git a/examples/BaSyxNGINX/docker-compose.yml b/examples/BaSyxNGINX/docker-compose.yml index caa8e67c1..4d79aa591 100644 --- a/examples/BaSyxNGINX/docker-compose.yml +++ b/examples/BaSyxNGINX/docker-compose.yml @@ -76,7 +76,7 @@ services: # AAS Web UI aas-web-ui: - image: eclipsebasyx/aas-gui:v2-240801 + image: eclipsebasyx/aas-gui:SNAPSHOT container_name: aas-ui ports: - "3000:3000" @@ -89,8 +89,7 @@ services: SUBMODEL_REPO_PATH: "http://localhost/aas-env/submodels" CD_REPO_PATH: "http://localhost/aas-env/concept-descriptions" BASE_PATH: "/aas-ui" # without trailing slash - LOGO_PATH: "Logo/HTW.svg" restart: always depends_on: aas-env: - condition: service_started \ No newline at end of file + condition: service_started diff --git a/examples/BaSyxOperationDelegation/docker-compose.yml b/examples/BaSyxOperationDelegation/docker-compose.yml index 388c88f33..7c4e33799 100644 --- a/examples/BaSyxOperationDelegation/docker-compose.yml +++ b/examples/BaSyxOperationDelegation/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: aas-env: - image: eclipsebasyx/aas-environment:2.0.0-milestone-03 + image: eclipsebasyx/aas-environment:2.0.0-milestone-04 container_name: aas-env volumes: - ./aas:/application/aas @@ -15,7 +15,7 @@ services: sm-registry: condition: service_healthy aas-registry: - image: eclipsebasyx/aas-registry-log-mem:2.0.0-milestone-03 + image: eclipsebasyx/aas-registry-log-mem:2.0.0-milestone-04 container_name: aas-registry ports: - '8082:8080' @@ -23,7 +23,7 @@ services: - ./basyx/aas-registry.yml:/workspace/config/application.yml restart: always sm-registry: - image: eclipsebasyx/submodel-registry-log-mem:2.0.0-milestone-03 + image: eclipsebasyx/submodel-registry-log-mem:2.0.0-milestone-04 container_name: sm-registry ports: - '8083:8080' @@ -31,7 +31,7 @@ services: - ./basyx/sm-registry.yml:/workspace/config/application.yml restart: always aas-web-ui: - image: eclipsebasyx/aas-gui:v2-240801 + image: eclipsebasyx/aas-gui:SNAPSHOT container_name: aas-ui ports: - '3000:3000' @@ -49,7 +49,7 @@ services: build: context: ./exampleOperationService dockerfile: Dockerfile - container_name: example-operation + container_name: example-operation-service ports: - '8087:8080' restart: always diff --git a/examples/BaSyxOperationDelegation/exampleOperationService/Dockerfile b/examples/BaSyxOperationDelegation/exampleOperationService/Dockerfile index 0fdf815fa..72131f74b 100644 --- a/examples/BaSyxOperationDelegation/exampleOperationService/Dockerfile +++ b/examples/BaSyxOperationDelegation/exampleOperationService/Dockerfile @@ -1,5 +1,7 @@ # Stage 1: Build Stage FROM maven:3.8.1-openjdk-17 AS build +ARG HTTP_PROXY +ARG HTTPS_PROXY WORKDIR /app # Kopiere die pom.xml und lade die Abhängigkeiten herunter COPY pom.xml . @@ -11,6 +13,8 @@ RUN mvn clean package -DskipTests # Stage 2: Runtime Stage FROM openjdk:17 +ARG HTTP_PROXY +ARG HTTPS_PROXY WORKDIR /app COPY --from=build /app/target/*.jar app.jar EXPOSE 8080 diff --git a/examples/BaSyxOperationDelegation/exampleOperationService/pom.xml b/examples/BaSyxOperationDelegation/exampleOperationService/pom.xml index 98ee4b0a1..bb35bc61e 100644 --- a/examples/BaSyxOperationDelegation/exampleOperationService/pom.xml +++ b/examples/BaSyxOperationDelegation/exampleOperationService/pom.xml @@ -16,7 +16,7 @@ BaSyx Operation Delegation Example 17 - 1.0.1 + 1.0.2 @@ -49,7 +49,7 @@ org.eclipse.digitaltwin.basyx basyx.submodelrepository-client - 2.0.0-milestone-03 + 2.0.0-milestone-04 diff --git a/examples/BaSyxOperationDelegation/exampleOperationService/src/main/java/org/eclipse/digitaltwin/basyx/examples/operationdelegation/configuration/SecurityConfiguration.java b/examples/BaSyxOperationDelegation/exampleOperationService/src/main/java/org/eclipse/digitaltwin/basyx/examples/operationdelegation/configuration/SecurityConfiguration.java new file mode 100644 index 000000000..85d109853 --- /dev/null +++ b/examples/BaSyxOperationDelegation/exampleOperationService/src/main/java/org/eclipse/digitaltwin/basyx/examples/operationdelegation/configuration/SecurityConfiguration.java @@ -0,0 +1,17 @@ +package org.eclipse.digitaltwin.basyx.examples.operationdelegation.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfiguration { + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + httpSecurity.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()).httpBasic(Customizer.withDefaults()).csrf(c -> c.ignoringRequestMatchers("/operation-invocation")); + + return httpSecurity.build(); + } +} diff --git a/examples/BaSyxSecured/docker-compose.yaml b/examples/BaSyxSecured/docker-compose.yaml index 458a71e35..1fd3f1e2e 100644 --- a/examples/BaSyxSecured/docker-compose.yaml +++ b/examples/BaSyxSecured/docker-compose.yaml @@ -75,7 +75,7 @@ services: # AAS Web UI aas-web-ui: - image: eclipsebasyx/aas-gui:v2-240801 + image: eclipsebasyx/aas-gui:SNAPSHOT container_name: aas-ui extra_hosts: - "keycloak:127.0.0.1" diff --git a/examples/BaSyxSecured/keycloak/Dockerfile b/examples/BaSyxSecured/keycloak/Dockerfile index e9a06667c..1af526960 100644 --- a/examples/BaSyxSecured/keycloak/Dockerfile +++ b/examples/BaSyxSecured/keycloak/Dockerfile @@ -1,5 +1,7 @@ # syntax=docker/dockerfile:1 FROM maven:3-eclipse-temurin-17 AS build +ARG HTTP_PROXY +ARG HTTPS_PROXY WORKDIR /workspace COPY ./initializer/pom.xml /workspace/pom.xml COPY ./initializer/src /workspace/src diff --git a/examples/README.md b/examples/README.md index fbd8a6274..560ff4f16 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,9 +1,16 @@ # BaSyx V2 Examples This folder contains example configurations and docker compose files for setting up the BaSyx V2 infrastructure. +## Minimal Example +See the separate [Minimal example](BaSyxMinimal) for a minimal infrastructure setup example. + ## Standalone Submodel For definining standalone submodels, see [Submodel Service](../basyx.submodelservice) +## Time Series Data Submodel Example +An example showcasing the integration of time series data leveraging the plugin in the AAS Web UI. +See [Time Series Data Example](https://github.com/eclipse-basyx/basyx-aas-web-ui/tree/main/examples/TimeSeriesData) in the repository of the AAS Web UI. + ## BaSyx with NGINX See the separate [NGINX example](BaSyxNGINX) for a comprehensive setup leveraging NGINX. @@ -14,4 +21,7 @@ See the separate [Time Series example](https://github.com/eclipse-basyx/basyx-ap See the separate [Operation Delegation Example](BaSyxOperationDelegation) for a comprehensive setup leveraging Operation Delegation. ## BaSyx Secure Setup Example -See the separate [Secure Setup Example](BaSyxSecured) for a comprehensive setup leveraging Keycloak. \ No newline at end of file +See the separate [Secure Setup Example](BaSyxSecured) for a comprehensive setup leveraging Keycloak. + +## BaSyx Client Example +See the separate [Client Example](BaSyxClient) for a comprehensive setup leveraging the BaSyx Java Client library. diff --git a/pom.xml b/pom.xml index 1688f0eb9..c806de04e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.2 + 3.3.5 org.eclipse.digitaltwin.basyx @@ -76,10 +76,10 @@ eclipsebasyx NOT_DEFINED_IN_MODULE ${revision} - linux/amd64, linux/arm64 + linux/amd64, linux/arm64, linux/arm/v7 8081 8081 - 1.0.2 + 1.0.3 @@ -97,7 +97,7 @@ org.codehaus.mojo flatten-maven-plugin - 1.5.0 + 1.6.0 true @@ -170,7 +170,7 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.1.2 + 3.5.2 integration-test @@ -191,7 +191,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.4 + 3.2.7 sign-artifacts @@ -225,7 +225,7 @@ io.fabric8 docker-maven-plugin - 0.40.1 + 0.45.1 @@ -377,7 +377,7 @@ org.apache.tika tika-core - 2.9.2 + 3.0.0 org.reflections @@ -395,7 +395,7 @@ commons-io commons-io - 2.16.1 + 2.18.0 @@ -448,6 +448,11 @@ basyx.filerepository-backend-mongodb ${revision} + + org.eclipse.digitaltwin.basyx + basyx.backend.inmemory.core + ${revision} + org.eclipse.digitaltwin.basyx