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
@@ -0,0 +1,46 @@
/*******************************************************************************
* 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.core.exceptions;

/**
* Indicates that the requested submodel element is not a Data SubmodelElement
*
* @author fried
*
*/
@SuppressWarnings("serial")
public class SubmodelElementNotADataElementException extends RuntimeException {
public SubmodelElementNotADataElementException() {
}

public SubmodelElementNotADataElementException(String elementId) {
super(getMsg(elementId));
}

private static String getMsg(String elementId) {
return "SubmodelElement with Id " + elementId + " is not a Data Element";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,16 @@ public ResponseEntity<Object> handleFeatureNotSupportedException(FeatureNotSuppo
public ResponseEntity<Object> handleNotInvokableException(NotInvokableException exception) {
return buildResponse(exception.getMessage(), HttpStatus.METHOD_NOT_ALLOWED, exception);
}

@ExceptionHandler(ElementNotAFileException.class)
public ResponseEntity<Object> handleElementNotAFileException(ElementNotAFileException exception) {
return buildResponse(exception.getMessage(), HttpStatus.PRECONDITION_FAILED, exception);
}

@ExceptionHandler(SubmodelElementNotADataElementException.class)
public ResponseEntity<Object> handleSubmodelElementNotADataElementException(SubmodelElementNotADataElementException exception) {
return buildResponse(exception.getMessage(), HttpStatus.BAD_REQUEST, exception);
}

@ExceptionHandler(InsufficientPermissionException.class)
public ResponseEntity<Object> handleInsufficientPermissionException(InsufficientPermissionException exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.eclipse.digitaltwin.basyx.common.backend.inmemory.core.InMemoryCrudRepository;
import org.eclipse.digitaltwin.basyx.core.exceptions.CollidingIdentifierException;
import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException;
import org.eclipse.digitaltwin.basyx.core.exceptions.SubmodelElementNotADataElementException;
import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult;
import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
import org.eclipse.digitaltwin.basyx.core.pagination.PaginationSupport;
Expand Down Expand Up @@ -126,6 +127,13 @@ public synchronized void createSubmodelElement(String submodelId, String idShort
} else if (parentSme instanceof Entity entity) {
List<SubmodelElement> submodelElements = entity.getStatements();
submodelElements.add(submodelElement);
} else if (parentSme instanceof AnnotatedRelationshipElement are) {
try {
List<DataElement> annotations = are.getAnnotations();
annotations.add((DataElement) submodelElement);
} catch (ClassCastException e) {
throw new SubmodelElementNotADataElementException(submodelElement.getIdShort());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,9 @@
import java.util.List;

import org.bson.Document;
import org.eclipse.digitaltwin.aas4j.v3.model.Entity;
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.SubmodelElementList;
import org.eclipse.digitaltwin.aas4j.v3.model.*;
import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException;
import org.eclipse.digitaltwin.basyx.core.exceptions.SubmodelElementNotADataElementException;
import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult;
import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
import org.eclipse.digitaltwin.basyx.submodelservice.backend.SubmodelOperations;
Expand Down Expand Up @@ -186,14 +183,22 @@ public void createSubmodelElement(String submodelId, String idShortPath, Submode
} else if (parentSme instanceof Entity entity) {
List<SubmodelElement> submodelElements = entity.getStatements();
submodelElements.add(submodelElement);
} else if (parentSme instanceof AnnotatedRelationshipElement are) {
try {
List<DataElement> annotations = are.getAnnotations();
annotations.add((DataElement) submodelElement);
} catch (ClassCastException e) {
throw new SubmodelElementNotADataElementException(submodelElement.getIdShort());
}
}

updateSubmodelElement(submodelId, idShortPath, parentSme);
}

@Override
public void updateSubmodelElement(String submodelId, String idShortPath, SubmodelElement submodelElement) throws ElementDoesNotExistException {
MongoFilterResult filterResult = MongoFilterBuilder.parse(idShortPath);
List<SubmodelElement> parentElements = getParentElements(submodelId, idShortPath);
MongoFilterResult filterResult = MongoFilterBuilder.parse(idShortPath, parentElements);

Query query = new Query(Criteria.where("_id").is(submodelId));
Update update = new Update().set(filterResult.key(), submodelElement);
Expand Down Expand Up @@ -326,6 +331,36 @@ private boolean existsSubmodelElement(String submodelId, String idShortPath){
}
}

/**
* Gets the list of parent SubmodelElements along the path.
* This is used to determine if any parent is an Entity (which uses 'statements' instead of 'value').
*/
private List<SubmodelElement> getParentElements(String submodelId, String idShortPath) {
List<SubmodelElement> parents = new ArrayList<>();
String[] segments = idShortPath.split("\\.");

if (segments.length <= 1) {
return parents;
}

StringBuilder currentPath = new StringBuilder();
for (int i = 0; i < segments.length - 1; i++) {
if (currentPath.length() > 0) {
currentPath.append(".");
}
currentPath.append(segments[i]);
try {
SubmodelElement element = getSubmodelElement(submodelId, currentPath.toString());
parents.add(element);
} catch (ElementDoesNotExistException e) {
// Element doesn't exist, stop here
break;
}
}

return parents;
}

private static boolean hasLimit(PaginationInfo pInfo) {
return pInfo.getLimit() != null && pInfo.getLimit() > 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
import java.util.Deque;
import java.util.List;

import org.eclipse.digitaltwin.aas4j.v3.model.AnnotatedRelationshipElement;
import org.eclipse.digitaltwin.aas4j.v3.model.Entity;
import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
import org.eclipse.digitaltwin.basyx.submodelservice.backend.IdShortPathParser.GenericPath;
import org.eclipse.digitaltwin.basyx.submodelservice.backend.IdShortPathParser.IdShortPath;
import org.eclipse.digitaltwin.basyx.submodelservice.backend.IdShortPathParser.IndexPath;
Expand All @@ -50,17 +53,30 @@
public final class MongoFilterBuilder{
static final String KEY_VALUE = "value";
static final String KEY_STATEMENTS = "statements";
static final String KEY_ANNOTATIONS = "annotations";
static final String KEY_SUBMODEL_ELEMENTS = "submodelElements";
static final String KEY_ID_SHORT = "idShort";

public static MongoFilterResult parse(@NonNull String idShortPath) {
return parse(idShortPath, List.of());
}

/**
* Parses the idShortPath and builds MongoDB filter result.
*
* @param idShortPath the path to parse
* @param parentElements list of parent SubmodelElements along the path (used to determine if Entity uses 'statements')
* @return MongoFilterResult with the update key and filters
*/
public static MongoFilterResult parse(@NonNull String idShortPath, @NonNull List<SubmodelElement> parentElements) {
Deque<GenericPath> paths = IdShortPathParser.parse(idShortPath);

assert !paths.isEmpty();

StringBuilder updateKey = new StringBuilder();
List<CriteriaDefinition> filterArray = new ArrayList<>();
int filterCounter = 0;
int parentIndex = 0;

updateKey.append(KEY_SUBMODEL_ELEMENTS);

Expand All @@ -74,7 +90,11 @@ public static MongoFilterResult parse(@NonNull String idShortPath) {

while (!paths.isEmpty()) {
GenericPath segment = paths.pop();
updateKey.append(".").append(KEY_VALUE);
// Determine the correct child key based on parent element type
String childKey = getChildKey(parentElements, parentIndex);
updateKey.append(".").append(childKey);
parentIndex++;

if (segment instanceof IdShortPath idPath) {
placeholder = "elem" + filterCounter++;
updateKey.append(".$[").append(placeholder).append("]");
Expand Down Expand Up @@ -104,21 +124,44 @@ public static List<AggregationOperation> buildAggregationOperations(@NonNull Str
currentPath = paths.pop();
ops.add(new UnwindOperation(Fields.field("$"+KEY_VALUE), true));
ops.add(new UnwindOperation(Fields.field("$"+KEY_STATEMENTS), true));
ops.add(new UnwindOperation(Fields.field("$"+KEY_ANNOTATIONS), true));
if (currentPath instanceof IdShortPath idPath) {
Criteria inValue = Criteria.where(joinKeys(KEY_VALUE, KEY_ID_SHORT)).is(idPath.idShort());
Criteria inStatements = Criteria.where(joinKeys(KEY_STATEMENTS, KEY_ID_SHORT)).is(idPath.idShort());
ops.add(Aggregation.match(new Criteria().orOperator(inValue, inStatements)));
Criteria inAnnotations = Criteria.where(joinKeys(KEY_ANNOTATIONS, KEY_ID_SHORT)).is(idPath.idShort());
ops.add(Aggregation.match(new Criteria().orOperator(inValue, inStatements, inAnnotations)));
} else if (currentPath instanceof IndexPath ixPath) {
ops.add(Aggregation.skip(ixPath.index()));
ops.add(Aggregation.limit(1));
}
ops.add(Aggregation.replaceRoot(IfNull.ifNull("$"+KEY_VALUE).then("$"+KEY_STATEMENTS)));
ops.add(Aggregation.replaceRoot(
IfNull.ifNull("$"+KEY_VALUE)
.thenValueOf(IfNull.ifNull("$"+KEY_STATEMENTS).then("$"+KEY_ANNOTATIONS))));

}

return ops;
}

/**
* Determines the correct child key based on the parent element type.
* - Entity uses 'statements'
* - AnnotatedRelationshipElement uses 'annotations'
* - Others (SubmodelElementList, SubmodelElementCollection) use 'value'
*/
private static String getChildKey(List<SubmodelElement> parentElements, int parentIndex) {
if (parentIndex >= parentElements.size()) {
return KEY_VALUE;
}
SubmodelElement parent = parentElements.get(parentIndex);
if (parent instanceof Entity) {
return KEY_STATEMENTS;
} else if (parent instanceof AnnotatedRelationshipElement) {
return KEY_ANNOTATIONS;
}
return KEY_VALUE;
}

static CriteriaDefinition buildCriteria(String key, String value) {
return Criteria.where(key).is(value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,7 @@
import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperationRequest;
import org.eclipse.digitaltwin.basyx.client.internal.ApiException;
import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException;
import org.eclipse.digitaltwin.basyx.core.exceptions.ElementNotAFileException;
import org.eclipse.digitaltwin.basyx.core.exceptions.FileDoesNotExistException;
import org.eclipse.digitaltwin.basyx.core.exceptions.NotInvokableException;
import org.eclipse.digitaltwin.basyx.core.exceptions.OperationDelegationException;
import org.eclipse.digitaltwin.basyx.core.exceptions.*;
import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult;
import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
import org.eclipse.digitaltwin.basyx.http.Base64UrlEncoder;
Expand Down Expand Up @@ -226,6 +222,10 @@ private RuntimeException mapExceptionSubmodelElementAccess(String idShortPath, A
if (e.getCode() == HttpStatus.NOT_FOUND.value()) {
return new ElementDoesNotExistException(idShortPath);
}
System.out.println(e.getMessage());
if (e.getCode() == HttpStatus.BAD_REQUEST.value() && e.getMessage().contains("is not a Data Element")) {
return new SubmodelElementNotADataElementException(idShortPath);
}

return e;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
package org.eclipse.digitaltwin.basyx.submodelservice.pathparsing;

import java.util.Collection;
import java.util.stream.Collectors;

import org.eclipse.digitaltwin.aas4j.v3.model.AnnotatedRelationshipElement;
import org.eclipse.digitaltwin.aas4j.v3.model.Entity;
import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection;
Expand Down Expand Up @@ -59,6 +61,12 @@ public SubmodelElement getSubmodelElement(SubmodelElement rootElement) {
Entity entity = (Entity) rootElement;

return filterSubmodelElement(entity.getStatements());
} else if (rootElement instanceof AnnotatedRelationshipElement) {
AnnotatedRelationshipElement are = (AnnotatedRelationshipElement) rootElement;

return filterSubmodelElement(are.getAnnotations().stream()
.map(de -> (SubmodelElement) de)
.collect(Collectors.toList()));
}

throw new ElementDoesNotExistException(token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,9 @@
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.NotImplementedException;
import org.eclipse.digitaltwin.aas4j.v3.model.DataTypeDefXsd;
import org.eclipse.digitaltwin.aas4j.v3.model.Entity;
import org.eclipse.digitaltwin.aas4j.v3.model.File;
import org.eclipse.digitaltwin.aas4j.v3.model.LangStringTextType;
import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable;
import org.eclipse.digitaltwin.aas4j.v3.model.Property;
import org.eclipse.digitaltwin.aas4j.v3.model.Range;
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.SubmodelElementList;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultEntity;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultFile;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultLangStringTextType;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList;
import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException;
import org.eclipse.digitaltwin.basyx.core.exceptions.ElementNotAFileException;
import org.eclipse.digitaltwin.basyx.core.exceptions.FileDoesNotExistException;
import org.eclipse.digitaltwin.basyx.core.exceptions.NotInvokableException;
import org.eclipse.digitaltwin.aas4j.v3.model.*;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.*;
import org.eclipse.digitaltwin.basyx.core.exceptions.*;
import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult;
import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
import org.eclipse.digitaltwin.basyx.submodelservice.value.FileBlobValue;
Expand Down Expand Up @@ -670,6 +651,50 @@ public void patchSubmodelElements() {
assertEquals(submodelElementsPatch, patchedSubmodel.getSubmodelElements());
}

@Test
public void addEntityToEntity() {
List<SubmodelElement> submodelElements = new ArrayList<>();
Entity entity = new DefaultEntity.Builder().idShort("MainEntity").build();
submodelElements.add(entity);
Submodel submodel = buildDummySubmodelWithSmElement(ID, submodelElements);
SubmodelService submodelService = getSubmodelService(submodel);

Entity subEntity = new DefaultEntity.Builder().idShort("SubEntity").build();
submodelService.createSubmodelElement("MainEntity", subEntity);


Entity sub_subEntity = new DefaultEntity.Builder().idShort("Sub_SubEntity").build();
submodelService.createSubmodelElement("MainEntity.SubEntity", sub_subEntity);

SubmodelElement sme = submodelService.getSubmodelElement("MainEntity.SubEntity.Sub_SubEntity");
assertEquals(sub_subEntity.getIdShort(), sme.getIdShort());
}

@Test
public void addDataElementToARE(){
List<SubmodelElement> submodelElements = new ArrayList<>();
AnnotatedRelationshipElement topAre = new DefaultAnnotatedRelationshipElement.Builder().idShort("MainAre").build();
Property property = createDummyProperty("Test");
submodelElements.add(topAre);
Submodel submodel = buildDummySubmodelWithSmElement(ID, submodelElements);
SubmodelService submodelService = getSubmodelService(submodel);

submodelService.createSubmodelElement("MainAre", property);
SubmodelElement sme = submodelService.getSubmodelElement("MainAre.Test");
assertEquals(sme.getIdShort(), sme.getIdShort());
}

@Test(expected = SubmodelElementNotADataElementException.class)
public void addNonDataElementToAre(){
List<SubmodelElement> submodelElements = new ArrayList<>();
AnnotatedRelationshipElement topAre = new DefaultAnnotatedRelationshipElement.Builder().idShort("MainAre").build();
AnnotatedRelationshipElement subAre = new DefaultAnnotatedRelationshipElement.Builder().idShort("SubAre").build();
submodelElements.add(topAre);
Submodel submodel = buildDummySubmodelWithSmElement(ID, submodelElements);
SubmodelService submodelService = getSubmodelService(submodel);
submodelService.createSubmodelElement("MainAre", subAre);
}

protected Submodel buildDummySubmodelWithSmElement(String id, List<SubmodelElement> submodelElements) {
return new DefaultSubmodel.Builder().id(id).submodelElements(submodelElements).build();
}
Expand Down