Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AssetAdministrationShell Repository - Registry Integration
This feature automatically integrates the Descriptor with the Registry while creation of the Shell at Repository. <br>
It also automatically removes the Descriptor from the Registry when the Shell is removed from the Repository.
This feature automatically creates and updates the Descriptor in the Registry when a Shell or its Asset Information changes in the Repository. <br>
It also automatically removes the Descriptor from the Registry when the Shell is removed from the Repository.

To enable this feature, the following two properties should be configured:

Expand Down Expand Up @@ -31,4 +31,14 @@ basyx.aasrepository.feature.registryintegration.authorization.client-secret = <c
basyx.aasrepository.feature.registryintegration.authorization.username=test
basyx.aasrepository.feature.registryintegration.authorization.password=test
basyx.aasrepository.feature.registryintegration.authorization.scopes=[]
```
```

The Registry permissions required by the integration are:

- `CREATE` when a Shell is created
- `READ` and `UPDATE` when a Shell or its Asset Information is updated
- `DELETE` when a Shell is deleted

`READ` is required during updates because the Registry API replaces complete descriptors. The integration reads the existing descriptor first so that Registry-managed endpoint metadata and Submodel Descriptors are retained.

The Registry API currently provides neither conditional updates nor a partial update operation for Shell Descriptors. Consequently, descriptor changes made directly in the Registry by another client must not run concurrently with an AAS update if both changes need to be retained.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@

