blob: 934df48486f438c864033294d383216c2e9b977b [file] [log] [blame]
package org.eclipse.basyx.submodel.restapi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.eclipse.basyx.submodel.metamodel.api.ISubModel;
import org.eclipse.basyx.submodel.metamodel.facade.SubmodelElementMapCollectionConverter;
import org.eclipse.basyx.submodel.metamodel.map.SubModel;
import org.eclipse.basyx.submodel.metamodel.map.submodelelement.SubmodelElement;
import org.eclipse.basyx.submodel.metamodel.map.submodelelement.dataelement.property.Property;
import org.eclipse.basyx.submodel.metamodel.map.submodelelement.operation.Operation;
import org.eclipse.basyx.submodel.restapi.api.ISubmodelAPI;
import org.eclipse.basyx.submodel.restapi.vab.VABSubmodelAPI;
import org.eclipse.basyx.vab.exception.provider.MalformedRequestException;
import org.eclipse.basyx.vab.exception.provider.ProviderException;
import org.eclipse.basyx.vab.modelprovider.VABPathTools;
import org.eclipse.basyx.vab.modelprovider.api.IModelProvider;
import org.eclipse.basyx.vab.modelprovider.lambda.VABLambdaProvider;
/**
* Additional VAB provider specific for providing submodels together
* with other SDKs by implementing the submodel API.
*
* The VAB provides generic models without considering high-level information.
* For example, Operation submodel elements are realized as a Map. The function
* is contained and can be accessed at /operations/opId/invokable. Therefore
* invoking the Operation directly at /operations/opId/ attempts to invoke the
* Map, which does not have any effect for the VABModelProvider.
*
* This provider maps these requests to the VAB primitives to generate the
* desired behavior as it is described in the BaSys documentation.
*
* @author espen, schnicke
*
*/
public class SubModelProvider implements IModelProvider {
public static final String VALUES = "values";
public static final String SUBMODEL = "submodel";
ISubmodelAPI submodelAPI;
/**
* Default constructor - based on an empty submodel with a lambda provider
*/
public SubModelProvider() {
this(new SubModel());
}
/**
* Creates a SubmodelProvider based on the VAB API, wrapping the passed provider
*
* @param provider
* to be wrapped by submodel API
*/
public SubModelProvider(IModelProvider provider) {
submodelAPI = new VABSubmodelAPI(provider);
}
/**
* Creates a SubModelProvider based on a lambda provider and a given model
*/
public SubModelProvider(SubModel model) {
submodelAPI = new VABSubmodelAPI(new VABLambdaProvider(model));
}
/**
* Creates a SubModelProvider based on a given ISubmodelAPI.
*/
public SubModelProvider(ISubmodelAPI submodelAPI) {
this.submodelAPI = submodelAPI;
}
/**
* Strips submodel prefix if it exists
*
* @param path
* @return
*/
private String removeSubmodelPrefix(String path) {
path = VABPathTools.stripSlashes(path);
String submodelWithSlash = SUBMODEL + "/";
if (path.startsWith(submodelWithSlash)) {
path = path.replaceFirst("submodel/", "");
} else if (path.equals(SUBMODEL)) {
path = "";
}
path = VABPathTools.stripSlashes(path);
return path;
}
@Override
public Object getModelPropertyValue(String path) throws ProviderException {
VABPathTools.checkPathForNull(path);
path = removeSubmodelPrefix(path);
if (path.isEmpty()) {
ISubModel sm = submodelAPI.getSubmodel();
// Change internal map representation to set
if (sm instanceof SubModel) {
return SubmodelElementMapCollectionConverter.smToMap((SubModel) sm);
} else {
return sm;
}
} else {
String[] splitted = VABPathTools.splitPath(path);
// Request for submodelElements
if (splitted.length == 1 && splitted[0].equals(VALUES)) {
// Request for values of all submodelElements
return submodelAPI.getSubmodel().getValues();
} else if (splitted.length == 1 && splitted[0].equals(MultiSubmodelElementProvider.ELEMENTS)) {
return submodelAPI.getSubmodelElements();
} else if (splitted.length >= 2 && isQualifier(splitted[0])) { // Request for element with specific idShort
// Remove initial "/submodelElements"
path = removeSMElementPrefix(path);
if (endsWithValue(splitted)) { // Request for the value of an property
String idShortPath = path.replace("/value", "");
return submodelAPI.getSubmodelElementValue(idShortPath);
} else if (isInvocationListPath(splitted)) {
List<String> idShorts = getIdShorts(splitted);
// Remove invocationList/{requestId} from the idShorts
idShorts.remove(idShorts.size() - 1);
idShorts.remove(idShorts.size() - 1);
return submodelAPI.getOperationResult(idShorts.get(0), splitted[splitted.length - 1]);
} else {
return submodelAPI.getSubmodelElement(path);
}
}
}
throw new MalformedRequestException("Unknown path " + path + " was requested");
}
private List<String> getIdShorts(String[] splitted) {
// Create list from array and wrap it in ArrayList to ensure modifiability
List<String> idShorts = new ArrayList<>(Arrays.asList(splitted));
// Drop inital "submodelElements"
idShorts.remove(0);
// If value is contained in path, remove it
if (splitted[splitted.length - 1].equals(Property.VALUE)) {
idShorts.remove(idShorts.size() - 1);
}
return idShorts;
}
private boolean endsWithValue(String[] splitted) {
return splitted[splitted.length - 1].equals(Property.VALUE);
}
private boolean isInvocationListPath(String[] splitted) {
return splitted.length > 2 && splitted[splitted.length - 2].equals(OperationProvider.INVOCATION_LIST);
}
@SuppressWarnings("unchecked")
@Override
public void setModelPropertyValue(String path, Object newValue) throws ProviderException {
path = removeSubmodelPrefix(path);
if (path.isEmpty()) {
throw new MalformedRequestException("Set on \"submodel\" not supported");
} else {
String[] splitted = VABPathTools.splitPath(path);
path = removeSMElementPrefix(path);
String idshortPath = path.replace("/value", "");
if (endsWithValue(splitted)) {
submodelAPI.updateSubmodelElement(idshortPath, newValue);
} else {
SubmodelElement element = SubmodelElement.createAsFacade((Map<String, Object>) newValue);
if(!path.endsWith(element.getIdShort())) {
throw new MalformedRequestException("The idShort of given Element '"
+ element.getIdShort() + "' does not match the ending of the given path '" + path + "'");
}
submodelAPI.addSubmodelElement(idshortPath, element);
}
}
}
@Override
public void createValue(String path, Object newEntity) throws ProviderException {
throw new MalformedRequestException("POST (create) on '" + path + "' not allowed. Use PUT (set) instead.");
}
@Override
public void deleteValue(String path) throws ProviderException {
path = removeSubmodelPrefix(path);
if (!path.isEmpty()) {
String[] splitted = VABPathTools.splitPath(path);
if (isQualifier(splitted[0])) {
if (splitted.length > 2) {
path = removeSMElementPrefix(path);
submodelAPI.deleteSubmodelElement(path);
} else {
submodelAPI.deleteSubmodelElement(splitted[1]);
}
} else {
throw new MalformedRequestException("Path " + path + " not supported for delete");
}
} else {
throw new MalformedRequestException("Path \"submodel\" not supported for delete");
}
}
private boolean isQualifier(String str) {
return str.equals(MultiSubmodelElementProvider.ELEMENTS);
}
@Override
public void deleteValue(String path, Object obj) throws ProviderException {
throw new MalformedRequestException("Delete with a passed argument not allowed");
}
@Override
public Object invokeOperation(String path, Object... parameters) throws ProviderException {
path = removeSubmodelPrefix(path);
if (path.isEmpty()) {
throw new MalformedRequestException("Given path must not be empty");
} else {
if (VABPathTools.isOperationInvokationPath(path)) {
if(path.endsWith(OperationProvider.ASYNC)) {
path = removeSMElementPrefix(path);
path = path.replace(Operation.INVOKE + OperationProvider.ASYNC, "");
return submodelAPI.invokeAsync(path, parameters);
} else {
path = removeSMElementPrefix(path);
return submodelAPI.invokeOperation(path, parameters);
}
} else {
throw new MalformedRequestException("Given path '" + path + "' does not end in /invoke");
}
}
}
protected void setAPI(ISubmodelAPI api) {
this.submodelAPI = api;
}
private String removeSMElementPrefix(String path) {
return path.replace("submodelElements", "");
}
}