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 4eeca9232..89c2df8a7 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/ADL14NodeIDConverter.java +++ b/aom/src/main/java/com/nedap/archie/adl14/ADL14NodeIDConverter.java @@ -399,8 +399,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 c76bffff4..34ad13a97 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java +++ b/aom/src/main/java/com/nedap/archie/adl14/ADL14TermConstraintConverter.java @@ -1,10 +1,10 @@ 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.*; 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; @@ -39,27 +39,31 @@ public void convert() { } private void convert(CObject cObject) { - - if (cObject instanceof CTerminologyCode) { - convertCTerminologyCode((CTerminologyCode) cObject); + if (cObject instanceof CTerminologyCodeADL14) { + CTerminologyCodeADL14 cTerminologyCode = (CTerminologyCodeADL14) cObject; + convertCTerminologyCode(cTerminologyCode); + replaceInParent(cTerminologyCode, toAdl2(cTerminologyCode)); } - for(CAttribute attribute:cObject.getAttributes()) { - convert(attribute); + for (CAttribute attribute : cObject.getAttributes()) { + for (CObject child : attribute.getChildren()) { + convert(child); + } } 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().size() == 1) { - String constraint = cTerminologyCode.getConstraint().get(0); - 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()); } } } @@ -71,12 +75,19 @@ private void convert(CObject cObject) { } } + 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) { 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++; @@ -85,13 +96,7 @@ private Set getCTerminologyCodeIndices(CAttributeTuple tuple) { return result; } - private void convert(CAttribute attribute) { - for(CObject object:attribute.getChildren()) { - convert(object); - } - } - - private void convertCTerminologyCode(CTerminologyCode cTerminologyCode) { + private void convertCTerminologyCode(CTerminologyCodeADL14 cTerminologyCode) { if(cTerminologyCode.getConstraint() != null && !cTerminologyCode.getConstraint().isEmpty()) { String firstConstraint = cTerminologyCode.getConstraint().get(0); TerminologyCode termCode = TerminologyCode.createFromString(firstConstraint); @@ -102,7 +107,7 @@ private void convertCTerminologyCode(CTerminologyCode cTerminologyCode) { //do not create a value set, just convert the code String newCode = converter.convertValueCode(firstConstraint); converter.addConvertedCode(firstConstraint, newCode); - cTerminologyCode.setConstraint(Lists.newArrayList(newCode)); + cTerminologyCode.setConstraint(new ArrayList<>(Collections.singletonList(newCode))); } else { Set localCodes = new LinkedHashSet<>(); for(String code:cTerminologyCode.getConstraint()) { @@ -112,7 +117,7 @@ private void convertCTerminologyCode(CTerminologyCode cTerminologyCode) { } ValueSet valueSet = findOrCreateValueSet(cTerminologyCode.getArchetype(), localCodes, cTerminologyCode); - cTerminologyCode.setConstraint(Lists.newArrayList(valueSet.getId())); + cTerminologyCode.setConstraint(new ArrayList<>(Collections.singletonList(valueSet.getId()))); } } else if (isLocalCode && AOMUtils.isValueSetCode(termCode.getCodeString())) { List newConstraint = new ArrayList<>(); @@ -133,7 +138,7 @@ private void convertCTerminologyCode(CTerminologyCode cTerminologyCode) { //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)); + cTerminologyCode.setConstraint(new ArrayList<>(Collections.singletonList(termBinding))); } catch (URISyntaxException e) { //TODO logger.error("error converting term", e); @@ -165,7 +170,7 @@ private void convertCTerminologyCode(CTerminologyCode cTerminologyCode) { } } ValueSet valueSet = findOrCreateValueSet(cTerminologyCode.getArchetype(), new LinkedHashSet<>(atCodes), cTerminologyCode); - cTerminologyCode.setConstraint(Lists.newArrayList(valueSet.getId())); + cTerminologyCode.setConstraint(new ArrayList<>(Collections.singletonList(valueSet.getId()))); } } @@ -191,6 +196,34 @@ private void convertCTerminologyCode(CTerminologyCode 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.

+ */ + static CTerminologyCode toAdl2(CTerminologyCodeADL14 source) { + CTerminologyCode result = new CTerminologyCode(); + // 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()); + 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)); + } + return result; + } + private Map findOrCreateTermBindings(TerminologyCode termCode) { return archetype.getTerminology().getTermBindings().computeIfAbsent(termCode.getTerminologyId(), k -> new LinkedHashMap<>()); } @@ -248,13 +281,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(cObject instanceof CTerminologyCodeADL14) { + CTerminologyCodeADL14 termCodeInParent = (CTerminologyCodeADL14) cObject; if(termCodeInParent.getConstraint() != null && !termCodeInParent.getConstraint().isEmpty()) { - if(termCodeInParent.getConstraint().get(0).startsWith("ac")) { - idInparent = termCodeInParent.getConstraint().get(0); + 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 473f6de6c..7e029e845 100644 --- a/aom/src/main/java/com/nedap/archie/adl14/PreviousConversionApplier.java +++ b/aom/src/main/java/com/nedap/archie/adl14/PreviousConversionApplier.java @@ -199,10 +199,9 @@ private Set gatherUsedValueSets(CAttribute attribute) { Set result = new LinkedHashSet<>(); for(CObject child:attribute.getChildren()) { if(child instanceof CTerminologyCode) { - for(String constraint:((CTerminologyCode) child).getConstraint()) { - if(constraint.startsWith("ac")) { - result.add(constraint); - } + String constraint = ((CTerminologyCode) child).getConstraint(); + if(constraint != null && constraint.startsWith("ac")) { + result.add(constraint); } } if(child.getRmTypeName().equalsIgnoreCase("DV_ORDINAL")) { @@ -214,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.getConstraint()); + 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 8f0f3ed99..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,12 +2,10 @@ 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; -import java.util.Arrays; - public class CDVOrdinalItem { private Integer value; @@ -30,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(Arrays.asList(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 26f8cbfd0..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,7 +203,7 @@ 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.addConstraint(code.getCodeString()); @@ -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,7 +273,7 @@ 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.addConstraint(cdvQuantity.getProperty().toString()); property.addChild(code); 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..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 @@ -91,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(); 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 a9d6604da..c47611ad5 100644 --- a/aom/src/main/java/com/nedap/archie/aom/Archetype.java +++ b/aom/src/main/java/com/nedap/archie/aom/Archetype.java @@ -205,8 +205,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 c14458a19..31c65e886 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; @@ -35,11 +36,10 @@ public abstract class CPrimitiveObject extends CDefinedOb public abstract void setAssumedValue(ValueType assumedValue); - public abstract List getConstraint(); + public abstract Constraint getConstraint(); - public abstract void setConstraint(List constraint); - - public abstract void addConstraint(Constraint constraint); + @JsonIgnore + public abstract List getConstraintAsList(); @JsonAlias("is_enumerated_type_constraint") @RMProperty("is_enumerated_type_constraint") @@ -74,10 +74,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(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:getConstraint()) { + 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 ad02763d5..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; @@ -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..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 @@ -7,12 +7,23 @@ import com.nedap.archie.base.Interval; import org.openehr.utils.message.I18n; +import java.util.List; import java.util.function.BiFunction; /** * 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(); + + public List> getConstraintAsList() { + return getConstraint(); + } + + public abstract void setConstraint(List> constraint); + + public abstract void addConstraint(Interval constraint); @Override @Deprecated 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 425ca8dcd..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 @@ -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 dc6fa3c6c..054dbc728 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.annotation.Nullable; 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; @@ -54,18 +55,17 @@ public void setAssumedValue(TerminologyCode assumedValue) { } @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() { @@ -88,7 +88,7 @@ public ConstraintStatus getEffectiveConstraintStatus() { @Override @Deprecated public boolean isValidValue(TerminologyCode value) { - if(getConstraint().isEmpty()) { + if(getConstraint() == null) { return true; } if(isConstraintRequired()) { @@ -137,7 +137,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 +182,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,11 +231,13 @@ public ConformanceCheckResult cConformsTo(CObject other, BiFunction 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)); + // 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.conforms(); } - 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(constraint == null) { + return ConformanceCheckResult.conforms(); } if(!getEffectiveConstraintStatus().cConformsTo(otherCode.getEffectiveConstraintStatus()) ) { @@ -243,8 +245,8 @@ public ConformanceCheckResult cConformsTo(CObject other, BiFunction, 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/aom/src/main/java/com/nedap/archie/aom/utils/AOMUtils.java b/aom/src/main/java/com/nedap/archie/aom/utils/AOMUtils.java index 456027c70..1db9f71c7 100644 --- a/aom/src/main/java/com/nedap/archie/aom/utils/AOMUtils.java +++ b/aom/src/main/java/com/nedap/archie/aom/utils/AOMUtils.java @@ -242,8 +242,7 @@ private static Boolean matchesExpression(Expression expression, String archetype 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()) { 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..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().isEmpty()) { + if (cobj.getConstraint() != null) { if(cobj.getConstraintStatus() != null) { String constraintStatusString = null; switch(cobj.getConstraintStatus()) { @@ -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/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..2a2da15e5 --- /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 java.util.List; + +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() { + 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(List.of("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]"))); + } +} 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 3ea7d117c..75b3377ef 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 d61053c5a..de6d2e71a 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..ec8fa25ea 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,11 @@ 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 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<>(); for(Object i:constraint) { if(i instanceof Interval) { @@ -43,7 +46,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..74544c959 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; } @@ -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) { return true; } if(terminologyCode.isConstraintRequired()) { 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 8734aa227..0ebae8a99 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 31b907d91..0e925668a 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/adl14/LargeSetOfADL14sTest.java b/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java index 0debed84d..a5cc2b485 100644 --- a/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java +++ b/tools/src/test/java/com/nedap/archie/adl14/LargeSetOfADL14sTest.java @@ -1,10 +1,19 @@ 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; +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 com.nedap.archie.serializer.adl.ADLArchetypeSerializer; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CodePointCharStream; import org.junit.jupiter.api.BeforeEach; @@ -20,6 +29,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 +62,88 @@ public void parseUrn() { assertEquals(1, adl14Lexer.getAllTokens().size()); } + /** + * 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. Converts it to ADL 2 and asserts that serialize → reparse → serialize-again produces identical + * text both times (a stable round-trip).
  6. + *
+ */ + @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()); + + // Round-trip the converted ADL 2 archetype: serialize → reparse → serialize-again and assert the two + // serialized strings are identical (a stable round-trip). + // 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); + assertTrue(adl2Parser.getErrors().hasNoErrors(), + () -> "roundtrip parsing of converted ADL 2 archetype produced errors: " + adl2Parser.getErrors()); + String serializedAgain = ADLArchetypeSerializer.serialize(reparsed); + assertEquals(serialized, serializedAgain, "serializing twice should produce identical text"); + } + + 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 { 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 75299c45f..bd90a4e68 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 1f7789620..1b4bb98c0 100644 --- a/tools/src/test/java/com/nedap/archie/aom/TerminologyCodeConstraintsTest.java +++ b/tools/src/test/java/com/nedap/archie/aom/TerminologyCodeConstraintsTest.java @@ -68,7 +68,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]"))); @@ -79,7 +79,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]"))); @@ -94,7 +94,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(); @@ -114,7 +114,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]"))); @@ -125,7 +125,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); } @@ -134,7 +134,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); } @@ -144,7 +144,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 e891cd840..2141d067e 100644 --- a/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java +++ b/tools/src/test/java/com/nedap/archie/json/AOMJacksonTest.java @@ -276,28 +276,28 @@ 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); assertTrue(json.contains("\"constraint_status\" : \"preferred\"")); - assertTrue(json.contains("\"constraint\" : [ \"ac23\" ]")); + assertTrue(json.contains("\"constraint\" : \"ac23\"")); CTerminologyCode parsedTermCode = objectMapper.readValue(json, CTerminologyCode.class); assertEquals(cTermCode.getConstraint(), parsedTermCode.getConstraint()); assertEquals(ConstraintStatus.PREFERRED, parsedTermCode.getConstraintStatus()); } @Test - public void forwardsCompatibilityCTerminologyCodeConstraintTypeFromJsonTest() throws Exception { + public void backwardsCompatibilityCTerminologyCodeConstraintTypeFromJsonTest() throws Exception { String json = "{\n" + " \"rm_type_name\" : \"terminology_code\",\n" + " \"node_id\" : \"id9999\",\n" + - " \"constraint\" : \"ac23\"\n" + + " \"constraint\" : [ \"ac23\" ]\n" + "}"; ObjectMapper objectMapper = JacksonUtil.getObjectMapper(ArchieJacksonConfiguration.createStandardsCompliant()); CTerminologyCode parsedTermCode = objectMapper.readValue(json, CTerminologyCode.class); - assertEquals("ac23", parsedTermCode.getConstraint().get(0)); + assertEquals("ac23", parsedTermCode.getConstraint()); } @Test 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 e6a442e58..25b501c29 100644 --- a/tools/src/test/java/com/nedap/archie/rmobjectvalidator/TerminologyCodeConstraintsTest.java +++ b/tools/src/test/java/com/nedap/archie/rmobjectvalidator/TerminologyCodeConstraintsTest.java @@ -76,7 +76,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]"))); @@ -87,7 +87,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]"))); @@ -101,7 +101,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(); @@ -121,7 +121,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(); @@ -141,7 +141,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]"))); @@ -152,7 +152,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); } @@ -161,7 +161,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); } @@ -171,7 +171,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 598007756..1a719eaf4 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();