import java.io.File;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell;
import org.eclipse.digitaltwin.aas4j.v3.model.AssetInformation;
Expand Down Expand Up @@ -54,17 +56,20 @@
*
*/
public class RegistryIntegrationAasRepository implements AasRepository {
private static final int SHELL_LOCK_COUNT = 64;
private static Logger logger = LoggerFactory.getLogger(RegistryIntegrationAasRepository.class);

private AasRepository decorated;

private AasRepositoryRegistryLink aasRepositoryRegistryLink;
private AttributeMapper attributeMapper;
private final Object[] shellMutationLocks = new Object[SHELL_LOCK_COUNT];

public RegistryIntegrationAasRepository(AasRepository decorated, AasRepositoryRegistryLink aasRepositoryRegistryLink, AttributeMapper attributeMapper) {
this.decorated = decorated;
this.aasRepositoryRegistryLink = aasRepositoryRegistryLink;
this.attributeMapper = attributeMapper;
Arrays.setAll(shellMutationLocks, index -> new Object());
}

@Override
Expand All @@ -79,31 +84,45 @@ public AssetAdministrationShell getAas(String shellId) throws ElementDoesNotExis

@Override
public void createAas(AssetAdministrationShell shell) throws CollidingIdentifierException {
AssetAdministrationShellDescriptor descriptor = new AasDescriptorFactory(aasRepositoryRegistryLink.getAasRepositoryBaseURLs(), attributeMapper).create(shell);
synchronized (getShellMutationLock(shell.getId())) {
AssetAdministrationShellDescriptor descriptor = createDescriptor(shell);

decorated.createAas(shell);
decorated.createAas(shell);

boolean registrationSuccessful = false;
boolean registrationSuccessful = false;

try {
registerAas(descriptor);
registrationSuccessful = true;
} finally {
if (!registrationSuccessful)
decorated.deleteAas(shell.getId());
try {
registerAas(descriptor);
registrationSuccessful = true;
} finally {
if (!registrationSuccessful)
decorated.deleteAas(shell.getId());
}
}
}

@Override
public void updateAas(String shellId, AssetAdministrationShell shell) {
decorated.updateAas(shellId, shell);
synchronized (getShellMutationLock(shellId)) {
AssetAdministrationShell previousShell = decorated.getAas(shellId);

decorated.updateAas(shellId, shell);

try {
updateDescriptor(shellId, shell);
} catch (RuntimeException e) {
rollbackAasUpdate(shellId, previousShell, e);
throw e;
}
}
}

@Override
public void deleteAas(String shellId) {
deleteFromRegistry(shellId);

decorated.deleteAas(shellId);
synchronized (getShellMutationLock(shellId)) {
deleteFromRegistry(shellId);
decorated.deleteAas(shellId);
}
}

@Override
Expand All @@ -118,17 +137,32 @@ public CursorResult<List<Reference>> getSubmodelReferences(String shellId, Pagin

@Override
public void addSubmodelReference(String shellId, Reference submodelReference) {
decorated.addSubmodelReference(shellId, submodelReference);
synchronized (getShellMutationLock(shellId)) {
decorated.addSubmodelReference(shellId, submodelReference);
}
}

@Override
public void removeSubmodelReference(String shellId, String submodelId) {
decorated.removeSubmodelReference(shellId, submodelId);
synchronized (getShellMutationLock(shellId)) {
decorated.removeSubmodelReference(shellId, submodelId);
}
}

@Override
public void setAssetInformation(String shellId, AssetInformation shellInfo) throws ElementDoesNotExistException {
decorated.setAssetInformation(shellId, shellInfo);
synchronized (getShellMutationLock(shellId)) {
AssetInformation previousAssetInformation = decorated.getAssetInformation(shellId);

decorated.setAssetInformation(shellId, shellInfo);

try {
updateDescriptor(shellId, decorated.getAas(shellId));
} catch (RuntimeException e) {
rollbackAssetInformationUpdate(shellId, previousAssetInformation, e);
throw e;
}
}
}

@Override
Expand All @@ -144,38 +178,135 @@ private void registerAas(AssetAdministrationShellDescriptor descriptor) {

logger.info("Shell '{}' has been automatically linked with the Registry", descriptor.getId());
} catch (ApiException e) {
throw new RepositoryRegistryLinkException(descriptor.getId(), e);
throw createRegistryLinkException(descriptor.getId(), "creation", e);
}
}

private void deleteFromRegistry(String shellId) {
private void updateDescriptor(String shellId, AssetAdministrationShell shell) {
RegistryAndDiscoveryInterfaceApi registryApi = aasRepositoryRegistryLink.getRegistryApi();

if (!shellExistsOnRegistry(shellId, registryApi)) {
logger.error("Unable to un-link the AAS descriptor '{}' from the Registry because it does not exist on the Registry.", shellId);

return;
AssetAdministrationShellDescriptor existingDescriptor;

try {
existingDescriptor = registryApi.getAssetAdministrationShellDescriptorById(shellId);
} catch (ApiException e) {
throw createRegistryLinkException(shellId, "update", e);
}

AssetAdministrationShellDescriptor updatedDescriptor = createDescriptor(shell);
preserveRegistryManagedAttributes(existingDescriptor, updatedDescriptor);

try {
registryApi.deleteAssetAdministrationShellDescriptorById(shellId);
registryApi.putAssetAdministrationShellDescriptorById(shellId, updatedDescriptor);

logger.info("Shell '{}' has been automatically un-linked from the Registry.", shellId);
logger.info("Shell descriptor '{}' has been automatically updated in the Registry", shellId);
} catch (ApiException e) {
throw new RepositoryRegistryUnlinkException(shellId, e);
if (descriptorMatchesAasDerivedState(shellId, updatedDescriptor, registryApi, e)) {
logger.warn("Registry update for shell descriptor '{}' returned an error, but a read-back confirmed the requested AAS-derived state.", shellId);
return;
}

throw createRegistryLinkException(shellId, "update", e);
}
}

private boolean shellExistsOnRegistry(String shellId, RegistryAndDiscoveryInterfaceApi registryApi) {

private void preserveRegistryManagedAttributes(AssetAdministrationShellDescriptor existingDescriptor, AssetAdministrationShellDescriptor updatedDescriptor) {
if (existingDescriptor.getEndpoints() != null)
updatedDescriptor.setEndpoints(existingDescriptor.getEndpoints());

updatedDescriptor.setSubmodelDescriptors(existingDescriptor.getSubmodelDescriptors());
}

private boolean descriptorMatchesAasDerivedState(String shellId, AssetAdministrationShellDescriptor expectedDescriptor, RegistryAndDiscoveryInterfaceApi registryApi, ApiException updateException) {
try {
registryApi.getAssetAdministrationShellDescriptorById(shellId);

return true;
} catch (ApiException e) {
AssetAdministrationShellDescriptor actualDescriptor = registryApi.getAssetAdministrationShellDescriptorById(shellId);
return aasDerivedAttributesEqual(expectedDescriptor, actualDescriptor);
} catch (ApiException verificationException) {
updateException.addSuppressed(verificationException);
return false;
}
}

private boolean aasDerivedAttributesEqual(AssetAdministrationShellDescriptor expected, AssetAdministrationShellDescriptor actual) {
return actual != null
&& Objects.equals(expected.getAdministration(), actual.getAdministration())
&& Objects.equals(expected.getAssetKind(), actual.getAssetKind())
&& Objects.equals(expected.getAssetType(), actual.getAssetType())
&& Objects.equals(expected.getDescription(), actual.getDescription())
&& Objects.equals(expected.getDisplayName(), actual.getDisplayName())
&& Objects.equals(expected.getExtensions(), actual.getExtensions())
&& Objects.equals(expected.getGlobalAssetId(), actual.getGlobalAssetId())
&& Objects.equals(expected.getId(), actual.getId())
&& Objects.equals(expected.getIdShort(), actual.getIdShort())
&& Objects.equals(expected.getSpecificAssetIds(), actual.getSpecificAssetIds());
}

private AssetAdministrationShellDescriptor createDescriptor(AssetAdministrationShell shell) {
return new AasDescriptorFactory(aasRepositoryRegistryLink.getAasRepositoryBaseURLs(), attributeMapper).create(shell);
}

private RepositoryRegistryLinkException createRegistryLinkException(String shellId, String operation, ApiException cause) {
return new RepositoryRegistryLinkException(shellId, createRegistryFailureDetails(operation, cause), cause);
}

private RepositoryRegistryUnlinkException createRegistryUnlinkException(String shellId, ApiException cause) {
return new RepositoryRegistryUnlinkException(shellId, createRegistryFailureDetails("deletion", cause), cause);
}

private String createRegistryFailureDetails(String operation, ApiException cause) {
StringBuilder details = new StringBuilder("Registry descriptor ").append(operation).append(" failed");

if (cause.getCode() > 0)
details.append(" with HTTP status ").append(cause.getCode());

String responseDetails = cause.getResponseBody();
if (responseDetails == null || responseDetails.isBlank())
responseDetails = cause.getMessage();

if (responseDetails != null && !responseDetails.isBlank())
details.append(": ").append(responseDetails);

return details.toString();
}

private void rollbackAasUpdate(String shellId, AssetAdministrationShell previousShell, RuntimeException updateException) {
try {
decorated.updateAas(shellId, previousShell);
} catch (RuntimeException rollbackException) {
updateException.addSuppressed(rollbackException);
logger.error("Unable to restore AAS '{}' after Registry synchronization failed.", shellId, rollbackException);
}
}

private void rollbackAssetInformationUpdate(String shellId, AssetInformation previousAssetInformation, RuntimeException updateException) {
try {
decorated.setAssetInformation(shellId, previousAssetInformation);
} catch (RuntimeException rollbackException) {
updateException.addSuppressed(rollbackException);
logger.error("Unable to restore Asset Information for AAS '{}' after Registry synchronization failed.", shellId, rollbackException);
}
}

private void deleteFromRegistry(String shellId) {
RegistryAndDiscoveryInterfaceApi registryApi = aasRepositoryRegistryLink.getRegistryApi();

try {
registryApi.deleteAssetAdministrationShellDescriptorById(shellId);

logger.info("Shell '{}' has been automatically un-linked from the Registry.", shellId);
} catch (ApiException e) {
if (e.getCode() == 404) {
logger.info("Shell descriptor '{}' was already absent from the Registry.", shellId);
return;
}

throw createRegistryUnlinkException(shellId, e);
}
}

private Object getShellMutationLock(String shellId) {
return shellMutationLocks[Math.floorMod(Objects.hashCode(shellId), shellMutationLocks.length)];
}

@Override
public File getThumbnail(String aasId) {
return decorated.getThumbnail(aasId);
Expand Down
Loading
Loading