| /******************************************************************************* |
| * Copyright (C) 2021 the Eclipse BaSyx Authors |
| * |
| * This program and the accompanying materials are made |
| * available under the terms of the Eclipse Public License 2.0 |
| * which is available at https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| ******************************************************************************/ |
| package org.eclipse.basyx.aas.restapi; |
| |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.stream.Collectors; |
| |
| import org.eclipse.basyx.aas.metamodel.map.AssetAdministrationShell; |
| import org.eclipse.basyx.aas.metamodel.map.descriptor.AASDescriptor; |
| import org.eclipse.basyx.aas.metamodel.map.descriptor.SubmodelDescriptor; |
| import org.eclipse.basyx.aas.registration.api.IAASRegistry; |
| import org.eclipse.basyx.aas.restapi.api.IAASAPI; |
| import org.eclipse.basyx.aas.restapi.api.IAASAPIFactory; |
| import org.eclipse.basyx.aas.restapi.vab.VABAASAPIFactory; |
| import org.eclipse.basyx.submodel.metamodel.api.identifier.IIdentifier; |
| import org.eclipse.basyx.submodel.metamodel.map.Submodel; |
| import org.eclipse.basyx.submodel.restapi.SubmodelProvider; |
| import org.eclipse.basyx.submodel.restapi.api.ISubmodelAPI; |
| import org.eclipse.basyx.submodel.restapi.api.ISubmodelAPIFactory; |
| import org.eclipse.basyx.submodel.restapi.vab.VABSubmodelAPIFactory; |
| import org.eclipse.basyx.vab.exception.provider.MalformedRequestException; |
| import org.eclipse.basyx.vab.exception.provider.ProviderException; |
| import org.eclipse.basyx.vab.exception.provider.ResourceNotFoundException; |
| import org.eclipse.basyx.vab.modelprovider.VABPathTools; |
| import org.eclipse.basyx.vab.modelprovider.api.IModelProvider; |
| import org.eclipse.basyx.vab.protocol.api.IConnectorFactory; |
| import org.eclipse.basyx.vab.protocol.http.connector.HTTPConnectorFactory; |
| |
| /** |
| * Provider class that implements the AssetAdministrationShellServices <br /> |
| * This provider supports operations on multiple sub models that are selected by |
| * path<br /> |
| * <br /> |
| * Supported API:<br /> |
| * - getModelPropertyValue<br /> |
| * /aas Returns the Asset Administration Shell<br /> |
| * /aas/submodels Retrieves all Submodels from the current Asset Administration |
| * Shell<br /> |
| * /aas/submodels/{subModelId} Retrieves a specific Submodel from a specific |
| * Asset Administration Shell<br /> |
| * /aas/submodels/{subModelId}/properties Retrieves all Properties from the |
| * current Submodel<br /> |
| * /aas/submodels/{subModelId}/operations Retrieves all Operations from the |
| * current Submodel<br /> |
| * /aas/submodels/{subModelId}/events Retrieves all Events from the current |
| * Submodel<br /> |
| * /aas/submodels/{subModelId}/properties/{propertyId} Retrieves a specific |
| * property from the AAS's Submodel<br /> |
| * /aas/submodels/{subModelId}/operations/{operationId} Retrieves a specific |
| * Operation from the AAS's Submodel<br /> |
| * /aas/submodels/{subModelId}/events/{eventId} Retrieves a specific event from |
| * the AAS's submodel |
| * <br /><br /> |
| * - createValue <br /> |
| * /aas/submodels Adds a new Submodel to an existing Asset Administration Shell |
| * <br /><br /> |
| * /aas/submodels/{subModelId}/properties Adds a new property to the AAS's |
| * submodel <br /> |
| * /aas/submodels/{subModelId}/operations Adds a new operation to the AAS's |
| * submodel <br /> |
| * /aas/submodels/{subModelId}/events Adds a new event to the AAS's submodel |
| * <br /><br /> |
| * - invokeOperation<br /> |
| * /aas/submodels/{subModelId}/operations/{operationId} Invokes a specific |
| * operation from the AAS' submodel with a list of input parameters |
| * <br /><br /> |
| * - deleteValue<br /> |
| * /aas/submodels/{subModelId} Deletes a specific Submodel from a specific Asset |
| * Administration Shell <br /> |
| * /aas/submodels/{subModelId}/properties/{propertyId} Deletes a specific |
| * Property from the AAS's Submodel<br /> |
| * /aas/submodels/{subModelId}/operations/{operationId} Deletes a specific |
| * Operation from the AAS's Submodel<br /> |
| * /aas/submodels/{subModelId}/events/{eventId} Deletes a specific event from |
| * the AAS's submodel |
| * <br /><br /> |
| * - setModelPropertyValue<br /> |
| * /aas/submodels/{subModelId}/properties/{propertyId} Sets the value of the |
| * AAS's Submodel's Property |
| * |
| * |
| * @author kuhn, pschorn |
| * |
| */ |
| public class MultiSubmodelProvider implements IModelProvider { |
| |
| public static final String AAS = "aas"; |
| |
| /** |
| * Store aas providers |
| */ |
| protected AASModelProvider aas_provider = null; |
| |
| /** |
| * Store aasId |
| */ |
| protected IIdentifier aasId = null; |
| |
| /** |
| * Store submodel providers |
| */ |
| protected Map<String, SubmodelProvider> submodel_providers = new HashMap<>(); |
| |
| /** |
| * Store AAS Registry |
| */ |
| protected IAASRegistry registry = null; |
| |
| /** |
| * Store HTTP Connector |
| */ |
| protected IConnectorFactory connectorFactory = null; |
| |
| /** |
| * Store AAS API Provider. By default, uses the VAB API Provider |
| */ |
| protected IAASAPIFactory aasApiProvider; |
| |
| /** |
| * Store Submodel API Provider. By default, uses the VAB Submodel Provider |
| */ |
| protected ISubmodelAPIFactory smApiProvider; |
| |
| /** |
| * Constructor with empty default aas and default VAB APIs |
| */ |
| public MultiSubmodelProvider() { |
| this.aasApiProvider = new VABAASAPIFactory(); |
| this.smApiProvider = new VABSubmodelAPIFactory(); |
| IAASAPI aasApi = aasApiProvider.getAASApi(new AssetAdministrationShell()); |
| setAssetAdministrationShell(new AASModelProvider(aasApi)); |
| } |
| |
| /** |
| * Constructor for using custom APIs |
| */ |
| public MultiSubmodelProvider(AASModelProvider contentProvider, IAASAPIFactory aasApiProvider, |
| ISubmodelAPIFactory smApiProvider) { |
| this.aasApiProvider = aasApiProvider; |
| this.smApiProvider = smApiProvider; |
| setAssetAdministrationShell(contentProvider); |
| } |
| |
| /** |
| * Constructor that accepts an AAS |
| */ |
| public MultiSubmodelProvider(AASModelProvider contentProvider) { |
| this.aasApiProvider = new VABAASAPIFactory(); |
| this.smApiProvider = new VABSubmodelAPIFactory(); |
| // Store content provider |
| setAssetAdministrationShell(contentProvider); |
| } |
| |
| /** |
| * Constructor that accepts Submodel |
| */ |
| public MultiSubmodelProvider(SubmodelProvider contentProvider) { |
| this(); |
| // Store content provider |
| addSubmodel(contentProvider); |
| } |
| |
| /** |
| * Constructor that accepts a registry and a connection provider |
| * @param registry |
| * @param provider |
| */ |
| public MultiSubmodelProvider(IAASRegistry registry, IConnectorFactory provider) { |
| this(); |
| this.registry = registry; |
| this.connectorFactory = provider; |
| } |
| |
| /** |
| * Constructor that accepts a registry, a connection provider and API providers |
| */ |
| public MultiSubmodelProvider(AASModelProvider contentProvider, IAASRegistry registry, |
| IConnectorFactory connectorFactory, ISubmodelAPIFactory smApiProvider, IAASAPIFactory aasApiProvider) { |
| this(contentProvider, aasApiProvider, smApiProvider); |
| this.registry = registry; |
| this.connectorFactory = connectorFactory; |
| } |
| |
| /** |
| * Constructor that accepts a aas provider, a registry and a connection provider |
| * |
| * @param contentProvider |
| * @param registry |
| * @param provider |
| */ |
| public MultiSubmodelProvider(AASModelProvider contentProvider, IAASRegistry registry, HTTPConnectorFactory provider) { |
| this(contentProvider); |
| this.registry = registry; |
| this.connectorFactory = provider; |
| } |
| |
| /** |
| * Set an AAS for this provider |
| * |
| * @param elementId |
| * Element ID |
| * @param modelContentProvider |
| * Model content provider |
| */ |
| @SuppressWarnings("unchecked") |
| public void setAssetAdministrationShell(AASModelProvider modelContentProvider) { |
| // Add model provider |
| aas_provider = modelContentProvider; |
| aasId = AssetAdministrationShell.createAsFacade((Map<String, Object>) modelContentProvider.getValue("")).getIdentification(); |
| } |
| |
| @SuppressWarnings("unchecked") |
| public void addSubmodel(SubmodelProvider modelContentProvider) { |
| Submodel sm = Submodel.createAsFacade((Map<String, Object>) modelContentProvider.getValue("/submodel")); |
| addSubmodel(sm, modelContentProvider); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private void createSubmodel(Object newSM) throws ProviderException { |
| // Adds a new submodel to the registered AAS |
| Submodel sm = Submodel.createAsFacade((Map<String, Object>) newSM); |
| |
| ISubmodelAPI smApi = smApiProvider.getSubmodelAPI(sm); |
| addSubmodel(sm, new SubmodelProvider(smApi)); |
| } |
| |
| private void addSubmodel(Submodel sm, SubmodelProvider modelContentProvider) { |
| String smIdShort = sm.getIdShort(); |
| submodel_providers.put(smIdShort, modelContentProvider); |
| aas_provider.createValue("/submodels", sm); |
| } |
| |
| /** |
| * Remove a provider |
| * |
| * @param elementId |
| * Element ID |
| */ |
| public void removeProvider(String elementId) { |
| // Remove model provider |
| submodel_providers.remove(elementId); |
| } |
| |
| /** |
| * Get the value of an element |
| */ |
| @Override |
| public Object getValue(String path) throws ProviderException { |
| VABPathTools.checkPathForNull(path); |
| path = VABPathTools.stripSlashes(path); |
| String[] pathElements = VABPathTools.splitPath(path); |
| if (pathElements.length > 0 && pathElements[0].equals(AAS)) { |
| if (pathElements.length == 1) { |
| return aas_provider.getValue(""); |
| } |
| if (pathElements[1].equals(AssetAdministrationShell.SUBMODELS)) { |
| if (pathElements.length == 2) { |
| return retrieveSubmodels(); |
| } else { |
| IModelProvider provider = submodel_providers.get(pathElements[2]); |
| |
| if (provider == null) { |
| // Get a model provider for the submodel in the registry |
| provider = getModelProvider(pathElements[2]); |
| } |
| |
| // - Retrieve submodel or property value |
| return provider.getValue(VABPathTools.buildPath(pathElements, 3)); |
| } |
| } else { |
| // Handle access to AAS |
| return aas_provider.getValue(VABPathTools.buildPath(pathElements, 1)); |
| } |
| } else { |
| throw new MalformedRequestException("The request " + path + " is not allowed for this endpoint"); |
| } |
| } |
| |
| /** |
| * Retrieves all submodels of the AAS. If there's a registry, remote Submodels |
| * will also be retrieved. |
| * |
| * @return |
| * @throws ProviderException |
| */ |
| @SuppressWarnings("unchecked") |
| private Object retrieveSubmodels() throws ProviderException { |
| // Make a list and return all local submodels |
| Collection<Submodel> submodels = new HashSet<>(); |
| for (IModelProvider submodel : submodel_providers.values()) { |
| submodels.add(Submodel.createAsFacade((Map<String, Object>) submodel.getValue("/submodel"))); |
| } |
| |
| // Check for remote submodels |
| if (registry != null) { |
| AASDescriptor desc = registry.lookupAAS(aasId); |
| |
| // Get the address of the AAS e.g. http://localhost:8080 |
| // This address should be equal to the address of this server |
| String aasEndpoint = desc.getFirstEndpoint(); |
| String aasServerURL = getServerURL(aasEndpoint); |
| |
| List<String> localIds = submodels.stream().map(sm -> sm.getIdentification().getId()).collect(Collectors.toList()); |
| List<IIdentifier> missingIds = desc.getSubmodelDescriptors().stream().map(d -> d.getIdentifier()). |
| filter(id -> !localIds.contains(id.getId())).collect(Collectors.toList()); |
| |
| if(!missingIds.isEmpty()) { |
| List<String> missingEndpoints = missingIds.stream().map(id -> desc.getSubmodelDescriptorFromIdentifierId(id.getId())) |
| .map(smDesc -> smDesc.getFirstEndpoint()).collect(Collectors.toList()); |
| |
| // Check if any of the missing Submodels have the same address as the AAS. |
| // This would mean, that the Submodel should be present on the same |
| // server of the AAS but is not |
| |
| // If this error would not be caught here an endless loop would develop |
| // as the registry would be asked for this Submodel and then it would be requested |
| // from this server again, which would ask the registry about it again |
| |
| // Such a situation might originate from a deleted but not unregistered Submodel |
| // or from a manually registered but never pushed Submodel |
| for(String missingEndpoint: missingEndpoints) { |
| if(getServerURL(missingEndpoint).equals(aasServerURL)) { |
| throw new ResourceNotFoundException("The Submodel at Endpoint '" + missingEndpoint + |
| "' does not exist on this server. It seems to be registered but not actually present."); |
| } |
| } |
| |
| List<Submodel> remoteSms = missingEndpoints.stream().map(endpoint -> connectorFactory.getConnector(endpoint)). |
| map(p -> (Map<String, Object>) p.getValue("")).map(m -> Submodel.createAsFacade(m)).collect(Collectors.toList()); |
| submodels.addAll(remoteSms); |
| } |
| } |
| |
| return submodels; |
| } |
| |
| /** |
| * Change a model property value |
| */ |
| @Override |
| public void setValue(String path, Object newValue) throws ProviderException { |
| VABPathTools.checkPathForNull(path); |
| path = VABPathTools.stripSlashes(path); |
| // Split path |
| String[] pathElements = VABPathTools.splitPath(path); |
| String propertyPath = VABPathTools.buildPath(pathElements, 3); |
| // - Ignore first 2 elements, as it is "/aas/submodels" --> 'aas','submodels' |
| |
| String submodelsPath = VABPathTools.concatenatePaths(AAS, AssetAdministrationShell.SUBMODELS); |
| if (path.equals(AAS)) { |
| createAssetAdministrationShell(newValue); |
| } else if (!path.startsWith(submodelsPath)) { |
| throw new MalformedRequestException("Access to MultiSubmodelProvider always has to start with \"" + submodelsPath + "\", was " + path); |
| } else if (propertyPath.isEmpty()) { |
| createSubmodel(newValue); |
| } else { |
| IModelProvider provider; |
| if (isSubmodelLocal(pathElements[2])) { |
| provider = submodel_providers.get(pathElements[2]); |
| } else { |
| // Get a model provider for the submodel in the registry |
| provider = getModelProvider(pathElements[2]); |
| } |
| provider.setValue(propertyPath, newValue); |
| } |
| } |
| |
| @Override |
| public void createValue(String path, Object newValue) throws ProviderException { |
| throw new MalformedRequestException("Create is not supported by VABMultiSubmodelProvider. Path was: " + path); |
| } |
| |
| |
| @SuppressWarnings("unchecked") |
| private void createAssetAdministrationShell(Object newAAS) { |
| Map<String, Object> aas = (Map<String, Object>) newAAS; |
| AssetAdministrationShell shell = AssetAdministrationShell.createAsFacade(aas); |
| IAASAPI aasApi = aasApiProvider.getAASApi(shell); |
| aas_provider = new AASModelProvider(aasApi); |
| } |
| |
| |
| @SuppressWarnings("unchecked") |
| @Override |
| public void deleteValue(String path) throws ProviderException { |
| VABPathTools.checkPathForNull(path); |
| path = VABPathTools.stripSlashes(path); |
| String[] pathElements = VABPathTools.splitPath(path); |
| String propertyPath = VABPathTools.buildPath(pathElements, 3); |
| // - Ignore first 2 elements, as it is "/aas/submodels" --> 'aas','submodels' |
| if (pathElements.length == 3) { |
| // Delete Submodel from registered AAS |
| String smIdShort = pathElements[2]; |
| if (!isSubmodelLocal(smIdShort)) { |
| return; |
| } |
| |
| // Delete submodel reference from aas |
| // TODO: This is a hack until the API is further clarified |
| Submodel sm = Submodel.createAsFacade((Map<String, Object>) submodel_providers.get(smIdShort).getValue("/submodel")); |
| aas_provider.deleteValue("aas/submodels/" + sm.getIdentification().getId()); |
| |
| // Remove submodel provider |
| submodel_providers.remove(smIdShort); |
| } else if (propertyPath.length() > 0) { |
| IModelProvider provider; |
| if (isSubmodelLocal(pathElements[2])) { |
| provider = submodel_providers.get(pathElements[2]); |
| } else { |
| // Get a model provider for the submodel in the registry |
| provider = getModelProvider(pathElements[2]); |
| } |
| |
| provider.deleteValue(propertyPath); |
| } |
| } |
| |
| @Override |
| public void deleteValue(String path, Object obj) throws ProviderException { |
| throw new MalformedRequestException("DeleteValue with a parameter is not supported. Path was: " + path); |
| } |
| |
| @Override |
| public Object invokeOperation(String path, Object... parameter) throws ProviderException { |
| VABPathTools.checkPathForNull(path); |
| path = VABPathTools.stripSlashes(path); |
| String[] pathElements = VABPathTools.splitPath(path); |
| String operationPath = VABPathTools.buildPath(pathElements, 3); |
| // - Ignore first 2 elements, as it is "/aas/submodels" --> 'aas','submodels' |
| // - Invoke provider and return result |
| IModelProvider provider; |
| if (isSubmodelLocal(pathElements[2])) { |
| provider = submodel_providers.get(pathElements[2]); |
| } else { |
| // Get a model provider for the submodel in the registry |
| provider = getModelProvider(pathElements[2]); |
| } |
| |
| return provider.invokeOperation(operationPath, parameter); |
| } |
| |
| /** |
| * Check whether the given submodel exists in submodel provider |
| * @param key to search the submodel |
| * @return boolean true/false |
| */ |
| private boolean isSubmodelLocal(String submodelId) { |
| return submodel_providers.containsKey(submodelId); |
| } |
| |
| /** |
| * Check whether a registry exists |
| * @return boolean true/false |
| */ |
| private boolean doesRegistryExist() { |
| return this.registry != null; |
| } |
| |
| /** |
| * Get submodel descriptor from the registry |
| * @param submodelId to search the submodel |
| * @return a specifi submodel descriptor |
| */ |
| private SubmodelDescriptor getSubmodelDescriptorFromRegistry(String submodelIdShort) { |
| AASDescriptor aasDescriptor = registry.lookupAAS(aasId); |
| SubmodelDescriptor desc = aasDescriptor.getSubmodelDescriptorFromIdShort(submodelIdShort); |
| if(desc == null) { |
| throw new ResourceNotFoundException("Could not resolve Submodel with idShort " + submodelIdShort + " for AAS " + aasId); |
| } |
| return desc; |
| } |
| |
| /** |
| * Get a model provider from a submodel descriptor |
| * @param submodelDescriptor |
| * @return a model provider |
| */ |
| private IModelProvider getModelProvider(SubmodelDescriptor submodelDescriptor) { |
| String endpoint = submodelDescriptor.getFirstEndpoint(); |
| |
| // Remove "/submodel" since it will be readded later |
| endpoint = endpoint.substring(0, endpoint.length() - SubmodelProvider.SUBMODEL.length() - 1); |
| |
| return connectorFactory.getConnector(endpoint); |
| } |
| |
| /** |
| * Get a model provider from a submodel id |
| * @param submodelId to select a specific submodel |
| * @throws ResourceNotFoundException if no registry is found |
| * @return a model provider |
| */ |
| private IModelProvider getModelProvider(String submodelId) { |
| if (!doesRegistryExist()) { |
| throw new ResourceNotFoundException("Submodel with id " + submodelId + " cannot be resolved locally, but no registry is passed"); |
| } |
| |
| SubmodelDescriptor submodelDescriptor = getSubmodelDescriptorFromRegistry(submodelId); |
| return getModelProvider(submodelDescriptor); |
| } |
| |
| /** |
| * Gets the server URL of a given endpoint. |
| * e.g. http://localhost:1234/x/y/z/aas/submodels/Sm1IdShort would return |
| * http://localhost:1234/x/y/z |
| * |
| * @param endpoint |
| * @return the server URL part of the given endpoint |
| */ |
| public static String getServerURL(String endpoint) { |
| int endServerURL = endpoint.indexOf("/aas"); |
| // if indexOf returned -1 ("/aas" not present in String) |
| // return the whole given path |
| if(endServerURL < 0) { |
| return endpoint; |
| } |
| return endpoint.substring(0, endServerURL); |
| } |
| } |