Skip to content

Implement CTerminologyCode.constraint as String#761

Open
MattijsK wants to merge 8 commits into670_feature_archie_4_major_versionfrom
643_terminology_contstraint_type_to_string
Open

Implement CTerminologyCode.constraint as String#761
MattijsK wants to merge 8 commits into670_feature_archie_4_major_versionfrom
643_terminology_contstraint_type_to_string

Conversation

@MattijsK
Copy link
Copy Markdown
Collaborator

@MattijsK MattijsK commented Mar 5, 2026

Summary

This PR fixes #643. As there was a difference between the openEHR specification and Archie. So it changes CTerminologyCode.constraint from List<String> to String.

Changes

Core model (CTerminologyCode)

  • constraint field changed from List<String> to String; getConstraint() now returns String
  • addConstraint(String) removed; setConstraint(String) takes a single value
  • getConstraintAsList() added as a convenience method for call sites that need list semantics,
    returning a singleton list or an empty list

Abstract primitive object interface (CPrimitiveObject)

  • getConstraint() return type generalised to Object to accommodate the now-divergent constraint
    types across subclasses
  • getConstraintAsList() added as abstract, implemented in all subclasses; annotated @JsonIgnore
    so it is not serialised as a separate property
  • addConstraint() removed from the abstract interface

ADL 1.4 → ADL 2 conversion (ADL14TermConstraintConverter, Adl14PrimitivesConstraintParser)

ADL 1.4 allows inline multi-code constraints (e.g. [local::at0001, at0002] and
[snomed-ct::12345, 67890]) which have no direct ADL 2 equivalent. These are handled in a
two-phase approach:

  • The parser stores the raw codes in a new @JsonIgnore @XmlTransient List<String> pendingCodes
    field on CTerminologyCode — used only during conversion, never serialised
  • The converter reads pendingCodes and creates a proper value set (ac-code) from them, setting
    the resulting ac-code as the single constraint
  • External multi-code constraints are normalised to full term code refs ([terminology::code]) by
    the parser so the converter can treat all pending codes uniformly
  • The converter is restructured into convertCTerminologyCode (single-code),
    convertMultiCodeTerminologyCode (multi-code), and convertAssumedValue (shared assumed-value
    logic)

Serialisation

  • JSON deserialisation of the old array format ("constraint": ["at1"]) is handled by the existing
    UNWRAP_SINGLE_VALUE_ARRAYS Jackson feature

@MattijsK MattijsK changed the base branch from master to 670_feature_archie_4_major_version March 5, 2026 13:22
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 74.28571% with 36 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.38%. Comparing base (d756c1a) to head (f0a3c43).
⚠️ Report is 1 commits behind head on 670_feature_archie_4_major_version.

Files with missing lines Patch % Lines
...dap/archie/adl14/ADL14TermConstraintConverter.java 70.42% 12 Missing and 9 partials ⚠️
.../nedap/archie/aom/primitives/CTerminologyCode.java 56.25% 2 Missing and 5 partials ⚠️
.../nedap/archie/adl14/PreviousConversionApplier.java 33.33% 1 Missing and 1 partial ⚠️
...er/adl/constraints/CTerminologyCodeSerializer.java 50.00% 0 Missing and 2 partials ⚠️
...a/com/nedap/archie/adl14/ADL14NodeIDConverter.java 0.00% 0 Missing and 1 partial ⚠️
...a/com/nedap/archie/adl14/aom14/CDVOrdinalItem.java 0.00% 1 Missing ⚠️
...main/java/com/nedap/archie/aom/utils/AOMUtils.java 0.00% 0 Missing and 1 partial ⚠️
.../archie/creation/ExampleJsonInstanceGenerator.java 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@                           Coverage Diff                            @@
##             670_feature_archie_4_major_version     #761      +/-   ##
========================================================================
+ Coverage                                 72.34%   72.38%   +0.03%     
- Complexity                                 7189     7193       +4     
========================================================================
  Files                                       678      678              
  Lines                                     23267    23299      +32     
  Branches                                   3764     3786      +22     
========================================================================
+ Hits                                      16833    16864      +31     
+ Misses                                     4707     4704       -3     
- Partials                                   1727     1731       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@MattijsK MattijsK changed the title ctermcode.constraint type to string Implement CTerminologyCode.constraint as String Apr 9, 2026
@MattijsK MattijsK marked this pull request as ready for review April 9, 2026 08:50
@MattijsK MattijsK linked an issue Apr 9, 2026 that may be closed by this pull request
@VeraPrinsen VeraPrinsen self-requested a review April 10, 2026 08:57
public abstract void setAssumedValue(ValueType assumedValue);

