diff --git a/PR.md b/PR.md new file mode 100644 index 000000000..3f8758dbc --- /dev/null +++ b/PR.md @@ -0,0 +1,15 @@ +## Summary +- prevent circular `referredSemanticId` chains in `DefaultReference` by rejecting self/indirect cycles during assignment +- add unit tests that cover self and indirect cycles plus happy-path acyclic chains +- include JUnit dependency in the model module to compile the new tests + +## Background +Jackson XML serialization blew up with a StackOverflow when `Reference.referredSemanticId` contained a cycle (e.g., `Reference -> Reference -> same Reference`). The model allowed such graphs because the setter did not guard against cycles. This PR keeps the model acyclic so downstream serializers do not need custom circular-reference handling. + +## Implementation Notes +- cycle detection uses identity (not equals) to avoid false positives from structurally equal references +- the guard walks the `referredSemanticId` chain once and fails fast with a clear `IllegalArgumentException` message +- tests cover self-reference, indirect cycles, and a valid chain + +## Testing +- `mvn -pl model test -Dmaven.repo.local=./.m2` diff --git a/model/pom.xml b/model/pom.xml index db21bf29e..ab2226b22 100644 --- a/model/pom.xml +++ b/model/pom.xml @@ -17,6 +17,13 @@ This project includes a Java representation of the classes defined in the Asset Administration Shell ontology. Asset Administration Shell Java Model + + + junit + junit + test + + 1.8 diff --git a/model/src/main/java/org/eclipse/digitaltwin/aas4j/v3/model/impl/DefaultReference.java b/model/src/main/java/org/eclipse/digitaltwin/aas4j/v3/model/impl/DefaultReference.java index a19a091e5..e5ab804fd 100644 --- a/model/src/main/java/org/eclipse/digitaltwin/aas4j/v3/model/impl/DefaultReference.java +++ b/model/src/main/java/org/eclipse/digitaltwin/aas4j/v3/model/impl/DefaultReference.java @@ -16,8 +16,11 @@ package org.eclipse.digitaltwin.aas4j.v3.model.impl; import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; +import java.util.Set; import org.eclipse.digitaltwin.aas4j.v3.model.Key; import org.eclipse.digitaltwin.aas4j.v3.model.Reference; import org.eclipse.digitaltwin.aas4j.v3.model.ReferenceTypes; @@ -93,6 +96,7 @@ public Reference getReferredSemanticId() { @Override public void setReferredSemanticId(Reference referredSemanticId) { + validateReferredSemanticId(referredSemanticId); this.referredSemanticId = referredSemanticId; } @@ -119,4 +123,19 @@ protected DefaultReference newBuildingInstance() { return new DefaultReference(); } } + + private void validateReferredSemanticId(Reference referredSemanticId) { + if (referredSemanticId == null) { + return; + } + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + Reference current = referredSemanticId; + while (current != null) { + if (current == this || !visited.add(current)) { + throw new IllegalArgumentException( + "referredSemanticId must not create a circular Reference"); + } + current = current.getReferredSemanticId(); + } + } } diff --git a/model/src/test/java/org/eclipse/digitaltwin/aas4j/v3/model/impl/DefaultReferenceTest.java b/model/src/test/java/org/eclipse/digitaltwin/aas4j/v3/model/impl/DefaultReferenceTest.java new file mode 100644 index 000000000..b64e244cb --- /dev/null +++ b/model/src/test/java/org/eclipse/digitaltwin/aas4j/v3/model/impl/DefaultReferenceTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e. V. + * Copyright (c) 2023, SAP SE or an SAP affiliate company + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.eclipse.digitaltwin.aas4j.v3.model.impl; + +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +public class DefaultReferenceTest { + + @Test(expected = IllegalArgumentException.class) + public void setReferredSemanticId_withSelfReference_throws() { + DefaultReference reference = new DefaultReference(); + + reference.setReferredSemanticId(reference); + } + + @Test(expected = IllegalArgumentException.class) + public void setReferredSemanticId_withIndirectCycle_throws() { + DefaultReference first = new DefaultReference(); + DefaultReference second = new DefaultReference(); + second.setReferredSemanticId(first); + + first.setReferredSemanticId(second); + } + + @Test + public void setReferredSemanticId_withAcyclicChain_succeeds() { + DefaultReference reference = new DefaultReference(); + DefaultReference semanticReference = new DefaultReference(); + DefaultReference semanticParent = new DefaultReference(); + + semanticReference.setReferredSemanticId(semanticParent); + reference.setReferredSemanticId(semanticReference); + + assertSame(semanticReference, reference.getReferredSemanticId()); + assertSame(semanticParent, semanticReference.getReferredSemanticId()); + } +}