diff --git a/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/AuthorizedConnectedAasRepository.java b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/AuthorizedConnectedAasRepository.java index c05643746..f97e28796 100644 --- a/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/AuthorizedConnectedAasRepository.java +++ b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/AuthorizedConnectedAasRepository.java @@ -25,7 +25,7 @@ package org.eclipse.digitaltwin.basyx.aasrepository.client; -import org.eclipse.digitaltwin.basyx.aasrepository.client.internal.AssetAdministrationShellRepositoryApi; +import org.eclipse.digitaltwin.basyx.aasrepository.client.internal.AssetAdministrationShellRepositoryApiFactory; import org.eclipse.digitaltwin.basyx.aasservice.client.ConnectedAasService; import org.eclipse.digitaltwin.basyx.aasservice.client.AuthorizedConnectedAasService; import org.eclipse.digitaltwin.basyx.client.internal.ApiException; @@ -41,7 +41,7 @@ public class AuthorizedConnectedAasRepository extends ConnectedAasRepository { private TokenManager tokenManager; public AuthorizedConnectedAasRepository(String repoUrl, TokenManager tokenManager) { - super(repoUrl, new AssetAdministrationShellRepositoryApi(repoUrl, tokenManager)); + super(repoUrl, AssetAdministrationShellRepositoryApiFactory.create(repoUrl, tokenManager)); this.tokenManager = tokenManager; } diff --git a/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/ConnectedAasRepository.java b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/ConnectedAasRepository.java index 32d1509a2..122592459 100644 --- a/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/ConnectedAasRepository.java +++ b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/ConnectedAasRepository.java @@ -38,6 +38,7 @@ import org.eclipse.digitaltwin.aas4j.v3.model.SpecificAssetId; import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository; import org.eclipse.digitaltwin.basyx.aasrepository.client.internal.AssetAdministrationShellRepositoryApi; +import org.eclipse.digitaltwin.basyx.aasrepository.client.internal.AssetAdministrationShellRepositoryApiFactory; import org.eclipse.digitaltwin.basyx.aasservice.client.ConnectedAasService; import org.eclipse.digitaltwin.basyx.client.internal.ApiException; import org.eclipse.digitaltwin.basyx.core.exceptions.CollidingIdentifierException; @@ -67,7 +68,7 @@ public class ConnectedAasRepository implements AasRepository { */ public ConnectedAasRepository(String repoUrl) { this.aasRepoUrl = repoUrl; - this.repoApi = new AssetAdministrationShellRepositoryApi(repoUrl); + this.repoApi = AssetAdministrationShellRepositoryApiFactory.create(repoUrl); } public ConnectedAasRepository(String repoUrl, AssetAdministrationShellRepositoryApi assetAdministrationShellRepositoryApi) { diff --git a/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/internal/AssetAdministrationShellRepositoryApi.java b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/internal/AssetAdministrationShellRepositoryApi.java index c74ea54de..46b037db6 100644 --- a/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/internal/AssetAdministrationShellRepositoryApi.java +++ b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/internal/AssetAdministrationShellRepositoryApi.java @@ -41,6 +41,7 @@ import org.eclipse.digitaltwin.aas4j.v3.model.AssetInformation; import org.eclipse.digitaltwin.aas4j.v3.model.Reference; import org.eclipse.digitaltwin.basyx.client.internal.ApiClient; +import org.eclipse.digitaltwin.basyx.client.internal.ApiClientPool; import org.eclipse.digitaltwin.basyx.client.internal.ApiException; import org.eclipse.digitaltwin.basyx.client.internal.ApiResponse; import org.eclipse.digitaltwin.basyx.client.internal.Pair; @@ -100,6 +101,10 @@ public AssetAdministrationShellRepositoryApi(ApiClient apiClient) { memberVarReadTimeout = apiClient.getReadTimeout(); } + public void setTokenManager(TokenManager tokenManager) { + this.tokenManager = tokenManager; + } + protected ApiException getApiException(String operationId, HttpResponse response) throws IOException { String message = formatExceptionMessage(operationId, response.statusCode(), response.body()); return new ApiException(response.statusCode(), message, response.headers(), response.body()); diff --git a/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/internal/AssetAdministrationShellRepositoryApiFactory.java b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/internal/AssetAdministrationShellRepositoryApiFactory.java new file mode 100644 index 000000000..5eca509ff --- /dev/null +++ b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/internal/AssetAdministrationShellRepositoryApiFactory.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (C) 2026 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.client.internal; + +import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonMapperFactory; +import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.SimpleAbstractTypeResolverFactory; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.ApiClient; +import org.eclipse.digitaltwin.basyx.client.internal.ApiClientPool; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Factory class for creating instances of {@link AssetAdministrationShellRepositoryApi} using an {@link ApiClientPool}. + * + * @author koort + */ +public class AssetAdministrationShellRepositoryApiFactory { + + private AssetAdministrationShellRepositoryApiFactory() { + } + + /** + * Creates a new instance of {@link AssetAdministrationShellRepositoryApi} with default configuration. + * + * @param repositoryBaseUri the base URI of the AAS repository + * @return a new AssetAdministrationShellRepositoryApi instance + */ + public static AssetAdministrationShellRepositoryApi create(String repositoryBaseUri) { + return create(repositoryBaseUri, null, null); + } + + /** + * Creates a new instance of {@link AssetAdministrationShellRepositoryApi} with the specified ObjectMapper. + * + * @param repositoryBaseUri the base URI of the AAS repository + * @param objectMapper the ObjectMapper + * @return a new AssetAdministrationShellRepositoryApi instance + */ + public static AssetAdministrationShellRepositoryApi create(String repositoryBaseUri, ObjectMapper objectMapper) { + return create(repositoryBaseUri, objectMapper, null); + } + + /** + * Creates a new instance of {@link AssetAdministrationShellRepositoryApi} with the specified TokenManager. + * + * @param repositoryBaseUri the base URI of the AAS repository + * @param tokenManager the TokenManager + * @return a new AssetAdministrationShellRepositoryApi instance + */ + public static AssetAdministrationShellRepositoryApi create(String repositoryBaseUri, TokenManager tokenManager) { + return create(repositoryBaseUri, null, tokenManager); + } + + /** + * Creates a new instance of {@link AssetAdministrationShellRepositoryApi} with the specified ObjectMapper and TokenManager. + * + * @param repositoryBaseUri the base URI of the AAS repository + * @param objectMapper the ObjectMapper + * @param tokenManager the TokenManager + * @return a new AssetAdministrationShellRepositoryApi instance + */ + public static AssetAdministrationShellRepositoryApi create(String repositoryBaseUri, ObjectMapper objectMapper, TokenManager tokenManager) { + ObjectMapper mapper = objectMapper != null ? objectMapper : new JsonMapperFactory().create(new SimpleAbstractTypeResolverFactory().create()); + + ApiClient apiClient = ApiClientPool.getInstance().getOrCreateAasRepoApiClient(repositoryBaseUri, mapper); + + AssetAdministrationShellRepositoryApi repoApi = new AssetAdministrationShellRepositoryApi(apiClient); + + if (tokenManager != null) { + repoApi.setTokenManager(tokenManager); + } + + return repoApi; + } +} diff --git a/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/AuthorizedConnectedAasService.java b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/AuthorizedConnectedAasService.java index 9f1f2ed47..c3bb8df98 100644 --- a/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/AuthorizedConnectedAasService.java +++ b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/AuthorizedConnectedAasService.java @@ -25,7 +25,7 @@ package org.eclipse.digitaltwin.basyx.aasservice.client; -import org.eclipse.digitaltwin.basyx.aasservice.client.internal.AssetAdministrationShellServiceApi; +import org.eclipse.digitaltwin.basyx.aasservice.client.internal.AssetAdministrationShellServiceApiFactory; import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; /** @@ -43,7 +43,7 @@ public class AuthorizedConnectedAasService extends ConnectedAasService { * @param tokenManager */ public AuthorizedConnectedAasService(String repoUrl, TokenManager tokenManager) { - super(new AssetAdministrationShellServiceApi(repoUrl, tokenManager)); + super(AssetAdministrationShellServiceApiFactory.create(repoUrl, tokenManager)); } } diff --git a/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/ConnectedAasService.java b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/ConnectedAasService.java index df9095b15..299d54acc 100644 --- a/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/ConnectedAasService.java +++ b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/ConnectedAasService.java @@ -35,6 +35,7 @@ import org.eclipse.digitaltwin.aas4j.v3.model.Reference; import org.eclipse.digitaltwin.basyx.aasservice.AasService; import org.eclipse.digitaltwin.basyx.aasservice.client.internal.AssetAdministrationShellServiceApi; +import org.eclipse.digitaltwin.basyx.aasservice.client.internal.AssetAdministrationShellServiceApiFactory; import org.eclipse.digitaltwin.basyx.client.internal.ApiException; import org.eclipse.digitaltwin.basyx.core.exceptions.CollidingSubmodelReferenceException; import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException; @@ -56,7 +57,7 @@ public class ConnectedAasService implements AasService { private AssetAdministrationShellServiceApi serviceApi; public ConnectedAasService(String aasServiceUrl) { - this.serviceApi = new AssetAdministrationShellServiceApi(aasServiceUrl); + this.serviceApi = AssetAdministrationShellServiceApiFactory.create(aasServiceUrl); } public ConnectedAasService(AssetAdministrationShellServiceApi assetAdministrationShellServiceApi) { diff --git a/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/internal/AssetAdministrationShellServiceApi.java b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/internal/AssetAdministrationShellServiceApi.java index 41cdf680c..8107b6006 100644 --- a/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/internal/AssetAdministrationShellServiceApi.java +++ b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/internal/AssetAdministrationShellServiceApi.java @@ -112,6 +112,10 @@ public AssetAdministrationShellServiceApi(ApiClient apiClient) { memberVarReadTimeout = apiClient.getReadTimeout(); } + public void setTokenManager(TokenManager tokenManager) { + this.tokenManager = tokenManager; + } + protected ApiException getApiException(String operationId, HttpResponse response) throws IOException { String message = formatExceptionMessage(operationId, response.statusCode(), response.body()); return new ApiException(response.statusCode(), message, response.headers(), response.body()); diff --git a/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/internal/AssetAdministrationShellServiceApiFactory.java b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/internal/AssetAdministrationShellServiceApiFactory.java new file mode 100644 index 000000000..9a5958e28 --- /dev/null +++ b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/internal/AssetAdministrationShellServiceApiFactory.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (C) 2026 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.aasservice.client.internal; + +import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonMapperFactory; +import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.SimpleAbstractTypeResolverFactory; +import org.eclipse.digitaltwin.basyx.client.internal.ApiClient; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.ApiClientPool; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Factory class for creating instances of {@link AssetAdministrationShellServiceApi} using an {@link ApiClientPool}. + * + * @author koort + */ +public class AssetAdministrationShellServiceApiFactory { + + private AssetAdministrationShellServiceApiFactory() { + } + + /** + * Creates a new instance of {@link AssetAdministrationShellServiceApi} with default configuration. + * + * @param aasServiceUrl the base URL of the AAS service + * @return a new AssetAdministrationShellServiceApi instance + */ + public static AssetAdministrationShellServiceApi create(String aasServiceUrl) { + return create(aasServiceUrl, null, null); + } + + /** + * Creates a new instance of {@link AssetAdministrationShellServiceApi} with the specified TokenManager. + * + * @param aasServiceUrl the base URL of the AAS service + * @param tokenManager the TokenManager + * @return a new AssetAdministrationShellServiceApi instance + */ + public static AssetAdministrationShellServiceApi create(String aasServiceUrl, TokenManager tokenManager) { + return create(aasServiceUrl, null, tokenManager); + } + + /** + * Creates a new instance of {@link AssetAdministrationShellServiceApi} with the specified ObjectMapper. + * + * @param aasServiceUrl the base URL of the AAS service + * @param objectMapper the ObjectMapper + * @return a new AssetAdministrationShellServiceApi instance + */ + public static AssetAdministrationShellServiceApi create(String aasServiceUrl, ObjectMapper objectMapper) { + return create(aasServiceUrl, objectMapper, null); + } + + /** + * Creates a new instance of {@link AssetAdministrationShellServiceApi} with the specified ObjectMapper and TokenManager. + * + * @param aasServiceUrl the base URL of the AAS service + * @param objectMapper the ObjectMapper + * @param tokenManager the TokenManager + * @return a new AssetAdministrationShellServiceApi instance + */ + public static AssetAdministrationShellServiceApi create(String aasServiceUrl, ObjectMapper objectMapper, TokenManager tokenManager) { + ObjectMapper mapper = objectMapper != null ? objectMapper : new JsonMapperFactory().create(new SimpleAbstractTypeResolverFactory().create()); + + ApiClient apiClient = ApiClientPool.getInstance().getOrCreateAasServiceApiClient(aasServiceUrl, mapper); + + AssetAdministrationShellServiceApi serviceApi = new AssetAdministrationShellServiceApi(apiClient); + + if (tokenManager != null) { + serviceApi.setTokenManager(tokenManager); + } + + return serviceApi; + } +} diff --git a/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/ApiClient.java b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/ApiClient.java index f1ab03e7e..233e662df 100644 --- a/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/ApiClient.java +++ b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/ApiClient.java @@ -83,6 +83,7 @@ public class ApiClient { private Consumer> asyncResponseInterceptor; private Duration readTimeout; private Duration connectTimeout; + private HttpClient httpClient; private static String valueToString(Object value) { if (value == null) { @@ -190,6 +191,7 @@ public ApiClient() { connectTimeout = null; responseInterceptor = null; asyncResponseInterceptor = null; + this.httpClient = this.builder.build(); } /** @@ -208,6 +210,7 @@ public ApiClient(HttpClient.Builder builder, ObjectMapper mapper, String baseUri connectTimeout = null; responseInterceptor = null; asyncResponseInterceptor = null; + this.httpClient = this.builder.build(); } protected ObjectMapper createDefaultObjectMapper() { @@ -249,6 +252,7 @@ public void updateBaseUri(String baseUri) { */ public ApiClient setHttpClientBuilder(HttpClient.Builder builder) { this.builder = builder; + this.httpClient = null; return this; } @@ -260,7 +264,10 @@ public ApiClient setHttpClientBuilder(HttpClient.Builder builder) { * @return The HTTP client. */ public HttpClient getHttpClient() { - return builder.build(); + if (httpClient == null) { + httpClient = builder.build(); + } + return httpClient; } /** diff --git a/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/ApiClientPool.java b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/ApiClientPool.java new file mode 100644 index 000000000..0183364d6 --- /dev/null +++ b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/ApiClientPool.java @@ -0,0 +1,205 @@ +/******************************************************************************* + * Copyright (C) 2026 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.client.internal; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import java.net.http.HttpClient; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Thread-safe pool for reusing ApiClient instances per URI. + * This avoids creating new HttpClient instances for each operation. + * + * @author koort + */ +public class ApiClientPool { + + private static final ApiClientPool INSTANCE = new ApiClientPool(); + + private final Map aasRepoApiClients = new ConcurrentHashMap<>(); + private final Map aasServiceApiClients = new ConcurrentHashMap<>(); + + private final Map submodelRepoApiClients = new ConcurrentHashMap<>(); + private final Map submodelServiceApiClients = new ConcurrentHashMap<>(); + + private ApiClientPool() { + } + + /** + * Gets the singleton instance of the ApiClientPool. + * + * @return the singleton ApiClientPool instance + */ + public static ApiClientPool getInstance() { + return INSTANCE; + } + + /** + * Gets or creates an AAS repository ApiClient for the given base URI and mapper. + * + * @param baseUri the base URI of the repository + * @param mapper the ObjectMapper + * @return cached or new ApiClient for AAS operations + */ + public ApiClient getOrCreateAasRepoApiClient(String baseUri, ObjectMapper mapper) { + return aasRepoApiClients.compute(baseUri, (uri, existingApiClient) -> { + if (existingApiClient != null) { + existingApiClient.setObjectMapper(mapper); + return existingApiClient; + } + return new ApiClient(HttpClient.newBuilder(), mapper, uri); + }); + } + + /** + * Gets or creates an AAS service ApiClient for the given service URL and mapper. + * + * @param serviceUrl the URL of the AAS service + * @param mapper the ObjectMapper + * @return cached or new ApiClient for Submodel operations + */ + public ApiClient getOrCreateAasServiceApiClient(String serviceUrl, ObjectMapper mapper) { + return aasServiceApiClients.compute(serviceUrl, (uri, existingApiClient) -> { + if (existingApiClient != null) { + existingApiClient.setObjectMapper(mapper); + return existingApiClient; + } + return new ApiClient(HttpClient.newBuilder(), mapper, uri); + }); + } + + /** + * Gets or creates a Submodel repository ApiClient for the given base URI and mapper. + * + * @param baseUri the base URI of the repository + * @param mapper the ObjectMapper + * @return cached or new ApiClient for Submodel operations + */ + public ApiClient getOrCreateSubmodelRepoApiClient(String baseUri, ObjectMapper mapper) { + return submodelRepoApiClients.compute(baseUri, (uri, existingApiClient) -> { + if (existingApiClient != null) { + existingApiClient.setObjectMapper(mapper); + return existingApiClient; + } + return new ApiClient(HttpClient.newBuilder(), mapper, uri); + }); + } + + /** + * Gets or creates a Submodel service ApiClient for the given service URL and mapper. + * + * @param serviceUrl the URL of the Submodel service + * @param mapper the ObjectMapper + * @return cached or new ApiClient for Submodel operations + */ + public ApiClient getOrCreateSubmodelServiceApiClient(String serviceUrl, ObjectMapper mapper) { + return submodelServiceApiClients.compute(serviceUrl, (uri, existingApiClient) -> { + if (existingApiClient != null) { + existingApiClient.setObjectMapper(mapper); + return existingApiClient; + } + return new ApiClient(HttpClient.newBuilder(), mapper, uri); + }); + } + + /** + * Clears all cached AAS repository ApiClients. + */ + public void clearAasRepoApiClients() { + aasRepoApiClients.clear(); + } + + /** + * Clears all cached AAS service ApiClients. + */ + public void clearAasServiceApiClients() { + aasServiceApiClients.clear(); + } + + /** + * Clears all cached Submodel repository ApiClients. + */ + public void clearSubmodelRepoApiClients() { + submodelRepoApiClients.clear(); + } + + /** + * Clears all cached Submodel service ApiClients. + */ + public void clearSubmodelServiceApiClients() { + submodelServiceApiClients.clear(); + } + + /** + * Clears all cached ApiClients. + */ + public void clearAll() { + aasRepoApiClients.clear(); + aasServiceApiClients.clear(); + submodelRepoApiClients.clear(); + submodelServiceApiClients.clear(); + } + + /** + * Removes a specific AAS repository ApiClient by base URI. + * + * @param baseUri the base URI to remove + */ + public void removeAasRepoApiClient(String baseUri) { + aasRepoApiClients.remove(baseUri); + } + + /** + * Removes a specific AAS service ApiClient by service URL. + * + * @param serviceUrl the service URL to remove + */ + public void removeAasServiceApiClient(String serviceUrl) { + aasServiceApiClients.remove(serviceUrl); + } + + /** + * Removes a specific Submodel repository ApiClient by base URI. + * + * @param baseUri the base URI to remove + */ + public void removeSubmodelRepoApiClient(String baseUri) { + submodelRepoApiClients.remove(baseUri); + } + + /** + * Removes a specific Submodel service ApiClient by service URL. + * + * @param serviceUrl the service URL to remove + */ + public void removeSubmodelServiceApiClient(String serviceUrl) { + submodelServiceApiClients.remove(serviceUrl); + } +} diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/AuthorizedConnectedSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/AuthorizedConnectedSubmodelRepository.java index 1a683a2ca..c8d5c0c3c 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/AuthorizedConnectedSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/AuthorizedConnectedSubmodelRepository.java @@ -27,7 +27,7 @@ import org.eclipse.digitaltwin.basyx.client.internal.ApiException; import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; -import org.eclipse.digitaltwin.basyx.submodelrepository.client.internal.SubmodelRepositoryApi; +import org.eclipse.digitaltwin.basyx.submodelrepository.client.internal.SubmodelRepositoryApiFactory; import org.eclipse.digitaltwin.basyx.submodelservice.client.AuthorizedConnectedSubmodelService; import org.eclipse.digitaltwin.basyx.submodelservice.client.ConnectedSubmodelService; @@ -41,7 +41,7 @@ public class AuthorizedConnectedSubmodelRepository extends ConnectedSubmodelRepo private TokenManager tokenManager; public AuthorizedConnectedSubmodelRepository(String repoUrl, TokenManager tokenManager) { - super(repoUrl, new SubmodelRepositoryApi(repoUrl, tokenManager)); + super(repoUrl, SubmodelRepositoryApiFactory.create(repoUrl, tokenManager)); this.tokenManager = tokenManager; } 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 b0d4675d1..814c52e25 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 @@ -46,6 +46,7 @@ import org.eclipse.digitaltwin.basyx.http.Base64UrlEncoder; import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; import org.eclipse.digitaltwin.basyx.submodelrepository.client.internal.SubmodelRepositoryApi; +import org.eclipse.digitaltwin.basyx.submodelrepository.client.internal.SubmodelRepositoryApiFactory; import org.eclipse.digitaltwin.basyx.submodelservice.client.ConnectedSubmodelService; import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelElementValue; import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelValueOnly; @@ -68,7 +69,7 @@ public class ConnectedSubmodelRepository implements SubmodelRepository { * the Url of the Submodel Repository without the "/submodels" part */ public ConnectedSubmodelRepository(String submodelRepoUrl) { - this.repoApi = new SubmodelRepositoryApi(submodelRepoUrl); + this.repoApi = SubmodelRepositoryApiFactory.create(submodelRepoUrl); this.submodelRepoUrl = submodelRepoUrl; } 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 82982354f..7adebfb83 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 @@ -97,6 +97,10 @@ public SubmodelRepositoryApi(ApiClient apiClient) { memberVarReadTimeout = apiClient.getReadTimeout(); } + public void setTokenManager(TokenManager tokenManager) { + this.tokenManager = tokenManager; + } + protected ApiException getApiException(String operationId, HttpResponse response) throws IOException { String message = formatExceptionMessage(operationId, response.statusCode(), response.body()); return new ApiException(response.statusCode(), message, response.headers(), response.body()); diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/internal/SubmodelRepositoryApiFactory.java b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/internal/SubmodelRepositoryApiFactory.java new file mode 100644 index 000000000..092b023a9 --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/internal/SubmodelRepositoryApiFactory.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (C) 2026 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.client.internal; + +import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonMapperFactory; +import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.SimpleAbstractTypeResolverFactory; +import org.eclipse.digitaltwin.basyx.client.internal.ApiClient; +import org.eclipse.digitaltwin.basyx.client.internal.ApiClientPool; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Factory class for creating instances of {@link SubmodelRepositoryApi} using an {@link ApiClientPool}. + * + * @author koort + */ +public class SubmodelRepositoryApiFactory { + + private SubmodelRepositoryApiFactory() { + } + + /** + * Creates a new instance of {@link SubmodelRepositoryApi} with default configuration. + * + * @param repositoryBaseUri the base URI of the submodel repository + * @return a new SubmodelRepositoryApi instance + */ + public static SubmodelRepositoryApi create(String repositoryBaseUri) { + return create(repositoryBaseUri, null, null); + } + + /** + * Creates a new instance of {@link SubmodelRepositoryApi} with the specified ObjectMapper. + * + * @param repositoryBaseUri the base URI of the submodel repository + * @param objectMapper the ObjectMapper + * @return a new SubmodelRepositoryApi instance + */ + public static SubmodelRepositoryApi create(String repositoryBaseUri, ObjectMapper objectMapper) { + return create(repositoryBaseUri, objectMapper, null); + } + + /** + * Creates a new instance of {@link SubmodelRepositoryApi} with the specified TokenManager. + * + * @param repositoryBaseUri the base URI of the submodel repository + * @param tokenManager the TokenManager + * @return a new SubmodelRepositoryApi instance + */ + public static SubmodelRepositoryApi create(String repositoryBaseUri, TokenManager tokenManager) { + return create(repositoryBaseUri, null, tokenManager); + } + + /** + * Creates a new instance of {@link SubmodelRepositoryApi} with the specified ObjectMapper and TokenManager. + * + * @param repositoryBaseUri the base URI of the submodel repository + * @param objectMapper the ObjectMapper + * @param tokenManager the TokenManager + * @return a new SubmodelRepositoryApi instance + */ + public static SubmodelRepositoryApi create(String repositoryBaseUri, ObjectMapper objectMapper, TokenManager tokenManager) { + ObjectMapper mapper = objectMapper != null ? objectMapper : new JsonMapperFactory().create(new SimpleAbstractTypeResolverFactory().create()); + + ApiClient apiClient = ApiClientPool.getInstance().getOrCreateSubmodelRepoApiClient(repositoryBaseUri, mapper); + + SubmodelRepositoryApi repoApi = new SubmodelRepositoryApi(apiClient); + + if (tokenManager != null) { + repoApi.setTokenManager(tokenManager); + } + + return repoApi; + } +} diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/AuthorizedConnectedSubmodelService.java b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/AuthorizedConnectedSubmodelService.java index 0cdff6f7d..587ea29f6 100644 --- a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/AuthorizedConnectedSubmodelService.java +++ b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/AuthorizedConnectedSubmodelService.java @@ -26,7 +26,7 @@ package org.eclipse.digitaltwin.basyx.submodelservice.client; import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; -import org.eclipse.digitaltwin.basyx.submodelservice.client.internal.SubmodelServiceApi; +import org.eclipse.digitaltwin.basyx.submodelservice.client.internal.SubmodelServiceApiFactory; /** * Provides access to an authorized Submodel Service on a remote server - regardless if it @@ -43,7 +43,7 @@ public class AuthorizedConnectedSubmodelService extends ConnectedSubmodelService * @param tokenManager */ public AuthorizedConnectedSubmodelService(String repoUrl, TokenManager tokenManager) { - super(new SubmodelServiceApi(repoUrl, tokenManager)); + super(SubmodelServiceApiFactory.create(repoUrl, tokenManager)); } } diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/ConnectedSubmodelService.java b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/ConnectedSubmodelService.java index d2863bb19..7a84d32d6 100644 --- a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/ConnectedSubmodelService.java +++ b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/ConnectedSubmodelService.java @@ -48,6 +48,7 @@ import org.eclipse.digitaltwin.basyx.http.Base64UrlEncoder; import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService; import org.eclipse.digitaltwin.basyx.submodelservice.client.internal.SubmodelServiceApi; +import org.eclipse.digitaltwin.basyx.submodelservice.client.internal.SubmodelServiceApiFactory; import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelElementValue; import org.eclipse.digitaltwin.basyx.submodelservice.value.factory.SubmodelElementValueMapperFactory; import org.springframework.http.HttpStatus; @@ -71,7 +72,7 @@ public class ConnectedSubmodelService implements SubmodelService { * submodels the "/submodel" part has to be included */ public ConnectedSubmodelService(String submodelServiceUrl) { - this.serviceApi = new SubmodelServiceApi(submodelServiceUrl); + this.serviceApi = SubmodelServiceApiFactory.create(submodelServiceUrl); this.submodelElementValueMapperFactory = new SubmodelElementValueMapperFactory(); } diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/internal/SubmodelServiceApi.java b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/internal/SubmodelServiceApi.java index 1e228c695..54bb0e2ef 100644 --- a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/internal/SubmodelServiceApi.java +++ b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/internal/SubmodelServiceApi.java @@ -114,6 +114,10 @@ public SubmodelServiceApi(ApiClient apiClient) { memberVarReadTimeout = apiClient.getReadTimeout(); } + public void setTokenManager(TokenManager tokenManager) { + this.tokenManager = tokenManager; + } + protected ApiException getApiException(String operationId, HttpResponse response) throws IOException { String message = formatExceptionMessage(operationId, response.statusCode(), response.body()); return new ApiException(response.statusCode(), message, response.headers(), response.body()); diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/internal/SubmodelServiceApiFactory.java b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/internal/SubmodelServiceApiFactory.java new file mode 100644 index 000000000..10a6393e2 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/internal/SubmodelServiceApiFactory.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (C) 2026 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.submodelservice.client.internal; + +import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonMapperFactory; +import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.SimpleAbstractTypeResolverFactory; +import org.eclipse.digitaltwin.basyx.client.internal.ApiClient; +import org.eclipse.digitaltwin.basyx.client.internal.ApiClientPool; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Factory class for creating instances of {@link SubmodelServiceApi} using an {@link ApiClientPool}. + * + * @author koort + */ +public class SubmodelServiceApiFactory { + + private SubmodelServiceApiFactory() { + } + + /** + * Creates a new instance of {@link SubmodelServiceApi} with default configuration. + * + * @param submodelServiceUrl the base URL of the submodel service + * @return a new SubmodelServiceApi instance + */ + public static SubmodelServiceApi create(String submodelServiceUrl) { + return create(submodelServiceUrl, null, null); + } + + /** + * Creates a new instance of {@link SubmodelServiceApi} with the specified TokenManager. + * + * @param submodelServiceUrl the base URL of the submodel service + * @param tokenManager the TokenManager + * @return a new SubmodelServiceApi instance + */ + public static SubmodelServiceApi create(String submodelServiceUrl, TokenManager tokenManager) { + return create(submodelServiceUrl, null, tokenManager); + } + + /** + * Creates a new instance of {@link SubmodelServiceApi} with the specified ObjectMapper. + * + * @param submodelServiceUrl the base URL of the submodel service + * @param objectMapper the ObjectMapper + * @return a new SubmodelServiceApi instance + */ + public static SubmodelServiceApi create(String submodelServiceUrl, ObjectMapper objectMapper) { + return create(submodelServiceUrl, objectMapper, null); + } + + /** + * Creates a new instance of {@link SubmodelServiceApi} with the specified ObjectMapper and TokenManager. + * + * @param submodelServiceUrl the base URL of the submodel service + * @param objectMapper the ObjectMapper + * @param tokenManager the TokenManager + * @return a new SubmodelServiceApi instance + */ + public static SubmodelServiceApi create(String submodelServiceUrl, ObjectMapper objectMapper, TokenManager tokenManager) { + ObjectMapper mapper = objectMapper != null ? objectMapper : new SubmodelSpecificJsonMapperFactory().create(); + + ApiClient apiClient = ApiClientPool.getInstance().getOrCreateSubmodelServiceApiClient(submodelServiceUrl, mapper); + + SubmodelServiceApi serviceApi = new SubmodelServiceApi(apiClient); + + if (tokenManager != null) { + serviceApi.setTokenManager(tokenManager); + } + + return serviceApi; + } +}