From fe7c286ad9b384c9c2ec092abc05d1429436420d Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Tue, 3 Mar 2026 16:36:35 +0100 Subject: [PATCH 01/15] ctermcode.constraint type to string --- .../archie/adl14/ADL14NodeIDConverter.java | 3 +- .../adl14/ADL14TermConstraintConverter.java | 14 +++---- .../adl14/PreviousConversionApplier.java | 5 ++- .../archie/adl14/aom14/CDVOrdinalItem.java | 4 +- .../Adl14CComplexObjectParser.java | 4 +- .../Adl14PrimitivesConstraintParser.java | 11 +++--- .../PrimitivesConstraintParser.java | 6 +-- .../java/com/nedap/archie/aom/Archetype.java | 4 +- .../nedap/archie/aom/CPrimitiveObject.java | 12 +++--- .../nedap/archie/aom/primitives/CBoolean.java | 5 ++- .../nedap/archie/aom/primitives/COrdered.java | 11 ++++++ .../nedap/archie/aom/primitives/CString.java | 5 ++- .../aom/primitives/CTerminologyCode.java | 37 +++++++++---------- .../com/nedap/archie/aom/utils/AOMUtils.java | 3 +- .../CTerminologyCodeSerializer.java | 6 +-- .../archie/rminfo/UpdatedValueHandler.java | 4 +- .../validations/CodeValidation.java | 3 +- .../ExampleJsonInstanceGenerator.java | 4 +- .../diff/UnconstrainedIntervalRemover.java | 9 +++-- .../PrimitiveObjectConstraintHelper.java | 4 +- .../RmPrimitiveObjectValidator.java | 2 +- .../RMPrimitiveObjectValidation.java | 2 +- .../evaluation/FixableAssertionsChecker.java | 2 +- ...DL14ExternalTerminologyConversionTest.java | 4 +- .../archie/adlparser/DefinitionTest.java | 3 +- .../aom/TerminologyCodeConstraintsTest.java | 14 +++---- .../com/nedap/archie/json/AOMJacksonTest.java | 2 +- .../TerminologyCodeConstraintsTest.java | 16 ++++---- ...rchetypeSerializerParserRoundtripTest.java | 2 +- .../com/nedap/archie/xml/JAXBAOMTest.java | 2 +- 30 files changed, 107 insertions(+), 96 deletions(-) diff --git a/aom/src/main/java/com/nedap/archie/adl14/ADL14NodeIDConverter.java b/aom/src/main/java/com/nedap/archie/adl14/ADL14NodeIDConverter.java index aa06afe1a..794ff054b 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/ADL14NodeIDConverter.java +++ b/aom/src/main/java/com/nedap/archie/adl14/ADL14NodeIDConverter.java @@ -396,8 +396,7 @@ private static void fixArchetypeSlotExpression(Expression expression) { Expression rightOperand = binary.getRightOperand(); if (rightOperand instanceof Constraint) { Constraint constraint = (Constraint) rightOperand; - if (constraint.getItem() != null && constraint.getItem().getConstraint() != null && !constraint.getItem().getConstraint().isEmpty() && - constraint.getItem() instanceof CString) { + if (constraint.getItem() != null && constraint.getItem() instanceof CString && constraint.getItem().getConstraint() != null && !((CString) constraint.getItem()).getConstraint().isEmpty()) { CString cString = (CString) constraint.getItem(); if (cString.getConstraint() == null || cString.getConstraint().isEmpty()) { return; diff --git a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java index 370b339be..3309fb88e 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java +++ b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java @@ -1,6 +1,5 @@ package com.nedap.archie.adl14; -import com.google.common.collect.Lists; import com.nedap.archie.adl14.log.CreatedCode; import com.nedap.archie.adl14.log.ReasonForCodeCreation; import com.nedap.archie.aom.*; @@ -16,7 +15,6 @@ import org.slf4j.LoggerFactory; import java.net.URI; -import java.net.URISyntaxException; import java.util.*; import java.util.stream.Collectors; @@ -56,8 +54,8 @@ private void convert(CObject cObject) { for (CPrimitiveObject cPrimitiveObject : termCodes) { CTerminologyCode cTerminologyCode = (CTerminologyCode) cPrimitiveObject; convertCTerminologyCode(cTerminologyCode); - if(cTerminologyCode.getConstraint().size() == 1) { - String constraint = cTerminologyCode.getConstraint().get(0); + if(cTerminologyCode.getConstraint() != null) { + String constraint = cTerminologyCode.getConstraint(); if(AOMUtils.isValueCode(constraint)) { atCodes.add(constraint); } @@ -93,7 +91,8 @@ private void convert(CAttribute attribute) { private void convertCTerminologyCode(CTerminologyCode cTerminologyCode) { if(cTerminologyCode.getConstraint() != null && !cTerminologyCode.getConstraint().isEmpty()) { - String firstConstraint = cTerminologyCode.getConstraint().get(0); + // TODO: this seems to try to fix a complicated case that was introduced in com.nedap.archie.adl14.treewalkers.Adl14PrimitivesConstraintParser.parseCTerminologyCode, fix later + /*String firstConstraint = cTerminologyCode.getConstraint().get(0); TerminologyCode termCode = TerminologyCode.createFromString(firstConstraint); boolean isLocalCode = termCode.getTerminologyId() == null || termCode.getTerminologyId().equalsIgnoreCase("local"); if(isLocalCode && AOMUtils.isValueCode(firstConstraint)) { @@ -188,6 +187,7 @@ private void convertCTerminologyCode(CTerminologyCode cTerminologyCode) { } } } + */ } } @@ -257,8 +257,8 @@ private ValueSet findOrCreateValueSet(Archetype archetype, Set localCode if(cObject instanceof CTerminologyCode) { CTerminologyCode termCodeInParent = (CTerminologyCode) cObject; if(termCodeInParent.getConstraint() != null && !termCodeInParent.getConstraint().isEmpty()) { - if(termCodeInParent.getConstraint().get(0).startsWith("ac")) { - idInparent = termCodeInParent.getConstraint().get(0); + if(termCodeInParent.getConstraint().startsWith("ac")) { + idInparent = termCodeInParent.getConstraint(); } } } diff --git a/aom/src/main/java/com/nedap/archie/adl14/PreviousConversionApplier.java b/aom/src/main/java/com/nedap/archie/adl14/PreviousConversionApplier.java index 473f6de6c..89870fca8 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/PreviousConversionApplier.java +++ b/aom/src/main/java/com/nedap/archie/adl14/PreviousConversionApplier.java @@ -199,7 +199,8 @@ private Set gatherUsedValueSets(CAttribute attribute) { Set result = new LinkedHashSet<>(); for(CObject child:attribute.getChildren()) { if(child instanceof CTerminologyCode) { - for(String constraint:((CTerminologyCode) child).getConstraint()) { + String constraint = ((CTerminologyCode) child).getConstraint(); + if(constraint != null) { if(constraint.startsWith("ac")) { result.add(constraint); } @@ -215,7 +216,7 @@ private Set gatherUsedValueSets(CAttribute attribute) { for(CPrimitiveTuple primitiveTuple:tuple.getTuples()) { CTerminologyCode cTermCode = (CTerminologyCode) primitiveTuple.getMember(symbolIndex); if(cTermCode != null) { - atCodes.addAll(cTermCode.getConstraint()); + atCodes.addAll(cTermCode.getConstraintAsList()); } } } diff --git a/aom/src/main/java/com/nedap/archie/adl14/aom14/CDVOrdinalItem.java b/aom/src/main/java/com/nedap/archie/adl14/aom14/CDVOrdinalItem.java index 8f0f3ed99..771c80009 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/aom14/CDVOrdinalItem.java +++ b/aom/src/main/java/com/nedap/archie/adl14/aom14/CDVOrdinalItem.java @@ -6,8 +6,6 @@ import com.nedap.archie.base.Interval; import com.nedap.archie.base.terminology.TerminologyCode; -import java.util.Arrays; - public class CDVOrdinalItem { private Integer value; @@ -35,7 +33,7 @@ public CTerminologyCode getSymbolAdl2() { return null; } CTerminologyCode result = new CTerminologyCode(); - result.setConstraint(Arrays.asList(symbol.toString())); + result.setConstraint(symbol.toString()); return result; } diff --git a/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14CComplexObjectParser.java b/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14CComplexObjectParser.java index 26f8cbfd0..2d2290808 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14CComplexObjectParser.java +++ b/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14CComplexObjectParser.java @@ -206,7 +206,7 @@ private CObject parseNonPrimitiveObject(C_non_primitive_objectContext objectCont CTerminologyCode cCode = new CTerminologyCode(); TerminologyCode code = TerminologyCode.createFromString(ordinal_termContext.c_terminology_code().getText()); - cCode.addConstraint(code.getCodeString()); + cCode.setConstraint(code.getCodeString()); primitiveTuple.addMember(cValue); primitiveTuple.addMember(cCode); @@ -275,7 +275,7 @@ private void parseCDVQuantity(C_non_primitive_objectContext objectContext, CComp CAttribute property = new CAttribute("property"); CTerminologyCode code = new CTerminologyCode(); //will be converted later - code.addConstraint(cdvQuantity.getProperty().toString()); + code.setConstraint(cdvQuantity.getProperty().toString()); property.addChild(code); result.addAttribute(property); } diff --git a/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParser.java b/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParser.java index 2aa0ddfcb..59315f648 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParser.java +++ b/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParser.java @@ -103,23 +103,24 @@ public CTerminologyCode parseCTerminologyCode(Adl14Parser.C_terminology_codeCont //need to do parsing here because the lexer matched the entire term code ref TerminologyCode terminologyCode = TerminologyCode.createFromString(qualifiedTermCodeContext.TERM_CODE_REF().getText()); if (terminologyCode.getTerminologyId() != null && terminologyCode.getTerminologyId().equalsIgnoreCase("local")) { - result.addConstraint(terminologyCode.getCodeString()); + result.setConstraint(terminologyCode.getCodeString()); } else { //non-local term constraints. Just add the text here, it will be converted later - result.addConstraint(qualifiedTermCodeContext.TERM_CODE_REF().getText()); + result.setConstraint(qualifiedTermCodeContext.TERM_CODE_REF().getText()); } } else { String terminologyId = qualifiedTermCodeContext.identifier(0).getText(); if (terminologyId.equalsIgnoreCase("local")) { //we need to create a value set. For now just add the constraint, the value set will come after //the parser + // TODO: the List type of the terminologycode.constraint seems to have been used here for a tmp storage to build a value set later, check this and make sure this is still working as intended after these changes... for (int i = 1; i < qualifiedTermCodeContext.identifier().size(); i++) { - result.addConstraint(qualifiedTermCodeContext.identifier(i).getText()); + // result.addConstraint(qualifiedTermCodeContext.identifier(i).getText()); } } else { //non-local term constraints. Add the text here, will be converted later for (int i = 0; i < qualifiedTermCodeContext.identifier().size(); i++) { - result.addConstraint(qualifiedTermCodeContext.identifier(i).getText()); + // result.addConstraint(qualifiedTermCodeContext.identifier(i).getText()); } } @@ -130,7 +131,7 @@ public CTerminologyCode parseCTerminologyCode(Adl14Parser.C_terminology_codeCont } else { //this is an AC-code. if(terminologyCodeContext.localTermCode().AC_CODE() != null) { - result.addConstraint(terminologyCodeContext.localTermCode().AC_CODE().getText()); + result.setConstraint(terminologyCodeContext.localTermCode().AC_CODE().getText()); } else { throw new RuntimeException("unknown terminology code format - this looks adl2 inside the adl 1.4 format?"); } diff --git a/aom/src/main/java/com/nedap/archie/adlparser/treewalkers/PrimitivesConstraintParser.java b/aom/src/main/java/com/nedap/archie/adlparser/treewalkers/PrimitivesConstraintParser.java index 534be0060..2e58b78dc 100644 --- a/aom/src/main/java/com/nedap/archie/adlparser/treewalkers/PrimitivesConstraintParser.java +++ b/aom/src/main/java/com/nedap/archie/adlparser/treewalkers/PrimitivesConstraintParser.java @@ -110,12 +110,12 @@ public CTerminologyCode parseCTerminologyCode(AdlParser.C_terminology_codeContex String assumedValueString = terminologyCodeContext.AT_CODE().getText(); assumedValue.setCodeString(assumedValueString); result.setAssumedValue(assumedValue); - result.addConstraint(assumedValue.getTerminologyIdString()); + result.setConstraint(assumedValue.getTerminologyIdString()); } else { if(terminologyCodeContext.AC_CODE() != null) { - result.addConstraint(terminologyCodeContext.AC_CODE().getText()); + result.setConstraint(terminologyCodeContext.AC_CODE().getText()); } else { - result.addConstraint(terminologyCodeContext.AT_CODE().getText()); + result.setConstraint(terminologyCodeContext.AT_CODE().getText()); } } diff --git a/aom/src/main/java/com/nedap/archie/aom/Archetype.java b/aom/src/main/java/com/nedap/archie/aom/Archetype.java index 8d85292f7..d371b1975 100644 --- a/aom/src/main/java/com/nedap/archie/aom/Archetype.java +++ b/aom/src/main/java/com/nedap/archie/aom/Archetype.java @@ -288,8 +288,8 @@ public Set getAllUsedCodes() { if(cObject instanceof CTerminologyCode) { CTerminologyCode terminologyCode = (CTerminologyCode) cObject; result.addAll(terminologyCode.getValueSetExpanded()); - if(!terminologyCode.getConstraint().isEmpty()) { - result.add(terminologyCode.getConstraint().get(0)); + if(terminologyCode.getConstraint() != null) { + result.add(terminologyCode.getConstraint()); } } for(CAttribute attribute:cObject.getAttributes()) { diff --git a/aom/src/main/java/com/nedap/archie/aom/CPrimitiveObject.java b/aom/src/main/java/com/nedap/archie/aom/CPrimitiveObject.java index 0f850b393..403d17b07 100644 --- a/aom/src/main/java/com/nedap/archie/aom/CPrimitiveObject.java +++ b/aom/src/main/java/com/nedap/archie/aom/CPrimitiveObject.java @@ -35,11 +35,9 @@ public abstract class CPrimitiveObject extends CDefinedOb public abstract void setAssumedValue(ValueType assumedValue); - public abstract List getConstraint(); + public abstract Object getConstraint(); - public abstract void setConstraint(List constraint); - - public abstract void addConstraint(Constraint constraint); + public abstract List getConstraintAsList(); @JsonAlias("is_enumerated_type_constraint") @RMProperty("is_enumerated_type_constraint") @@ -74,10 +72,10 @@ public void setNodeId(String nodeId) { */ @Deprecated public boolean isValidValue(ValueType value) { - if(getConstraint().isEmpty()) { + if(getConstraintAsList().isEmpty()) { return true; } - for(Constraint constraint:getConstraint()) { + for(Constraint constraint:getConstraintAsList()) { if(Objects.equals(constraint, value)) { return true; } @@ -105,7 +103,7 @@ public String toString() { StringBuilder result = new StringBuilder(); result.append("{"); boolean first = true; - for(Constraint constraint:getConstraint()) { + for(Constraint constraint:getConstraintAsList()) { if(!first) { result.append(", "); } diff --git a/aom/src/main/java/com/nedap/archie/aom/primitives/CBoolean.java b/aom/src/main/java/com/nedap/archie/aom/primitives/CBoolean.java index 2918dda27..ddb344e87 100644 --- a/aom/src/main/java/com/nedap/archie/aom/primitives/CBoolean.java +++ b/aom/src/main/java/com/nedap/archie/aom/primitives/CBoolean.java @@ -43,12 +43,15 @@ public List getConstraint() { } @Override + public List getConstraintAsList() { + return getConstraint(); + } + public void setConstraint(List constraint) { this.constraint = constraint; } - @Override public void addConstraint(Boolean constraint) { this.constraint.add(constraint); } diff --git a/aom/src/main/java/com/nedap/archie/aom/primitives/COrdered.java b/aom/src/main/java/com/nedap/archie/aom/primitives/COrdered.java index 7bd3a4f60..3b467e1ae 100644 --- a/aom/src/main/java/com/nedap/archie/aom/primitives/COrdered.java +++ b/aom/src/main/java/com/nedap/archie/aom/primitives/COrdered.java @@ -7,6 +7,7 @@ import com.nedap.archie.base.Interval; import org.openehr.utils.message.I18n; +import java.util.List; import java.util.function.BiFunction; /** @@ -14,6 +15,16 @@ */ public abstract class COrdered extends CPrimitiveObject, T> { + public abstract List> getConstraint(); + + public List> getConstraintAsList() { + return getConstraint(); + } + + public abstract void setConstraint(List> constraint); + + public abstract void addConstraint(Interval constraint); + @Override @Deprecated public boolean isValidValue(T value) { diff --git a/aom/src/main/java/com/nedap/archie/aom/primitives/CString.java b/aom/src/main/java/com/nedap/archie/aom/primitives/CString.java index 00c3a9554..5db6d7d7a 100644 --- a/aom/src/main/java/com/nedap/archie/aom/primitives/CString.java +++ b/aom/src/main/java/com/nedap/archie/aom/primitives/CString.java @@ -53,11 +53,14 @@ public List getConstraint() { } @Override + public List getConstraintAsList() { + return getConstraint(); + } + public void setConstraint(List constraint) { this.constraint = constraint; } - @Override public void addConstraint(String constraint) { this.constraint.add(constraint); } diff --git a/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java b/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java index d52798671..65412dc8c 100644 --- a/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java +++ b/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java @@ -24,6 +24,7 @@ import javax.xml.bind.annotation.XmlType; import java.net.URI; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.BiFunction; @@ -38,7 +39,7 @@ public class CTerminologyCode extends CPrimitiveObject @XmlElement(name="assumed_value") @Nullable private TerminologyCode assumedValue; - private List constraint = new ArrayList<>(); + private String constraint; @Nullable private ConstraintStatus constraintStatus; @@ -53,19 +54,19 @@ public void setAssumedValue(TerminologyCode assumedValue) { this.assumedValue = assumedValue; } + // TODO: check usages of getConstraint().isEmpty() and (probably?) replace with null-check @Override - public List getConstraint() { + public String getConstraint() { return this.constraint; } @Override - public void setConstraint(List constraint) { - this.constraint = constraint; + public List getConstraintAsList() { + return getConstraint() == null ? Collections.emptyList() : Collections.singletonList(getConstraint()); } - @Override - public void addConstraint(String constraint) { - this.constraint.add(constraint); + public void setConstraint(String constraint) { + this.constraint = constraint; } public ConstraintStatus getConstraintStatus() { @@ -137,7 +138,7 @@ public List getTerms() { ArchetypeTerminology terminology = archetype.getTerminology(this); String language = ArchieLanguageConfiguration.getMeaningAndDescriptionLanguage(); String defaultLanguage = ArchieLanguageConfiguration.getDefaultMeaningAndDescriptionLanguage(); - for(String constraint:getConstraint()) { + if(constraint != null) { if(constraint.startsWith("at")) { ArchetypeTerm termDefinition = terminology.getTermDefinition(language, constraint); if(termDefinition == null) { @@ -182,7 +183,7 @@ private ArchetypeTerminology getTerminology() { public List getValueSetExpanded() { List result = new ArrayList<>(); ArchetypeTerminology terminology = getTerminology(); - for(String constraint:getConstraint()) { + if(constraint != null) { if(constraint.startsWith("at")) { result.add(constraint); } else if (constraint.startsWith("ac")) { @@ -231,10 +232,11 @@ public ConformanceCheckResult cConformsTo(CObject other, BiFunction valueSet = getValueSetExpanded(); List otherValueSet = otherCode.getValueSetExpanded(); - if(constraint.size() != 1) { + // TODO: does this need to be removed/reworded or anything else? + if(constraint == null) { return ConformanceCheckResult.fails(ErrorType.VPOV, I18n.t("child CTerminology code contains more than one constraint, that is not valid. Constraints are: {0}", constraint)); } - if(otherCode.constraint.size() != 1) { + if(otherCode.constraint == null) { return ConformanceCheckResult.fails(ErrorType.VPOV, I18n.t("parent CTerminology code contains more than one constraint, that is not valid. Constraints are: {0}", constraint)); } @@ -243,8 +245,8 @@ public ConformanceCheckResult cConformsTo(CObject other, BiFunction constraint = (Constraint) rightOperand; - if(constraint.getItem() != null && constraint.getItem().getConstraint() != null && constraint.getItem().getConstraint().size() > 0 && - constraint.getItem() instanceof CString) { + if(constraint.getItem() != null && constraint.getItem() instanceof CString && constraint.getItem().getConstraint() != null && ((CString) constraint.getItem()).getConstraint().size() > 0) { String pattern = ((CString) constraint.getItem()).getConstraint().get(0); if (pattern.startsWith("^") || pattern.startsWith("/")) { //regexp diff --git a/aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeSerializer.java b/aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeSerializer.java index d5e2242be..1faa2139c 100644 --- a/aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeSerializer.java +++ b/aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeSerializer.java @@ -39,7 +39,7 @@ public void serialize(CTerminologyCode cobj) { builder.append(" "); } builder.append("["); - String constraint = cobj.getConstraint().get(0); + String constraint = cobj.getConstraint(); builder.append(constraint); if (cobj.getAssumedValue() != null && cobj.getAssumedValue().getCodeString()!=null) { builder.append("; ").append(cobj.getAssumedValue().getCodeString()); @@ -49,8 +49,8 @@ public void serialize(CTerminologyCode cobj) { } public String getSimpleCommentText(CTerminologyCode cobj) { - if (!cobj.getConstraint().isEmpty()) { - String constraint = cobj.getConstraint().get(0); + if (cobj.getConstraint() != null) { + String constraint = cobj.getConstraint(); if(AOMUtils.isValueSetCode(constraint) || AOMUtils.isValueCode(constraint)) { return serializer.getTermText(cobj, constraint); } diff --git a/openehr-rm/src/main/java/com/nedap/archie/rminfo/UpdatedValueHandler.java b/openehr-rm/src/main/java/com/nedap/archie/rminfo/UpdatedValueHandler.java index 1242d9a94..28fdd37ae 100644 --- a/openehr-rm/src/main/java/com/nedap/archie/rminfo/UpdatedValueHandler.java +++ b/openehr-rm/src/main/java/com/nedap/archie/rminfo/UpdatedValueHandler.java @@ -69,8 +69,8 @@ private static Map fixDvOrdinalOrDvScale(Object rmObject, Archet int symbolIndex = socParent.getMemberIndex("symbol"); if (valueIndex != -1 && symbolIndex != -1) { for (CPrimitiveTuple tuple : socParent.getTuples()) { - if ((ordered instanceof DvOrdinal && tuple.getMembers().get(symbolIndex).getConstraint().get(0).equals(((DvOrdinal) ordered).getSymbol().getDefiningCode().getCodeString())) || - ordered instanceof DvScale && tuple.getMembers().get(symbolIndex).getConstraint().get(0).equals(((DvScale) ordered).getSymbol().getDefiningCode().getCodeString())) { + if ((ordered instanceof DvOrdinal && tuple.getMembers().get(symbolIndex).getConstraint().equals(((DvOrdinal) ordered).getSymbol().getDefiningCode().getCodeString())) || + ordered instanceof DvScale && tuple.getMembers().get(symbolIndex).getConstraint().equals(((DvScale) ordered).getSymbol().getDefiningCode().getCodeString())) { List> valueConstraint = (List>) tuple.getMembers().get(valueIndex).getConstraint(); if(valueConstraint.size() == 1) { Interval interval = valueConstraint.get(0); diff --git a/tools/src/main/java/com/nedap/archie/archetypevalidator/validations/CodeValidation.java b/tools/src/main/java/com/nedap/archie/archetypevalidator/validations/CodeValidation.java index da52929ab..05e734771 100644 --- a/tools/src/main/java/com/nedap/archie/archetypevalidator/validations/CodeValidation.java +++ b/tools/src/main/java/com/nedap/archie/archetypevalidator/validations/CodeValidation.java @@ -42,7 +42,8 @@ public void validate(CTerminologyCode cTerminologyCode) { //validate CTerminology codes int archetypeSpecializationDepth = archetype.specializationDepth(); - for(String constraint:cTerminologyCode.getConstraint()) { + String constraint = cTerminologyCode.getConstraint(); + if(constraint != null) { if(AOMUtils.isValueSetCode(constraint)) { int codeSpecializationDepth = AOMUtils.getSpecializationDepthFromCode(constraint); if(codeSpecializationDepth > archetypeSpecializationDepth) { diff --git a/tools/src/main/java/com/nedap/archie/creation/ExampleJsonInstanceGenerator.java b/tools/src/main/java/com/nedap/archie/creation/ExampleJsonInstanceGenerator.java index ee302ad85..74bd7287e 100644 --- a/tools/src/main/java/com/nedap/archie/creation/ExampleJsonInstanceGenerator.java +++ b/tools/src/main/java/com/nedap/archie/creation/ExampleJsonInstanceGenerator.java @@ -447,7 +447,7 @@ protected Object generateTerminologyCode(CTerminologyCode child) { terminologyId.put("value", "local"); String termString = "term"; ArchetypeTerminology terminology = archetype.getTerminology(child); - if(child.getConstraint().isEmpty()) { + if(child.getConstraint() == null) { codeString = "term code"; CAttribute attribute = child.getParent(); CComplexObject parent = (CComplexObject) attribute.getParent(); @@ -456,7 +456,7 @@ protected Object generateTerminologyCode(CTerminologyCode child) { return potentialResult; } } else { - String constraint = child.getConstraint().get(0); + String constraint = child.getConstraint(); if(constraint.startsWith("ac")) { ValueSet valueSet = terminology.getValueSets().get(constraint); diff --git a/tools/src/main/java/com/nedap/archie/diff/UnconstrainedIntervalRemover.java b/tools/src/main/java/com/nedap/archie/diff/UnconstrainedIntervalRemover.java index 0052ddd70..3b6cd5e7e 100644 --- a/tools/src/main/java/com/nedap/archie/diff/UnconstrainedIntervalRemover.java +++ b/tools/src/main/java/com/nedap/archie/diff/UnconstrainedIntervalRemover.java @@ -1,6 +1,7 @@ package com.nedap.archie.diff; import com.nedap.archie.aom.*; +import com.nedap.archie.aom.primitives.COrdered; import com.nedap.archie.base.Interval; import java.util.ArrayList; @@ -29,9 +30,9 @@ public static void removeUnconstrainedIntervals(CAttribute cAttribute) { for(CObject cObject:cAttribute.getChildren()) { if(cObject instanceof CComplexObject) { removeUnconstrainedIntervals((CComplexObject) cObject); - } else if (cObject instanceof CPrimitiveObject) { - CPrimitiveObject cPrimitiveObject = (CPrimitiveObject) cObject; - List constraint = cPrimitiveObject.getConstraint(); + } else if (cObject instanceof COrdered) { + COrdered cOrdered = (COrdered) cObject; + List constraint = cOrdered.getConstraint(); List toRemove = new ArrayList<>(); for(Object i:constraint) { if(i instanceof Interval) { @@ -43,7 +44,7 @@ public static void removeUnconstrainedIntervals(CAttribute cAttribute) { } constraint.removeAll(toRemove); if(constraint.isEmpty()) { - cObjectsToRemove.add(cPrimitiveObject); + cObjectsToRemove.add(cOrdered); } } } diff --git a/tools/src/main/java/com/nedap/archie/rmobjectvalidator/PrimitiveObjectConstraintHelper.java b/tools/src/main/java/com/nedap/archie/rmobjectvalidator/PrimitiveObjectConstraintHelper.java index be0aa493c..9e3916f3c 100644 --- a/tools/src/main/java/com/nedap/archie/rmobjectvalidator/PrimitiveObjectConstraintHelper.java +++ b/tools/src/main/java/com/nedap/archie/rmobjectvalidator/PrimitiveObjectConstraintHelper.java @@ -45,10 +45,10 @@ boolean isValidValue(CPrimitiveObject cPrimitiveObject } private boolean isValidValue_inner(CPrimitiveObject cPrimitiveObject, ValueType value) { - if(cPrimitiveObject.getConstraint().isEmpty()) { + if(cPrimitiveObject.getConstraintAsList().isEmpty()) { return true; } - for(Object constraint:cPrimitiveObject.getConstraint()) { + for(Object constraint:cPrimitiveObject.getConstraintAsList()) { if(Objects.equals(constraint, value)) { return true; } diff --git a/tools/src/main/java/com/nedap/archie/rmobjectvalidator/RmPrimitiveObjectValidator.java b/tools/src/main/java/com/nedap/archie/rmobjectvalidator/RmPrimitiveObjectValidator.java index 79c3bb0c4..7283e4e77 100644 --- a/tools/src/main/java/com/nedap/archie/rmobjectvalidator/RmPrimitiveObjectValidator.java +++ b/tools/src/main/java/com/nedap/archie/rmobjectvalidator/RmPrimitiveObjectValidator.java @@ -41,7 +41,7 @@ List validate_inner(Object rmObject, String pathSoFar } private RMObjectValidationMessage createValidationMessage(Object value, String pathSoFar, CPrimitiveObject cobject) { - List constraint = cobject.getConstraint(); + List constraint = cobject.getConstraintAsList(); String message; if(constraint.size() == 1) { diff --git a/tools/src/main/java/com/nedap/archie/rmobjectvalidator/validations/RMPrimitiveObjectValidation.java b/tools/src/main/java/com/nedap/archie/rmobjectvalidator/validations/RMPrimitiveObjectValidation.java index d059dfda7..580a2f3da 100644 --- a/tools/src/main/java/com/nedap/archie/rmobjectvalidator/validations/RMPrimitiveObjectValidation.java +++ b/tools/src/main/java/com/nedap/archie/rmobjectvalidator/validations/RMPrimitiveObjectValidation.java @@ -52,7 +52,7 @@ static List validate_inner(ModelInfoLookup lookup, Ob } private static RMObjectValidationMessage createValidationMessage(Object value, String pathSoFar, CPrimitiveObject cobject) { - List constraint = cobject.getConstraint(); + List constraint = cobject.getConstraintAsList(); String message; if(constraint.size() == 1) { diff --git a/tools/src/main/java/com/nedap/archie/rules/evaluation/FixableAssertionsChecker.java b/tools/src/main/java/com/nedap/archie/rules/evaluation/FixableAssertionsChecker.java index 368260f83..4af6e8027 100644 --- a/tools/src/main/java/com/nedap/archie/rules/evaluation/FixableAssertionsChecker.java +++ b/tools/src/main/java/com/nedap/archie/rules/evaluation/FixableAssertionsChecker.java @@ -127,7 +127,7 @@ private void handleMatches(AssertionResult assertionResult, int index, BinaryOpe if(rightOperand instanceof Constraint) { Constraint c = (Constraint) rightOperand; CPrimitiveObject object = c.getItem(); - List constraints = object.getConstraint(); + List constraints = object.getConstraintAsList(); if(constraints.size() != 1) { return; } diff --git a/tools/src/test/java/com/nedap/archie/adl14/ADL14ExternalTerminologyConversionTest.java b/tools/src/test/java/com/nedap/archie/adl14/ADL14ExternalTerminologyConversionTest.java index 8ef00be52..f85a12cca 100644 --- a/tools/src/test/java/com/nedap/archie/adl14/ADL14ExternalTerminologyConversionTest.java +++ b/tools/src/test/java/com/nedap/archie/adl14/ADL14ExternalTerminologyConversionTest.java @@ -26,7 +26,7 @@ void terminologyBindingsConverted() throws IOException, ADLParseException { Lists.newArrayList(new ADL14Parser(BuiltinReferenceModels.getMetaModelProvider()).parse(stream, conversionConfiguration))); Archetype converted = result.getConversionResults().get(0).getArchetype(); CTerminologyCode termCodeConstraint = converted.itemAtPath("/items/value/property[1]"); - String atCode = termCodeConstraint.getConstraint().get(0); + String atCode = termCodeConstraint.getConstraint(); Assertions.assertTrue(AOMUtils.isValueCode(atCode), "code must be a value, not a value set"); Assertions.assertEquals("Mass", converted.getTerminology().getTermDefinition("en", atCode).getText()); Assertions.assertEquals("Mass", converted.getTerminology().getTermDefinition("en", atCode).getDescription()); @@ -50,7 +50,7 @@ void twoTermbindingsInOneConstraint() throws Exception { Archetype converted = result.getConversionResults().get(0).getArchetype(); CTerminologyCode termCodeConstraint = converted.itemAtPath("/items/value/defining_code[1]"); - String acCode = termCodeConstraint.getConstraint().get(0); + String acCode = termCodeConstraint.getConstraint(); Assertions.assertTrue(AOMUtils.isValueSetCode(acCode), "the code should have been converted to a value set"); List atCodes = termCodeConstraint.getValueSetExpanded(); Assertions.assertEquals(2, atCodes.size(), atCodes.toString()); diff --git a/tools/src/test/java/com/nedap/archie/adlparser/DefinitionTest.java b/tools/src/test/java/com/nedap/archie/adlparser/DefinitionTest.java index 2ccdd1dcb..66739b6ef 100644 --- a/tools/src/test/java/com/nedap/archie/adlparser/DefinitionTest.java +++ b/tools/src/test/java/com/nedap/archie/adlparser/DefinitionTest.java @@ -45,8 +45,7 @@ public void definition() throws Exception { CTerminologyCode code = (CTerminologyCode) categoryDefinition.getAttribute("defining_code").getChildren().get(0); assertNull(code.getAssumedValue()); - assertEquals(1, code.getConstraint().size()); - assertEquals("at17", code.getConstraint().get(0)); + assertEquals("at17", code.getConstraint()); } @Test diff --git a/tools/src/test/java/com/nedap/archie/aom/TerminologyCodeConstraintsTest.java b/tools/src/test/java/com/nedap/archie/aom/TerminologyCodeConstraintsTest.java index 68982fd44..ca9305ada 100644 --- a/tools/src/test/java/com/nedap/archie/aom/TerminologyCodeConstraintsTest.java +++ b/tools/src/test/java/com/nedap/archie/aom/TerminologyCodeConstraintsTest.java @@ -70,7 +70,7 @@ public void noConstraint() { public void terminologyIdConstraint() { CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("ac12"); + code.setConstraint("ac12"); assertTrue(code.isValidValue(TerminologyCode.createFromString("[ac12::at23]"))); assertTrue(code.isValidValue(TerminologyCode.createFromString("[ac12::at24]"))); assertFalse(code.isValidValue(TerminologyCode.createFromString("[ac12::at25]"))); @@ -81,7 +81,7 @@ public void terminologyIdConstraint() { public void externalTerminology() { CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("ac12"); + code.setConstraint("ac12"); ValidationConfiguration.setFailOnUnknownTerminologyId(false); assertTrue(code.isValidValue(TerminologyCode.createFromString("[snomedct::72489423]"))); @@ -96,7 +96,7 @@ public void externalTerminology() { public void openEHRTerminology() { CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("at9000"); + code.setConstraint("at9000"); code.setConstraintStatus(ConstraintStatus.REQUIRED); DvCodedText text = new DvCodedText(); @@ -116,7 +116,7 @@ public void openEHRTerminology() { public void terminologyCodeConstraint() { CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("at23"); + code.setConstraint("at23"); assertTrue(code.isValidValue(TerminologyCode.createFromString("[ac12::at23]"))); assertTrue(code.isValidValue(TerminologyCode.createFromString("[ac13::at23]"))); assertFalse(code.isValidValue(TerminologyCode.createFromString("[ac13::at24]"))); @@ -127,7 +127,7 @@ public void dvCodedText() { //DV_CODED_TEXT can be constrained by a C_TERMINOLOGY_CONSTRAINT, according to lots of DV_ORDINAL usage in the CKM CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("at23"); + code.setConstraint("at23"); termCodeAssertions(code); } @@ -136,7 +136,7 @@ public void requiredBindingStrength() { //DV_CODED_TEXT can be constrained by a C_TERMINOLOGY_CONSTRAINT, according to lots of DV_ORDINAL usage in the CKM CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("at23"); + code.setConstraint("at23"); code.setConstraintStatus(ConstraintStatus.REQUIRED); termCodeAssertions(code); } @@ -146,7 +146,7 @@ public void otherBindingStrength() { //DV_CODED_TEXT can be constrained by a C_TERMINOLOGY_CONSTRAINT, according to lots of DV_ORDINAL usage in the CKM CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("at23"); + code.setConstraint("at23"); Set nonRequiredBindings = EnumSet.of(ConstraintStatus.EXTENSIBLE, ConstraintStatus.EXAMPLE, ConstraintStatus.PREFERRED); for(ConstraintStatus status:nonRequiredBindings) { code.setConstraintStatus(status); diff --git a/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java b/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java index 877e170ef..f40f0f36b 100644 --- a/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java +++ b/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java @@ -270,7 +270,7 @@ public void cDurationPeriodDuration() throws Exception { @Test public void cTerminologyCode() throws Exception { CTerminologyCode cTermCode = new CTerminologyCode(); - cTermCode.setConstraint(Lists.newArrayList("ac23")); + cTermCode.setConstraint("ac23"); cTermCode.setConstraintStatus(ConstraintStatus.PREFERRED); ObjectMapper objectMapper = JacksonUtil.getObjectMapper(ArchieJacksonConfiguration.createStandardsCompliant()); String json = objectMapper.writeValueAsString(cTermCode); diff --git a/tools/src/test/java/com/nedap/archie/rmobjectvalidator/TerminologyCodeConstraintsTest.java b/tools/src/test/java/com/nedap/archie/rmobjectvalidator/TerminologyCodeConstraintsTest.java index cc2f08ecb..9164afe84 100644 --- a/tools/src/test/java/com/nedap/archie/rmobjectvalidator/TerminologyCodeConstraintsTest.java +++ b/tools/src/test/java/com/nedap/archie/rmobjectvalidator/TerminologyCodeConstraintsTest.java @@ -78,7 +78,7 @@ public void noConstraint() { public void terminologyIdConstraint() { CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("ac12"); + code.setConstraint("ac12"); assertTrue(primitiveObjectConstraintHelper.isValidValue(code, TerminologyCode.createFromString("[ac12::at23]"))); assertTrue(primitiveObjectConstraintHelper.isValidValue(code, TerminologyCode.createFromString("[ac12::at24]"))); assertFalse(primitiveObjectConstraintHelper.isValidValue(code, TerminologyCode.createFromString("[ac12::at25]"))); @@ -89,7 +89,7 @@ public void terminologyIdConstraint() { public void externalTerminology() { CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("ac12"); + code.setConstraint("ac12"); assertTrue(primitiveObjectConstraintHelper.isValidValue(code, TerminologyCode.createFromString("[snomedct::72489423]"))); assertTrue(primitiveObjectConstraintHelper.isValidValue(code, TerminologyCode.createFromString("[anything::atall]"))); @@ -103,7 +103,7 @@ public void externalTerminology() { public void openEHRTerminology() { CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("at9000"); + code.setConstraint("at9000"); code.setConstraintStatus(ConstraintStatus.REQUIRED); DvCodedText text = new DvCodedText(); @@ -123,7 +123,7 @@ public void openEHRTerminology() { public void IANATerminology() { CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("at9001"); + code.setConstraint("at9001"); code.setConstraintStatus(ConstraintStatus.REQUIRED); DvCodedText text = new DvCodedText(); @@ -143,7 +143,7 @@ public void IANATerminology() { public void terminologyCodeConstraint() { CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("at23"); + code.setConstraint("at23"); assertTrue(primitiveObjectConstraintHelper.isValidValue(code, TerminologyCode.createFromString("[ac12::at23]"))); assertTrue(primitiveObjectConstraintHelper.isValidValue(code, TerminologyCode.createFromString("[ac13::at23]"))); assertFalse(primitiveObjectConstraintHelper.isValidValue(code, TerminologyCode.createFromString("[ac13::at24]"))); @@ -154,7 +154,7 @@ public void dvCodedText() { //DV_CODED_TEXT can be constrained by a C_TERMINOLOGY_CONSTRAINT, according to lots of DV_ORDINAL usage in the CKM CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("at23"); + code.setConstraint("at23"); termCodeAssertions(code); } @@ -163,7 +163,7 @@ public void requiredBindingStrength() { //DV_CODED_TEXT can be constrained by a C_TERMINOLOGY_CONSTRAINT, according to lots of DV_ORDINAL usage in the CKM CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("at23"); + code.setConstraint("at23"); code.setConstraintStatus(ConstraintStatus.REQUIRED); termCodeAssertions(code); } @@ -173,7 +173,7 @@ public void otherBindingStrength() { //DV_CODED_TEXT can be constrained by a C_TERMINOLOGY_CONSTRAINT, according to lots of DV_ORDINAL usage in the CKM CTerminologyCode code = new CTerminologyCode(); code.setParent(new DummyRulesPrimitiveObjectParent(archetype)); - code.addConstraint("at23"); + code.setConstraint("at23"); Set nonRequiredBindings = EnumSet.of(ConstraintStatus.EXTENSIBLE, ConstraintStatus.EXAMPLE, ConstraintStatus.PREFERRED); for(ConstraintStatus status:nonRequiredBindings) { code.setConstraintStatus(status); diff --git a/tools/src/test/java/com/nedap/archie/serializer/adl/ADLArchetypeSerializerParserRoundtripTest.java b/tools/src/test/java/com/nedap/archie/serializer/adl/ADLArchetypeSerializerParserRoundtripTest.java index 6b2bc0f83..c07700dcc 100644 --- a/tools/src/test/java/com/nedap/archie/serializer/adl/ADLArchetypeSerializerParserRoundtripTest.java +++ b/tools/src/test/java/com/nedap/archie/serializer/adl/ADLArchetypeSerializerParserRoundtripTest.java @@ -43,7 +43,7 @@ public void basic() throws Exception { CAttribute defining_code = archetype.itemAtPath("/category[id10]/defining_code"); CTerminologyCode termCode = (CTerminologyCode) defining_code.getChildren().get(0); - assertThat(termCode.getConstraint(), hasItem("at17")); + assertEquals("at17", termCode.getConstraint()); assertThat(archetype.getDescription().getDetails().get("en").getKeywords(), hasItems("ADL", "test")); assertThat(archetype.getTerminology().getTermBinding("openehr", "at17"), equalTo(URI.create("http://openehr.org/id/433"))); diff --git a/tools/src/test/java/com/nedap/archie/xml/JAXBAOMTest.java b/tools/src/test/java/com/nedap/archie/xml/JAXBAOMTest.java index e84d668bb..f00630928 100644 --- a/tools/src/test/java/com/nedap/archie/xml/JAXBAOMTest.java +++ b/tools/src/test/java/com/nedap/archie/xml/JAXBAOMTest.java @@ -71,7 +71,7 @@ public void serializeCDuration() throws Exception { public void testCTerminologyCode() throws Exception { CTerminologyCode cTerminologyCode = new CTerminologyCode(); - cTerminologyCode.setConstraint(Lists.newArrayList("ac23")); + cTerminologyCode.setConstraint("ac23"); cTerminologyCode.setConstraintStatus(ConstraintStatus.PREFERRED); valueAttribute.addChild(cTerminologyCode); StringWriter writer = new StringWriter(); From 58fd4b8e0fade6159e866908df9e5a7b37ad0f9c Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Wed, 8 Apr 2026 12:38:09 +0700 Subject: [PATCH 02/15] Add extra checks --- .../com/nedap/archie/aom/primitives/CTerminologyCode.java | 4 ++-- .../adl/constraints/CTerminologyCodeSerializer.java | 2 +- .../rmobjectvalidator/PrimitiveObjectConstraintHelper.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java b/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java index 65412dc8c..38c451405 100644 --- a/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java +++ b/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java @@ -54,7 +54,7 @@ public void setAssumedValue(TerminologyCode assumedValue) { this.assumedValue = assumedValue; } - // TODO: check usages of getConstraint().isEmpty() and (probably?) replace with null-check + // TODO: check usages of getConstraint().isEmpty() and (probably?) replace with null-check. Note that this only needs to be done for the cases where getConstraint() returns a String. Not an array, like instances of CTerminologyCode. @Override public String getConstraint() { return this.constraint; @@ -89,7 +89,7 @@ public ConstraintStatus getEffectiveConstraintStatus() { @Override @Deprecated public boolean isValidValue(TerminologyCode value) { - if(getConstraint().isEmpty()) { + if(getConstraint() == null || getConstraint().isEmpty()) { return true; } if(isConstraintRequired()) { diff --git a/aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeSerializer.java b/aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeSerializer.java index 1faa2139c..3fa04bc45 100644 --- a/aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeSerializer.java +++ b/aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeSerializer.java @@ -16,7 +16,7 @@ public CTerminologyCodeSerializer(ADLDefinitionSerializer serializer) { @Override public void serialize(CTerminologyCode cobj) { - if (!cobj.getConstraint().isEmpty()) { + if (cobj.getConstraint() != null && !cobj.getConstraint().isEmpty()) { if(cobj.getConstraintStatus() != null) { String constraintStatusString = null; switch(cobj.getConstraintStatus()) { diff --git a/tools/src/main/java/com/nedap/archie/rmobjectvalidator/PrimitiveObjectConstraintHelper.java b/tools/src/main/java/com/nedap/archie/rmobjectvalidator/PrimitiveObjectConstraintHelper.java index 9e3916f3c..2caa3cfe2 100644 --- a/tools/src/main/java/com/nedap/archie/rmobjectvalidator/PrimitiveObjectConstraintHelper.java +++ b/tools/src/main/java/com/nedap/archie/rmobjectvalidator/PrimitiveObjectConstraintHelper.java @@ -116,7 +116,7 @@ private boolean isValidValue(CTemporal cTemporal, T value) { } private boolean isValidValue(CTerminologyCode terminologyCode, TerminologyCode value) { - if(terminologyCode.getConstraint().isEmpty()) { + if(terminologyCode.getConstraint() == null || terminologyCode.getConstraint().isEmpty()) { return true; } if(terminologyCode.isConstraintRequired()) { From 1cd0f21abbfe8f1c9184e287432cc8387350b769 Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Thu, 9 Apr 2026 14:42:54 +0700 Subject: [PATCH 03/15] Fix parsing and converting of adl14 term codes --- .../adl14/ADL14TermConstraintConverter.java | 212 ++++++++++-------- .../Adl14PrimitivesConstraintParser.java | 20 +- .../nedap/archie/aom/CPrimitiveObject.java | 2 + .../aom/primitives/CTerminologyCode.java | 17 +- 4 files changed, 151 insertions(+), 100 deletions(-) diff --git a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java index e05d9a7b3..a94981c91 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java +++ b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java @@ -15,6 +15,7 @@ import org.slf4j.LoggerFactory; import java.net.URI; +import java.net.URISyntaxException; import java.util.*; import java.util.stream.Collectors; @@ -89,105 +90,132 @@ private void convert(CAttribute attribute) { } } + /** + * Converts a single-code {@link CTerminologyCode} constraint from ADL 1.4 to ADL 2 format. + * Multi-code constraints (stored via {@link CTerminologyCode#getPendingCodes()} by the ADL 1.4 parser) + * are delegated to {@link #convertMultiCodeTerminologyCode}. + *

+ * Three cases are handled: + *

    + *
  • Local at-code (e.g. {@code at0001}): converted to its ADL 2 equivalent.
  • + *
  • Local ac-code (e.g. {@code ac0001}): converted to its ADL 2 value-set code equivalent.
  • + *
  • External terminology code (e.g. {@code [snomed-ct::12345]}): a term binding is created and + * a new local at-code is generated to reference it.
  • + *
+ */ private void convertCTerminologyCode(CTerminologyCode cTerminologyCode) { - if(cTerminologyCode.getConstraint() != null && !cTerminologyCode.getConstraint().isEmpty()) { - // TODO: this seems to try to fix a complicated case that was introduced in com.nedap.archie.adl14.treewalkers.Adl14PrimitivesConstraintParser.parseCTerminologyCode, fix later - /*String firstConstraint = cTerminologyCode.getConstraint().get(0); - TerminologyCode termCode = TerminologyCode.createFromString(firstConstraint); - boolean isLocalCode = termCode.getTerminologyId() == null || termCode.getTerminologyId().equalsIgnoreCase("local"); - if(isLocalCode && AOMUtils.isValueCode(firstConstraint)) { - //local codes - if(cTerminologyCode.getConstraint().size() == 1) { - //do not create a value set, just convert the code - String newCode = converter.convertValueCode(firstConstraint); - converter.addConvertedCode(firstConstraint, newCode); - cTerminologyCode.setConstraint(Lists.newArrayList(newCode)); - } else { - Set localCodes = new LinkedHashSet<>(); - for(String code:cTerminologyCode.getConstraint()) { - String newCode = converter.convertValueCode(code); - converter.addConvertedCode(code, newCode); - localCodes.add(newCode); - } + List pendingCodes = cTerminologyCode.getPendingCodes(); + if (pendingCodes != null && !pendingCodes.isEmpty()) { + convertMultiCodeTerminologyCode(cTerminologyCode, pendingCodes); + return; + } - ValueSet valueSet = findOrCreateValueSet(cTerminologyCode.getArchetype(), localCodes, cTerminologyCode); - cTerminologyCode.setConstraint(Lists.newArrayList(valueSet.getId())); - } - } else if (isLocalCode && AOMUtils.isValueSetCode(termCode.getCodeString())) { - List newConstraint = new ArrayList<>(); - for(String constraint:cTerminologyCode.getConstraint()) { - TerminologyCode code = TerminologyCode.createFromString(constraint); - String newCode = converter.convertValueSetCode(code.getCodeString()); - converter.addConvertedCode(termCode.getCodeString(), newCode); - newConstraint.add(newCode); - } - cTerminologyCode.setConstraint(newConstraint); - - } else { - if (cTerminologyCode.getConstraint().size() == 1) { - try { - //do not create a value set, create a code plus binding to the old non-local code - URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(termCode); - Map termBindingsMap = findOrCreateTermBindings(termCode); - - //TODO: check if this is a converted or old term binding - old is unusual, but could be possible! - String termBinding = findOrAddTermBindingAndCode(termCode, uri, termBindingsMap); - cTerminologyCode.setConstraint(Lists.newArrayList(termBinding)); - } catch (URISyntaxException e) { - //TODO - logger.error("error converting term", e); - } - } else { - String terminologyId = cTerminologyCode.getConstraint().get(0); - termCode = TerminologyCode.createFromString(terminologyId, null, cTerminologyCode.getConstraint().get(1)); - Map termBindingsMap = findOrCreateTermBindings(termCode); - List atCodes = new ArrayList<>(); - List constraints = new ArrayList<>(cTerminologyCode.getConstraint()); - cTerminologyCode.setConstraint(atCodes); - for(int i = 1; i < constraints.size(); i++) { - String constraint = constraints.get(i); - try { - if(constraint.startsWith("[") && constraint.endsWith("]")) { - TerminologyCode constraintCode = TerminologyCode.createFromString(constraint); - URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(constraintCode); - atCodes.add(findOrAddTermBindingAndCode(constraintCode, uri, termBindingsMap)); - } else { - TerminologyCode constraintCode = new TerminologyCode(); - constraintCode.setTerminologyId(terminologyId); - constraintCode.setCodeString(constraint); - URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(constraintCode); - atCodes.add(findOrAddTermBindingAndCode(constraintCode, uri, termBindingsMap)); - } + String constraint = cTerminologyCode.getConstraint(); + if (constraint == null || constraint.isEmpty()) { + return; + } - } catch (URISyntaxException e) { - e.printStackTrace(); - } - } - ValueSet valueSet = findOrCreateValueSet(cTerminologyCode.getArchetype(), new LinkedHashSet<>(atCodes), cTerminologyCode); - cTerminologyCode.setConstraint(Lists.newArrayList(valueSet.getId())); + TerminologyCode termCode = TerminologyCode.createFromString(constraint); + boolean isLocalCode = termCode.getTerminologyId() == null + || termCode.getTerminologyId().equalsIgnoreCase("local"); - } + if (isLocalCode && AOMUtils.isValueCode(constraint)) { + // Single local at-code: convert it to its ADL 2 equivalent + String newCode = converter.convertValueCode(constraint); + converter.addConvertedCode(constraint, newCode); + cTerminologyCode.setConstraint(newCode); + } else if (isLocalCode && AOMUtils.isValueSetCode(termCode.getCodeString())) { + // Local value-set reference: convert the ac-code to its ADL 2 equivalent + String newCode = converter.convertValueSetCode(termCode.getCodeString()); + converter.addConvertedCode(termCode.getCodeString(), newCode); + cTerminologyCode.setConstraint(newCode); + } else { + // External terminology: create a term binding and generate a new at-code to reference it + try { + URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(termCode); + Map termBindingsMap = findOrCreateTermBindings(termCode); + cTerminologyCode.setConstraint(findOrAddTermBindingAndCode(termCode, uri, termBindingsMap)); + } catch (URISyntaxException e) { + logger.error("error converting term", e); } - if(cTerminologyCode.getAssumedValue() != null) { - TerminologyCode assumedValue = cTerminologyCode.getAssumedValue(); - if(isLocalCode) { - String newCode = converter.convertValueCode(assumedValue.getCodeString()); - assumedValue.setCodeString(newCode); - assumedValue.setTerminologyId(null); - } else { - try { - Map termBindingsMap = findOrCreateTermBindings(assumedValue); - URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(assumedValue); - assumedValue.setCodeString(findOrAddTermBindingAndCode(assumedValue, uri, termBindingsMap)); - assumedValue.setTerminologyId(null); - assumedValue.setTerminologyVersion(null); - } catch (URISyntaxException e) { - //TODO - e.printStackTrace(); - } + } + + convertAssumedValue(cTerminologyCode, isLocalCode); + } + + /** + * Converts a multi-code {@link CTerminologyCode} constraint from ADL 1.4 to ADL 2 format. + * In ADL 1.4, a constraint may reference multiple codes inline (e.g. + * {@code [local::at0001, at0002]} or {@code [snomed-ct::12345, 67890]}). ADL 2 represents + * these as a value set (ac-code), which this method creates. + *

+ * The {@code pendingCodes} list was populated by the ADL 1.4 parser: + *

    + *
  • For local codes: raw at-codes, e.g. {@code ["at0001", "at0002"]}.
  • + *
  • For external codes: full term code refs normalised by the parser, + * e.g. {@code ["[snomed-ct::12345]", "[snomed-ct::67890]"]}.
  • + *
+ * In both cases a value set is created and the constraint is set to its ac-code. + */ + private void convertMultiCodeTerminologyCode(CTerminologyCode cTerminologyCode, List pendingCodes) { + TerminologyCode firstCode = TerminologyCode.createFromString(pendingCodes.get(0)); + boolean isLocalCode = firstCode.getTerminologyId() == null + || firstCode.getTerminologyId().equalsIgnoreCase("local"); + + if (isLocalCode) { + // Convert each at-code and group them into a new value set + Set convertedCodes = new LinkedHashSet<>(); + for (String code : pendingCodes) { + String newCode = converter.convertValueCode(code); + converter.addConvertedCode(code, newCode); + convertedCodes.add(newCode); + } + ValueSet valueSet = findOrCreateValueSet(cTerminologyCode.getArchetype(), convertedCodes, cTerminologyCode); + cTerminologyCode.setConstraint(valueSet.getId()); + } else { + // Create a term binding for each external code, then group the resulting at-codes into a value set + Map termBindingsMap = findOrCreateTermBindings(firstCode); + List atCodes = new ArrayList<>(); + for (String code : pendingCodes) { + TerminologyCode termCode = TerminologyCode.createFromString(code); + try { + URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(termCode); + atCodes.add(findOrAddTermBindingAndCode(termCode, uri, termBindingsMap)); + } catch (URISyntaxException e) { + logger.error("error converting term", e); } } - */ + if (!atCodes.isEmpty()) { + ValueSet valueSet = findOrCreateValueSet(cTerminologyCode.getArchetype(), new LinkedHashSet<>(atCodes), cTerminologyCode); + cTerminologyCode.setConstraint(valueSet.getId()); + } + } + + convertAssumedValue(cTerminologyCode, isLocalCode); + } + + /** + * Converts the assumed value of a {@link CTerminologyCode} from ADL 1.4 to ADL 2 format, + * mirroring the logic used for the constraint itself. + */ + private void convertAssumedValue(CTerminologyCode cTerminologyCode, boolean isLocalCode) { + if (cTerminologyCode.getAssumedValue() == null) { + return; + } + TerminologyCode assumedValue = cTerminologyCode.getAssumedValue(); + if (isLocalCode) { + assumedValue.setCodeString(converter.convertValueCode(assumedValue.getCodeString())); + assumedValue.setTerminologyId(null); + } else { + try { + Map termBindingsMap = findOrCreateTermBindings(assumedValue); + URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(assumedValue); + assumedValue.setCodeString(findOrAddTermBindingAndCode(assumedValue, uri, termBindingsMap)); + assumedValue.setTerminologyId(null); + assumedValue.setTerminologyVersion(null); + } catch (URISyntaxException e) { + logger.error("error converting term", e); + } } } diff --git a/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParser.java b/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParser.java index 59315f648..be423ccde 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParser.java +++ b/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParser.java @@ -17,6 +17,7 @@ import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.TerminalNode; +import java.util.ArrayList; import java.util.List; /** @@ -111,16 +112,21 @@ public CTerminologyCode parseCTerminologyCode(Adl14Parser.C_terminology_codeCont } else { String terminologyId = qualifiedTermCodeContext.identifier(0).getText(); if (terminologyId.equalsIgnoreCase("local")) { - //we need to create a value set. For now just add the constraint, the value set will come after - //the parser - // TODO: the List type of the terminologycode.constraint seems to have been used here for a tmp storage to build a value set later, check this and make sure this is still working as intended after these changes... + List pendingCodes = new ArrayList<>(); for (int i = 1; i < qualifiedTermCodeContext.identifier().size(); i++) { - // result.addConstraint(qualifiedTermCodeContext.identifier(i).getText()); + pendingCodes.add(qualifiedTermCodeContext.identifier(i).getText()); + } + if (!pendingCodes.isEmpty()) { + result.setPendingCodes(pendingCodes); } } else { - //non-local term constraints. Add the text here, will be converted later - for (int i = 0; i < qualifiedTermCodeContext.identifier().size(); i++) { - // result.addConstraint(qualifiedTermCodeContext.identifier(i).getText()); + // Normalise to full term code refs so the converter can treat all pending codes uniformly + List pendingCodes = new ArrayList<>(); + for (int i = 1; i < qualifiedTermCodeContext.identifier().size(); i++) { + pendingCodes.add("[" + terminologyId + "::" + qualifiedTermCodeContext.identifier(i).getText() + "]"); + } + if (!pendingCodes.isEmpty()) { + result.setPendingCodes(pendingCodes); } } diff --git a/aom/src/main/java/com/nedap/archie/aom/CPrimitiveObject.java b/aom/src/main/java/com/nedap/archie/aom/CPrimitiveObject.java index 640e1ea7e..b841b59ac 100644 --- a/aom/src/main/java/com/nedap/archie/aom/CPrimitiveObject.java +++ b/aom/src/main/java/com/nedap/archie/aom/CPrimitiveObject.java @@ -1,6 +1,7 @@ package com.nedap.archie.aom; import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.nedap.archie.aom.utils.ConformanceCheckResult; import com.nedap.archie.archetypevalidator.ErrorType; import com.nedap.archie.rminfo.ArchieModelNamingStrategy; @@ -37,6 +38,7 @@ public abstract class CPrimitiveObject extends CDefinedOb public abstract Object getConstraint(); + @JsonIgnore public abstract List getConstraintAsList(); @JsonAlias("is_enumerated_type_constraint") diff --git a/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java b/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java index 0388340e9..4542aba99 100644 --- a/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java +++ b/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java @@ -18,6 +18,7 @@ import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlTransient; import jakarta.xml.bind.annotation.XmlType; import org.openehr.utils.message.I18n; @@ -44,6 +45,11 @@ public class CTerminologyCode extends CPrimitiveObject @Nullable private ConstraintStatus constraintStatus; + /** Temporary storage for multi-code ADL 1.4 constraints during conversion. Never serialized. */ + @JsonIgnore + @XmlTransient + private List pendingCodes; + @Override public TerminologyCode getAssumedValue() { return assumedValue; @@ -54,7 +60,6 @@ public void setAssumedValue(TerminologyCode assumedValue) { this.assumedValue = assumedValue; } - // TODO: check usages of getConstraint().isEmpty() and (probably?) replace with null-check. Note that this only needs to be done for the cases where getConstraint() returns a String. Not an array, like instances of CTerminologyCode. @Override public String getConstraint() { return this.constraint; @@ -77,6 +82,16 @@ public void setConstraintStatus(ConstraintStatus constraintStatus) { this.constraintStatus = constraintStatus; } + @JsonIgnore + public List getPendingCodes() { + return pendingCodes; + } + + @JsonIgnore + public void setPendingCodes(List pendingCodes) { + this.pendingCodes = pendingCodes; + } + @JsonIgnore public boolean isConstraintRequired() { return getEffectiveConstraintStatus() == ConstraintStatus.REQUIRED; From 462743b5fcaf1c6fb791dafee7322e25466c9dbd Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Tue, 14 Apr 2026 12:36:14 +0700 Subject: [PATCH 04/15] Change constraint type in CPrimitiveObject Make it Constraint again instead of Object --- .../main/java/com/nedap/archie/aom/CPrimitiveObject.java | 8 ++++---- .../java/com/nedap/archie/aom/primitives/CBoolean.java | 2 +- .../java/com/nedap/archie/aom/primitives/COrdered.java | 2 +- .../java/com/nedap/archie/aom/primitives/CString.java | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/aom/src/main/java/com/nedap/archie/aom/CPrimitiveObject.java b/aom/src/main/java/com/nedap/archie/aom/CPrimitiveObject.java index b841b59ac..31c65e886 100644 --- a/aom/src/main/java/com/nedap/archie/aom/CPrimitiveObject.java +++ b/aom/src/main/java/com/nedap/archie/aom/CPrimitiveObject.java @@ -36,10 +36,10 @@ public abstract class CPrimitiveObject extends CDefinedOb public abstract void setAssumedValue(ValueType assumedValue); - public abstract Object getConstraint(); + public abstract Constraint getConstraint(); @JsonIgnore - public abstract List getConstraintAsList(); + public abstract List getConstraintAsList(); @JsonAlias("is_enumerated_type_constraint") @RMProperty("is_enumerated_type_constraint") @@ -77,7 +77,7 @@ public boolean isValidValue(ValueType value) { if(getConstraintAsList().isEmpty()) { return true; } - for(Constraint constraint:getConstraintAsList()) { + for(Object constraint:getConstraintAsList()) { if(Objects.equals(constraint, value)) { return true; } @@ -105,7 +105,7 @@ public String toString() { StringBuilder result = new StringBuilder(); result.append("{"); boolean first = true; - for(Constraint constraint:getConstraintAsList()) { + for(Object constraint:getConstraintAsList()) { if(!first) { result.append(", "); } diff --git a/aom/src/main/java/com/nedap/archie/aom/primitives/CBoolean.java b/aom/src/main/java/com/nedap/archie/aom/primitives/CBoolean.java index f8fa50eaa..40f5911d0 100644 --- a/aom/src/main/java/com/nedap/archie/aom/primitives/CBoolean.java +++ b/aom/src/main/java/com/nedap/archie/aom/primitives/CBoolean.java @@ -20,7 +20,7 @@ */ @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name="C_BOOLEAN") -public class CBoolean extends CPrimitiveObject { +public class CBoolean extends CPrimitiveObject, Boolean> { @XmlElement(name="assumed_value") @Nullable private Boolean assumedValue; diff --git a/aom/src/main/java/com/nedap/archie/aom/primitives/COrdered.java b/aom/src/main/java/com/nedap/archie/aom/primitives/COrdered.java index 3b467e1ae..170d76b99 100644 --- a/aom/src/main/java/com/nedap/archie/aom/primitives/COrdered.java +++ b/aom/src/main/java/com/nedap/archie/aom/primitives/COrdered.java @@ -13,7 +13,7 @@ /** * Created by pieter.bos on 15/10/15. */ -public abstract class COrdered extends CPrimitiveObject, T> { +public abstract class COrdered extends CPrimitiveObject>, T> { public abstract List> getConstraint(); diff --git a/aom/src/main/java/com/nedap/archie/aom/primitives/CString.java b/aom/src/main/java/com/nedap/archie/aom/primitives/CString.java index 5ff326b7c..6ae793b49 100644 --- a/aom/src/main/java/com/nedap/archie/aom/primitives/CString.java +++ b/aom/src/main/java/com/nedap/archie/aom/primitives/CString.java @@ -22,7 +22,7 @@ */ @XmlType(name="C_STRING") @XmlAccessorType(XmlAccessType.FIELD) -public class CString extends CPrimitiveObject { +public class CString extends CPrimitiveObject, String> { @XmlElement(name="assumed_value") @Nullable From 1551e687130bffc6ff2bfd75e702d11e3e17c306 Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Tue, 14 Apr 2026 14:41:16 +0700 Subject: [PATCH 05/15] Requested changes --- .../archie/adl14/ADL14TermConstraintConverter.java | 4 ++-- .../archie/aom/primitives/CTerminologyCode.java | 13 +++++++------ .../adl/constraints/CTerminologyCodeSerializer.java | 2 +- .../archie/diff/UnconstrainedIntervalRemover.java | 2 ++ .../PrimitiveObjectConstraintHelper.java | 2 +- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java index a94981c91..d75d6dd97 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java +++ b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java @@ -111,7 +111,7 @@ private void convertCTerminologyCode(CTerminologyCode cTerminologyCode) { } String constraint = cTerminologyCode.getConstraint(); - if (constraint == null || constraint.isEmpty()) { + if (constraint == null) { return; } @@ -278,7 +278,7 @@ private ValueSet findOrCreateValueSet(Archetype archetype, Set localCode CObject cObject = cAttributeInParent.getChildren().get(0); if(cObject instanceof CTerminologyCode) { CTerminologyCode termCodeInParent = (CTerminologyCode) cObject; - if(termCodeInParent.getConstraint() != null && !termCodeInParent.getConstraint().isEmpty()) { + if(termCodeInParent.getConstraint() != null) { if(termCodeInParent.getConstraint().startsWith("ac")) { idInparent = termCodeInParent.getConstraint(); } diff --git a/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java b/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java index 4542aba99..bba7040f9 100644 --- a/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java +++ b/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java @@ -104,7 +104,7 @@ public ConstraintStatus getEffectiveConstraintStatus() { @Override @Deprecated public boolean isValidValue(TerminologyCode value) { - if(getConstraint() == null || getConstraint().isEmpty()) { + if(getConstraint() == null) { return true; } if(isConstraintRequired()) { @@ -247,12 +247,13 @@ public ConformanceCheckResult cConformsTo(CObject other, BiFunction valueSet = getValueSetExpanded(); List otherValueSet = otherCode.getValueSetExpanded(); - // TODO: does this need to be removed/reworded or anything else? - if(constraint == null) { - return ConformanceCheckResult.fails(ErrorType.VPOV, I18n.t("child CTerminology code contains more than one constraint, that is not valid. Constraints are: {0}", constraint)); - } + // A null constraint means unconstrained — an unconstrained parent accepts anything, + // and an unconstrained child trivially conforms to any parent. if(otherCode.constraint == null) { - return ConformanceCheckResult.fails(ErrorType.VPOV, I18n.t("parent CTerminology code contains more than one constraint, that is not valid. Constraints are: {0}", constraint)); + return ConformanceCheckResult.conforms(); + } + if(constraint == null) { + return ConformanceCheckResult.conforms(); } if(!getEffectiveConstraintStatus().cConformsTo(otherCode.getEffectiveConstraintStatus()) ) { diff --git a/aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeSerializer.java b/aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeSerializer.java index 3fa04bc45..e071c0983 100644 --- a/aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeSerializer.java +++ b/aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeSerializer.java @@ -16,7 +16,7 @@ public CTerminologyCodeSerializer(ADLDefinitionSerializer serializer) { @Override public void serialize(CTerminologyCode cobj) { - if (cobj.getConstraint() != null && !cobj.getConstraint().isEmpty()) { + if (cobj.getConstraint() != null) { if(cobj.getConstraintStatus() != null) { String constraintStatusString = null; switch(cobj.getConstraintStatus()) { diff --git a/tools/src/main/java/com/nedap/archie/diff/UnconstrainedIntervalRemover.java b/tools/src/main/java/com/nedap/archie/diff/UnconstrainedIntervalRemover.java index 3b6cd5e7e..ec8fa25ea 100644 --- a/tools/src/main/java/com/nedap/archie/diff/UnconstrainedIntervalRemover.java +++ b/tools/src/main/java/com/nedap/archie/diff/UnconstrainedIntervalRemover.java @@ -31,6 +31,8 @@ public static void removeUnconstrainedIntervals(CAttribute cAttribute) { if(cObject instanceof CComplexObject) { removeUnconstrainedIntervals((CComplexObject) cObject); } else if (cObject instanceof COrdered) { + // COrdered is used explicitly rather than CPrimitiveObject + getConstraintAsList(), + // because the list is mutated in-place and getConstraintAsList() does not guarantee a mutable list. COrdered cOrdered = (COrdered) cObject; List constraint = cOrdered.getConstraint(); List toRemove = new ArrayList<>(); diff --git a/tools/src/main/java/com/nedap/archie/rmobjectvalidator/PrimitiveObjectConstraintHelper.java b/tools/src/main/java/com/nedap/archie/rmobjectvalidator/PrimitiveObjectConstraintHelper.java index 2caa3cfe2..74544c959 100644 --- a/tools/src/main/java/com/nedap/archie/rmobjectvalidator/PrimitiveObjectConstraintHelper.java +++ b/tools/src/main/java/com/nedap/archie/rmobjectvalidator/PrimitiveObjectConstraintHelper.java @@ -116,7 +116,7 @@ private boolean isValidValue(CTemporal cTemporal, T value) { } private boolean isValidValue(CTerminologyCode terminologyCode, TerminologyCode value) { - if(terminologyCode.getConstraint() == null || terminologyCode.getConstraint().isEmpty()) { + if(terminologyCode.getConstraint() == null) { return true; } if(terminologyCode.isConstraintRequired()) { From 4d80e8ed708fe51ff1c5271805795f85e2791de4 Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Thu, 16 Apr 2026 16:53:21 +0700 Subject: [PATCH 06/15] Add backwards compatibility tests --- .../java/com/nedap/archie/json/AOMJacksonTest.java | 13 +++++++++++++ .../test/java/com/nedap/archie/xml/JAXBAOMTest.java | 1 + 2 files changed, 14 insertions(+) diff --git a/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java b/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java index 220079b40..84985b11a 100644 --- a/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java +++ b/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java @@ -70,6 +70,18 @@ public void roundTripDeliriumObservationScreening() throws Exception { } } + @Test + public void backwardsCompatibilityCTerminologyCodeTypeFromJsonTest() throws Exception { + String json = "{\n" + + " \"rm_type_name\" : \"terminology_code\",\n" + + " \"node_id\" : \"id9999\",\n" + + " \"constraint\" : [ \"ac23\" ]\n" + + "}"; + ObjectMapper objectMapper = JacksonUtil.getObjectMapper(ArchieJacksonConfiguration.createStandardsCompliant()); + CTerminologyCode parsedTermCode = objectMapper.readValue(json, CTerminologyCode.class); + assertEquals("ac23", parsedTermCode.getConstraint()); + } + @Test public void parseLifecycleStateStringTest() throws Exception { ObjectMapper objectMapper = JacksonUtil.getObjectMapper(ArchieJacksonConfiguration.createLegacyConfiguration()); @@ -282,6 +294,7 @@ public void cTerminologyCode() throws Exception { String json = objectMapper.writeValueAsString(cTermCode); assertTrue(json.contains("\"constraint_status\" : \"preferred\"")); + assertTrue(json.contains("\"constraint\" : \"ac23\"")); CTerminologyCode parsedTermCode = objectMapper.readValue(json, CTerminologyCode.class); assertEquals(cTermCode.getConstraint(), parsedTermCode.getConstraint()); assertEquals(ConstraintStatus.PREFERRED, parsedTermCode.getConstraintStatus()); diff --git a/tools/src/test/java/com/nedap/archie/xml/JAXBAOMTest.java b/tools/src/test/java/com/nedap/archie/xml/JAXBAOMTest.java index 0159c3ef2..1a719eaf4 100644 --- a/tools/src/test/java/com/nedap/archie/xml/JAXBAOMTest.java +++ b/tools/src/test/java/com/nedap/archie/xml/JAXBAOMTest.java @@ -81,6 +81,7 @@ public void testCTerminologyCode() throws Exception { String xml = writer.toString(); assertThat(xml, xml.contains("preferred")); + assertThat(xml, xml.contains("ac23")); Unmarshaller unmarshaller = JAXBUtil.getArchieJAXBContext().createUnmarshaller(); Archetype unmarshalled = (Archetype) unmarshaller.unmarshal(new StringReader(xml)); From f0a3c43ccf56a6fd117bff46e54e527282fd9bba Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Fri, 17 Apr 2026 16:52:34 +0700 Subject: [PATCH 07/15] Move and rename test --- .../com/nedap/archie/json/AOMJacksonTest.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java b/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java index 84985b11a..2141d067e 100644 --- a/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java +++ b/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java @@ -70,18 +70,6 @@ public void roundTripDeliriumObservationScreening() throws Exception { } } - @Test - public void backwardsCompatibilityCTerminologyCodeTypeFromJsonTest() throws Exception { - String json = "{\n" + - " \"rm_type_name\" : \"terminology_code\",\n" + - " \"node_id\" : \"id9999\",\n" + - " \"constraint\" : [ \"ac23\" ]\n" + - "}"; - ObjectMapper objectMapper = JacksonUtil.getObjectMapper(ArchieJacksonConfiguration.createStandardsCompliant()); - CTerminologyCode parsedTermCode = objectMapper.readValue(json, CTerminologyCode.class); - assertEquals("ac23", parsedTermCode.getConstraint()); - } - @Test public void parseLifecycleStateStringTest() throws Exception { ObjectMapper objectMapper = JacksonUtil.getObjectMapper(ArchieJacksonConfiguration.createLegacyConfiguration()); @@ -300,6 +288,18 @@ public void cTerminologyCode() throws Exception { assertEquals(ConstraintStatus.PREFERRED, parsedTermCode.getConstraintStatus()); } + @Test + public void backwardsCompatibilityCTerminologyCodeConstraintTypeFromJsonTest() throws Exception { + String json = "{\n" + + " \"rm_type_name\" : \"terminology_code\",\n" + + " \"node_id\" : \"id9999\",\n" + + " \"constraint\" : [ \"ac23\" ]\n" + + "}"; + ObjectMapper objectMapper = JacksonUtil.getObjectMapper(ArchieJacksonConfiguration.createStandardsCompliant()); + CTerminologyCode parsedTermCode = objectMapper.readValue(json, CTerminologyCode.class); + assertEquals("ac23", parsedTermCode.getConstraint()); + } + @Test public void rmOverlay() throws Exception { Archetype archetype = TestUtil.parseFailOnErrors("/com/nedap/archie/flattener/openEHR-EHR-OBSERVATION.to_flatten_parent_with_overlay.v1.0.0.adls"); From 4c9295e71780eaabd220ee2d42d9226567927806 Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Mon, 18 May 2026 13:18:50 +0200 Subject: [PATCH 08/15] Create CTerminologyCodeADL14 ... and use this in ADL14 related classes. Instead of the new CTerminologyCode with the pendingCodes object. --- .../adl14/ADL14TermConstraintConverter.java | 286 ++++++++-------- .../adl14/PreviousConversionApplier.java | 10 +- .../archie/adl14/aom14/CDVOrdinalItem.java | 8 +- .../Adl14CComplexObjectParser.java | 12 +- .../Adl14PrimitivesConstraintParser.java | 29 +- .../aom/primitives/CTerminologyCode.java | 16 - .../aom/primitives/CTerminologyCodeADL14.java | 315 ++++++++++++++++++ .../archie/adl14/LargeSetOfADL14sTest.java | 74 ++++ 8 files changed, 560 insertions(+), 190 deletions(-) create mode 100644 aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCodeADL14.java diff --git a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java index d75d6dd97..dda394bbf 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java +++ b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java @@ -4,6 +4,7 @@ import com.nedap.archie.adl14.log.ReasonForCodeCreation; import com.nedap.archie.aom.*; import com.nedap.archie.aom.primitives.CTerminologyCode; +import com.nedap.archie.aom.primitives.CTerminologyCodeADL14; import com.nedap.archie.aom.terminology.ArchetypeTerm; import com.nedap.archie.aom.terminology.ValueSet; import com.nedap.archie.aom.utils.AOMUtils; @@ -38,27 +39,24 @@ public void convert() { } private void convert(CObject cObject) { - - if (cObject instanceof CTerminologyCode) { - convertCTerminologyCode((CTerminologyCode) cObject); - } for(CAttribute attribute:cObject.getAttributes()) { - convert(attribute); + convertChildren(attribute); } if(cObject instanceof CComplexObject) { for (CAttributeTuple tuple : ((CComplexObject) cObject).getAttributeTuples()) { //tuples have not been properly converted to CAttributes in this parsed model, so we can ignore them above Set tupleTermCodeIndices = getCTerminologyCodeIndices(tuple); for (Integer index : tupleTermCodeIndices) { - List> termCodes = tuple.getTuples().stream().map(p -> p.getMember(index)).collect(Collectors.toList()); Set atCodes = new LinkedHashSet<>(); - for (CPrimitiveObject cPrimitiveObject : termCodes) { - CTerminologyCode cTerminologyCode = (CTerminologyCode) cPrimitiveObject; - convertCTerminologyCode(cTerminologyCode); - if(cTerminologyCode.getConstraint() != null) { - String constraint = cTerminologyCode.getConstraint(); - if(AOMUtils.isValueCode(constraint)) { - atCodes.add(constraint); + for (CPrimitiveTuple primitiveTuple : tuple.getTuples()) { + CPrimitiveObject member = primitiveTuple.getMember(index); + if (member instanceof CTerminologyCodeADL14) { + CTerminologyCodeADL14 cTerminologyCode = (CTerminologyCodeADL14) member; + convertCTerminologyCode(cTerminologyCode); + CTerminologyCode replacement = toAdl2(cTerminologyCode); + primitiveTuple.getMembers().set(index, replacement); + if (replacement.getConstraint() != null && AOMUtils.isValueCode(replacement.getConstraint())) { + atCodes.add(replacement.getConstraint()); } } } @@ -70,12 +68,28 @@ private void convert(CObject cObject) { } } + private void convertChildren(CAttribute attribute) { + List children = attribute.getChildren(); + for (int i = 0; i < children.size(); i++) { + CObject child = children.get(i); + if (child instanceof CTerminologyCodeADL14) { + CTerminologyCodeADL14 cTerminologyCode = (CTerminologyCodeADL14) child; + convertCTerminologyCode(cTerminologyCode); + CTerminologyCode replacement = toAdl2(cTerminologyCode); + children.set(i, replacement); + replacement.setParent(attribute); + } else { + convert(child); + } + } + } + private Set getCTerminologyCodeIndices(CAttributeTuple tuple) { Set result = new LinkedHashSet<>(); for (CPrimitiveTuple primitiveTuple : tuple.getTuples()) { int i = 0; for (CPrimitiveObject cPrimitiveObject : primitiveTuple.getMembers()) { - if(cPrimitiveObject instanceof CTerminologyCode) { + if(cPrimitiveObject instanceof CTerminologyCodeADL14) { result.add(i); } i++; @@ -84,139 +98,124 @@ private Set getCTerminologyCodeIndices(CAttributeTuple tuple) { return result; } - private void convert(CAttribute attribute) { - for(CObject object:attribute.getChildren()) { - convert(object); - } - } - - /** - * Converts a single-code {@link CTerminologyCode} constraint from ADL 1.4 to ADL 2 format. - * Multi-code constraints (stored via {@link CTerminologyCode#getPendingCodes()} by the ADL 1.4 parser) - * are delegated to {@link #convertMultiCodeTerminologyCode}. - *

- * Three cases are handled: - *

    - *
  • Local at-code (e.g. {@code at0001}): converted to its ADL 2 equivalent.
  • - *
  • Local ac-code (e.g. {@code ac0001}): converted to its ADL 2 value-set code equivalent.
  • - *
  • External terminology code (e.g. {@code [snomed-ct::12345]}): a term binding is created and - * a new local at-code is generated to reference it.
  • - *
- */ - private void convertCTerminologyCode(CTerminologyCode cTerminologyCode) { - List pendingCodes = cTerminologyCode.getPendingCodes(); - if (pendingCodes != null && !pendingCodes.isEmpty()) { - convertMultiCodeTerminologyCode(cTerminologyCode, pendingCodes); - return; - } - - String constraint = cTerminologyCode.getConstraint(); - if (constraint == null) { - return; - } - - TerminologyCode termCode = TerminologyCode.createFromString(constraint); - boolean isLocalCode = termCode.getTerminologyId() == null - || termCode.getTerminologyId().equalsIgnoreCase("local"); - - if (isLocalCode && AOMUtils.isValueCode(constraint)) { - // Single local at-code: convert it to its ADL 2 equivalent - String newCode = converter.convertValueCode(constraint); - converter.addConvertedCode(constraint, newCode); - cTerminologyCode.setConstraint(newCode); - } else if (isLocalCode && AOMUtils.isValueSetCode(termCode.getCodeString())) { - // Local value-set reference: convert the ac-code to its ADL 2 equivalent - String newCode = converter.convertValueSetCode(termCode.getCodeString()); - converter.addConvertedCode(termCode.getCodeString(), newCode); - cTerminologyCode.setConstraint(newCode); - } else { - // External terminology: create a term binding and generate a new at-code to reference it - try { - URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(termCode); - Map termBindingsMap = findOrCreateTermBindings(termCode); - cTerminologyCode.setConstraint(findOrAddTermBindingAndCode(termCode, uri, termBindingsMap)); - } catch (URISyntaxException e) { - logger.error("error converting term", e); - } - } + private void convertCTerminologyCode(CTerminologyCodeADL14 cTerminologyCode) { + if(cTerminologyCode.getConstraint() != null && !cTerminologyCode.getConstraint().isEmpty()) { + String firstConstraint = cTerminologyCode.getConstraint().get(0); + TerminologyCode termCode = TerminologyCode.createFromString(firstConstraint); + boolean isLocalCode = termCode.getTerminologyId() == null || termCode.getTerminologyId().equalsIgnoreCase("local"); + if(isLocalCode && AOMUtils.isValueCode(firstConstraint)) { + //local codes + if(cTerminologyCode.getConstraint().size() == 1) { + //do not create a value set, just convert the code + String newCode = converter.convertValueCode(firstConstraint); + converter.addConvertedCode(firstConstraint, newCode); + cTerminologyCode.setConstraint(new ArrayList<>(Collections.singletonList(newCode))); + } else { + Set localCodes = new LinkedHashSet<>(); + for(String code:cTerminologyCode.getConstraint()) { + String newCode = converter.convertValueCode(code); + converter.addConvertedCode(code, newCode); + localCodes.add(newCode); + } - convertAssumedValue(cTerminologyCode, isLocalCode); - } + ValueSet valueSet = findOrCreateValueSet(cTerminologyCode.getArchetype(), localCodes, cTerminologyCode); + cTerminologyCode.setConstraint(new ArrayList<>(Collections.singletonList(valueSet.getId()))); + } + } else if (isLocalCode && AOMUtils.isValueSetCode(termCode.getCodeString())) { + List newConstraint = new ArrayList<>(); + for(String constraint:cTerminologyCode.getConstraint()) { + TerminologyCode code = TerminologyCode.createFromString(constraint); + String newCode = converter.convertValueSetCode(code.getCodeString()); + converter.addConvertedCode(termCode.getCodeString(), newCode); + newConstraint.add(newCode); + } + cTerminologyCode.setConstraint(newConstraint); + + } else { + if (cTerminologyCode.getConstraint().size() == 1) { + try { + //do not create a value set, create a code plus binding to the old non-local code + URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(termCode); + Map termBindingsMap = findOrCreateTermBindings(termCode); + + //TODO: check if this is a converted or old term binding - old is unusual, but could be possible! + String termBinding = findOrAddTermBindingAndCode(termCode, uri, termBindingsMap); + cTerminologyCode.setConstraint(new ArrayList<>(Collections.singletonList(termBinding))); + } catch (URISyntaxException e) { + //TODO + logger.error("error converting term", e); + } + } else { + String terminologyId = cTerminologyCode.getConstraint().get(0); + termCode = TerminologyCode.createFromString(terminologyId, null, cTerminologyCode.getConstraint().get(1)); + Map termBindingsMap = findOrCreateTermBindings(termCode); + List atCodes = new ArrayList<>(); + List constraints = new ArrayList<>(cTerminologyCode.getConstraint()); + cTerminologyCode.setConstraint(atCodes); + for(int i = 1; i < constraints.size(); i++) { + String constraint = constraints.get(i); + try { + if(constraint.startsWith("[") && constraint.endsWith("]")) { + TerminologyCode constraintCode = TerminologyCode.createFromString(constraint); + URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(constraintCode); + atCodes.add(findOrAddTermBindingAndCode(constraintCode, uri, termBindingsMap)); + } else { + TerminologyCode constraintCode = new TerminologyCode(); + constraintCode.setTerminologyId(terminologyId); + constraintCode.setCodeString(constraint); + URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(constraintCode); + atCodes.add(findOrAddTermBindingAndCode(constraintCode, uri, termBindingsMap)); + } - /** - * Converts a multi-code {@link CTerminologyCode} constraint from ADL 1.4 to ADL 2 format. - * In ADL 1.4, a constraint may reference multiple codes inline (e.g. - * {@code [local::at0001, at0002]} or {@code [snomed-ct::12345, 67890]}). ADL 2 represents - * these as a value set (ac-code), which this method creates. - *

- * The {@code pendingCodes} list was populated by the ADL 1.4 parser: - *

    - *
  • For local codes: raw at-codes, e.g. {@code ["at0001", "at0002"]}.
  • - *
  • For external codes: full term code refs normalised by the parser, - * e.g. {@code ["[snomed-ct::12345]", "[snomed-ct::67890]"]}.
  • - *
- * In both cases a value set is created and the constraint is set to its ac-code. - */ - private void convertMultiCodeTerminologyCode(CTerminologyCode cTerminologyCode, List pendingCodes) { - TerminologyCode firstCode = TerminologyCode.createFromString(pendingCodes.get(0)); - boolean isLocalCode = firstCode.getTerminologyId() == null - || firstCode.getTerminologyId().equalsIgnoreCase("local"); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + } + ValueSet valueSet = findOrCreateValueSet(cTerminologyCode.getArchetype(), new LinkedHashSet<>(atCodes), cTerminologyCode); + cTerminologyCode.setConstraint(new ArrayList<>(Collections.singletonList(valueSet.getId()))); - if (isLocalCode) { - // Convert each at-code and group them into a new value set - Set convertedCodes = new LinkedHashSet<>(); - for (String code : pendingCodes) { - String newCode = converter.convertValueCode(code); - converter.addConvertedCode(code, newCode); - convertedCodes.add(newCode); - } - ValueSet valueSet = findOrCreateValueSet(cTerminologyCode.getArchetype(), convertedCodes, cTerminologyCode); - cTerminologyCode.setConstraint(valueSet.getId()); - } else { - // Create a term binding for each external code, then group the resulting at-codes into a value set - Map termBindingsMap = findOrCreateTermBindings(firstCode); - List atCodes = new ArrayList<>(); - for (String code : pendingCodes) { - TerminologyCode termCode = TerminologyCode.createFromString(code); - try { - URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(termCode); - atCodes.add(findOrAddTermBindingAndCode(termCode, uri, termBindingsMap)); - } catch (URISyntaxException e) { - logger.error("error converting term", e); } } - if (!atCodes.isEmpty()) { - ValueSet valueSet = findOrCreateValueSet(cTerminologyCode.getArchetype(), new LinkedHashSet<>(atCodes), cTerminologyCode); - cTerminologyCode.setConstraint(valueSet.getId()); + if(cTerminologyCode.getAssumedValue() != null) { + TerminologyCode assumedValue = cTerminologyCode.getAssumedValue(); + if(isLocalCode) { + String newCode = converter.convertValueCode(assumedValue.getCodeString()); + assumedValue.setCodeString(newCode); + assumedValue.setTerminologyId(null); + } else { + try { + Map termBindingsMap = findOrCreateTermBindings(assumedValue); + URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(assumedValue); + assumedValue.setCodeString(findOrAddTermBindingAndCode(assumedValue, uri, termBindingsMap)); + assumedValue.setTerminologyId(null); + assumedValue.setTerminologyVersion(null); + } catch (URISyntaxException e) { + //TODO + e.printStackTrace(); + } + } } } - - convertAssumedValue(cTerminologyCode, isLocalCode); } /** - * Converts the assumed value of a {@link CTerminologyCode} from ADL 1.4 to ADL 2 format, - * mirroring the logic used for the constraint itself. + * Build an ADL 2 {@link CTerminologyCode} from a converted {@link CTerminologyCodeADL14}. + * Copies all relevant CObject/CPrimitiveObject fields and collapses the single-element + * post-conversion constraint list into a single String. */ - private void convertAssumedValue(CTerminologyCode cTerminologyCode, boolean isLocalCode) { - if (cTerminologyCode.getAssumedValue() == null) { - return; - } - TerminologyCode assumedValue = cTerminologyCode.getAssumedValue(); - if (isLocalCode) { - assumedValue.setCodeString(converter.convertValueCode(assumedValue.getCodeString())); - assumedValue.setTerminologyId(null); - } else { - try { - Map termBindingsMap = findOrCreateTermBindings(assumedValue); - URI uri = new ADL14ConversionUtil(converter.getConversionConfiguration()).convertToUri(assumedValue); - assumedValue.setCodeString(findOrAddTermBindingAndCode(assumedValue, uri, termBindingsMap)); - assumedValue.setTerminologyId(null); - assumedValue.setTerminologyVersion(null); - } catch (URISyntaxException e) { - logger.error("error converting term", e); - } + private CTerminologyCode toAdl2(CTerminologyCodeADL14 source) { + CTerminologyCode result = new CTerminologyCode(); + result.setRmTypeName(source.getRmTypeName()); + result.setOccurrences(source.getOccurrences()); + result.setDeprecated(source.getDeprecated()); + result.setSiblingOrder(source.getSiblingOrder()); + result.setEnumeratedTypeConstraint(source.getEnumeratedTypeConstraint()); + result.setAssumedValue(source.getAssumedValue()); + result.setConstraintStatus(source.getConstraintStatus()); + if (source.getConstraint() != null && !source.getConstraint().isEmpty()) { + result.setConstraint(source.getConstraint().get(0)); } + return result; } private Map findOrCreateTermBindings(TerminologyCode termCode) { @@ -276,13 +275,20 @@ private ValueSet findOrCreateValueSet(Archetype archetype, Set localCode CAttribute cAttributeInParent = (CAttribute) inParent; if(!cAttributeInParent.getChildren().isEmpty()) { CObject cObject = cAttributeInParent.getChildren().get(0); - if(cObject instanceof CTerminologyCode) { - CTerminologyCode termCodeInParent = (CTerminologyCode) cObject; - if(termCodeInParent.getConstraint() != null) { - if(termCodeInParent.getConstraint().startsWith("ac")) { - idInparent = termCodeInParent.getConstraint(); + if(cObject instanceof CTerminologyCodeADL14) { + CTerminologyCodeADL14 termCodeInParent = (CTerminologyCodeADL14) cObject; + if(termCodeInParent.getConstraint() != null && !termCodeInParent.getConstraint().isEmpty()) { + String firstConstraint = termCodeInParent.getConstraint().get(0); + if(firstConstraint.startsWith("ac")) { + idInparent = firstConstraint; } } + } else if(cObject instanceof CTerminologyCode) { + CTerminologyCode termCodeInParent = (CTerminologyCode) cObject; + String parentConstraint = termCodeInParent.getConstraint(); + if(parentConstraint != null && parentConstraint.startsWith("ac")) { + idInparent = parentConstraint; + } } } } diff --git a/aom/src/main/java/com/nedap/archie/adl14/PreviousConversionApplier.java b/aom/src/main/java/com/nedap/archie/adl14/PreviousConversionApplier.java index 89870fca8..7e029e845 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/PreviousConversionApplier.java +++ b/aom/src/main/java/com/nedap/archie/adl14/PreviousConversionApplier.java @@ -200,10 +200,8 @@ private Set gatherUsedValueSets(CAttribute attribute) { for(CObject child:attribute.getChildren()) { if(child instanceof CTerminologyCode) { String constraint = ((CTerminologyCode) child).getConstraint(); - if(constraint != null) { - if(constraint.startsWith("ac")) { - result.add(constraint); - } + if(constraint != null && constraint.startsWith("ac")) { + result.add(constraint); } } if(child.getRmTypeName().equalsIgnoreCase("DV_ORDINAL")) { @@ -215,8 +213,8 @@ private Set gatherUsedValueSets(CAttribute attribute) { if(symbolIndex >= 0) { for(CPrimitiveTuple primitiveTuple:tuple.getTuples()) { CTerminologyCode cTermCode = (CTerminologyCode) primitiveTuple.getMember(symbolIndex); - if(cTermCode != null) { - atCodes.addAll(cTermCode.getConstraintAsList()); + if(cTermCode != null && cTermCode.getConstraint() != null) { + atCodes.add(cTermCode.getConstraint()); } } } diff --git a/aom/src/main/java/com/nedap/archie/adl14/aom14/CDVOrdinalItem.java b/aom/src/main/java/com/nedap/archie/adl14/aom14/CDVOrdinalItem.java index 771c80009..56d67ed55 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/aom14/CDVOrdinalItem.java +++ b/aom/src/main/java/com/nedap/archie/adl14/aom14/CDVOrdinalItem.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.nedap.archie.aom.primitives.CInteger; -import com.nedap.archie.aom.primitives.CTerminologyCode; +import com.nedap.archie.aom.primitives.CTerminologyCodeADL14; import com.nedap.archie.base.Interval; import com.nedap.archie.base.terminology.TerminologyCode; @@ -28,12 +28,12 @@ public void setSymbol(TerminologyCode symbol) { } @JsonIgnore - public CTerminologyCode getSymbolAdl2() { + public CTerminologyCodeADL14 getSymbolAdl2() { if(symbol == null) { return null; } - CTerminologyCode result = new CTerminologyCode(); - result.setConstraint(symbol.toString()); + CTerminologyCodeADL14 result = new CTerminologyCodeADL14(); + result.addConstraint(symbol.toString()); return result; } diff --git a/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14CComplexObjectParser.java b/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14CComplexObjectParser.java index 2d2290808..fd720e87c 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14CComplexObjectParser.java +++ b/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14CComplexObjectParser.java @@ -11,7 +11,7 @@ import com.nedap.archie.aom.primitives.CInteger; import com.nedap.archie.aom.primitives.CReal; import com.nedap.archie.aom.primitives.CString; -import com.nedap.archie.aom.primitives.CTerminologyCode; +import com.nedap.archie.aom.primitives.CTerminologyCodeADL14; import com.nedap.archie.base.Cardinality; import com.nedap.archie.base.Interval; import com.nedap.archie.base.MultiplicityInterval; @@ -203,10 +203,10 @@ private CObject parseNonPrimitiveObject(C_non_primitive_objectContext objectCont CPrimitiveTuple primitiveTuple = new CPrimitiveTuple(); - CTerminologyCode cCode = new CTerminologyCode(); + CTerminologyCodeADL14 cCode = new CTerminologyCodeADL14(); TerminologyCode code = TerminologyCode.createFromString(ordinal_termContext.c_terminology_code().getText()); - cCode.setConstraint(code.getCodeString()); + cCode.addConstraint(code.getCodeString()); primitiveTuple.addMember(cValue); primitiveTuple.addMember(cCode); @@ -256,7 +256,7 @@ private void parseCDVOrdinal(C_non_primitive_objectContext objectContext, CCompl if (item.getSymbol() != null) { primitiveTuple.addMember(item.getSymbolAdl2()); } else if (hasSymbol) { - CTerminologyCode code = new CTerminologyCode(); + CTerminologyCodeADL14 code = new CTerminologyCodeADL14(); primitiveTuple.addMember(code);//nothing we can do here! } @@ -273,9 +273,9 @@ private void parseCDVQuantity(C_non_primitive_objectContext objectContext, CComp CDVQuantity cdvQuantity = odinParser.convert(objectContext.domainSpecificExtension().odin_text().getText(), CDVQuantity.class); if(cdvQuantity.getProperty() != null) { CAttribute property = new CAttribute("property"); - CTerminologyCode code = new CTerminologyCode(); + CTerminologyCodeADL14 code = new CTerminologyCodeADL14(); //will be converted later - code.setConstraint(cdvQuantity.getProperty().toString()); + code.addConstraint(cdvQuantity.getProperty().toString()); property.addChild(code); result.addAttribute(property); } diff --git a/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParser.java b/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParser.java index be423ccde..5527aa7e8 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParser.java +++ b/aom/src/main/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParser.java @@ -17,7 +17,6 @@ import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.TerminalNode; -import java.util.ArrayList; import java.util.List; /** @@ -92,8 +91,8 @@ private void parseBooleanValues(CBoolean result, List bool } } - public CTerminologyCode parseCTerminologyCode(Adl14Parser.C_terminology_codeContext terminologyCodeContext) { - CTerminologyCode result = new CTerminologyCode(); + public CTerminologyCodeADL14 parseCTerminologyCode(Adl14Parser.C_terminology_codeContext terminologyCodeContext) { + CTerminologyCodeADL14 result = new CTerminologyCodeADL14(); boolean containsAssumedValue = !terminologyCodeContext.getTokens(Adl14Lexer.SYM_SEMICOLON).isEmpty(); @@ -104,29 +103,23 @@ public CTerminologyCode parseCTerminologyCode(Adl14Parser.C_terminology_codeCont //need to do parsing here because the lexer matched the entire term code ref TerminologyCode terminologyCode = TerminologyCode.createFromString(qualifiedTermCodeContext.TERM_CODE_REF().getText()); if (terminologyCode.getTerminologyId() != null && terminologyCode.getTerminologyId().equalsIgnoreCase("local")) { - result.setConstraint(terminologyCode.getCodeString()); + result.addConstraint(terminologyCode.getCodeString()); } else { //non-local term constraints. Just add the text here, it will be converted later - result.setConstraint(qualifiedTermCodeContext.TERM_CODE_REF().getText()); + result.addConstraint(qualifiedTermCodeContext.TERM_CODE_REF().getText()); } } else { String terminologyId = qualifiedTermCodeContext.identifier(0).getText(); if (terminologyId.equalsIgnoreCase("local")) { - List pendingCodes = new ArrayList<>(); + //we need to create a value set. For now just add the constraint, the value set will come after + //the parser for (int i = 1; i < qualifiedTermCodeContext.identifier().size(); i++) { - pendingCodes.add(qualifiedTermCodeContext.identifier(i).getText()); - } - if (!pendingCodes.isEmpty()) { - result.setPendingCodes(pendingCodes); + result.addConstraint(qualifiedTermCodeContext.identifier(i).getText()); } } else { - // Normalise to full term code refs so the converter can treat all pending codes uniformly - List pendingCodes = new ArrayList<>(); - for (int i = 1; i < qualifiedTermCodeContext.identifier().size(); i++) { - pendingCodes.add("[" + terminologyId + "::" + qualifiedTermCodeContext.identifier(i).getText() + "]"); - } - if (!pendingCodes.isEmpty()) { - result.setPendingCodes(pendingCodes); + //non-local term constraints. Add the text here, will be converted later + for (int i = 0; i < qualifiedTermCodeContext.identifier().size(); i++) { + result.addConstraint(qualifiedTermCodeContext.identifier(i).getText()); } } @@ -137,7 +130,7 @@ public CTerminologyCode parseCTerminologyCode(Adl14Parser.C_terminology_codeCont } else { //this is an AC-code. if(terminologyCodeContext.localTermCode().AC_CODE() != null) { - result.setConstraint(terminologyCodeContext.localTermCode().AC_CODE().getText()); + result.addConstraint(terminologyCodeContext.localTermCode().AC_CODE().getText()); } else { throw new RuntimeException("unknown terminology code format - this looks adl2 inside the adl 1.4 format?"); } diff --git a/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java b/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java index bba7040f9..44616c541 100644 --- a/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java +++ b/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCode.java @@ -18,7 +18,6 @@ import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlTransient; import jakarta.xml.bind.annotation.XmlType; import org.openehr.utils.message.I18n; @@ -45,11 +44,6 @@ public class CTerminologyCode extends CPrimitiveObject @Nullable private ConstraintStatus constraintStatus; - /** Temporary storage for multi-code ADL 1.4 constraints during conversion. Never serialized. */ - @JsonIgnore - @XmlTransient - private List pendingCodes; - @Override public TerminologyCode getAssumedValue() { return assumedValue; @@ -82,16 +76,6 @@ public void setConstraintStatus(ConstraintStatus constraintStatus) { this.constraintStatus = constraintStatus; } - @JsonIgnore - public List getPendingCodes() { - return pendingCodes; - } - - @JsonIgnore - public void setPendingCodes(List pendingCodes) { - this.pendingCodes = pendingCodes; - } - @JsonIgnore public boolean isConstraintRequired() { return getEffectiveConstraintStatus() == ConstraintStatus.REQUIRED; diff --git a/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCodeADL14.java b/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCodeADL14.java new file mode 100644 index 000000000..808a36247 --- /dev/null +++ b/aom/src/main/java/com/nedap/archie/aom/primitives/CTerminologyCodeADL14.java @@ -0,0 +1,315 @@ +package com.nedap.archie.aom.primitives; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.nedap.archie.ArchieLanguageConfiguration; +import com.nedap.archie.ValidationConfiguration; +import com.nedap.archie.aom.Archetype; +import com.nedap.archie.aom.CObject; +import com.nedap.archie.aom.CPrimitiveObject; +import com.nedap.archie.aom.terminology.ArchetypeTerm; +import com.nedap.archie.aom.terminology.ArchetypeTerminology; +import com.nedap.archie.aom.terminology.TerminologyCodeWithArchetypeTerm; +import com.nedap.archie.aom.terminology.ValueSet; +import com.nedap.archie.aom.utils.AOMUtils; +import com.nedap.archie.aom.utils.ConformanceCheckResult; +import com.nedap.archie.archetypevalidator.ErrorType; +import com.nedap.archie.base.terminology.TerminologyCode; +import com.nedap.archie.terminology.OpenEHRTerminologyAccess; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlType; +import org.openehr.utils.message.I18n; + +import javax.annotation.Nullable; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; + +@XmlType(name="C_TERMINOLOGY_CODE") +@XmlAccessorType(XmlAccessType.FIELD) +public class CTerminologyCodeADL14 extends CPrimitiveObject, TerminologyCode> { + + @XmlElement(name="assumed_value") + @Nullable + private TerminologyCode assumedValue; + private List constraint = new ArrayList<>(); + + @Nullable + private ConstraintStatus constraintStatus; + + @Override + public TerminologyCode getAssumedValue() { + return assumedValue; + } + + @Override + public void setAssumedValue(TerminologyCode assumedValue) { + this.assumedValue = assumedValue; + } + + @Override + public List getConstraint() { + return this.constraint; + } + + @Override + public List getConstraintAsList() { + return this.constraint; + } + + public void setConstraint(List constraint) { + this.constraint = constraint; + } + + public void addConstraint(String constraint) { + this.constraint.add(constraint); + } + + public ConstraintStatus getConstraintStatus() { + return constraintStatus; + } + + public void setConstraintStatus(ConstraintStatus constraintStatus) { + this.constraintStatus = constraintStatus; + } + + @JsonIgnore + public boolean isConstraintRequired() { + return getEffectiveConstraintStatus() == ConstraintStatus.REQUIRED; + } + + public ConstraintStatus getEffectiveConstraintStatus() { + return constraintStatus == null ? ConstraintStatus.REQUIRED : constraintStatus; + } + + @Override + @Deprecated + public boolean isValidValue(TerminologyCode value) { + if(getConstraint().isEmpty()) { + return true; + } + if(isConstraintRequired()) { + if (value == null) return false; + + List values; + String terminologyId = value.getTerminologyId(); + if (terminologyId == null || terminologyId.equalsIgnoreCase("local") || AOMUtils.isValueSetCode(value.getTerminologyId())) { + values = this.getValueSetExpanded(); + } else if (terminologyId.equalsIgnoreCase("openehr")) { + values = this.getOpenEHRValueSetExpanded(); + } else { + // This is not a local nor an openehr terminology. + // If a term binding is there, we may be able to validate, if external, we wil not be able to. + // Return true for now for non-local terminology values. + //TODO: implement checking for direct term bindings later + return !ValidationConfiguration.isFailOnUnknownTerminologyId(); + } + + if(values != null && !values.isEmpty()) { + return value.getCodeString() != null && values.contains(value.getCodeString()); + } + } else { + return true; + } + + return false; + } + + + /** + * Get the ArchetypeTerms in the selected meaning and description language for all the possible options if this is a + * locally defined terminology. + * See the ArchieLanguageConfiguration for the language settings. + * + * @return + */ + public List getTerms() { + List result = new ArrayList<>(); + Archetype archetype = getArchetype(); + if(archetype == null) { + //ideally this would not happen, but no reference to archetype exists in leaf constraints in rules so far + //so for now fix it so it doesn't throw a NullPointerException + return result; + } + ArchetypeTerminology terminology = archetype.getTerminology(this); + String language = ArchieLanguageConfiguration.getMeaningAndDescriptionLanguage(); + String defaultLanguage = ArchieLanguageConfiguration.getDefaultMeaningAndDescriptionLanguage(); + for(String constraint:getConstraint()) { + if(constraint.startsWith("at")) { + ArchetypeTerm termDefinition = terminology.getTermDefinition(language, constraint); + if(termDefinition == null) { + termDefinition = terminology.getTermDefinition(defaultLanguage, constraint); + } + if(termDefinition != null) { + result.add(new TerminologyCodeWithArchetypeTerm(constraint, termDefinition)); + } + } else if (constraint.startsWith("ac")) { + ValueSet acValueSet = terminology.getValueSets().get(constraint); + if(acValueSet != null) { + for(String atCode:acValueSet.getMembers()) { + ArchetypeTerm termDefinition = terminology.getTermDefinition(language, atCode); + if(termDefinition == null) { + termDefinition = terminology.getTermDefinition(defaultLanguage, atCode); + } + if(termDefinition != null) { + result.add(new TerminologyCodeWithArchetypeTerm(atCode, termDefinition)); + } + } + } + } + } + return result; + } + + private void setTerms(List terms) { + //hack for jackson to work + } + + private ArchetypeTerminology getTerminology() { + Archetype archetype = getArchetype(); + if(archetype != null) { + //ideally this would not happen, but no reference to archetype exists in leaf constraints in rules so far + //so for now fix it so it doesn't throw a NullPointerException + return archetype.getTerminology(this); + } + return null; + } + + @JsonIgnore + public List getValueSetExpanded() { + List result = new ArrayList<>(); + ArchetypeTerminology terminology = getTerminology(); + for(String constraint:getConstraint()) { + if(constraint.startsWith("at")) { + result.add(constraint); + } else if (constraint.startsWith("ac")) { + if(terminology != null) { + ValueSet acValueSet = terminology.getValueSets().get(constraint); + if (acValueSet != null) { + result.addAll(AOMUtils.getExpandedValueSetMembers(terminology.getValueSets(), acValueSet)); + } + } + } + } + return result; + } + + private List getOpenEHRValueSetExpanded() { + List atCodes = getValueSetExpanded(); + ArchetypeTerminology terminology = getTerminology(); + OpenEHRTerminologyAccess terminologyAccess = OpenEHRTerminologyAccess.getInstance(); + List result = new ArrayList<>(); + + if(terminology == null) { + return result; + } + + for(String atCode : atCodes) { + URI termBinding = terminology.getTermBinding("openehr", atCode); + if (termBinding != null) { + String code = terminologyAccess.parseTerminologyURI(termBinding.toString()); + if (code != null) { + result.add(code); + } + } + } + + return result; + } + + @Override + public ConformanceCheckResult cConformsTo(CObject other, BiFunction rmTypesConformant) { + ConformanceCheckResult superResult = super.cConformsTo(other, rmTypesConformant); + if(!superResult.doesConform()) { + return superResult; + } + //now guaranteed to be the same class + CTerminologyCodeADL14 otherCode = (CTerminologyCodeADL14) other; + List valueSet = getValueSetExpanded(); + List otherValueSet = otherCode.getValueSetExpanded(); + + if(constraint.size() != 1) { + return ConformanceCheckResult.fails(ErrorType.VPOV, I18n.t("child CTerminology code contains more than one constraint, that is not valid. Constraints are: {0}", constraint)); + } + if(otherCode.constraint.size() != 1) { + return ConformanceCheckResult.fails(ErrorType.VPOV, I18n.t("parent CTerminology code contains more than one constraint, that is not valid. Constraints are: {0}", constraint)); + } + + if(!getEffectiveConstraintStatus().cConformsTo(otherCode.getEffectiveConstraintStatus()) ) { + //PROBLEM: if this child CTerminologyCodeADL14 has no constraint status, it should override its parent. + //it does not here! + return ConformanceCheckResult.fails(ErrorType.VPOV, I18n.t("specialized CTerminology code constraint status {0} is wider more than parent contraint status {1}", getEffectiveConstraintStatus(), otherCode.getEffectiveConstraintStatus())); + } + String thisConstraint = constraint.get(0); + String otherConstraint = otherCode.constraint.get(0); + Archetype archetype = this.getArchetype(); + if(AOMUtils.isValidValueSetCode(thisConstraint) && AOMUtils.isValidValueSetCode(otherConstraint)) { + if (otherValueSet.isEmpty()) { + return ConformanceCheckResult.conforms(); + } + + if(otherCode.isConstraintRequired()) { + //if required, codes can be: + // - reused directly + // - specialized + //this includes the value set codes + if (!AOMUtils.codesConformant(thisConstraint, otherConstraint)) { + return ConformanceCheckResult.fails(ErrorType.VPOV, I18n.t("child terminology constraint value set code {0} does not conform to parent constraint with value set code {1}", thisConstraint, otherConstraint)); + } + for (String value : valueSet) { + if( !AOMUtils.valueSetContainsCodeOrParent(otherValueSet, value)) { + return ConformanceCheckResult.fails(ErrorType.VPOV, I18n.t("child terminology constraint value code {0} is not contained in {1}, or a direct specialization of one of its values", value, otherValueSet)); + } + } + } else { + //if not required, everything goes + return ConformanceCheckResult.conforms(); +// for (String value : valueSet) { +// if(valueSetContainsCodeOrSpecialization(otherValueSet, value) || +// AOMUtils.getSpecialisationStatusFromCode(value, archetypeSpecialisationDepth) == CodeRedefinitionStatus.ADDED) { +// return false; +// } +// } + } + return ConformanceCheckResult.conforms(); + } else { + if(!AOMUtils.codesConformant(thisConstraint, otherConstraint)) { + return ConformanceCheckResult.fails(ErrorType.VPOV, I18n.t("child terminology constraint value code {0} does not conform to parent constraint with value code {1}", thisConstraint, otherConstraint)); + } + return ConformanceCheckResult.conforms(); + } + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("{["); + boolean first = true; + for(String constraint:getConstraint()) { + if(!first) { + result.append(", "); + } + first = false; + result.append(constraint.toString()); + } + result.append("]}"); + return result.toString(); + } + + @Override + public String getRmTypeName() { + if (getParent() == null || getParent().getRmAttributeName() == null) { + return "terminology_code"; + } + switch (getParent().getRmAttributeName()) { + case "defining_code": + return "CODE_PHRASE"; + case "symbol": + return "DV_CODED_TEXT"; + default: + return "terminology_code"; + } + } +} diff --git a/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java b/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java index 0debed84d..85312f6fa 100644 --- a/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java +++ b/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java @@ -3,6 +3,13 @@ import com.nedap.archie.adlparser.antlr.Adl14Lexer; import com.nedap.archie.antlr.errors.ANTLRParserErrors; import com.nedap.archie.aom.Archetype; +import com.nedap.archie.aom.CAttribute; +import com.nedap.archie.aom.CAttributeTuple; +import com.nedap.archie.aom.CComplexObject; +import com.nedap.archie.aom.CObject; +import com.nedap.archie.aom.CPrimitiveObject; +import com.nedap.archie.aom.CPrimitiveTuple; +import com.nedap.archie.aom.primitives.CTerminologyCodeADL14; import com.nedap.archie.archetypevalidator.ValidationResult; import com.nedap.archie.flattener.InMemoryFullArchetypeRepository; import org.antlr.v4.runtime.CharStreams; @@ -20,6 +27,8 @@ import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -51,6 +60,71 @@ public void parseUrn() { assertEquals(1, adl14Lexer.getAllTokens().size()); } + /** + * Parses a demo ADL 1.4 archetype that exercises (almost) every constraint type and asserts that every + * {@link CTerminologyCodeADL14} in the resulting tree has a non-empty constraint list. This guards against + * the regression that motivated this PR: multi-code constraints like {@code [local::at0001, at0002]} and + * {@code [openehr::271, 272, 273, 253]} silently parsing to an empty constraint, which existing bulk-parse + * tests didn't catch because they only counted parse exceptions. + */ + @Test + public void testDemoArchetype() throws Exception { + ADL14Parser parser = new ADL14Parser(BuiltinReferenceModels.getMetaModelProvider()); + Archetype archetype; + try (InputStream stream = getClass().getResourceAsStream( + "/adl14/entry/observation/openEHR-EHR-OBSERVATION.demo.v1.adl")) { + assertNotNull(stream, "demo archetype resource not found on classpath"); + archetype = parser.parse(stream, conversionConfiguration); + } + assertNotNull(archetype); + assertTrue(parser.errorListener.getErrors().getErrors().isEmpty(), + () -> "unexpected parse errors: " + parser.errorListener.getErrors()); + + List termCodes = new ArrayList<>(); + collectTerminologyCodes(archetype.getDefinition(), termCodes); + assertFalse(termCodes.isEmpty(), "no CTerminologyCodeADL14 nodes found - demo archetype unexpectedly empty"); + for (CTerminologyCodeADL14 termCode : termCodes) { + assertFalse(termCode.getConstraint().isEmpty(), + () -> "empty constraint at path " + termCode.getPath() + + " - multi-code parsing likely regressed"); + } + + // The demo includes both a local multi-code (4 at-codes) and an openehr multi-code (4 codes). + // Both used to parse to empty lists before the parser fix. + long multiCodeCount = termCodes.stream().filter(t -> t.getConstraint().size() > 1).count(); + assertTrue(multiCodeCount >= 2, + "expected at least two multi-code terminology constraints in the demo, found " + multiCodeCount); + + // Sanity check: end-to-end conversion of the demo also succeeds. + ADL2ConversionResultList converted = new ADL14Converter( + BuiltinReferenceModels.getMetaModelProvider(), conversionConfiguration) + .convert(Collections.singletonList(archetype)); + ADL2ConversionResult result = converted.getConversionResults().get(0); + assertNotNull(result.getArchetype(), () -> "conversion returned null archetype, exception: " + result.getException()); + } + + private void collectTerminologyCodes(CObject cObject, List out) { + if (cObject instanceof CTerminologyCodeADL14) { + out.add((CTerminologyCodeADL14) cObject); + } + for (CAttribute attribute : cObject.getAttributes()) { + for (CObject child : attribute.getChildren()) { + collectTerminologyCodes(child, out); + } + } + if (cObject instanceof CComplexObject) { + for (CAttributeTuple tuple : ((CComplexObject) cObject).getAttributeTuples()) { + for (CPrimitiveTuple primitiveTuple : tuple.getTuples()) { + for (CPrimitiveObject member : primitiveTuple.getMembers()) { + if (member instanceof CTerminologyCodeADL14) { + out.add((CTerminologyCodeADL14) member); + } + } + } + } + } + } + @Test public void testRiskFamilyhistory() throws Exception { From da8583cd95df2fabb6c440de80d7e2fc73ff33e7 Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Fri, 22 May 2026 11:25:53 +0200 Subject: [PATCH 09/15] Inline child traversal into convert(CObject) --- .../adl14/ADL14TermConstraintConverter.java | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java index dda394bbf..29e219771 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java +++ b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java @@ -39,8 +39,15 @@ public void convert() { } private void convert(CObject cObject) { - for(CAttribute attribute:cObject.getAttributes()) { - convertChildren(attribute); + if (cObject instanceof CTerminologyCodeADL14) { + CTerminologyCodeADL14 cTerminologyCode = (CTerminologyCodeADL14) cObject; + convertCTerminologyCode(cTerminologyCode); + replaceInParent(cTerminologyCode, toAdl2(cTerminologyCode)); + } + for (CAttribute attribute : cObject.getAttributes()) { + for (CObject child : attribute.getChildren()) { + convert(child); + } } if(cObject instanceof CComplexObject) { for (CAttributeTuple tuple : ((CComplexObject) cObject).getAttributeTuples()) { @@ -68,20 +75,11 @@ private void convert(CObject cObject) { } } - private void convertChildren(CAttribute attribute) { - List children = attribute.getChildren(); - for (int i = 0; i < children.size(); i++) { - CObject child = children.get(i); - if (child instanceof CTerminologyCodeADL14) { - CTerminologyCodeADL14 cTerminologyCode = (CTerminologyCodeADL14) child; - convertCTerminologyCode(cTerminologyCode); - CTerminologyCode replacement = toAdl2(cTerminologyCode); - children.set(i, replacement); - replacement.setParent(attribute); - } else { - convert(child); - } - } + private void replaceInParent(CObject original, CObject replacement) { + CAttribute parent = original.getParent(); + int index = parent.getChildren().indexOf(original); + parent.getChildren().set(index, replacement); + replacement.setParent(parent); } private Set getCTerminologyCodeIndices(CAttributeTuple tuple) { From 5aeae67afc154597b4b6806c65c82a86afb1f359 Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Wed, 27 May 2026 11:18:16 +0200 Subject: [PATCH 10/15] Add ADL 2 roundtrip assertions to testDemoArchetype --- .../nedap/archie/adl14/LargeSetOfADL14sTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java b/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java index 85312f6fa..f03fefed8 100644 --- a/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java +++ b/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java @@ -1,5 +1,6 @@ package com.nedap.archie.adl14; +import com.nedap.archie.adlparser.ADLParser; import com.nedap.archie.adlparser.antlr.Adl14Lexer; import com.nedap.archie.antlr.errors.ANTLRParserErrors; import com.nedap.archie.aom.Archetype; @@ -12,6 +13,7 @@ import com.nedap.archie.aom.primitives.CTerminologyCodeADL14; import com.nedap.archie.archetypevalidator.ValidationResult; import com.nedap.archie.flattener.InMemoryFullArchetypeRepository; +import com.nedap.archie.serializer.adl.ADLArchetypeSerializer; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CodePointCharStream; import org.junit.jupiter.api.BeforeEach; @@ -101,6 +103,17 @@ public void testDemoArchetype() throws Exception { .convert(Collections.singletonList(archetype)); ADL2ConversionResult result = converted.getConversionResults().get(0); assertNotNull(result.getArchetype(), () -> "conversion returned null archetype, exception: " + result.getException()); + + // Round-trip the converted ADL 2 archetype: serialize → reparse → serialize-again and assert idempotent. + // The first serialize throws AssertionError if any CTerminologyCodeADL14 slipped through unconverted, + // since ADLDefinitionSerializer has no serializer registered for that type. + String serialized = ADLArchetypeSerializer.serialize(result.getArchetype()); + ADLParser adl2Parser = new ADLParser(BuiltinReferenceModels.getMetaModelProvider()); + Archetype reparsed = adl2Parser.parse(serialized); + assertTrue(adl2Parser.getErrors().hasNoErrors(), + () -> "roundtrip parsing of converted ADL 2 archetype produced errors: " + adl2Parser.getErrors()); + String serializedAgain = ADLArchetypeSerializer.serialize(reparsed); + assertEquals(serialized, serializedAgain, "roundtrip serialization should be idempotent"); } private void collectTerminologyCodes(CObject cObject, List out) { From bd701284ee81fe034af557babb2fdf3670c87a49 Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Wed, 27 May 2026 11:27:30 +0200 Subject: [PATCH 11/15] Copy defaultValue and socParent in toAdl2 --- .../com/nedap/archie/adl14/ADL14TermConstraintConverter.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java index 29e219771..5e5fc5d7e 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java +++ b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java @@ -209,7 +209,12 @@ private CTerminologyCode toAdl2(CTerminologyCodeADL14 source) { result.setSiblingOrder(source.getSiblingOrder()); result.setEnumeratedTypeConstraint(source.getEnumeratedTypeConstraint()); result.setAssumedValue(source.getAssumedValue()); + result.setDefaultValue(source.getDefaultValue()); result.setConstraintStatus(source.getConstraintStatus()); + // Copy the tuple back-pointer too: when this CTerminologyCodeADL14 sits inside a CPrimitiveTuple, + // its socParent links back to that tuple. The caller swaps it in via members.set(index, replacement), + // which (unlike CPrimitiveTuple.addMember) does not set socParent, so we copy it here. + result.setSocParent(source.getSocParent()); if (source.getConstraint() != null && !source.getConstraint().isEmpty()) { result.setConstraint(source.getConstraint().get(0)); } From c2d0957f45e622fb47bf8ca750939c3f29612aa6 Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Wed, 27 May 2026 13:58:27 +0200 Subject: [PATCH 12/15] Add unit tests for CTerminologyCode, CTerminologyCodeADL14, toAdl2 and ADL 1.4 terminology parser --- .../adl14/ADL14TermConstraintConverter.java | 7 +- .../ADL14TermConstraintConverterTest.java | 94 ++++++++++++ .../Adl14PrimitivesConstraintParserTest.java | 90 ++++++++++++ .../primitives/CTerminologyCodeADL14Test.java | 136 ++++++++++++++++++ .../aom/primitives/CTerminologyCodeTest.java | 90 ++++++++++++ 5 files changed, 415 insertions(+), 2 deletions(-) create mode 100644 aom/src/test/java/com/nedap/archie/adl14/ADL14TermConstraintConverterTest.java create mode 100644 aom/src/test/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParserTest.java create mode 100644 aom/src/test/java/com/nedap/archie/aom/primitives/CTerminologyCodeADL14Test.java create mode 100644 aom/src/test/java/com/nedap/archie/aom/primitives/CTerminologyCodeTest.java diff --git a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java index 5e5fc5d7e..34ad13a97 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java +++ b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java @@ -200,10 +200,13 @@ private void convertCTerminologyCode(CTerminologyCodeADL14 cTerminologyCode) { * Build an ADL 2 {@link CTerminologyCode} from a converted {@link CTerminologyCodeADL14}. * Copies all relevant CObject/CPrimitiveObject fields and collapses the single-element * post-conversion constraint list into a single String. + * + *

Package-private for unit testing — see ADL14TermConstraintConverterTest.

*/ - private CTerminologyCode toAdl2(CTerminologyCodeADL14 source) { + static CTerminologyCode toAdl2(CTerminologyCodeADL14 source) { CTerminologyCode result = new CTerminologyCode(); - result.setRmTypeName(source.getRmTypeName()); + // rmTypeName is not copied: CPrimitiveObject overrides getRmTypeName() to compute it from the + // class name, ignoring the field. Setting it would be a no-op. result.setOccurrences(source.getOccurrences()); result.setDeprecated(source.getDeprecated()); result.setSiblingOrder(source.getSiblingOrder()); diff --git a/aom/src/test/java/com/nedap/archie/adl14/ADL14TermConstraintConverterTest.java b/aom/src/test/java/com/nedap/archie/adl14/ADL14TermConstraintConverterTest.java new file mode 100644 index 000000000..8872c8b3a --- /dev/null +++ b/aom/src/test/java/com/nedap/archie/adl14/ADL14TermConstraintConverterTest.java @@ -0,0 +1,94 @@ +package com.nedap.archie.adl14; + +import com.nedap.archie.aom.CAttribute; +import com.nedap.archie.aom.CAttributeTuple; +import com.nedap.archie.aom.CPrimitiveTuple; +import com.nedap.archie.aom.SiblingOrder; +import com.nedap.archie.aom.primitives.CTerminologyCode; +import com.nedap.archie.aom.primitives.CTerminologyCodeADL14; +import com.nedap.archie.aom.primitives.ConstraintStatus; +import com.nedap.archie.base.MultiplicityInterval; +import com.nedap.archie.base.terminology.TerminologyCode; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class ADL14TermConstraintConverterTest { + + @Test + public void toAdl2CopiesAllCObjectAndPrimitiveFields() { + // Note: rmTypeName is not asserted here. CPrimitiveObject overrides getRmTypeName() to compute + // it from the class name ("terminology_code"), ignoring the field. So toAdl2's copy of the + // field is effectively a no-op for primitives — not worth pinning in a test. + CTerminologyCodeADL14 source = new CTerminologyCodeADL14(); + source.setOccurrences(MultiplicityInterval.createMandatory()); + source.setDeprecated(Boolean.TRUE); + SiblingOrder siblingOrder = SiblingOrder.createBefore("id1"); + source.setSiblingOrder(siblingOrder); + source.setEnumeratedTypeConstraint(Boolean.TRUE); + TerminologyCode assumed = TerminologyCode.createFromString("[local::at0001]"); + source.setAssumedValue(assumed); + source.setConstraintStatus(ConstraintStatus.PREFERRED); + source.addConstraint("at0042"); + + CTerminologyCode result = ADL14TermConstraintConverter.toAdl2(source); + + assertEquals(MultiplicityInterval.createMandatory(), result.getOccurrences()); + assertEquals(Boolean.TRUE, result.getDeprecated()); + assertSame(siblingOrder, result.getSiblingOrder()); + assertEquals(Boolean.TRUE, result.getEnumeratedTypeConstraint()); + assertSame(assumed, result.getAssumedValue()); + assertEquals(ConstraintStatus.PREFERRED, result.getConstraintStatus()); + assertEquals("at0042", result.getConstraint()); + } + + @Test + public void toAdl2CopiesSocParentForTupleMembers() { + // Tuple members have socParent set to the CPrimitiveTuple (rather than parent set to a CAttribute). + // The converter swaps members via list.set(...), which does not call setSocParent, so toAdl2 must copy it. + CPrimitiveTuple primitiveTuple = new CPrimitiveTuple(); + CTerminologyCodeADL14 source = new CTerminologyCodeADL14(); + primitiveTuple.addMember(source); + + CTerminologyCode result = ADL14TermConstraintConverter.toAdl2(source); + + assertSame(primitiveTuple, result.getSocParent(), + "socParent must be carried over so tuple-aware validators still recognise this as a tuple member"); + } + + @Test + public void toAdl2CollapsesSingleElementConstraintListToString() { + CTerminologyCodeADL14 source = new CTerminologyCodeADL14(); + source.setConstraint(Arrays.asList("ac0001")); + + CTerminologyCode result = ADL14TermConstraintConverter.toAdl2(source); + + assertEquals("ac0001", result.getConstraint()); + } + + @Test + public void toAdl2LeavesConstraintNullWhenSourceConstraintListIsEmpty() { + CTerminologyCodeADL14 source = new CTerminologyCodeADL14(); + // empty list — converter would have produced this for an empty source; CTerminologyCode keeps it null. + CTerminologyCode result = ADL14TermConstraintConverter.toAdl2(source); + assertNull(result.getConstraint()); + } + + @Test + public void toAdl2DoesNotPropagateParentLink() { + // toAdl2 itself only does a value copy. The structural setParent call lives in the converter's + // replaceInParent helper. Verify the source's parent link is not leaked to the result. + CAttribute attribute = new CAttribute("defining_code"); + CTerminologyCodeADL14 source = new CTerminologyCodeADL14(); + source.setParent(attribute); + + CTerminologyCode result = ADL14TermConstraintConverter.toAdl2(source); + + assertNull(result.getParent(), "toAdl2 should not copy the parent CAttribute link"); + assertSame(attribute, source.getParent(), "source's own parent link must remain intact"); + } +} diff --git a/aom/src/test/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParserTest.java b/aom/src/test/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParserTest.java new file mode 100644 index 000000000..08a372f47 --- /dev/null +++ b/aom/src/test/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParserTest.java @@ -0,0 +1,90 @@ +package com.nedap.archie.adl14.treewalkers; + +import com.nedap.archie.adlparser.antlr.Adl14Lexer; +import com.nedap.archie.adlparser.antlr.Adl14Parser; +import com.nedap.archie.antlr.errors.ANTLRParserErrors; +import com.nedap.archie.aom.primitives.CTerminologyCodeADL14; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for {@link Adl14PrimitivesConstraintParser#parseCTerminologyCode}. + * Each test parses a single terminology code snippet and asserts on the resulting + * {@link CTerminologyCodeADL14} — covering the four branches in the parser: + * single TERM_CODE_REF (local), single TERM_CODE_REF (external), qualified multi-code + * (local), qualified multi-code (external), and AC-code. + */ +public class Adl14PrimitivesConstraintParserTest { + + @Test + public void parsesSingleLocalAtCode() { + CTerminologyCodeADL14 result = parse("[local::at0001]"); + assertEquals(Arrays.asList("at0001"), result.getConstraint()); + assertNull(result.getAssumedValue()); + } + + @Test + public void parsesSingleExternalTermCode() { + CTerminologyCodeADL14 result = parse("[snomed-ct::12345]"); + // For external single-code TERM_CODE_REF the parser keeps the full bracketed form so the + // converter can later create a binding from it. + assertEquals(Arrays.asList("[snomed-ct::12345]"), result.getConstraint()); + } + + @Test + public void parsesMultiCodeLocalConstraint() { + // This is the case that motivated the PR — used to silently parse to an empty list. + CTerminologyCodeADL14 result = parse("[local::at0001, at0002, at0003]"); + assertEquals(Arrays.asList("at0001", "at0002", "at0003"), result.getConstraint()); + } + + @Test + public void parsesMultiCodeExternalConstraint() { + CTerminologyCodeADL14 result = parse("[openehr::271, 272, 273, 253]"); + // The parser stores the terminologyId as the first entry and the bare codes after it, + // which the converter then assembles into full term-code refs during conversion. + assertEquals(Arrays.asList("openehr", "271", "272", "273", "253"), result.getConstraint()); + } + + @Test + public void parsesAcCode() { + CTerminologyCodeADL14 result = parse("[ac0001]"); + assertEquals(Arrays.asList("ac0001"), result.getConstraint()); + assertNull(result.getAssumedValue()); + } + + @Test + public void parsesQualifiedTermCodeWithAssumedValue() { + // Qualified-form term codes carry their assumed value via the assumed_value grammar rule, + // which the parser hands directly to setAssumedValue. + CTerminologyCodeADL14 result = parse("[local::at0001; at0002]"); + assertEquals(Arrays.asList("at0001"), result.getConstraint()); + assertEquals("at0002", result.getAssumedValue().getCodeString()); + } + + @Test + public void parserReportsNoErrorsForValidInputs() { + ANTLRParserErrors errors = new ANTLRParserErrors(); + parse("[local::at0001, at0002]", errors); + assertTrue(errors.getErrors().isEmpty(), + () -> "unexpected parse errors: " + errors); + } + + private CTerminologyCodeADL14 parse(String terminologyCodeText) { + return parse(terminologyCodeText, new ANTLRParserErrors()); + } + + private CTerminologyCodeADL14 parse(String terminologyCodeText, ANTLRParserErrors errors) { + Adl14Lexer lexer = new Adl14Lexer(CharStreams.fromString(terminologyCodeText)); + Adl14Parser parser = new Adl14Parser(new CommonTokenStream(lexer)); + Adl14Parser.C_terminology_codeContext context = parser.c_terminology_code(); + return new Adl14PrimitivesConstraintParser(errors).parseCTerminologyCode(context); + } +} diff --git a/aom/src/test/java/com/nedap/archie/aom/primitives/CTerminologyCodeADL14Test.java b/aom/src/test/java/com/nedap/archie/aom/primitives/CTerminologyCodeADL14Test.java new file mode 100644 index 000000000..06d8d6ecc --- /dev/null +++ b/aom/src/test/java/com/nedap/archie/aom/primitives/CTerminologyCodeADL14Test.java @@ -0,0 +1,136 @@ +package com.nedap.archie.aom.primitives; + +import com.nedap.archie.aom.CAttribute; +import com.nedap.archie.base.terminology.TerminologyCode; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CTerminologyCodeADL14Test { + + @Test + public void newInstanceHasEmptyConstraintList() { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + assertTrue(cTerminologyCode.getConstraint().isEmpty()); + assertTrue(cTerminologyCode.getConstraintAsList().isEmpty()); + } + + @Test + public void addConstraintAccumulates() { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + cTerminologyCode.addConstraint("at0001"); + cTerminologyCode.addConstraint("at0002"); + assertEquals(Arrays.asList("at0001", "at0002"), cTerminologyCode.getConstraint()); + } + + @Test + public void setConstraintReplacesList() { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + cTerminologyCode.addConstraint("at0001"); + cTerminologyCode.setConstraint(Arrays.asList("at0042", "at0043")); + assertEquals(Arrays.asList("at0042", "at0043"), cTerminologyCode.getConstraint()); + } + + @Test + public void assumedValueGetterSetterRoundtrip() { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + assertNull(cTerminologyCode.getAssumedValue()); + TerminologyCode assumed = TerminologyCode.createFromString("[local::at0001]"); + cTerminologyCode.setAssumedValue(assumed); + assertSame(assumed, cTerminologyCode.getAssumedValue()); + } + + @Test + public void constraintStatusDefaultsToRequired() { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + assertNull(cTerminologyCode.getConstraintStatus(), "raw status should be null until set"); + assertEquals(ConstraintStatus.REQUIRED, cTerminologyCode.getEffectiveConstraintStatus()); + assertTrue(cTerminologyCode.isConstraintRequired()); + } + + @Test + public void constraintStatusGetterSetterRoundtrip() { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + cTerminologyCode.setConstraintStatus(ConstraintStatus.EXTENSIBLE); + assertEquals(ConstraintStatus.EXTENSIBLE, cTerminologyCode.getEffectiveConstraintStatus()); + assertFalse(cTerminologyCode.isConstraintRequired()); + } + + @Test + public void toStringRendersAllConstraints() { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + cTerminologyCode.addConstraint("at0001"); + cTerminologyCode.addConstraint("at0002"); + assertEquals("{[at0001, at0002]}", cTerminologyCode.toString()); + } + + @Test + public void toStringForEmptyConstraint() { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + assertEquals("{[]}", cTerminologyCode.toString()); + } + + @Test + public void getRmTypeNameDefaultWhenNoParent() { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + assertEquals("terminology_code", cTerminologyCode.getRmTypeName()); + } + + @Test + public void getRmTypeNameForDefiningCodeAttribute() { + CTerminologyCodeADL14 cTerminologyCode = withParentAttribute("defining_code"); + assertEquals("CODE_PHRASE", cTerminologyCode.getRmTypeName()); + } + + @Test + public void getRmTypeNameForSymbolAttribute() { + CTerminologyCodeADL14 cTerminologyCode = withParentAttribute("symbol"); + assertEquals("DV_CODED_TEXT", cTerminologyCode.getRmTypeName()); + } + + @Test + public void getRmTypeNameForOtherAttributeFallsBackToDefault() { + CTerminologyCodeADL14 cTerminologyCode = withParentAttribute("something_else"); + assertEquals("terminology_code", cTerminologyCode.getRmTypeName()); + } + + @Test + public void getValueSetExpandedCollectsAtCodes() { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + cTerminologyCode.addConstraint("at0001"); + cTerminologyCode.addConstraint("at0002"); + // With no archetype attached, ac-codes are not expandable, but at-codes pass through. + List expanded = cTerminologyCode.getValueSetExpanded(); + assertEquals(Arrays.asList("at0001", "at0002"), expanded); + } + + @Test + public void getValueSetExpandedSkipsAcCodesWithoutArchetype() { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + cTerminologyCode.addConstraint("ac0001"); + // ac-codes need a terminology to resolve, and without one they're silently skipped. + assertTrue(cTerminologyCode.getValueSetExpanded().isEmpty()); + } + + @Test + public void getTermsReturnsEmptyWhenNoArchetype() { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + cTerminologyCode.addConstraint("at0001"); + // No archetype attached -> early return with empty list (guards against NPE in rule contexts). + assertTrue(cTerminologyCode.getTerms().isEmpty()); + } + + private CTerminologyCodeADL14 withParentAttribute(String rmAttributeName) { + CTerminologyCodeADL14 cTerminologyCode = new CTerminologyCodeADL14(); + CAttribute parent = new CAttribute(rmAttributeName); + cTerminologyCode.setParent(parent); + return cTerminologyCode; + } +} diff --git a/aom/src/test/java/com/nedap/archie/aom/primitives/CTerminologyCodeTest.java b/aom/src/test/java/com/nedap/archie/aom/primitives/CTerminologyCodeTest.java new file mode 100644 index 000000000..285a1ad90 --- /dev/null +++ b/aom/src/test/java/com/nedap/archie/aom/primitives/CTerminologyCodeTest.java @@ -0,0 +1,90 @@ +package com.nedap.archie.aom.primitives; + +import com.nedap.archie.base.terminology.TerminologyCode; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CTerminologyCodeTest { + + @Test + public void constraintGetterSetterRoundtrip() { + CTerminologyCode cTerminologyCode = new CTerminologyCode(); + assertNull(cTerminologyCode.getConstraint()); + cTerminologyCode.setConstraint("at0001"); + assertEquals("at0001", cTerminologyCode.getConstraint()); + } + + @Test + public void getConstraintAsListReturnsEmptyWhenConstraintIsNull() { + CTerminologyCode cTerminologyCode = new CTerminologyCode(); + assertEquals(Collections.emptyList(), cTerminologyCode.getConstraintAsList()); + } + + @Test + public void getConstraintAsListReturnsSingletonWhenConstraintIsSet() { + CTerminologyCode cTerminologyCode = new CTerminologyCode(); + cTerminologyCode.setConstraint("at0001"); + List asList = cTerminologyCode.getConstraintAsList(); + assertEquals(1, asList.size()); + assertEquals("at0001", asList.get(0)); + } + + @Test + public void assumedValueGetterSetterRoundtrip() { + CTerminologyCode cTerminologyCode = new CTerminologyCode(); + assertNull(cTerminologyCode.getAssumedValue()); + TerminologyCode assumed = TerminologyCode.createFromString("[local::at0001]"); + cTerminologyCode.setAssumedValue(assumed); + assertSame(assumed, cTerminologyCode.getAssumedValue()); + } + + @Test + public void constraintStatusDefaultsToRequired() { + CTerminologyCode cTerminologyCode = new CTerminologyCode(); + assertNull(cTerminologyCode.getConstraintStatus(), "raw status should be null until set"); + assertEquals(ConstraintStatus.REQUIRED, cTerminologyCode.getEffectiveConstraintStatus(), + "effective status should fall back to REQUIRED"); + assertTrue(cTerminologyCode.isConstraintRequired()); + } + + @Test + public void constraintStatusGetterSetterRoundtrip() { + CTerminologyCode cTerminologyCode = new CTerminologyCode(); + cTerminologyCode.setConstraintStatus(ConstraintStatus.PREFERRED); + assertEquals(ConstraintStatus.PREFERRED, cTerminologyCode.getConstraintStatus()); + assertEquals(ConstraintStatus.PREFERRED, cTerminologyCode.getEffectiveConstraintStatus()); + assertFalse(cTerminologyCode.isConstraintRequired()); + } + + @Test + public void isValidValueReturnsTrueWhenConstraintIsNull() { + CTerminologyCode cTerminologyCode = new CTerminologyCode(); + // Unconstrained: any value is valid, including null. + assertTrue(cTerminologyCode.isValidValue(null)); + assertTrue(cTerminologyCode.isValidValue(TerminologyCode.createFromString("[local::at0001]"))); + } + + @Test + public void isValidValueReturnsFalseForNullValueWhenRequired() { + CTerminologyCode cTerminologyCode = new CTerminologyCode(); + cTerminologyCode.setConstraint("at0001"); + assertFalse(cTerminologyCode.isValidValue(null)); + } + + @Test + public void isValidValueAcceptsAnythingWhenConstraintNotRequired() { + CTerminologyCode cTerminologyCode = new CTerminologyCode(); + cTerminologyCode.setConstraint("at0001"); + cTerminologyCode.setConstraintStatus(ConstraintStatus.PREFERRED); + // PREFERRED is non-required, so any non-null value is accepted without value-set lookup. + assertTrue(cTerminologyCode.isValidValue(TerminologyCode.createFromString("[local::at9999]"))); + } +} From f1a84b4b3f3ac96d8309f405d99c82ea29a688c4 Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Thu, 28 May 2026 11:40:07 +0200 Subject: [PATCH 13/15] Small changes --- .../treewalkers/Adl14PrimitivesConstraintParserTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aom/src/test/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParserTest.java b/aom/src/test/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParserTest.java index 08a372f47..2a2da15e5 100644 --- a/aom/src/test/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParserTest.java +++ b/aom/src/test/java/com/nedap/archie/adl14/treewalkers/Adl14PrimitivesConstraintParserTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import java.util.Arrays; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -40,7 +41,6 @@ public void parsesSingleExternalTermCode() { @Test public void parsesMultiCodeLocalConstraint() { - // This is the case that motivated the PR — used to silently parse to an empty list. CTerminologyCodeADL14 result = parse("[local::at0001, at0002, at0003]"); assertEquals(Arrays.asList("at0001", "at0002", "at0003"), result.getConstraint()); } @@ -65,7 +65,7 @@ public void parsesQualifiedTermCodeWithAssumedValue() { // Qualified-form term codes carry their assumed value via the assumed_value grammar rule, // which the parser hands directly to setAssumedValue. CTerminologyCodeADL14 result = parse("[local::at0001; at0002]"); - assertEquals(Arrays.asList("at0001"), result.getConstraint()); + assertEquals(List.of("at0001"), result.getConstraint()); assertEquals("at0002", result.getAssumedValue().getCodeString()); } From b8d51221a6dc4523df5eb41092ef9dd99b8d46bb Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Tue, 9 Jun 2026 13:30:10 +0200 Subject: [PATCH 14/15] Add CTerminologyCodeADL14Serializer --- .../adl/ADLDefinitionSerializer.java | 1 + .../CTerminologyCodeADL14Serializer.java | 109 ++++++++++++++++++ .../CTerminologyCodeADL14SerializerTest.java | 82 +++++++++++++ .../archie/adl14/LargeSetOfADL14sTest.java | 36 ++++-- 4 files changed, 219 insertions(+), 9 deletions(-) create mode 100644 aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeADL14Serializer.java create mode 100644 aom/src/test/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeADL14SerializerTest.java diff --git a/aom/src/main/java/com/nedap/archie/serializer/adl/ADLDefinitionSerializer.java b/aom/src/main/java/com/nedap/archie/serializer/adl/ADLDefinitionSerializer.java index b1dd190ed..0c30f81ca 100644 --- a/aom/src/main/java/com/nedap/archie/serializer/adl/ADLDefinitionSerializer.java +++ b/aom/src/main/java/com/nedap/archie/serializer/adl/ADLDefinitionSerializer.java @@ -43,6 +43,7 @@ public ADLDefinitionSerializer(ADLStringBuilder builder, Function} (one or more inline codes) rather than the single + * {@code String} of the ADL 2 {@link com.nedap.archie.aom.primitives.CTerminologyCode}. + *

+ * This mirrors {@link CTerminologyCodeSerializer} but reconstructs the ADL 1.4 syntax from the list + * the parser produced. The parser stores codes in one of these shapes: + *

    + *
  • a single local code, e.g. {@code ["at0001"]} or {@code ["ac0001"]} → {@code [at0001]} / {@code [ac0001]};
  • + *
  • a single external term code already in full form, e.g. {@code ["[snomed-ct::12345]"]} → appended as-is;
  • + *
  • multiple local codes, e.g. {@code ["at0001", "at0002"]} → {@code [local::at0001, at0002]};
  • + *
  • an external terminology with multiple codes, with the terminology id first, e.g. + * {@code ["openehr", "271", "272"]} → {@code [openehr::271, 272]}.
  • + *
+ */ +public class CTerminologyCodeADL14Serializer extends ConstraintSerializer { + + public CTerminologyCodeADL14Serializer(ADLDefinitionSerializer serializer) { + super(serializer); + } + + @Override + public void serialize(CTerminologyCodeADL14 cobj) { + List constraints = cobj.getConstraint(); + if (constraints == null || constraints.isEmpty()) { + return; + } + appendConstraintStatus(cobj); + + String assumedValue = cobj.getAssumedValue() != null ? cobj.getAssumedValue().getCodeString() : null; + + if (constraints.size() == 1 && constraints.get(0).startsWith("[")) { + // already a full external term code ref like [snomed-ct::12345] + appendFullTermCodeRef(constraints.get(0), assumedValue); + return; + } + + builder.append("["); + if (constraints.size() == 1) { + // single local at- or ac-code + builder.append(constraints.get(0)); + } else if (AOMUtils.isValueCode(constraints.get(0)) || AOMUtils.isValueSetCode(constraints.get(0))) { + // multiple local codes are an implicit local value set + builder.append("local::").append(String.join(", ", constraints)); + } else { + // external terminology: first entry is the terminology id, the rest are codes + builder.append(constraints.get(0)).append("::") + .append(String.join(", ", constraints.subList(1, constraints.size()))); + } + if (assumedValue != null) { + builder.append("; ").append(assumedValue); + } + builder.append("]"); + } + + private void appendFullTermCodeRef(String fullRef, String assumedValue) { + if (assumedValue != null && fullRef.endsWith("]")) { + builder.append(fullRef.substring(0, fullRef.length() - 1)).append("; ").append(assumedValue).append("]"); + } else { + builder.append(fullRef); + } + } + + private void appendConstraintStatus(CTerminologyCodeADL14 cobj) { + if (cobj.getConstraintStatus() == null) { + return; + } + String constraintStatusString; + switch (cobj.getConstraintStatus()) { + case REQUIRED: + constraintStatusString = "required"; + break; + case EXTENSIBLE: + constraintStatusString = "extensible"; + break; + case PREFERRED: + constraintStatusString = "preferred"; + break; + case EXAMPLE: + constraintStatusString = "example"; + break; + default: + throw new RuntimeException("constraint status " + cobj.getConstraintStatus() + " unknown, cannot be serialized"); + } + builder.append(constraintStatusString); + builder.append(" "); + } + + @Override + public String getSimpleCommentText(CTerminologyCodeADL14 cobj) { + List constraints = cobj.getConstraint(); + if (constraints != null && constraints.size() == 1) { + String constraint = constraints.get(0); + if (AOMUtils.isValueSetCode(constraint) || AOMUtils.isValueCode(constraint)) { + return serializer.getTermText(cobj, constraint); + } + } + return null; + } +} diff --git a/aom/src/test/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeADL14SerializerTest.java b/aom/src/test/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeADL14SerializerTest.java new file mode 100644 index 000000000..b3577c4e0 --- /dev/null +++ b/aom/src/test/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeADL14SerializerTest.java @@ -0,0 +1,82 @@ +package com.nedap.archie.serializer.adl.constraints; + +import com.nedap.archie.aom.primitives.CTerminologyCodeADL14; +import com.nedap.archie.aom.primitives.ConstraintStatus; +import com.nedap.archie.base.terminology.TerminologyCode; +import com.nedap.archie.serializer.adl.ADLDefinitionSerializer; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for {@link CTerminologyCodeADL14Serializer}, covering each reconstruction branch + * directly via the static {@link ADLDefinitionSerializer#serialize} entry point. + */ +public class CTerminologyCodeADL14SerializerTest { + + @Test + public void serializesSingleLocalAtCode() { + assertEquals("[at0001]", serialize("at0001")); + } + + @Test + public void serializesSingleAcCode() { + assertEquals("[ac0001]", serialize("ac0001")); + } + + @Test + public void serializesSingleExternalTermCodeAsIs() { + // Stored already in full bracketed form by the parser. + assertEquals("[snomed-ct::12345]", serialize("[snomed-ct::12345]")); + } + + @Test + public void serializesMultipleLocalCodesAsLocalValueSet() { + assertEquals("[local::at0007, at0008, at0009, at0010]", + serialize("at0007", "at0008", "at0009", "at0010")); + } + + @Test + public void serializesExternalMultiCodeWithTerminologyIdFirst() { + assertEquals("[openehr::271, 272, 273, 253]", + serialize("openehr", "271", "272", "273", "253")); + } + + @Test + public void serializesConstraintStatusPrefix() { + CTerminologyCodeADL14 cobj = new CTerminologyCodeADL14(); + cobj.addConstraint("at0001"); + cobj.setConstraintStatus(ConstraintStatus.PREFERRED); + assertEquals("preferred [at0001]", ADLDefinitionSerializer.serialize(cobj)); + } + + @Test + public void serializesAssumedValueForLocalCode() { + CTerminologyCodeADL14 cobj = new CTerminologyCodeADL14(); + cobj.addConstraint("ac0001"); + cobj.setAssumedValue(TerminologyCode.createFromString("at0002")); + assertEquals("[ac0001; at0002]", ADLDefinitionSerializer.serialize(cobj)); + } + + @Test + public void serializesAssumedValueInsideExternalTermCodeRef() { + CTerminologyCodeADL14 cobj = new CTerminologyCodeADL14(); + cobj.addConstraint("[snomed-ct::12345]"); + cobj.setAssumedValue(TerminologyCode.createFromString("12345")); + assertEquals("[snomed-ct::12345; 12345]", ADLDefinitionSerializer.serialize(cobj)); + } + + @Test + public void serializesEmptyConstraintAsEmptyString() { + CTerminologyCodeADL14 cobj = new CTerminologyCodeADL14(); + assertEquals("", ADLDefinitionSerializer.serialize(cobj)); + } + + private String serialize(String... constraints) { + CTerminologyCodeADL14 cobj = new CTerminologyCodeADL14(); + cobj.setConstraint(Arrays.asList(constraints)); + return ADLDefinitionSerializer.serialize(cobj); + } +} diff --git a/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java b/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java index f03fefed8..ff3fbb5fe 100644 --- a/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java +++ b/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java @@ -63,11 +63,19 @@ public void parseUrn() { } /** - * Parses a demo ADL 1.4 archetype that exercises (almost) every constraint type and asserts that every - * {@link CTerminologyCodeADL14} in the resulting tree has a non-empty constraint list. This guards against - * the regression that motivated this PR: multi-code constraints like {@code [local::at0001, at0002]} and - * {@code [openehr::271, 272, 273, 253]} silently parsing to an empty constraint, which existing bulk-parse - * tests didn't catch because they only counted parse exceptions. + * End-to-end check on a demo ADL 1.4 archetype that exercises (almost) every constraint type: + *
    + *
  1. Parses it and asserts there are no parse errors.
  2. + *
  3. Asserts every {@link CTerminologyCodeADL14} in the tree has a non-empty constraint list, and that + * at least two are multi-code. This guards against a regression where multi-code constraints like + * {@code [local::at0001, at0002]} and {@code [openehr::271, 272, 273, 253]} silently parse to an empty + * constraint — a bug that bulk-parse tests miss because they only count parse exceptions.
  4. + *
  5. Serializes the parsed (pre-conversion) archetype back to ADL 1.4, exercising + * {@link com.nedap.archie.serializer.adl.constraints.CTerminologyCodeADL14Serializer} and asserting + * the multi-code forms reappear in the output.
  6. + *
  7. Converts it to ADL 2 and asserts that serialize → reparse → serialize-again produces identical + * text both times (a stable round-trip).
  8. + *
*/ @Test public void testDemoArchetype() throws Exception { @@ -97,6 +105,15 @@ public void testDemoArchetype() throws Exception { assertTrue(multiCodeCount >= 2, "expected at least two multi-code terminology constraints in the demo, found " + multiCodeCount); + // Serializing the parsed (pre-conversion) ADL 1.4 archetype must work too: this exercises + // CTerminologyCodeADL14Serializer. Before that serializer existed, ADLDefinitionSerializer threw + // an AssertionError on CTerminologyCodeADL14. Assert the multi-code forms round-trip into the text. + String serializedAdl14 = ADLArchetypeSerializer.serialize(archetype); + assertTrue(serializedAdl14.contains("[local::at0007, at0008, at0009, at0010]"), + () -> "expected local multi-code constraint in serialized ADL 1.4, got:\n" + serializedAdl14); + assertTrue(serializedAdl14.contains("[openehr::271, 272, 273, 253]"), + () -> "expected external multi-code constraint in serialized ADL 1.4, got:\n" + serializedAdl14); + // Sanity check: end-to-end conversion of the demo also succeeds. ADL2ConversionResultList converted = new ADL14Converter( BuiltinReferenceModels.getMetaModelProvider(), conversionConfiguration) @@ -104,16 +121,17 @@ public void testDemoArchetype() throws Exception { ADL2ConversionResult result = converted.getConversionResults().get(0); assertNotNull(result.getArchetype(), () -> "conversion returned null archetype, exception: " + result.getException()); - // Round-trip the converted ADL 2 archetype: serialize → reparse → serialize-again and assert idempotent. - // The first serialize throws AssertionError if any CTerminologyCodeADL14 slipped through unconverted, - // since ADLDefinitionSerializer has no serializer registered for that type. + // Round-trip the converted ADL 2 archetype: serialize → reparse → serialize-again and assert the two + // serialized strings are identical (a stable round-trip). + // If any CTerminologyCodeADL14 slipped through unconverted it would serialize as ADL 1.4 syntax, + // which the ADL 2 parser would then reject on reparse below. String serialized = ADLArchetypeSerializer.serialize(result.getArchetype()); ADLParser adl2Parser = new ADLParser(BuiltinReferenceModels.getMetaModelProvider()); Archetype reparsed = adl2Parser.parse(serialized); assertTrue(adl2Parser.getErrors().hasNoErrors(), () -> "roundtrip parsing of converted ADL 2 archetype produced errors: " + adl2Parser.getErrors()); String serializedAgain = ADLArchetypeSerializer.serialize(reparsed); - assertEquals(serialized, serializedAgain, "roundtrip serialization should be idempotent"); + assertEquals(serialized, serializedAgain, "serializing twice should produce identical text"); } private void collectTerminologyCodes(CObject cObject, List out) { From c08a2953b34ef939dd8773eea66b0be475537f03 Mon Sep 17 00:00:00 2001 From: Mattijs Kuhlmann Date: Thu, 11 Jun 2026 10:34:53 +0200 Subject: [PATCH 15/15] Revert the CTerminologyCodeADL14Serializer changes --- .../adl/ADLDefinitionSerializer.java | 1 - .../CTerminologyCodeADL14Serializer.java | 109 ------------------ .../CTerminologyCodeADL14SerializerTest.java | 82 ------------- .../archie/adl14/LargeSetOfADL14sTest.java | 16 +-- 4 files changed, 2 insertions(+), 206 deletions(-) delete mode 100644 aom/src/main/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeADL14Serializer.java delete mode 100644 aom/src/test/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeADL14SerializerTest.java diff --git a/aom/src/main/java/com/nedap/archie/serializer/adl/ADLDefinitionSerializer.java b/aom/src/main/java/com/nedap/archie/serializer/adl/ADLDefinitionSerializer.java index 0c30f81ca..b1dd190ed 100644 --- a/aom/src/main/java/com/nedap/archie/serializer/adl/ADLDefinitionSerializer.java +++ b/aom/src/main/java/com/nedap/archie/serializer/adl/ADLDefinitionSerializer.java @@ -43,7 +43,6 @@ public ADLDefinitionSerializer(ADLStringBuilder builder, Function} (one or more inline codes) rather than the single - * {@code String} of the ADL 2 {@link com.nedap.archie.aom.primitives.CTerminologyCode}. - *

- * This mirrors {@link CTerminologyCodeSerializer} but reconstructs the ADL 1.4 syntax from the list - * the parser produced. The parser stores codes in one of these shapes: - *

    - *
  • a single local code, e.g. {@code ["at0001"]} or {@code ["ac0001"]} → {@code [at0001]} / {@code [ac0001]};
  • - *
  • a single external term code already in full form, e.g. {@code ["[snomed-ct::12345]"]} → appended as-is;
  • - *
  • multiple local codes, e.g. {@code ["at0001", "at0002"]} → {@code [local::at0001, at0002]};
  • - *
  • an external terminology with multiple codes, with the terminology id first, e.g. - * {@code ["openehr", "271", "272"]} → {@code [openehr::271, 272]}.
  • - *
- */ -public class CTerminologyCodeADL14Serializer extends ConstraintSerializer { - - public CTerminologyCodeADL14Serializer(ADLDefinitionSerializer serializer) { - super(serializer); - } - - @Override - public void serialize(CTerminologyCodeADL14 cobj) { - List constraints = cobj.getConstraint(); - if (constraints == null || constraints.isEmpty()) { - return; - } - appendConstraintStatus(cobj); - - String assumedValue = cobj.getAssumedValue() != null ? cobj.getAssumedValue().getCodeString() : null; - - if (constraints.size() == 1 && constraints.get(0).startsWith("[")) { - // already a full external term code ref like [snomed-ct::12345] - appendFullTermCodeRef(constraints.get(0), assumedValue); - return; - } - - builder.append("["); - if (constraints.size() == 1) { - // single local at- or ac-code - builder.append(constraints.get(0)); - } else if (AOMUtils.isValueCode(constraints.get(0)) || AOMUtils.isValueSetCode(constraints.get(0))) { - // multiple local codes are an implicit local value set - builder.append("local::").append(String.join(", ", constraints)); - } else { - // external terminology: first entry is the terminology id, the rest are codes - builder.append(constraints.get(0)).append("::") - .append(String.join(", ", constraints.subList(1, constraints.size()))); - } - if (assumedValue != null) { - builder.append("; ").append(assumedValue); - } - builder.append("]"); - } - - private void appendFullTermCodeRef(String fullRef, String assumedValue) { - if (assumedValue != null && fullRef.endsWith("]")) { - builder.append(fullRef.substring(0, fullRef.length() - 1)).append("; ").append(assumedValue).append("]"); - } else { - builder.append(fullRef); - } - } - - private void appendConstraintStatus(CTerminologyCodeADL14 cobj) { - if (cobj.getConstraintStatus() == null) { - return; - } - String constraintStatusString; - switch (cobj.getConstraintStatus()) { - case REQUIRED: - constraintStatusString = "required"; - break; - case EXTENSIBLE: - constraintStatusString = "extensible"; - break; - case PREFERRED: - constraintStatusString = "preferred"; - break; - case EXAMPLE: - constraintStatusString = "example"; - break; - default: - throw new RuntimeException("constraint status " + cobj.getConstraintStatus() + " unknown, cannot be serialized"); - } - builder.append(constraintStatusString); - builder.append(" "); - } - - @Override - public String getSimpleCommentText(CTerminologyCodeADL14 cobj) { - List constraints = cobj.getConstraint(); - if (constraints != null && constraints.size() == 1) { - String constraint = constraints.get(0); - if (AOMUtils.isValueSetCode(constraint) || AOMUtils.isValueCode(constraint)) { - return serializer.getTermText(cobj, constraint); - } - } - return null; - } -} diff --git a/aom/src/test/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeADL14SerializerTest.java b/aom/src/test/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeADL14SerializerTest.java deleted file mode 100644 index b3577c4e0..000000000 --- a/aom/src/test/java/com/nedap/archie/serializer/adl/constraints/CTerminologyCodeADL14SerializerTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.nedap.archie.serializer.adl.constraints; - -import com.nedap.archie.aom.primitives.CTerminologyCodeADL14; -import com.nedap.archie.aom.primitives.ConstraintStatus; -import com.nedap.archie.base.terminology.TerminologyCode; -import com.nedap.archie.serializer.adl.ADLDefinitionSerializer; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * Unit tests for {@link CTerminologyCodeADL14Serializer}, covering each reconstruction branch - * directly via the static {@link ADLDefinitionSerializer#serialize} entry point. - */ -public class CTerminologyCodeADL14SerializerTest { - - @Test - public void serializesSingleLocalAtCode() { - assertEquals("[at0001]", serialize("at0001")); - } - - @Test - public void serializesSingleAcCode() { - assertEquals("[ac0001]", serialize("ac0001")); - } - - @Test - public void serializesSingleExternalTermCodeAsIs() { - // Stored already in full bracketed form by the parser. - assertEquals("[snomed-ct::12345]", serialize("[snomed-ct::12345]")); - } - - @Test - public void serializesMultipleLocalCodesAsLocalValueSet() { - assertEquals("[local::at0007, at0008, at0009, at0010]", - serialize("at0007", "at0008", "at0009", "at0010")); - } - - @Test - public void serializesExternalMultiCodeWithTerminologyIdFirst() { - assertEquals("[openehr::271, 272, 273, 253]", - serialize("openehr", "271", "272", "273", "253")); - } - - @Test - public void serializesConstraintStatusPrefix() { - CTerminologyCodeADL14 cobj = new CTerminologyCodeADL14(); - cobj.addConstraint("at0001"); - cobj.setConstraintStatus(ConstraintStatus.PREFERRED); - assertEquals("preferred [at0001]", ADLDefinitionSerializer.serialize(cobj)); - } - - @Test - public void serializesAssumedValueForLocalCode() { - CTerminologyCodeADL14 cobj = new CTerminologyCodeADL14(); - cobj.addConstraint("ac0001"); - cobj.setAssumedValue(TerminologyCode.createFromString("at0002")); - assertEquals("[ac0001; at0002]", ADLDefinitionSerializer.serialize(cobj)); - } - - @Test - public void serializesAssumedValueInsideExternalTermCodeRef() { - CTerminologyCodeADL14 cobj = new CTerminologyCodeADL14(); - cobj.addConstraint("[snomed-ct::12345]"); - cobj.setAssumedValue(TerminologyCode.createFromString("12345")); - assertEquals("[snomed-ct::12345; 12345]", ADLDefinitionSerializer.serialize(cobj)); - } - - @Test - public void serializesEmptyConstraintAsEmptyString() { - CTerminologyCodeADL14 cobj = new CTerminologyCodeADL14(); - assertEquals("", ADLDefinitionSerializer.serialize(cobj)); - } - - private String serialize(String... constraints) { - CTerminologyCodeADL14 cobj = new CTerminologyCodeADL14(); - cobj.setConstraint(Arrays.asList(constraints)); - return ADLDefinitionSerializer.serialize(cobj); - } -} diff --git a/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java b/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java index ff3fbb5fe..a5cc2b485 100644 --- a/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java +++ b/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java @@ -70,9 +70,6 @@ public void parseUrn() { * at least two are multi-code. This guards against a regression where multi-code constraints like * {@code [local::at0001, at0002]} and {@code [openehr::271, 272, 273, 253]} silently parse to an empty * constraint — a bug that bulk-parse tests miss because they only count parse exceptions. - *
  • Serializes the parsed (pre-conversion) archetype back to ADL 1.4, exercising - * {@link com.nedap.archie.serializer.adl.constraints.CTerminologyCodeADL14Serializer} and asserting - * the multi-code forms reappear in the output.
  • *
  • Converts it to ADL 2 and asserts that serialize → reparse → serialize-again produces identical * text both times (a stable round-trip).
  • * @@ -105,15 +102,6 @@ public void testDemoArchetype() throws Exception { assertTrue(multiCodeCount >= 2, "expected at least two multi-code terminology constraints in the demo, found " + multiCodeCount); - // Serializing the parsed (pre-conversion) ADL 1.4 archetype must work too: this exercises - // CTerminologyCodeADL14Serializer. Before that serializer existed, ADLDefinitionSerializer threw - // an AssertionError on CTerminologyCodeADL14. Assert the multi-code forms round-trip into the text. - String serializedAdl14 = ADLArchetypeSerializer.serialize(archetype); - assertTrue(serializedAdl14.contains("[local::at0007, at0008, at0009, at0010]"), - () -> "expected local multi-code constraint in serialized ADL 1.4, got:\n" + serializedAdl14); - assertTrue(serializedAdl14.contains("[openehr::271, 272, 273, 253]"), - () -> "expected external multi-code constraint in serialized ADL 1.4, got:\n" + serializedAdl14); - // Sanity check: end-to-end conversion of the demo also succeeds. ADL2ConversionResultList converted = new ADL14Converter( BuiltinReferenceModels.getMetaModelProvider(), conversionConfiguration) @@ -123,8 +111,8 @@ public void testDemoArchetype() throws Exception { // Round-trip the converted ADL 2 archetype: serialize → reparse → serialize-again and assert the two // serialized strings are identical (a stable round-trip). - // If any CTerminologyCodeADL14 slipped through unconverted it would serialize as ADL 1.4 syntax, - // which the ADL 2 parser would then reject on reparse below. + // The first serialize throws AssertionError if any CTerminologyCodeADL14 slipped through unconverted, + // since ADLDefinitionSerializer has no serializer registered for that ADL 1.4-only type. String serialized = ADLArchetypeSerializer.serialize(result.getArchetype()); ADLParser adl2Parser = new ADLParser(BuiltinReferenceModels.getMetaModelProvider()); Archetype reparsed = adl2Parser.parse(serialized);