public abstract List<Constraint> getConstraint();
public abstract Object getConstraint();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be possible to do:

Suggested change
public abstract Object getConstraint();
public abstract Constraint getConstraint();

Meaning that when classes extend CPrimitiveObject, they have to edit the type of constraint, for example CBoolean has to do:
public class CBoolean extends CPrimitiveObject<List<Boolean>, Boolean>

Then we also don't need the convenience method getConstraintAsList, as getConstraint will return a String for CTerminologyCode, but a List for CBoolean.

addConstraint(Constraint constraint) will have to be removed from CPrimitiveObject, but can then be added as a convenience method to objects that have a list as constraint, like CBoolean.

For me this feels a bit more like how the specifications intended it, instead of a little workaround.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the type here back to Constraint. Can you check if it makes more sense now?
I still kept getConstraintAsList in though.

@Deprecated
public boolean isValidValue(TerminologyCode value) {
if(getConstraint().isEmpty()) {
if(getConstraint() == null || getConstraint().isEmpty()) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel the functionality has changed here, because previously if the constraint list contained an empty string this if statement wasn't true and now it is. And also further in the code I don't see any reason why a getConstraint() of an empty string would return true by default.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree

List<String> otherValueSet = otherCode.getValueSetExpanded();

if(constraint.size() != 1) {
// TODO: does this need to be removed/reworded or anything else?
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think this should be reworded. I don't see any specific wording for this in the specifications, so I guess we can just change the wording?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these checks are completely changed now and the objects would actually conform to if the constraint of the parent or itself are null now. I changed this, please check if it makes sense to you.

@Override
public void serialize(CTerminologyCode cobj) {
if (!cobj.getConstraint().isEmpty()) {
if (cobj.getConstraint() != null && !cobj.getConstraint().isEmpty()) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (cobj.getConstraint() != null && !cobj.getConstraint().isEmpty()) {
if (cobj.getConstraint() != null) {

I think empty strings would just be used in the code previously, so I think we shouldn't change that.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree

} else if (cObject instanceof CPrimitiveObject) {
CPrimitiveObject<?, ?> cPrimitiveObject = (CPrimitiveObject<?, ?>) cObject;
List<?> constraint = cPrimitiveObject.getConstraint();
} else if (cObject instanceof COrdered) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we keep the getConstraintAsList() it is maybe better to use that instead, in the case (that will probably never happen) that a new CPrimitiveObject based on an Interval constraint is introduced.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I can keep it the way it is, I added a clarifying comment though.


private static RMObjectValidationMessage createValidationMessage(Object value, String pathSoFar, CPrimitiveObject<?, ?> cobject) {
List<?> constraint = cobject.getConstraint();
List<?> constraint = cobject.getConstraintAsList();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe just use getConstraint() and instead of checking if the constraint.size() == 1, check if the constraint is a list (and maybe has more than 1 element).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this makes much sense. I still kept in getConstraintAsList, so this keeps it the simplest.


private boolean isValidValue(CTerminologyCode terminologyCode, TerminologyCode value) {
if(terminologyCode.getConstraint().isEmpty()) {
if(terminologyCode.getConstraint() == null || terminologyCode.getConstraint().isEmpty()) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if(terminologyCode.getConstraint() == null || terminologyCode.getConstraint().isEmpty()) {
if(terminologyCode.getConstraint() == null

Again I don't think previously an empty string would by default return true.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree

public abstract List<Constraint> getConstraint();
public abstract Constraint getConstraint();

public abstract void setConstraint(List<Constraint> constraint);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This abstract method can stay, but just with the parameter Constraint instead of List

/** Temporary storage for multi-code ADL 1.4 constraints during conversion. Never serialized. */
@JsonIgnore
@XmlTransient
private List<String> pendingCodes;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When parsing a ADL1.4 archetype, the codes will end up here. But when you try and serialize the archetype again, the constraint of the defining code now stays empty.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So maybe we should add in the serializer that the pendingCodes should be converted into a list of strings for the constraint. Or we should maybe have two separate CTerminologyCode classes. One for ADL1.4 and one for ADL2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CTerminologyCode.constraint has incorrect type

2 participants