From 2b97ef388cc6dcf115e91f26b6c1860c1e695e77 Mon Sep 17 00:00:00 2001 From: Quentin Ligier Date: Mon, 14 Oct 2024 10:00:36 +0200 Subject: [PATCH 1/6] Refactor ValueSetCodeValidationProvider --- .../CodeSystemCodeValidationProvider.java | 6 +- .../terminology/TerminologyUtils.java | 8 +- .../ValueSetCodeValidationProvider.java | 217 ++++++++++-------- 3 files changed, 123 insertions(+), 108 deletions(-) diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/CodeSystemCodeValidationProvider.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/CodeSystemCodeValidationProvider.java index 9572a05cebb..ed835dad9d3 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/CodeSystemCodeValidationProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/CodeSystemCodeValidationProvider.java @@ -15,12 +15,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - import static ch.ahdis.matchbox.terminology.TerminologyUtils.mapErrorToOperationOutcome; import static java.util.Objects.requireNonNull; @@ -58,7 +54,7 @@ public IAnyResource validateCode(@ResourceParam final IBaseParameters baseParame if (request.hasParameter("coding") && request.getParameterValue("coding") instanceof final Coding coding) { log.debug("Validating code in CS: {}|{}", coding.getCode(), coding.getSystem()); - final var outputParams = TerminologyUtils.mapCodingToSuccessfulParameters(coding); + final var outputParams = TerminologyUtils.createSuccessfulResponseParameters(coding, null); return switch (this.fhirContext.getVersion().getVersion()) { case R4 -> VersionConvertorFactory_40_50.convertResource(outputParams); case R4B -> VersionConvertorFactory_43_50.convertResource(outputParams); diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/TerminologyUtils.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/TerminologyUtils.java index 421877c5476..1e7a357164c 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/TerminologyUtils.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/TerminologyUtils.java @@ -5,6 +5,8 @@ import org.hl7.fhir.r5.model.OperationOutcome; import org.hl7.fhir.r5.model.Parameters; +import javax.annotation.Nullable; + /** * Some utilities for the internal terminology server. * @@ -27,7 +29,8 @@ public static Parameters mapCodeToSuccessfulParameters(final String code) { return parameters; } - public static Parameters mapCodingToSuccessfulParameters(final Coding coding) { + public static Parameters createSuccessfulResponseParameters(@Nullable final Coding coding, + @Nullable final CodeableConcept codeableConcept) { final var parameters = new Parameters(); parameters.setParameter("result", true); if (coding.hasVersion()) { @@ -42,6 +45,9 @@ public static Parameters mapCodingToSuccessfulParameters(final Coding coding) { if (coding.hasDisplay()) { parameters.setParameter("display", coding.getDisplayElement()); } + if (codeableConcept != null) { + parameters.setParameter("codeableConcept", codeableConcept); + } return parameters; } diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/ValueSetCodeValidationProvider.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/ValueSetCodeValidationProvider.java index 350f990b16b..189aca97726 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/ValueSetCodeValidationProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/ValueSetCodeValidationProvider.java @@ -23,6 +23,8 @@ import org.slf4j.LoggerFactory; import jakarta.servlet.http.HttpServletResponse; +import org.w3._1999.xhtml.B; + import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -91,133 +93,144 @@ public IAnyResource validateCode(@ResourceParam final IBaseParameters baseParame ? request.getParameterValue("cache-id").toString() : null; - final boolean inferSystem = request.hasParameter("inferSystem") - ? request.getParameterBool("inferSystem") - : false; + final boolean inferSystem = request.hasParameter("inferSystem") && request.getParameterBool("inferSystem"); final String code = request.hasParameter("code") ? request.getParameterValue("code").toString() : null; - final boolean lenientDisplayValidation = request.hasParameter("lenient-display-validation") - ? request.getParameterBool("lenient-display-validation") - : false; + if (code != null) { + return mapCodeToSuccessfulParameters(code); + } - final String mode = request.hasParameter("mode") - ? request.getParameterValue("mode").toString() : null; + //final boolean lenientDisplayValidation = request.hasParameter("lenient-display-validation") + // && request.getParameterBool("lenient-display-validation"); - // parameter default-to-lastest-version (Booelan) - // parameter profile-url "http://hl7.org/fhir/ExpansionProfile/dc8fd4bc-091a-424a-8a3b-6198ef146891" + //final String mode = request.hasParameter("mode") + // ? request.getParameterValue("mode").toString() : null; - if (code!=null) { - return mapCodeToSuccessfulParameters(code); - } + // parameter default-to-lastest-version (Boolean) + // parameter profile-url "http://hl7.org/fhir/ExpansionProfile/dc8fd4bc-091a-424a-8a3b-6198ef146891" if (!request.hasParameter("coding")) { servletResponse.setStatus(422); return mapErrorToOperationOutcome("Missing parameter 'coding' in the request"); } - if (request.getParameterValue("coding") instanceof final Coding coding) { - if ("NO_MEMBERSHIP_CHECK".equals(valueSetMode)) { - return mapCodingToSuccessfulParameters(coding); - } - String url = null; - ValueSet valueSet = null; - boolean cachedValueSet = false; - if (request.hasParameter("url")) { - url = request.getParameterValue("url").toString(); - valueSet = this.getExpandedValueSet(cacheId, url); - cachedValueSet = true; - } else if (request.hasParameter("valueSet")) { - valueSet = (ValueSet) request.getParameter("valueSet").getResource(); - url = valueSet.getUrl(); - } - - if (valueSet == null) { - // That value set is not cached - log.debug("OK - cache miss, value set is null"); - return mapCodingToSuccessfulParameters(coding); - } - log.debug("Validating code in VS: {}|{} in {}", coding.getCode(), coding.getSystem(), url); + if (!(request.getParameterValue("coding") instanceof Coding)) { + servletResponse.setStatus(422); + // The original error message is: + // Unable to find code to validate (looked for coding | codeableConcept | code) + return mapErrorToOperationOutcome("Unable to find code to validate (looked for 'coding')"); + } - if (inferSystem && !coding.hasSystem()) { - // Infer the coding system as the first included system in the composition - coding.setSystem(valueSet.getCompose().getIncludeFirstRep().getSystem()); - } + final Coding coding = (Coding) request.getParameterValue("coding"); + if ("NO_MEMBERSHIP_CHECK".equals(valueSetMode)) { + return createSuccessfulResponseParameters(coding, null); + } - if (!cachedValueSet) { - // We have to expand the value set - final IValidationSupport.ValueSetExpansionOutcome result = this.inMemoryTerminologySupport.expandValueSet( - this.validationSupportContext, - this.expansionOptions, - valueSet); - if (result == null || result.getValueSet() == null) { - // We have failed expanding the value set, this means it may be a complex one - log.debug("OK - expansion failed"); - final var membership = this.evaluateCodeInComposition(coding, valueSet.getCompose()); - if (membership == CodeMembership.EXCLUDED) { - log.debug("OK - code is excluded from value set composition"); - return mapCodeErrorToParameters( - "The code '%s' is excluded from the value set '%s' composition".formatted( - coding.getCode(), - url - ), - coding - ); - } else if (membership == CodeMembership.INCLUDED) { - log.debug("OK - code is included in value set composition"); - } else { - log.debug("OK - code is not included/excluded from value set composition, inferring inclusion"); - } - return mapCodingToSuccessfulParameters(coding); - } + String url = null; + ValueSet valueSet = null; + boolean cachedValueSet = false; + if (request.hasParameter("url")) { + url = request.getParameterValue("url").toString(); + valueSet = this.getExpandedValueSet(cacheId, url); + cachedValueSet = true; + } else if (request.hasParameter("valueSet")) { + valueSet = (ValueSet) request.getParameter("valueSet").getResource(); + url = valueSet.getUrl(); + } - // Value set is expanded - final var baseValueSet = (IDomainResource) result.getValueSet(); - if (baseValueSet instanceof final ValueSet valueSetR5) { - valueSet = valueSetR5; - } else if (baseValueSet instanceof final org.hl7.fhir.r4.model.ValueSet valueSetR4) { - valueSet = (ValueSet) VersionConvertorFactory_40_50.convertResource(valueSetR4); - } else if (baseValueSet instanceof final org.hl7.fhir.r4b.model.ValueSet valueSetR4B) { - valueSet = (ValueSet) VersionConvertorFactory_43_50.convertResource(valueSetR4B); + if (valueSet == null) { + // That value set is not cached + log.debug("OK - cache miss, value set is null"); + return createSuccessfulResponseParameters(coding, null); + } + log.debug("Validating code '{}|{}' in ValueSet '{}'", coding.getCode(), coding.getSystem(), url); + + if (!cachedValueSet) { + // We have to expand the value set + final IValidationSupport.ValueSetExpansionOutcome result = this.inMemoryTerminologySupport.expandValueSet( + this.validationSupportContext, + this.expansionOptions, + valueSet); + if (result == null || result.getValueSet() == null) { + // The value set expansion has failed; this means it may be too complex for the current implementation + // We try to infer the code membership from the value set definition as a last resort + log.debug("OK - expansion failed"); + final var membership = this.evaluateCodeInComposition(coding, valueSet.getCompose()); + if (membership == CodeMembership.EXCLUDED) { + log.debug("OK - code is excluded from value set composition"); + return mapCodeErrorToParameters( + "The code '%s' is excluded from the value set '%s' composition".formatted( + coding.getCode(), + url + ), + coding + ); + } else if (membership == CodeMembership.INCLUDED) { + log.debug("OK - code is included in value set composition"); } else { - throw new MatchboxUnsupportedFhirVersionException("ValueSetCodeValidationProvider", - this.fhirContext.getVersion().getVersion()); + log.debug("OK - code is not included/excluded from value set composition, inferring inclusion"); } + return createSuccessfulResponseParameters(coding, null); + } - if (valueSet.getExpansion().getContains().isEmpty()) { - // The value set is empty - log.debug("OK - expansion failed without reporting errors (empty value set)"); - return mapCodingToSuccessfulParameters(coding); - } + // Value set is expanded, convert it to R5 for internal use + final var baseValueSet = (IDomainResource) result.getValueSet(); + if (baseValueSet instanceof final ValueSet valueSetR5) { + valueSet = valueSetR5; + } else if (baseValueSet instanceof final org.hl7.fhir.r4.model.ValueSet valueSetR4) { + valueSet = (ValueSet) VersionConvertorFactory_40_50.convertResource(valueSetR4); + } else if (baseValueSet instanceof final org.hl7.fhir.r4b.model.ValueSet valueSetR4B) { + valueSet = (ValueSet) VersionConvertorFactory_43_50.convertResource(valueSetR4B); + } else { + throw new MatchboxUnsupportedFhirVersionException("ValueSetCodeValidationProvider", + this.fhirContext.getVersion().getVersion()); + } - if (cacheId != null) { - this.cacheExpandedValueSet(cacheId, url, valueSet); - } + if (valueSet.getExpansion().getContains().isEmpty()) { + // The value set expansion is successful but empty + log.debug("OK - expansion failed without reporting errors (empty value set)"); + return createSuccessfulResponseParameters(coding, null); } - // Now we have an expanded value set, we can properly validate the code - if (this.validateCodeInExpandedValueSet(coding, valueSet)) { - log.debug("OK - present in expanded value set (expansion contains {} codes)", - valueSet.getExpansion().getContains().size()); - return mapCodingToSuccessfulParameters(coding); + if (cacheId != null) { + this.cacheExpandedValueSet(cacheId, url, valueSet); } - log.debug("FAIL - not present in expanded value set (expansion contains {} codes)", - valueSet.getExpansion().getContains().size()); - return mapCodeErrorToParameters( - "The code '%s' is not in the value set '%s' (expansion contains %d codes)".formatted( - coding.getCode(), - url, - valueSet.getExpansion().getContains().size() - ), - coding - ); } - servletResponse.setStatus(422); - // Original error message is: - // Unable to find code to validate (looked for coding | codeableConcept | code) - return mapErrorToOperationOutcome("Unable to find code to validate (looked for coding)"); + if (this.evaluateCodingInExpandedValueSet(coding, valueSet, inferSystem)) { + return createSuccessfulResponseParameters(coding, null); + } + return mapCodeErrorToParameters( + "The code '%s' is not in the value set '%s' (expansion contains %d codes)".formatted( + coding.getCode(), + url, + valueSet.getExpansion().getContains().size() + ), + coding + ); + } + + /** + * Try to infer from an expanded value set if a coding is included or excluded. + */ + private boolean evaluateCodingInExpandedValueSet(final Coding coding, + final ValueSet valueSet, + final boolean inferSystem) { + if (inferSystem && !coding.hasSystem()) { + // Infer the coding system as the first included system in the composition + coding.setSystem(valueSet.getCompose().getIncludeFirstRep().getSystem()); + } + + if (this.validateCodeInExpandedValueSet(coding, valueSet)) { + log.debug("OK - present in expanded value set (expansion contains {} codes)", + valueSet.getExpansion().getContains().size()); + return true; + } + log.debug("FAIL - not present in expanded value set (expansion contains {} codes)", + valueSet.getExpansion().getContains().size()); + return false; } /** From 7e7d802d2113e10f2e816d0bd4b50eeb0cf8bddd Mon Sep 17 00:00:00 2001 From: Quentin Ligier Date: Tue, 15 Oct 2024 14:34:18 +0200 Subject: [PATCH 2/6] Terminology: support CodeableConcept in ValueSet/$validate operation Fixes #291 --- docs/changelog.md | 1 + .../CodeSystemCodeValidationProvider.java | 2 +- .../terminology/TerminologyUtils.java | 38 ++++---- .../ValueSetCodeValidationProvider.java | 95 ++++++++++++------- 4 files changed, 86 insertions(+), 50 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index c7118ca7fd7..673b2ff9054 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,7 @@ Unreleased - Tutorial for validation FHIR resources with [matchbox](https://ahdis.github.io/matchbox/validation-tutorial/) - Gazelle reports: add test to ensure https://gazelle.ihe.net/jira/browse/EHS-831 is fixed - Allow validating a resource through the GUI with URL search parameters [#288](https://github.com/ahdis/matchbox/issues/288) +- Terminology: support CodeableConcept in ValueSet/$validate operation [#291](https://github.com/ahdis/matchbox/issues/291) 2024/10/07 Release 3.9.3 diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/CodeSystemCodeValidationProvider.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/CodeSystemCodeValidationProvider.java index ed835dad9d3..1f490e9d629 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/CodeSystemCodeValidationProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/CodeSystemCodeValidationProvider.java @@ -54,7 +54,7 @@ public IAnyResource validateCode(@ResourceParam final IBaseParameters baseParame if (request.hasParameter("coding") && request.getParameterValue("coding") instanceof final Coding coding) { log.debug("Validating code in CS: {}|{}", coding.getCode(), coding.getSystem()); - final var outputParams = TerminologyUtils.createSuccessfulResponseParameters(coding, null); + final var outputParams = TerminologyUtils.createSuccessfulResponseParameters(coding); return switch (this.fhirContext.getVersion().getVersion()) { case R4 -> VersionConvertorFactory_40_50.convertResource(outputParams); case R4B -> VersionConvertorFactory_43_50.convertResource(outputParams); diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/TerminologyUtils.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/TerminologyUtils.java index 1e7a357164c..30747a1e8ae 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/TerminologyUtils.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/TerminologyUtils.java @@ -29,8 +29,7 @@ public static Parameters mapCodeToSuccessfulParameters(final String code) { return parameters; } - public static Parameters createSuccessfulResponseParameters(@Nullable final Coding coding, - @Nullable final CodeableConcept codeableConcept) { + public static Parameters createSuccessfulResponseParameters(final Coding coding) { final var parameters = new Parameters(); parameters.setParameter("result", true); if (coding.hasVersion()) { @@ -45,28 +44,33 @@ public static Parameters createSuccessfulResponseParameters(@Nullable final Codi if (coding.hasDisplay()) { parameters.setParameter("display", coding.getDisplayElement()); } - if (codeableConcept != null) { - parameters.setParameter("codeableConcept", codeableConcept); - } return parameters; } - public static Parameters mapCodeErrorToParameters(final String message, - final Coding coding) { + public static Parameters createErrorResponseParameters(final String message, + @Nullable final Coding coding, + @Nullable final CodeableConcept codeableConcept) { final var parameters = new Parameters(); parameters.setParameter("result", false); parameters.setParameter("message", message); - if (coding.hasVersion()) { - parameters.setParameter("version", coding.getVersionElement()); - } - if (coding.hasCode()) { - parameters.setParameter("code", coding.getCodeElement()); - } - if (coding.hasSystem()) { - parameters.setParameter("system", coding.getSystemElement()); + + if (coding != null) { + if (coding.hasVersion()) { + parameters.setParameter("version", coding.getVersionElement()); + } + if (coding.hasCode()) { + parameters.setParameter("code", coding.getCodeElement()); + } + if (coding.hasSystem()) { + parameters.setParameter("system", coding.getSystemElement()); + } + if (coding.hasDisplay()) { + parameters.setParameter("display", coding.getDisplayElement()); + } } - if (coding.hasDisplay()) { - parameters.setParameter("display", coding.getDisplayElement()); + + if (codeableConcept != null) { + parameters.setParameter("codeableConcept", codeableConcept); } final var oo = new OperationOutcome(); diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/ValueSetCodeValidationProvider.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/ValueSetCodeValidationProvider.java index 189aca97726..364f32b2730 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/ValueSetCodeValidationProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/terminology/ValueSetCodeValidationProvider.java @@ -16,6 +16,7 @@ import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IDomainResource; +import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.Parameters; import org.hl7.fhir.r5.model.ValueSet; @@ -23,9 +24,9 @@ import org.slf4j.LoggerFactory; import jakarta.servlet.http.HttpServletResponse; -import org.w3._1999.xhtml.B; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -111,21 +112,41 @@ public IAnyResource validateCode(@ResourceParam final IBaseParameters baseParame // parameter default-to-lastest-version (Boolean) // parameter profile-url "http://hl7.org/fhir/ExpansionProfile/dc8fd4bc-091a-424a-8a3b-6198ef146891" - if (!request.hasParameter("coding")) { + if (!request.hasParameter("coding") && !request.hasParameter("codeableConcept")) { servletResponse.setStatus(422); - return mapErrorToOperationOutcome("Missing parameter 'coding' in the request"); + return mapErrorToOperationOutcome("Missing parameter 'coding' or 'codeableConcept' in the request"); } - if (!(request.getParameterValue("coding") instanceof Coding)) { + if (!(request.getParameterValue("coding") instanceof Coding) + && !(request.getParameterValue("codeableConcept") instanceof CodeableConcept)) { servletResponse.setStatus(422); // The original error message is: // Unable to find code to validate (looked for coding | codeableConcept | code) - return mapErrorToOperationOutcome("Unable to find code to validate (looked for 'coding')"); + return mapErrorToOperationOutcome("Unable to find code to validate (looked for 'coding' and 'codeableConcept')"); + } + + final Coding coding; + if (request.hasParameter("coding")) { + coding = (Coding) request.getParameterValue("coding"); + } else { + coding = null; + } + final CodeableConcept codeableConcept; + if (request.hasParameter("codeableConcept")) { + codeableConcept = (CodeableConcept) request.getParameterValue("codeableConcept"); + } else { + codeableConcept = null; + } + + final List codings; + if (coding != null) { + codings = List.of(coding); + } else { + codings = codeableConcept.getCoding(); } - final Coding coding = (Coding) request.getParameterValue("coding"); if ("NO_MEMBERSHIP_CHECK".equals(valueSetMode)) { - return createSuccessfulResponseParameters(coding, null); + return createSuccessfulResponseParameters(codings.getFirst()); } String url = null; @@ -143,9 +164,13 @@ public IAnyResource validateCode(@ResourceParam final IBaseParameters baseParame if (valueSet == null) { // That value set is not cached log.debug("OK - cache miss, value set is null"); - return createSuccessfulResponseParameters(coding, null); + return createSuccessfulResponseParameters(codings.getFirst()); + } + if (coding != null) { + log.debug("Validating code '{}|{}' in ValueSet '{}'", coding.getCode(), coding.getSystem(), url); + } else { + log.debug("Validating codeableConcept '{}' in ValueSet '{}'", codeableConcept, url); } - log.debug("Validating code '{}|{}' in ValueSet '{}'", coding.getCode(), coding.getSystem(), url); if (!cachedValueSet) { // We have to expand the value set @@ -156,23 +181,27 @@ public IAnyResource validateCode(@ResourceParam final IBaseParameters baseParame if (result == null || result.getValueSet() == null) { // The value set expansion has failed; this means it may be too complex for the current implementation // We try to infer the code membership from the value set definition as a last resort - log.debug("OK - expansion failed"); - final var membership = this.evaluateCodeInComposition(coding, valueSet.getCompose()); - if (membership == CodeMembership.EXCLUDED) { - log.debug("OK - code is excluded from value set composition"); - return mapCodeErrorToParameters( - "The code '%s' is excluded from the value set '%s' composition".formatted( - coding.getCode(), - url - ), - coding - ); - } else if (membership == CodeMembership.INCLUDED) { - log.debug("OK - code is included in value set composition"); - } else { - log.debug("OK - code is not included/excluded from value set composition, inferring inclusion"); + log.debug(" - expansion failed"); + + for (final var validatedCoding : codings) { + final var membership = this.evaluateCodeInComposition(validatedCoding, valueSet.getCompose()); + if (membership == CodeMembership.EXCLUDED) { + log.debug(" - code '{}' is excluded from value set composition", validatedCoding.getCode()); + } else if (membership == CodeMembership.INCLUDED) { + log.debug(" - code '{}' is included in value set composition", validatedCoding.getCode()); + // We can stop here, we've found a Coding explicitly included + return createSuccessfulResponseParameters(validatedCoding); + } else { + log.debug(" - code '{}' is not included/excluded from value set composition", validatedCoding.getCode()); + // We can stop here, we've found a Coding explicitly included + return createSuccessfulResponseParameters(validatedCoding); + } } - return createSuccessfulResponseParameters(coding, null); + return createErrorResponseParameters( + "The provided Coding/CodeableConcept is excluded from the value set '%s' composition".formatted(url), + coding, + codeableConcept + ); } // Value set is expanded, convert it to R5 for internal use @@ -191,7 +220,7 @@ public IAnyResource validateCode(@ResourceParam final IBaseParameters baseParame if (valueSet.getExpansion().getContains().isEmpty()) { // The value set expansion is successful but empty log.debug("OK - expansion failed without reporting errors (empty value set)"); - return createSuccessfulResponseParameters(coding, null); + return createSuccessfulResponseParameters(codings.getFirst()); } if (cacheId != null) { @@ -199,16 +228,18 @@ public IAnyResource validateCode(@ResourceParam final IBaseParameters baseParame } } - if (this.evaluateCodingInExpandedValueSet(coding, valueSet, inferSystem)) { - return createSuccessfulResponseParameters(coding, null); + for (final var validatedCoding : codings) { + if (this.evaluateCodingInExpandedValueSet(validatedCoding, valueSet, inferSystem)) { + return createSuccessfulResponseParameters(validatedCoding); + } } - return mapCodeErrorToParameters( - "The code '%s' is not in the value set '%s' (expansion contains %d codes)".formatted( - coding.getCode(), + return createErrorResponseParameters( + "The provided Coding/CodeableConcept is not in the value set '%s' (expansion contains %d codes)".formatted( url, valueSet.getExpansion().getContains().size() ), - coding + coding, + codeableConcept ); } From fbe17fc245890b40ba5b3edb683c89fb9db54d87 Mon Sep 17 00:00:00 2001 From: oliveregger Date: Thu, 10 Oct 2024 17:11:06 +0200 Subject: [PATCH 3/6] update core to 6.3.30 --- docs/changelog.md | 2 + matchbox-engine/pom.xml | 4 + .../engine/ValidationPolicyAdvisor.java | 2 +- .../fhir/r5/context/BaseWorkerContext.java | 20 +- .../hl7/fhir/r5/elementmodel/XmlParser.java | 207 ++++++----- .../structuremap/StructureMapUtilities.java | 19 +- .../npm/FilesystemPackageCacheManager.java | 111 ++++-- .../hl7/fhir/utilities/npm/NpmPackage.java | 20 +- .../instance/InstanceValidator.java | 340 ++++++++++++++---- pom.xml | 7 +- 10 files changed, 522 insertions(+), 210 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 673b2ff9054..e44805e368c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,8 @@ Unreleased - Gazelle reports: add test to ensure https://gazelle.ihe.net/jira/browse/EHS-831 is fixed - Allow validating a resource through the GUI with URL search parameters [#288](https://github.com/ahdis/matchbox/issues/288) - Terminology: support CodeableConcept in ValueSet/$validate operation [#291](https://github.com/ahdis/matchbox/issues/291) +- Upgrade hapifhir org.hl7.fhir.core to 6.3.30 +- FML: Use FMLParser in StructureMapUtilities and support for identity transform [#289](https://github.com/ahdis/matchbox/issues/289) 2024/10/07 Release 3.9.3 diff --git a/matchbox-engine/pom.xml b/matchbox-engine/pom.xml index 87a7e96291c..dc183e0500e 100644 --- a/matchbox-engine/pom.xml +++ b/matchbox-engine/pom.xml @@ -37,6 +37,10 @@ ca.uhn.hapi.fhir org.hl7.fhir.utilities + + org.fhir + ucum + diff --git a/matchbox-engine/src/main/java/ch/ahdis/matchbox/engine/ValidationPolicyAdvisor.java b/matchbox-engine/src/main/java/ch/ahdis/matchbox/engine/ValidationPolicyAdvisor.java index a56545ffc87..bf3fa219687 100644 --- a/matchbox-engine/src/main/java/ch/ahdis/matchbox/engine/ValidationPolicyAdvisor.java +++ b/matchbox-engine/src/main/java/ch/ahdis/matchbox/engine/ValidationPolicyAdvisor.java @@ -3,7 +3,7 @@ import org.hl7.fhir.r5.utils.validation.IResourceValidator; import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; -import org.hl7.fhir.validation.instance.BasePolicyAdvisorForFullValidation; +import org.hl7.fhir.validation.instance.advisor.BasePolicyAdvisorForFullValidation; import org.hl7.fhir.validation.instance.InstanceValidator; public class ValidationPolicyAdvisor extends BasePolicyAdvisorForFullValidation { diff --git a/matchbox-engine/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java b/matchbox-engine/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java index cc465964ef3..2f20b8e73af 100644 --- a/matchbox-engine/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java +++ b/matchbox-engine/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java @@ -1283,9 +1283,9 @@ public ValidationResult validateCode(final ValidationOptions optionsArg, final C } public ValidationResult validateCode(final ValidationOptions optionsArg, String path, final Coding code, final ValueSet vs, final ValidationContextCarrier ctxt) { - + ValidationOptions options = optionsArg != null ? optionsArg : ValidationOptions.defaults(); - + if (code.hasSystem()) { codeSystemsUsed.add(code.getSystem()); } @@ -1310,6 +1310,9 @@ public ValidationResult validateCode(final ValidationOptions optionsArg, String // ok, first we try to validate locally try { ValueSetValidator vsc = constructValueSetCheckerSimple(options, vs, ctxt); + if (vsc.getOpContext() != null) { + vsc.getOpContext().note("Validate "+code.toString()+" @ "+path+" against "+(vs == null ? "null" : vs.getVersionedUrl())); + } vsc.setUnknownSystems(unknownSystems); vsc.setThrowToServer(options.isUseServer() && terminologyClientManager.hasClient()); if (!ValueSetUtilities.isServerSide(code.getSystem())) { @@ -1478,15 +1481,15 @@ public Boolean processSubsumesResult(Parameters pOut, String server) { } protected ValueSetExpander constructValueSetExpanderSimple(ValidationOptions options) { - return new ValueSetExpander(this, new TerminologyOperationContext(this, options)).setDebug(logger.isDebugLogging()); + return new ValueSetExpander(this, new TerminologyOperationContext(this, options, "expansion")).setDebug(logger.isDebugLogging()); } protected ValueSetValidator constructValueSetCheckerSimple(ValidationOptions options, ValueSet vs, ValidationContextCarrier ctxt) { - return new ValueSetValidator(this, new TerminologyOperationContext(this, options), options, vs, ctxt, expParameters, terminologyClientManager); + return new ValueSetValidator(this, new TerminologyOperationContext(this, options, "validation"), options, vs, ctxt, expParameters, terminologyClientManager); } protected ValueSetValidator constructValueSetCheckerSimple( ValidationOptions options, ValueSet vs) { - return new ValueSetValidator(this, new TerminologyOperationContext(this, options), options, vs, expParameters, terminologyClientManager); + return new ValueSetValidator(this, new TerminologyOperationContext(this, options, "validation"), options, vs, expParameters, terminologyClientManager); } protected Parameters constructParameters(TerminologyClientContext tcd, ValueSet vs, boolean hierarchical) { @@ -1650,7 +1653,11 @@ public ValidationResult validateCode(ValidationOptions options, CodeableConcept Parameters pIn = constructParameters(options, code); res = validateOnServer(tc, vs, pIn, options); } catch (Exception e) { - res = new ValidationResult(IssueSeverity.ERROR, e.getMessage() == null ? e.getClass().getName() : e.getMessage(), null).setTxLink(txLog == null ? null : txLog.getLastId()); + issues.clear(); + OperationOutcomeIssueComponent iss = new OperationOutcomeIssueComponent(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.ERROR, org.hl7.fhir.r5.model.OperationOutcome.IssueType.EXCEPTION); + iss.getDetails().setText(e.getMessage()); + issues.add(iss); + res = new ValidationResult(IssueSeverity.ERROR, e.getMessage() == null ? e.getClass().getName() : e.getMessage(), issues).setTxLink(txLog == null ? null : txLog.getLastId()).setErrorClass(TerminologyServiceErrorClass.SERVER_ERROR); } if (cachingAllowed) { txCache.cacheValidation(cacheToken, res, TerminologyCache.PERMANENT); @@ -1786,6 +1793,7 @@ protected void addServerValidationParameters(TerminologyClientContext terminolog if (options.isDisplayWarningMode()) { pin.addParameter("mode","lenient-display-validation"); } + pin.addParameter("diagnostics", true); } private boolean addDependentResources(TerminologyClientContext tc, Parameters pin, ValueSet vs) { diff --git a/matchbox-engine/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java b/matchbox-engine/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java index 2dd2b784d1b..044f55503b3 100644 --- a/matchbox-engine/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java +++ b/matchbox-engine/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java @@ -42,7 +42,6 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import java.util.List; import java.util.Set; -import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.SAXParser; @@ -95,6 +94,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS public class XmlParser extends ParserBase { private boolean allowXsiLocation; private String version; + private boolean elideElements; public XmlParser(IWorkerContext context) { super(context); @@ -145,12 +145,7 @@ public List parse(InputStream inStream) throws FHIRFormatErro stream.reset(); // use a slower parser that keeps location data - - // MATCHBOX PATCH: xxe protection: https://github.com/ahdis/matchbox/security/code-scanning/45 - TransformerFactory transformerFactory = TransformerFactory.newDefaultInstance(); - transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); - + TransformerFactory transformerFactory = XMLUtil.newXXEProtectedTransformerFactory(); Transformer nullTransformer = transformerFactory.newTransformer(); DocumentBuilder docBuilder = factory.newDocumentBuilder(); doc = docBuilder.newDocument(); @@ -826,97 +821,126 @@ public void compose(Element e, IXMLWriter xml) throws Exception { } private void composeElement(IXMLWriter xml, Element element, String elementName, boolean root) throws IOException, FHIRException { - if (showDecorations) { - @SuppressWarnings("unchecked") - List decorations = (List) element.getUserData("fhir.decorations"); - if (decorations != null) - for (ElementDecoration d : decorations) - xml.decorate(d); - } - for (String s : element.getComments()) { - xml.comment(s, true); + if (!(isElideElements() && element.isElided())) { + if (showDecorations) { + @SuppressWarnings("unchecked") + List decorations = (List) element.getUserData("fhir.decorations"); + if (decorations != null) + for (ElementDecoration d : decorations) + xml.decorate(d); + } + for (String s : element.getComments()) { + xml.comment(s, true); + } } if (isText(element.getProperty())) { - if (linkResolver != null) - xml.link(linkResolver.resolveProperty(element.getProperty())); - xml.enter(element.getProperty().getXmlNamespace(),elementName); - if (linkResolver != null && element.getProperty().isReference()) { - String ref = linkResolver.resolveReference(getReferenceForElement(element)); - if (ref != null) { - xml.externalLink(ref); + if (isElideElements() && element.isElided() && xml.canElide()) + xml.elide(); + else { + if (linkResolver != null) + xml.link(linkResolver.resolveProperty(element.getProperty())); + xml.enter(element.getProperty().getXmlNamespace(),elementName); + if (linkResolver != null && element.getProperty().isReference()) { + String ref = linkResolver.resolveReference(getReferenceForElement(element)); + if (ref != null) { + xml.externalLink(ref); + } } + xml.text(element.getValue()); + xml.exit(element.getProperty().getXmlNamespace(),elementName); } - xml.text(element.getValue()); - xml.exit(element.getProperty().getXmlNamespace(),elementName); } else if (!element.hasChildren() && !element.hasValue()) { - if (element.getExplicitType() != null) - xml.attribute("xsi:type", element.getExplicitType()); - xml.element(elementName); + if (isElideElements() && element.isElided() && xml.canElide()) + xml.elide(); + else { + if (element.getExplicitType() != null) + xml.attribute("xsi:type", element.getExplicitType()); + xml.element(elementName); + } } else if (element.isPrimitive() || (element.hasType() && isPrimitive(element.getType()))) { if (element.getType().equals("xhtml")) { - String rawXhtml = element.getValue(); - if (isCdaText(element.getProperty())) { - new CDANarrativeFormat().convert(xml, new XhtmlParser().parseFragment(rawXhtml)); - } else { - xml.escapedText(rawXhtml); - if (!markedXhtml) { - xml.anchor("end-xhtml"); - markedXhtml = true; + if (isElideElements() && element.isElided() && xml.canElide()) + xml.elide(); + else { + String rawXhtml = element.getValue(); + if (isCdaText(element.getProperty())) { + new CDANarrativeFormat().convert(xml, new XhtmlParser().parseFragment(rawXhtml)); + } else { + xml.escapedText(rawXhtml); + if (!markedXhtml) { + xml.anchor("end-xhtml"); + markedXhtml = true; + } } } } else if (isText(element.getProperty())) { - if (linkResolver != null) - xml.link(linkResolver.resolveProperty(element.getProperty())); - xml.text(element.getValue()); - } else { - setXsiTypeIfIsTypeAttr(xml, element); - if (element.hasValue()) { + if (isElideElements() && element.isElided() && xml.canElide()) + xml.elide(); + else { if (linkResolver != null) - xml.link(linkResolver.resolveType(element.getType())); - xml.attribute("value", element.getValue()); + xml.link(linkResolver.resolveProperty(element.getProperty())); + xml.text(element.getValue()); } - if (linkResolver != null) - xml.link(linkResolver.resolveProperty(element.getProperty())); - if (element.hasChildren()) { - xml.enter(element.getProperty().getXmlNamespace(), elementName); - if (linkResolver != null && element.getProperty().isReference()) { - String ref = linkResolver.resolveReference(getReferenceForElement(element)); - if (ref != null) { - xml.externalLink(ref); - } + } else { + if (isElideElements() && element.isElided()) + xml.attributeElide(); + else { + setXsiTypeIfIsTypeAttr(xml, element); + if (element.hasValue()) { + if (linkResolver != null) + xml.link(linkResolver.resolveType(element.getType())); + xml.attribute("value", element.getValue()); } - for (Element child : element.getChildren()) - composeElement(xml, child, child.getName(), false); - xml.exit(element.getProperty().getXmlNamespace(),elementName); - } else - xml.element(elementName); + if (linkResolver != null) + xml.link(linkResolver.resolveProperty(element.getProperty())); + if (element.hasChildren()) { + xml.enter(element.getProperty().getXmlNamespace(), elementName); + if (linkResolver != null && element.getProperty().isReference()) { + String ref = linkResolver.resolveReference(getReferenceForElement(element)); + if (ref != null) { + xml.externalLink(ref); + } + } + for (Element child : element.getChildren()) + composeElement(xml, child, child.getName(), false); + xml.exit(element.getProperty().getXmlNamespace(),elementName); + } else + xml.element(elementName); + } } } else { - setXsiTypeIfIsTypeAttr(xml, element); - Set handled = new HashSet<>(); + if (isElideElements() && element.isElided() && xml.canElide()) + xml.elide(); + else { + setXsiTypeIfIsTypeAttr(xml, element); + Set handled = new HashSet<>(); for (Element child : element.getChildren()) { if (!handled.contains(child.getName()) && isAttr(child.getProperty()) && wantCompose(element.getPath(), child)) { handled.add(child.getName()); - String av = child.getValue(); - if (child.getProperty().isList()) { - for (Element c2 : element.getChildren()) { - if (c2 != child && c2.getName().equals(child.getName())) { - av = av + " "+c2.getValue(); + if (isElideElements() && child.isElided()) + xml.attributeElide(); + else { + String av = child.getValue(); + if (child.getProperty().isList()) { + for (Element c2 : element.getChildren()) { + if (c2 != child && c2.getName().equals(child.getName())) { + if (c2.isElided()) + av = av + " ..."; + else + av = av + " " + c2.getValue(); + } } - } - } - if (linkResolver != null) - xml.link(linkResolver.resolveType(child.getType())); - if (ToolingExtensions.hasExtension(child.getProperty().getDefinition(), - ToolingExtensions.EXT_DATE_FORMAT)) - av = convertForDateFormatToExternal(ToolingExtensions.readStringExtension(child.getProperty().getDefinition(), - ToolingExtensions.EXT_DATE_FORMAT), - av); - // MATCHBOX PATCH: adjusting it for pharm + } + if (linkResolver != null) + xml.link(linkResolver.resolveType(child.getType())); + if (ToolingExtensions.hasExtension(child.getProperty().getDefinition(), ToolingExtensions.EXT_DATE_FORMAT)) + av = convertForDateFormatToExternal(ToolingExtensions.readStringExtension(child.getProperty().getDefinition(), ToolingExtensions.EXT_DATE_FORMAT), av); + // MATCHBOX PATCH: adjusting it for pharm xml.attribute(child.getProperty().getXmlName(), av); - } - } - // matchbox cda logical model 2.0.0-sd-snapshot1 #196 + } + } + } + } if (!element.getProperty().getDefinition().hasExtension(ToolingExtensions.EXT_ID_CHOICE_GROUP)) { if (linkResolver != null) xml.link(linkResolver.resolveProperty(element.getProperty())); @@ -925,6 +949,7 @@ private void composeElement(IXMLWriter xml, Element element, String elementName, xml.namespace(element.getProperty().getXmlNamespace(), abbrev); } xml.enter(element.getProperty().getXmlNamespace(), elementName); + } if (!root && element.getSpecial() != null) { if (linkResolver != null) @@ -935,17 +960,20 @@ private void composeElement(IXMLWriter xml, Element element, String elementName, String ref = linkResolver.resolveReference(getReferenceForElement(element)); if (ref != null) { xml.externalLink(ref); - } } } for (Element child : element.getChildren()) { if (wantCompose(element.getPath(), child)) { - if (isText(child.getProperty())) { - if (linkResolver != null) - xml.link(linkResolver.resolveProperty(element.getProperty())); - xml.text(child.getValue()); - } else if (!isAttr(child.getProperty())) { - composeElement(xml, child, child.getName(), false); + if (isElideElements() && child.isElided() && xml.canElide()) + xml.elide(); + else { + if (isText(child.getProperty())) { + if (linkResolver != null) + xml.link(linkResolver.resolveProperty(element.getProperty())); + xml.text(child.getValue()); + } else if (!isAttr(child.getProperty())) { + composeElement(xml, child, child.getName(), false); + } } } } @@ -1060,4 +1088,13 @@ public void warning(SAXParseException e) { // do nothing } } + + public boolean isElideElements() { + return elideElements; + } + + public void setElideElements(boolean elideElements) { + this.elideElements = elideElements; + } + } \ No newline at end of file diff --git a/matchbox-engine/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java b/matchbox-engine/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java index adae142b1b2..4ecc686bf69 100644 --- a/matchbox-engine/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java +++ b/matchbox-engine/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java @@ -985,6 +985,17 @@ private void parseRule(StructureMap map, List li rule.getSourceFirstRep().setVariable(AUTO_VAR_NAME); rule.getTargetFirstRep().setVariable(AUTO_VAR_NAME); rule.getTargetFirstRep().setTransform(StructureMapTransform.CREATE); // with no parameter - e.g. imply what is to be created + var dependent = rule.addDependent(); + dependent.setName(StructureMapUtilities.DEF_GROUP_NAME); + dependent.addParameter().setValue(new IdType(AUTO_VAR_NAME)); + dependent.addParameter().setValue(new IdType(AUTO_VAR_NAME)); + + // FMLParser + // Element dep = rule.forceElement("dependent").markLocation(rule); + // dep.makeElement("name").markLocation(rule).setValue(StructureMapUtilities.DEF_GROUP_NAME); + // dep.addElement("parameter").markLocation(dep).makeElement("valueId").markLocation(dep).setValue(StructureMapUtilities.AUTO_VAR_NAME); + // dep.addElement("parameter").markLocation(dep).makeElement("valueId").markLocation(dep).setValue(StructureMapUtilities.AUTO_VAR_NAME); + // no dependencies - imply what is to be done based on types } if (newFmt) { @@ -1081,15 +1092,13 @@ private void parseSource(StructureMapGroupRuleComponent rule, FHIRLexer lexer) t if (lexer.hasToken("check")) { lexer.take(); ExpressionNode node = fpe.parse(lexer); - // matchbox patch https://github.com/hapifhir/org.hl7.fhir.core/issues/1748 source.setUserData(MAP_WHERE_CHECK, node); source.setCheck(node.toString()); } if (lexer.hasToken("log")) { lexer.take(); ExpressionNode node = fpe.parse(lexer); - // matchbox patch https://github.com/hapifhir/org.hl7.fhir.core/issues/1748 - // source.setUserData(MAP_WHERE_LOG, node); + source.setUserData(MAP_WHERE_LOG, node); source.setLogMessage(node.toString()); } } @@ -1652,7 +1661,6 @@ private List processSource(String ruleId, TransformContext context, V } List remove = new ArrayList(); for (Base item : items) { - // matchbox patch https://github.com/hapifhir/org.hl7.fhir.core/issues/1748 Variables varsForSource = vars.copy(); if (src.hasVariable()) { varsForSource.add(VariableMode.INPUT, src.getVariable(), item); @@ -1672,7 +1680,6 @@ private List processSource(String ruleId, TransformContext context, V expr = fpe.parse(src.getCheck()); src.setUserData(MAP_WHERE_CHECK, expr); } - // matchbox patch https://github.com/hapifhir/org.hl7.fhir.core/issues/1748 for (Base item : items) { Variables varsForSource = vars.copy(); if (src.hasVariable()) { @@ -1690,7 +1697,6 @@ private List processSource(String ruleId, TransformContext context, V src.setUserData(MAP_WHERE_LOG, expr); } CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - // matchbox patch https://github.com/hapifhir/org.hl7.fhir.core/issues/1748 for (Base item : items) { Variables varsForSource = vars.copy(); if (src.hasVariable()) { @@ -2546,7 +2552,6 @@ private TypeDetails analyseTransform(TransformContext context, StructureMap map, ExpressionNode expr = (ExpressionNode) tgt.getUserData(MAP_EXPRESSION); if (expr == null) { expr = fpe.parse(getParamString(vars, tgt.getParameter().get(tgt.getParameter().size() - 1))); - // matchbox patch https://github.com/hapifhir/org.hl7.fhir.core/issues/1748 } return fpe.check(vars, null, expr); case TRANSLATE: diff --git a/matchbox-engine/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheManager.java b/matchbox-engine/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheManager.java index 77e3f207eae..c237f3438b5 100644 --- a/matchbox-engine/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheManager.java +++ b/matchbox-engine/src/main/java/org/hl7/fhir/utilities/npm/FilesystemPackageCacheManager.java @@ -8,6 +8,7 @@ import java.util.*; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import lombok.Getter; import lombok.Setter; @@ -76,6 +77,8 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS public class FilesystemPackageCacheManager extends BasePackageCacheManager implements IPackageCacheManager { private final FilesystemPackageCacheManagerLocks locks; + private final FilesystemPackageCacheManagerLocks.LockParameters lockParameters; + private final static String VER_XVER_PROVIDED = "0.0.13"; // When running in testing mode, some packages are provided from the test case repository rather than by the normal means @@ -83,7 +86,6 @@ public class FilesystemPackageCacheManager extends BasePackageCacheManager imple // then the normal means will be used public interface IPackageProvider { boolean handlesPackage(String id, String version); - InputStreamWithSrc provide(String id, String version) throws IOException; } @@ -93,6 +95,7 @@ public interface IPackageProvider { public static final String PACKAGE_VERSION_REGEX_OPT = "^[A-Za-z][A-Za-z0-9\\_\\-]*(\\.[A-Za-z0-9\\_\\-]+)+(\\#[A-Za-z0-9\\-\\_]+(\\.[A-Za-z0-9\\-\\_]+)*)?$"; private static final Logger ourLog = LoggerFactory.getLogger(FilesystemPackageCacheManager.class); private static final String CACHE_VERSION = "3"; // second version - see wiki page + @Nonnull private final File cacheFolder; @@ -101,6 +104,7 @@ public interface IPackageProvider { private final Map ciList = new HashMap<>(); private JsonArray buildInfo; private boolean suppressErrors; + @Setter @Getter private boolean minimalMemory; @@ -114,9 +118,20 @@ public static class Builder { @Getter private final List packageServers; + @With + @Getter + private final FilesystemPackageCacheManagerLocks.LockParameters lockParameters; + public Builder() throws IOException { this.cacheFolder = getUserCacheFolder(); this.packageServers = getPackageServersFromFHIRSettings(); + this.lockParameters = null; + } + + private Builder(File cacheFolder, List packageServers, FilesystemPackageCacheManagerLocks.LockParameters lockParameters) { + this.cacheFolder = cacheFolder; + this.packageServers = packageServers; + this.lockParameters = lockParameters; } private File getUserCacheFolder() throws IOException { @@ -144,17 +159,12 @@ protected List getConfiguredServers() { return PackageServer.getConfiguredServers(); } - private Builder(File cacheFolder, List packageServers) { - this.cacheFolder = cacheFolder; - this.packageServers = packageServers; - } - public Builder withCacheFolder(String cacheFolderPath) throws IOException { File cacheFolder = ManagedFileAccess.file(cacheFolderPath); if (!cacheFolder.exists()) { throw new FHIRException("The folder '" + cacheFolder + "' could not be found"); } - return new Builder(cacheFolder, this.packageServers); + return new Builder(cacheFolder, this.packageServers, this.lockParameters); } public Builder withSystemCacheFolder() throws IOException { @@ -164,32 +174,33 @@ public Builder withSystemCacheFolder() throws IOException { } else { systemCacheFolder = ManagedFileAccess.file(Utilities.path("/var", "lib", ".fhir", "packages")); } - return new Builder(systemCacheFolder, this.packageServers); + return new Builder(systemCacheFolder, this.packageServers, this.lockParameters); } public Builder withTestingCacheFolder() throws IOException { - return new Builder(ManagedFileAccess.file(Utilities.path("[tmp]", ".fhir", "packages")), this.packageServers); + return new Builder(ManagedFileAccess.file(Utilities.path("[tmp]", ".fhir", "packages")), this.packageServers, this.lockParameters); } public FilesystemPackageCacheManager build() throws IOException { - return new FilesystemPackageCacheManager(cacheFolder, packageServers); + final FilesystemPackageCacheManagerLocks locks; + try { + locks = FilesystemPackageCacheManagerLocks.getFilesystemPackageCacheManagerLocks(cacheFolder); + } catch (RuntimeException e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } else { + throw e; + } + } + return new FilesystemPackageCacheManager(cacheFolder, packageServers, locks, lockParameters); } } - private FilesystemPackageCacheManager(@Nonnull File cacheFolder, @Nonnull List packageServers) throws IOException { + private FilesystemPackageCacheManager(@Nonnull File cacheFolder, @Nonnull List packageServers, @Nonnull FilesystemPackageCacheManagerLocks locks, @Nullable FilesystemPackageCacheManagerLocks.LockParameters lockParameters) throws IOException { super(packageServers); this.cacheFolder = cacheFolder; - - try { - this.locks = FilesystemPackageCacheManagerLocks.getFilesystemPackageCacheManagerLocks(cacheFolder); - } catch (RuntimeException e) { - if (e.getCause() instanceof IOException) { - throw (IOException) e.getCause(); - } else { - throw e; - } - } - + this.locks = locks; + this.lockParameters = lockParameters; prepareCacheFolder(); } @@ -211,26 +222,54 @@ protected void prepareCacheFolder() throws IOException { Utilities.createDirectory(cacheFolder.getAbsolutePath()); createIniFile(); } else { - if (!isCacheFolderValid()) { + if (!iniFileExists()) { + createIniFile(); + } + if (!isIniFileCurrentVersion()) { clearCache(); createIniFile(); - } else { - deleteOldTempDirectories(); } + deleteOldTempDirectories(); + cleanUpCorruptPackages(); } return null; }); } - private boolean isCacheFolderValid() throws IOException { + /* + Look for .lock files that are not actively held by a process. If found, delete the lock file, and the package + referenced. + */ + protected void cleanUpCorruptPackages() throws IOException { + for (File file : Objects.requireNonNull(cacheFolder.listFiles())) { + if (file.getName().endsWith(".lock")) { + if (locks.getCacheLock().canLockFileBeHeldByThisProcess(file)) { + String packageDirectoryName = file.getName().substring(0, file.getName().length() - 5); + log("Detected potential incomplete package installed in cache: " + packageDirectoryName + ". Attempting to delete"); + + File packageDirectory = ManagedFileAccess.file(Utilities.path(cacheFolder, packageDirectoryName)); + if (packageDirectory.exists()) { + Utilities.clearDirectory(packageDirectory.getAbsolutePath()); + packageDirectory.delete(); + } + file.delete(); + log("Deleted potential incomplete package: " + packageDirectoryName); + } + } + } + } + + private boolean iniFileExists() throws IOException { String iniPath = getPackagesIniPath(); File iniFile = ManagedFileAccess.file(iniPath); - if (!(iniFile.exists())) { - return false; - } + return iniFile.exists(); + } + + private boolean isIniFileCurrentVersion() throws IOException { + String iniPath = getPackagesIniPath(); IniFile ini = new IniFile(iniPath); - String v = ini.getStringProperty("cache", "version"); - return CACHE_VERSION.equals(v); + String version = ini.getStringProperty("cache", "version"); + return CACHE_VERSION.equals(version); } private void deleteOldTempDirectories() throws IOException { @@ -441,7 +480,7 @@ public void removePackage(String id, String version) throws IOException { } return null; - }); + }, lockParameters); } /** @@ -457,7 +496,7 @@ public void removePackage(String id, String version) throws IOException { * @throws IOException If the package cannot be loaded */ @Override - public NpmPackage loadPackageFromCacheOnly(String id, String version) throws IOException { + public NpmPackage loadPackageFromCacheOnly(String id, @Nullable String version) throws IOException { if (!Utilities.noString(version) && version.startsWith("file:")) { return loadPackageFromFile(id, version.substring(5)); @@ -485,7 +524,7 @@ public NpmPackage loadPackageFromCacheOnly(String id, String version) throws IOE return null; } return loadPackageInfo(path); - }); + }, lockParameters); if (foundPackage != null) { if (foundPackage.isIndexed()){ return foundPackage; @@ -508,7 +547,7 @@ public NpmPackage loadPackageFromCacheOnly(String id, String version) throws IOE String path = Utilities.path(cacheFolder, foundPackageFolder); output.checkIndexed(path); return output; - }); + }, lockParameters); } } } @@ -620,7 +659,7 @@ public NpmPackage addPackageToCache(String id, final String version, InputStream throw e; } return npmPackage; - }); + }, lockParameters); } private void log(String s) { diff --git a/matchbox-engine/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java b/matchbox-engine/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java index 66ce63caf48..5011558b225 100644 --- a/matchbox-engine/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java +++ b/matchbox-engine/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java @@ -237,9 +237,11 @@ public boolean readIndex(JsonObject index, Map> typeMap) { public List listFiles() { List res = new ArrayList<>(); if (folder != null) { - for (File f : folder.listFiles()) { - if (!f.isDirectory() && !Utilities.existsInList(f.getName(), "package.json", ".index.json", ".index.db", ".oids.json", ".oids.db")) { - res.add(f.getName()); + if (folder.exists()) { + for (File f : folder.listFiles()) { + if (!f.isDirectory() && !Utilities.existsInList(f.getName(), "package.json", ".index.json", ".index.db", ".oids.json", ".oids.db")) { + res.add(f.getName()); + } } } } else { @@ -648,7 +650,17 @@ public void checkIndexed(String desc) throws IOException { } - + /** + * Create a package .index.json file for a package folder. + *

+ * See the FHIR specification for details on .index.json + * format and usage. + * + * @param desc + * @param folder + * @throws FileNotFoundException + * @throws IOException + */ public void indexFolder(String desc, NpmPackageFolder folder) throws FileNotFoundException, IOException { List remove = new ArrayList<>(); NpmPackageIndexBuilder indexer = new NpmPackageIndexBuilder(); diff --git a/matchbox-engine/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/matchbox-engine/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index d50245e7bbb..7ac23a53e2e 100644 --- a/matchbox-engine/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/matchbox-engine/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -46,7 +46,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.StringUtils; import org.fhir.ucum.Decimal; -import org.hl7.elm.r1.Code; + import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.PathEngineException; @@ -157,6 +157,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.r5.utils.XVerExtensionManager; import org.hl7.fhir.r5.utils.XVerExtensionManager.XVerExtensionStatus; import org.hl7.fhir.r5.utils.sql.Validator; +import org.hl7.fhir.r5.utils.sql.Validator.TrueFalseOrUnknown; import org.hl7.fhir.r5.utils.validation.BundleValidationRule; import org.hl7.fhir.r5.utils.validation.IMessagingServices; import org.hl7.fhir.r5.utils.validation.IResourceValidator; @@ -201,6 +202,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.validation.cli.utils.QuestionnaireMode; import org.hl7.fhir.validation.codesystem.CodingsObserver; import org.hl7.fhir.validation.instance.InstanceValidator.BindingContext; +import org.hl7.fhir.validation.instance.advisor.BasePolicyAdvisorForFullValidation; import org.hl7.fhir.validation.instance.type.BundleValidator; import org.hl7.fhir.validation.instance.type.CodeSystemValidator; import org.hl7.fhir.validation.instance.type.ConceptMapValidator; @@ -217,6 +219,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.validation.instance.utils.*; import org.w3c.dom.Document; + /** * Thinking of using this in a java program? Don't! * You should use one of the wrappers instead. Either in HAPI, or use ValidationEngine @@ -599,6 +602,7 @@ public FHIRPathEngine getFHIRPathEngine() { private IDigitalSignatureServices signatureServices; private ContextUtilities cu; private boolean unknownCodeSystemsCauseErrors; + private boolean noExperimentalContent; public InstanceValidator(@Nonnull IWorkerContext theContext, @Nonnull IEvaluationContext hostServices, @Nonnull XVerExtensionManager xverManager) { super(theContext, xverManager, false); @@ -1355,19 +1359,20 @@ private boolean checkCodeableConcept(List errors, String path BindingStrength strength = binding.getStrength(); Extension maxVS = binding.getExtensionByUrl(ToolingExtensions.EXT_MAX_VALUESET); - checkDisp = validateBindingCodeableConcept(errors, path, element, profile, stack, bh, checkDisp, checked, cc, vsRef, valueset, strength, maxVS, true); + checkDisp = validateBindingCodeableConcept(errors, path, element, profile, stack, bh, checkDisp, checked, cc, vsRef, valueset, strength, maxVS, true, null); // } else if (binding.hasValueSet()) { // hint(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_BINDING_CANTCHECK); } else if (!noBindingMsgSuppressed) { hint(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_BINDING_NOSOURCE, path); } for (ElementDefinitionBindingAdditionalComponent ab : binding.getAdditional()) { - if (isTestableBinding(ab) && isInScope(ab)) { + StringBuilder b = new StringBuilder(); + if (isTestableBinding(ab) && isInScope(ab, profile, getResource(stack), b)) { String vsRef = ab.getValueSet(); ValueSet valueset = resolveBindingReference(profile, vsRef, profile.getUrl(), profile); BindingStrength strength = convertPurposeToStrength(ab.getPurpose()); - checkDisp = validateBindingCodeableConcept(errors, path, element, profile, stack, bh, checkDisp, checked, cc, vsRef, valueset, strength, null, false) && checkDisp; + checkDisp = validateBindingCodeableConcept(errors, path, element, profile, stack, bh, checkDisp, checked, cc, vsRef, valueset, strength, null, false, b.toString()) && checkDisp; } } } catch (CheckCodeOnServerException e) { @@ -1393,25 +1398,186 @@ private boolean checkCodeableConcept(List errors, String path return checkDisp; } - private boolean isInScope(ElementDefinitionBindingAdditionalComponent ab) { + private boolean isInScope(ElementDefinitionBindingAdditionalComponent ab, StructureDefinition profile, Element resource, StringBuilder b) { + if (ab.getUsage().isEmpty()) { + return true; + } + boolean ok = true; for (UsageContext usage : ab.getUsage()) { - if (isInScope(usage)) { - return true; + if (!isInScope(usage, profile, resource, b)) { + ok = false; } } - return ab.getUsage().isEmpty(); + return ok; } - private boolean isInScope(UsageContext usage) { + private boolean isInScope(UsageContext usage, StructureDefinition profile, Element resource, StringBuilder b) { if (isKnownUsage(usage)) { return true; } + if (usage.getCode().hasSystem() && (usage.getCode().getSystem().equals(profile.getUrl()) || usage.getCode().getSystem().equals(profile.getVersionedUrl()))) { + // if it's not a defined usage from external sources, it might match something in the data content + List items = findDataValue(resource, usage.getCode().getCode()); + if (matchesUsage(items, usage.getValue())) { + b.append(context.formatMessage(I18nConstants.BINDING_ADDITIONAL_USAGE, displayCoding(usage.getCode()), display(usage.getValue()))); + return true; + } + } + return false; + } + + private String displayCoding(Coding value) { + return value.getCode(); + } + + private String displayCodeableConcept(CodeableConcept value) { + for (Coding c : value.getCoding()) { + String s = displayCoding(c); + if (s != null) { + return s; + } + } + return value.getText(); + } + + private String display(DataType value) { + switch (value.fhirType()) { + case "Coding" : return displayCoding((Coding) value); + case "CodeableConcept" : return displayCodeableConcept((CodeableConcept) value); + } + return value.fhirType(); + } + + private boolean matchesUsage(List items, DataType value) { + for (Element item : items) { + if (matchesUsage(item, value)) { + return true; + } + } return false; } + + + private String display(List items) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (Element item : items) { + display(b, item); + } + return b.toString(); + } + + private void display(CommaSeparatedStringBuilder b, Element item) { + if (item.isPrimitive()) { + b.append(item.primitiveValue()); + } else if (item.fhirType().equals("CodeableConcept")) { + for (Element c : item.getChildren("coding")) { + b.append(c.getNamedChildValue("code")); + } + } else { + b.append(item.toString()); + } + + } + + private boolean matchesUsage(Element item, DataType value) { + switch (value.fhirType()) { + case "CodeableConcept": return matchesUsageCodeableConcept(item, (CodeableConcept) value); + case "Quantity": return false; + case "Range": return false; + case "Reference": return false; + default: return false; + } + } + + private boolean matchesUsageCodeableConcept(Element item, CodeableConcept value) { + switch (item.fhirType()) { + case "CodeableConcept": return matchesUsageCodeableConceptCodeableConcept(item, value); + case "Coding": return matchesUsageCodeableConceptCoding(item, value); + default: return false; + } + } + + private boolean matchesUsageCodeableConceptCoding(Element item, CodeableConcept value) { + String system = item.getNamedChildValue("system"); + String version = item.getNamedChildValue("version"); + String code = item.getNamedChildValue("code"); + for (Coding c : value.getCoding()) { + if (system == null || !system.equals(c.getSystem())) { + return false; + } + if (code == null || !code.equals(c.getCode())) { + return false; + } + if (c.hasVersion()) { + if (version == null || !version.equals(c.getVersion())) { + return false; + } + } + return true; + } + return false; + } + + private boolean matchesUsageCodeableConceptCodeableConcept(Element item, CodeableConcept value) { + for (Element code : item.getChildren("coding")) { + if (matchesUsageCodeableConceptCoding(code, value)) { + return true; + } + } + return false; + } + + private List findDataValue(Element resource, String code) { + List items = new ArrayList(); + if (resource != null) { + findDataValues(items, resource, code); + } + return items; + } + + private void findDataValues(List items, Element element, String path) { + if (element.getPath() == null) { + return; + } + if (pathMatches(element.getPath(), path)) { + items.add(element); + } else if (element.hasChildren() && path.startsWith(element.getPath())) { + for (Element child : element.getChildren()) { + findDataValues(items, child, path); + } + } + } + + private boolean pathMatches(String actualPath, String pathSpec) { + String[] ap = actualPath.split("\\."); + String[] ps = pathSpec.split("\\."); + if (ap.length != ps.length) { + return false; + } + for (int i = 0; i < ap.length; i++) { + if (!pathSegmentMatches(ap[i], ps[i])) { + return false; + } + } + return true; + } + + private boolean pathSegmentMatches(String ap, String ps) { + if (ps.contains("[")) { + return ap.equals(ps); + } else { + if (ap.contains("[")) { + ap = ap.substring(0, ap.indexOf("[")); + } + return ap.equals(ps); + } + } + private BindingStrength convertPurposeToStrength(AdditionalBindingPurposeVS purpose) { switch (purpose) { case MAXIMUM: return BindingStrength.REQUIRED; + case EXTENSIBLE: return BindingStrength.EXTENSIBLE; case PREFERRED: return BindingStrength.PREFERRED; case REQUIRED: return BindingStrength.REQUIRED; default: return null; @@ -1423,7 +1589,7 @@ private boolean isTestableBinding(ElementDefinitionBindingAdditionalComponent ab } private boolean validateBindingCodeableConcept(List errors, String path, Element element, StructureDefinition profile, NodeStack stack, BooleanHolder bh, boolean checkDisp, BooleanHolder checked, - CodeableConcept cc, String vsRef, ValueSet valueset, BindingStrength strength, Extension maxVS, boolean base) throws CheckCodeOnServerException { + CodeableConcept cc, String vsRef, ValueSet valueset, BindingStrength strength, Extension maxVS, boolean base, String usageNote) throws CheckCodeOnServerException { if (valueset == null) { CodeSystem cs = context.fetchCodeSystem(vsRef); if (rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, cs == null, I18nConstants.TERMINOLOGY_TX_VALUESET_NOTFOUND_CS, describeReference(vsRef))) { @@ -1435,12 +1601,12 @@ private boolean validateBindingCodeableConcept(List errors, S BindingContext bc = base ? BindingContext.BASE : BindingContext.ADDITIONAL; if (!cc.hasCoding()) { if (strength == BindingStrength.REQUIRED) - bh.see(rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CODE_VALUESET, describeReference(vsRef, valueset, bc))); + bh.see(rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CODE_VALUESET, describeReference(vsRef, valueset, bc, usageNote))); else if (strength == BindingStrength.EXTENSIBLE) { if (maxVS != null) bh.see(rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CODE_VALUESETMAX, describeReference(ToolingExtensions.readStringFromExtension(maxVS)), valueset.getVersionedUrl())); else if (!noExtensibleWarnings) { - warning(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CODE_VALUESET_EXT, describeReference(vsRef, valueset, bc)); + warning(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CODE_VALUESET_EXT, describeReference(vsRef, valueset, bc, usageNote)); } } } else { @@ -1490,17 +1656,17 @@ else if (!noExtensibleWarnings) } } else if (vr.getErrorClass() != null && vr.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED) { // we've already handled the warnings / errors about this, and set the status correctly. We don't need to do anything more? - } else { + } else if (vr.getErrorClass() != TerminologyServiceErrorClass.SERVER_ERROR) { // (should have?) already handled server error if (strength == BindingStrength.REQUIRED) { - bh.see(txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_1_CC, describeReference(vsRef, valueset, bc), ccSummary(cc))); + bh.see(txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_1_CC, describeReference(vsRef, valueset, bc, usageNote), ccSummary(cc))); } else if (strength == BindingStrength.EXTENSIBLE) { if (maxVS != null) bh.see(checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringFromExtension(maxVS), cc, stack)); if (!noExtensibleWarnings) - txWarningForLaterRemoval(element, errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_2_CC, describeReference(vsRef, valueset, bc), ccSummary(cc)); + txWarningForLaterRemoval(element, errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_2_CC, describeReference(vsRef, valueset, bc, usageNote), ccSummary(cc)); } else if (strength == BindingStrength.PREFERRED) { if (baseOnly) { - txHint(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_3_CC, describeReference(vsRef, valueset, bc), ccSummary(cc)); + txHint(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_3_CC, describeReference(vsRef, valueset, bc, usageNote), ccSummary(cc)); } } } @@ -1647,7 +1813,7 @@ private boolean checkCDACodeableConcept(List errors, String p BindingStrength strength = binding.getStrength(); Extension vsMax = binding.getExtensionByUrl(ToolingExtensions.EXT_MAX_VALUESET); - validateBindingCodeableConcept(errors, path, element, profile, stack, ok, false, new BooleanHolder(), cc, vsRef, valueset, strength, vsMax, true); + validateBindingCodeableConcept(errors, path, element, profile, stack, ok, false, new BooleanHolder(), cc, vsRef, valueset, strength, vsMax, true, null); // special case: if the logical model has both CodeableConcept and Coding mappings, we'll also check the first coding. if (getMapping("http://hl7.org/fhir/terminology-pattern", logical, logical.getSnapshot().getElementFirstRep()).contains("Coding")) { @@ -1659,11 +1825,12 @@ private boolean checkCDACodeableConcept(List errors, String p hint(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_BINDING_NOSOURCE, path); } for (ElementDefinitionBindingAdditionalComponent ab : binding.getAdditional()) { - if (isTestableBinding(ab) && isInScope(ab)) { + StringBuilder b = new StringBuilder(); + if (isTestableBinding(ab) && isInScope(ab, profile, getResource(stack), b)) { String vsRef = ab.getValueSet(); ValueSet valueset = resolveBindingReference(profile, vsRef, profile.getUrl(), profile); BindingStrength strength = convertPurposeToStrength(ab.getPurpose()); - validateBindingCodeableConcept(errors, path, element, profile, stack, ok, false, new BooleanHolder(), cc, vsRef, valueset, strength, null, false); + validateBindingCodeableConcept(errors, path, element, profile, stack, ok, false, new BooleanHolder(), cc, vsRef, valueset, strength, null, false, b.toString()); } } } @@ -1697,18 +1864,19 @@ private boolean checkTerminologyCoding(List errors, String pa BindingStrength strength = binding.getStrength(); Extension vsMax = binding.getExtensionByUrl(ToolingExtensions.EXT_MAX_VALUESET); - ok = validateBindingTerminologyCoding(errors, path, element, profile, stack, ok, c, code, system, display, vsRef, valueset, strength, vsMax, true); + ok = validateBindingTerminologyCoding(errors, path, element, profile, stack, ok, c, code, system, display, vsRef, valueset, strength, vsMax, true, null); } else if (binding.hasValueSet()) { hint(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_BINDING_CANTCHECK); } else if (!inCodeableConcept && !noBindingMsgSuppressed) { hint(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_BINDING_NOSOURCE, path); } for (ElementDefinitionBindingAdditionalComponent ab : binding.getAdditional()) { - if (isTestableBinding(ab) && isInScope(ab)) { + StringBuilder b = new StringBuilder(); + if (isTestableBinding(ab) && isInScope(ab, profile, getResource(stack), b)) { String vsRef = ab.getValueSet(); ValueSet valueset = resolveBindingReference(profile, vsRef, profile.getUrl(), profile); BindingStrength strength = convertPurposeToStrength(ab.getPurpose()); - ok = validateBindingTerminologyCoding(errors, path, element, profile, stack, ok, c, code, system, display, vsRef, valueset, strength, null, true) && ok; + ok = validateBindingTerminologyCoding(errors, path, element, profile, stack, ok, c, code, system, display, vsRef, valueset, strength, null, true, b.toString()) && ok; } } } @@ -1727,7 +1895,7 @@ private boolean checkTerminologyCoding(List errors, String pa private boolean validateBindingTerminologyCoding(List errors, String path, Element element, StructureDefinition profile, NodeStack stack, boolean ok, Coding c, String code, String system, String display, - String vsRef, ValueSet valueset, BindingStrength strength, Extension vsMax, boolean base) { + String vsRef, ValueSet valueset, BindingStrength strength, Extension vsMax, boolean base, String usageNote) { if (valueset == null) { CodeSystem cs = context.fetchCodeSystem(vsRef); if (rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, cs == null, I18nConstants.TERMINOLOGY_TX_VALUESET_NOTFOUND_CS, describeReference(vsRef))) { @@ -1753,27 +1921,27 @@ private boolean validateBindingTerminologyCoding(List errors, txHint(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_BINDING_NOSERVER, system+"#"+code); else if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) { if (strength == BindingStrength.REQUIRED) - txWarning(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CONFIRM_4a, describeReference(vsRef, valueset, bc), vr.getMessage(), system+"#"+code); + txWarning(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CONFIRM_4a, describeReference(vsRef, valueset, bc, usageNote), vr.getMessage(), system+"#"+code); else if (strength == BindingStrength.EXTENSIBLE) { if (vsMax != null) checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringFromExtension(vsMax), c, stack); else if (!noExtensibleWarnings) - txWarningForLaterRemoval(element, errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CONFIRM_5, describeReference(vsRef, valueset, bc)); + txWarningForLaterRemoval(element, errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CONFIRM_5, describeReference(vsRef, valueset, bc, usageNote)); } else if (strength == BindingStrength.PREFERRED) { if (baseOnly) { - txHint(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CONFIRM_6, describeReference(vsRef, valueset, bc)); + txHint(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CONFIRM_6, describeReference(vsRef, valueset, bc, usageNote)); } } } else if (strength == BindingStrength.REQUIRED) - ok= txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_4, describeReference(vsRef, valueset, bc), (vr.getMessage() != null ? " (error message = " + vr.getMessage() + ")" : ""), system+"#"+code) && ok; + ok= txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_4, describeReference(vsRef, valueset, bc, usageNote), (vr.getMessage() != null ? " (error message = " + vr.getMessage() + ")" : ""), system+"#"+code) && ok; else if (strength == BindingStrength.EXTENSIBLE) { if (vsMax != null) ok = checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringFromExtension(vsMax), c, stack) && ok; else - txWarning(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_5, describeReference(vsRef, valueset, bc), (vr.getMessage() != null ? " (error message = " + vr.getMessage() + ")" : ""), system+"#"+code); + txWarning(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_5, describeReference(vsRef, valueset, bc, usageNote), (vr.getMessage() != null ? " (error message = " + vr.getMessage() + ")" : ""), system+"#"+code); } else if (strength == BindingStrength.PREFERRED) { if (baseOnly) { - txHint(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_6, describeReference(vsRef, valueset, bc), (vr.getMessage() != null ? " (error message = " + vr.getMessage() + ")" : ""), system+"#"+code); + txHint(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_6, describeReference(vsRef, valueset, bc, usageNote), (vr.getMessage() != null ? " (error message = " + vr.getMessage() + ")" : ""), system+"#"+code); } } } else if (vr != null && vr.getMessage() != null){ @@ -1895,9 +2063,9 @@ private boolean checkMaxValueSet(List errors, String path, El timeTracker.tx(t, "vc "+cc.toString()); if (!vr.isOk()) { if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) - txWarning(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_7, describeReference(maxVSUrl, valueset, BindingContext.MAXVS), vr.getMessage()); + txWarning(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_7, describeReference(maxVSUrl, valueset, BindingContext.MAXVS, null), vr.getMessage()); else - ok = txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_8, describeReference(maxVSUrl, valueset, BindingContext.MAXVS), ccSummary(cc)) && ok; + ok = txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_8, describeReference(maxVSUrl, valueset, BindingContext.MAXVS, null), ccSummary(cc)) && ok; } } catch (CheckCodeOnServerException e) { if (STACK_TRACE) e.getCause().printStackTrace(); @@ -1935,9 +2103,9 @@ private boolean checkMaxValueSet(List errors, String path, El timeTracker.tx(t, "vc "+c.getSystem()+"#"+c.getCode()+" '"+c.getDisplay()+"'"); if (!vr.isOk()) { if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) - txWarning(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_9, describeReference(maxVSUrl, valueset, BindingContext.MAXVS), vr.getMessage(), c.getSystem()+"#"+c.getCode()); + txWarning(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_9, describeReference(maxVSUrl, valueset, BindingContext.MAXVS, null), vr.getMessage(), c.getSystem()+"#"+c.getCode()); else - ok = txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_10, describeReference(maxVSUrl, valueset, BindingContext.MAXVS), c.getSystem(), c.getCode()) && ok; + ok = txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_10, describeReference(maxVSUrl, valueset, BindingContext.MAXVS, null), c.getSystem(), c.getCode()) && ok; } } catch (Exception e) { if (STACK_TRACE) e.printStackTrace(); @@ -1965,9 +2133,9 @@ private boolean checkMaxValueSet(List errors, String path, El timeTracker.tx(t, "vc "+value); if (!vr.isOk()) { if (vr.getErrorClass() != null && vr.getErrorClass().isInfrastructure()) - txWarning(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_9, describeReference(maxVSUrl, valueset, BindingContext.BASE), vr.getMessage(), value); + txWarning(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_9, describeReference(maxVSUrl, valueset, BindingContext.BASE, null), vr.getMessage(), value); else { - ok = txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_11, describeReference(maxVSUrl, valueset, BindingContext.BASE), vr.getMessage()) && ok; + ok = txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_11, describeReference(maxVSUrl, valueset, BindingContext.BASE, null), vr.getMessage()) && ok; } } } catch (Exception e) { @@ -2028,7 +2196,7 @@ private boolean checkCodedElement(List errors, String path, E BindingStrength strength = binding.getStrength(); Extension vsMax = binding.getExtensionByUrl(ToolingExtensions.EXT_MAX_VALUESET); - ok = validateBindingCodedElement(errors, path, element, profile, stack, theCode, theSystem, ok, checked, c, vsRef, valueset, strength, vsMax, true); + ok = validateBindingCodedElement(errors, path, element, profile, stack, theCode, theSystem, ok, checked, c, vsRef, valueset, strength, vsMax, true, null); // } else if (binding.hasValueSet()) { // hint(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_BINDING_CANTCHECK); @@ -2037,12 +2205,13 @@ private boolean checkCodedElement(List errors, String path, E } for (ElementDefinitionBindingAdditionalComponent ab : binding.getAdditional()) { - if (isTestableBinding(ab) && isInScope(ab)) { + StringBuilder b = new StringBuilder(); + if (isTestableBinding(ab) && isInScope(ab, profile, getResource(stack), b)) { String vsRef = ab.getValueSet(); ValueSet valueset = resolveBindingReference(profile, vsRef, profile.getUrl(), profile); BindingStrength strength = convertPurposeToStrength(ab.getPurpose()); - ok = validateBindingCodedElement(errors, path, element, profile, stack, theCode, theSystem, ok, checked, c, vsRef, valueset, strength, null, false) && ok; + ok = validateBindingCodedElement(errors, path, element, profile, stack, theCode, theSystem, ok, checked, c, vsRef, valueset, strength, null, false, b.toString()) && ok; } } } catch (Exception e) { @@ -2067,9 +2236,19 @@ private boolean checkCodedElement(List errors, String path, E return ok; } + private Element getResource(NodeStack stack) { + if (stack.getElement().isResource()) { + return stack.getElement(); + } + if (stack.getParent() == null) { + return null; + } + return getResource(stack.getParent()); + } + private boolean validateBindingCodedElement(List errors, String path, Element element, StructureDefinition profile, NodeStack stack, String theCode, String theSystem, boolean ok, BooleanHolder checked, - Coding c, String vsRef, ValueSet valueset, BindingStrength strength, Extension vsMax, boolean base) { + Coding c, String vsRef, ValueSet valueset, BindingStrength strength, Extension vsMax, boolean base, String usageNote) { if (valueset == null) { CodeSystem cs = context.fetchCodeSystem(vsRef); if (rule(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, cs == null, I18nConstants.TERMINOLOGY_TX_VALUESET_NOTFOUND_CS, describeReference(vsRef))) { @@ -2097,28 +2276,28 @@ private boolean validateBindingCodedElement(List errors, Stri txHint(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_BINDING_NOSERVER, theSystem+"#"+theCode); else if (vr.getErrorClass() != null && !vr.getErrorClass().isInfrastructure()) { if (strength == BindingStrength.REQUIRED) - ok = txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CONFIRM_4a, describeReference(vsRef, valueset, bc), vr.getMessage(), theSystem+"#"+theCode) && ok; + ok = txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CONFIRM_4a, describeReference(vsRef, valueset, bc, usageNote), vr.getMessage(), theSystem+"#"+theCode) && ok; else if (strength == BindingStrength.EXTENSIBLE) { if (vsMax != null) checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringFromExtension(vsMax), c, stack); else if (!noExtensibleWarnings) - txWarningForLaterRemoval(element, errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CONFIRM_5, describeReference(vsRef, valueset, bc), theSystem+"#"+theCode); + txWarningForLaterRemoval(element, errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CONFIRM_5, describeReference(vsRef, valueset, bc, usageNote), theSystem+"#"+theCode); } else if (strength == BindingStrength.PREFERRED) { if (baseOnly) { - txHint(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CONFIRM_6, describeReference(vsRef, valueset, bc), theSystem+"#"+theCode); + txHint(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_CONFIRM_6, describeReference(vsRef, valueset, bc, usageNote), theSystem+"#"+theCode); } } } else if (strength == BindingStrength.REQUIRED) - ok = txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_12, describeReference(vsRef, valueset, bc), getErrorMessage(vr.getMessage()), theSystem+"#"+theCode) && ok; + ok = txRule(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_12, describeReference(vsRef, valueset, bc, usageNote), getErrorMessage(vr.getMessage()), theSystem+"#"+theCode) && ok; else if (strength == BindingStrength.EXTENSIBLE) { if (vsMax != null) ok = checkMaxValueSet(errors, path, element, profile, ToolingExtensions.readStringFromExtension(vsMax), c, stack) && ok; else if (!noExtensibleWarnings) { - txWarningForLaterRemoval(element, errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_13, describeReference(vsRef, valueset, bc), getErrorMessage(vr.getMessage()), c.getSystem()+"#"+c.getCode()); + txWarningForLaterRemoval(element, errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_13, describeReference(vsRef, valueset, bc, usageNote), getErrorMessage(vr.getMessage()), c.getSystem()+"#"+c.getCode()); } } else if (strength == BindingStrength.PREFERRED) { if (baseOnly) { - txHint(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_14, describeReference(vsRef, valueset, bc), getErrorMessage(vr.getMessage()), theSystem+"#"+theCode); + txHint(errors, NO_RULE_DATE, vr.getTxLink(), IssueType.CODEINVALID, element.line(), element.col(), path, false, I18nConstants.TERMINOLOGY_TX_NOVALID_14, describeReference(vsRef, valueset, bc, usageNote), getErrorMessage(vr.getMessage()), theSystem+"#"+theCode); } } } else if (vr != null && vr.getMessage() != null) { @@ -3016,7 +3195,7 @@ else if (Utilities.isAllWhitespace(e.primitiveValue())) // check that no illegal elements and attributes have been used ok = checkInnerNames(errors, e, path, xhtml.getChildNodes(), false) && ok; ok = checkUrls(errors, e, path, xhtml.getChildNodes()) && ok; - ok = checkIdRefs(errors, e, path, xhtml, resource) && ok; + ok = checkIdRefs(errors, e, path, xhtml, resource, node) && ok; if (true) { ok = checkReferences(valContext, errors, e, path, "div", xhtml, resource) && ok; } @@ -3471,11 +3650,11 @@ private boolean checkImageSources(ValidationContext valContext, List errors, Element e, String path, XhtmlNode node, Element resource) { + private boolean checkIdRefs(List errors, Element e, String path, XhtmlNode node, Element resource, NodeStack stack) { boolean ok = true; if (node.getNodeType() == NodeType.Element && node.getAttribute("idref") != null) { String idref = node.getAttribute("idref"); - int count = countFragmentMatches(resource, idref); + int count = countFragmentMatches(resource, idref, stack); if (count == 0) { ok = warning(errors, "2023-12-01", IssueType.INVALID, e.line(), e.col(), path, idref == null, I18nConstants.XHTML_IDREF_NOT_FOUND, idref) && ok; } else if (count > 1) { @@ -3484,7 +3663,7 @@ private boolean checkIdRefs(List errors, Element e, String pa } if (node.hasChildren()) { for (XhtmlNode child : node.getChildNodes()) { - checkIdRefs(errors, e, path, child, resource); + checkIdRefs(errors, e, path, child, resource, stack); } } return ok; @@ -3568,15 +3747,15 @@ private boolean checkPrimitiveBinding(ValidationContext valContext, List(), null, null, null); + Validator sqlv = new Validator(context, fpe, new ArrayList<>(), TrueFalseOrUnknown.UNKNOWN, TrueFalseOrUnknown.UNKNOWN, TrueFalseOrUnknown.UNKNOWN); sqlv.checkViewDefinition(stack.getLiteralPath(), json); errors.addAll(sqlv.getIssues()); ok = sqlv.isOk() && ok; @@ -5897,20 +6080,22 @@ private boolean validateResourceRules(List errors, Element el Element div = text.getNamedChild("div", false); if (lang != null && div != null) { XhtmlNode xhtml = div.getXhtml(); - String l = xhtml.getAttribute("lang"); - String xl = xhtml.getAttribute("xml:lang"); - if (l == null && xl == null) { - warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, I18nConstants.LANGUAGE_XHTML_LANG_MISSING1); - } else { - if (l == null) { - warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, I18nConstants.LANGUAGE_XHTML_LANG_MISSING2); - } else if (!l.equals(lang)) { - warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, I18nConstants.LANGUAGE_XHTML_LANG_DIFFERENT1, lang, l); - } - if (xl == null) { - warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, I18nConstants.LANGUAGE_XHTML_LANG_MISSING3); - } else if (!xl.equals(lang)) { - warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, I18nConstants.LANGUAGE_XHTML_LANG_DIFFERENT2, lang, xl); + if (xhtml != null) { + String l = xhtml.getAttribute("lang"); + String xl = xhtml.getAttribute("xml:lang"); + if (l == null && xl == null) { + warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, I18nConstants.LANGUAGE_XHTML_LANG_MISSING1); + } else { + if (l == null) { + warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, I18nConstants.LANGUAGE_XHTML_LANG_MISSING2); + } else if (!l.equals(lang)) { + warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, I18nConstants.LANGUAGE_XHTML_LANG_DIFFERENT1, lang, l); + } + if (xl == null) { + warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, I18nConstants.LANGUAGE_XHTML_LANG_MISSING3); + } else if (!xl.equals(lang)) { + warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, div.line(), div.col(), stack.getLiteralPath(), false, I18nConstants.LANGUAGE_XHTML_LANG_DIFFERENT2, lang, xl); + } } } } @@ -6900,7 +7085,7 @@ public List assignChildren(ValidationContext valContext, List UTF-8 UTF-8 - 6.3.24 + 6.3.30 7.4.0 1.5.5 @@ -164,6 +164,11 @@ org.hl7.fhir.convertors ${fhir.core.version} + + org.fhir + ucum + 1.0.8 + ca.uhn.hapi.fhir org.hl7.fhir.validation From 0b129d2ef579b95f52339b8989121b42e205b7d7 Mon Sep 17 00:00:00 2001 From: oliveregger Date: Sat, 12 Oct 2024 13:13:01 +0200 Subject: [PATCH 4/6] FML: Use FMLParser in StructureMapUtilities and support for identity transform #289 --- .../ahdis/matchbox/engine/MatchboxEngine.java | 11 +- .../hl7/fhir/r5/elementmodel/FmlParser.java | 668 +++++++++++++++++ .../structuremap/StructureMapUtilities.java | 669 ++---------------- .../tests/FhirMappingLanguageTests.java | 1 - .../tutorial/step1/map/step1.xml | 30 +- .../tutorial/step1/map/step1b.map | 9 +- .../tutorial/step1/map/step1b.xml | 69 +- .../tutorial/step3/map/step3b.map | 2 +- .../tutorial/step3/map/step3c.map | 2 +- updatehapi.sh | 1 + 10 files changed, 836 insertions(+), 626 deletions(-) create mode 100644 matchbox-engine/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java diff --git a/matchbox-engine/src/main/java/ch/ahdis/matchbox/engine/MatchboxEngine.java b/matchbox-engine/src/main/java/ch/ahdis/matchbox/engine/MatchboxEngine.java index bf3315dd9fb..a946aedf102 100644 --- a/matchbox-engine/src/main/java/ch/ahdis/matchbox/engine/MatchboxEngine.java +++ b/matchbox-engine/src/main/java/ch/ahdis/matchbox/engine/MatchboxEngine.java @@ -911,7 +911,16 @@ public IBaseResource getCanonicalResourceById(final String type, final @NonNull * @throws FHIRException FHIR Exception */ public org.hl7.fhir.r5.model.StructureMap parseMapR5(String content) throws FHIRException { - SimpleWorkerContext context = this.getContext(); + SimpleWorkerContext context = null; + try { + context = this.getContextForFhirVersion("5.0.0"); + } catch (FHIRException e) { + log.error("error creating context",e); + return null; + } catch (IOException e) { + log.error("error creating context",e); + return null; + } List outputs = new ArrayList<>(); StructureMapUtilities scu = new MatchboxStructureMapUtilities(context, new TransformSupportServices(context, outputs), this); diff --git a/matchbox-engine/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java b/matchbox-engine/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java new file mode 100644 index 00000000000..75313d08185 --- /dev/null +++ b/matchbox-engine/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java @@ -0,0 +1,668 @@ +package org.hl7.fhir.r5.elementmodel; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.Element.SpecialElement; +import org.hl7.fhir.r5.fhirpath.ExpressionNode; +import org.hl7.fhir.r5.fhirpath.FHIRLexer; +import org.hl7.fhir.r5.fhirpath.FHIRPathEngine; +import org.hl7.fhir.r5.fhirpath.FHIRLexer.FHIRLexerException; +import org.hl7.fhir.r5.formats.IParser.OutputStyle; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupUnmappedMode; +import org.hl7.fhir.r5.model.Enumerations.ConceptMapRelationship; +import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupTypeMode; +import org.hl7.fhir.r5.model.StructureMap.StructureMapTransform; +import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities; +import org.hl7.fhir.utilities.SourceLocation; +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.VersionUtilities; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.utilities.validation.ValidationMessage.Source; + +public class FmlParser extends ParserBase { + + private FHIRPathEngine fpe; + + public FmlParser(IWorkerContext context) { + super(context); + fpe = new FHIRPathEngine(context); + } + + @Override + public List parse(InputStream inStream) throws IOException, FHIRFormatError, DefinitionException, FHIRException { + byte[] content = TextFile.streamToBytes(inStream); + ByteArrayInputStream stream = new ByteArrayInputStream(content); + String text = TextFile.streamToString(stream); + List result = new ArrayList<>(); + ValidatedFragment focusFragment = new ValidatedFragment(ValidatedFragment.FOCUS_NAME, "fml", content, false); + focusFragment.setElement(parse(focusFragment.getErrors(), text)); + result.add(focusFragment); + return result; + } + + @Override + public void compose(Element e, OutputStream destination, OutputStyle style, String base) + throws FHIRException, IOException { + throw new Error("Not done yet"); + } + + public Element parse(List errors, String text) throws FHIRException { + FHIRLexer lexer = new FHIRLexer(text, "source", true, true); + if (lexer.done()) + throw lexer.error("Map Input cannot be empty"); + Element result = Manager.build(context, context.fetchTypeDefinition("StructureMap")); + try { + if (lexer.hasToken("map")) { + lexer.token("map"); + result.makeElement("url").markLocation(lexer.getCurrentLocation()).setValue(lexer.readConstant("url")); + lexer.token("="); + result.makeElement("name").markLocation(lexer.getCurrentLocation()).setValue(lexer.readConstant("name")); + if (lexer.hasComments()) { + result.makeElement("description").markLocation(lexer.getCurrentLocation()).setValue(lexer.getAllComments()); + } + } + while (lexer.hasToken("///")) { + lexer.next(); + String fid = lexer.takeDottedToken(); + Element e = result.makeElement(fid).markLocation(lexer.getCurrentLocation()); + lexer.token("="); + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 + String multiline = lexer.getCurrent(); + if (fid.equals("description")) { + String descr = lexer.readConstant("description"); + if ("\"\"".equals(multiline)) { + descr = lexer.readConstant("description multiline"); + if (descr.startsWith("\r")) { + descr = descr.substring(1); + } + if (descr.startsWith("\n")) { + descr = descr.substring(1); + } + if (descr.endsWith("\n")) { + descr = descr.substring(0, descr.length()-1); + } + if (descr.endsWith("\r")) { + descr = descr.substring(0, descr.length()-1); + } + lexer.skipToken("\"\""); + } + e.setValue(descr); + } else { + e.setValue(lexer.readConstant("meta value")); + } + } + lexer.setMetadataFormat(false); + if (!result.hasChild("status")) { + result.makeElement("status").setValue("draft"); + } + if (!result.hasChild("id") && result.hasChild("name")) { + String id = Utilities.makeId(result.getChildValue("name")); + if (!Utilities.noString(id)) { + result.makeElement("id").setValue(id); + } + } + if (!result.hasChild("description") && result.hasChild("title")) { + result.makeElement("description").setValue(Utilities.makeId(result.getChildValue("title"))); + } + + while (lexer.hasToken("conceptmap")) + parseConceptMap(result, lexer); + + while (lexer.hasToken("uses")) + parseUses(result, lexer); + while (lexer.hasToken("imports")) + parseImports(result, lexer); + + while (lexer.hasToken("conceptmap")) + parseConceptMap(result, lexer); + + while (!lexer.done()) { + parseGroup(result, lexer); + } + } catch (FHIRLexerException e) { + if (policy == ValidationPolicy.NONE) { + throw e; + } else { + logError(errors, "2023-02-24", e.getLocation().getLine(), e.getLocation().getColumn(), "??", IssueType.INVALID, e.getMessage(), IssueSeverity.FATAL); + } + } catch (Exception e) { + if (policy == ValidationPolicy.NONE) { + throw e; + } else { + logError(errors, "2023-02-24", -1, -1, "?", IssueType.INVALID, e.getMessage(), IssueSeverity.FATAL); + } + } + result.setIgnorePropertyOrder(true); + return result; + } + + private void parseConceptMap(Element structureMap, FHIRLexer lexer) throws FHIRLexerException { + lexer.token("conceptmap"); + Element map = structureMap.makeElement("contained"); + StructureDefinition sd = context.fetchTypeDefinition("ConceptMap"); + map.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd, getProfileUtilities(), getContextUtilities()), SpecialElement.fromProperty(map.getElementProperty() != null ? map.getElementProperty() : map.getProperty()), map.getProperty()); + map.setType("ConceptMap"); + Element eid = map.makeElement("id").markLocation(lexer.getCurrentLocation()); + String id = lexer.readConstant("map id"); + if (id.startsWith("#")) + throw lexer.error("Concept Map identifier must not start with #"); + eid.setValue(id); + map.makeElement("status").setValue(structureMap.getChildValue("status")); + lexer.token("{"); + // lexer.token("source"); + // map.setSource(new UriType(lexer.readConstant("source"))); + // lexer.token("target"); + // map.setSource(new UriType(lexer.readConstant("target"))); + Map prefixes = new HashMap(); + while (lexer.hasToken("prefix")) { + lexer.token("prefix"); + String n = lexer.take(); + lexer.token("="); + String v = lexer.readConstant("prefix url"); + prefixes.put(n, v); + } + while (lexer.hasToken("unmapped")) { + lexer.token("unmapped"); + lexer.token("for"); + String n = readPrefix(prefixes, lexer); + Element g = getGroupE(map, n, null); + lexer.token("="); + SourceLocation loc = lexer.getCurrentLocation(); + String v = lexer.take(); + if (v.equals("provided")) { + g.makeElement("unmapped").makeElement("mode").markLocation(loc).setValue(ConceptMapGroupUnmappedMode.USESOURCECODE.toCode()); + } else + throw lexer.error("Only unmapped mode PROVIDED is supported at this time"); + } + while (!lexer.hasToken("}")) { + String comments = lexer.hasComments() ? lexer.getAllComments() : null; + String srcs = readPrefix(prefixes, lexer); + lexer.token(":"); + SourceLocation scloc = lexer.getCurrentLocation(); + String sc = lexer.getCurrent().startsWith("\"") ? lexer.readConstant("code") : lexer.take(); + SourceLocation relLoc = lexer.getCurrentLocation(); + ConceptMapRelationship rel = readRelationship(lexer); + String tgts = readPrefix(prefixes, lexer); + Element g = getGroupE(map, srcs, tgts); + Element e = g.addElement("element"); + if (comments != null) { + for (String s : comments.split("\\r\\n")) { + e.getComments().add(s); + } + } + e.makeElement("code").markLocation(scloc).setValue(sc.startsWith("\"") ? lexer.processConstant(sc) : sc); + Element tgt = e.addElement("target"); + tgt.makeElement("relationship").markLocation(relLoc).setValue(rel.toCode()); + lexer.token(":"); + tgt.makeElement("code").markLocation(lexer.getCurrentLocation()).setValue(lexer.getCurrent().startsWith("\"") ? lexer.readConstant("code") : lexer.take()); + if (lexer.hasComments()) { + tgt.makeElement("comment").markLocation(lexer.getCommentLocation()).setValue(lexer.getFirstComment()); + } + } + lexer.token("}"); + } + + private Element getGroupE(Element map, String srcs, String tgts) { + for (Element grp : map.getChildrenByName("group")) { + if (grp.getChildValue("source").equals(srcs)) { + Element tgt = grp.getNamedChild("target"); + if (tgt == null || tgts == null || tgts.equals(tgt.getValue())) { + if (tgt == null && tgts != null) + grp.makeElement("target").setValue(tgts); + return grp; + } + } + } + Element grp = map.addElement("group"); + grp.makeElement("source").setValue(srcs); + grp.makeElement("target").setValue(tgts); + return grp; + } + + private String readPrefix(Map prefixes, FHIRLexer lexer) throws FHIRLexerException { + String prefix = lexer.take(); + if (!prefixes.containsKey(prefix)) + throw lexer.error("Unknown prefix '" + prefix + "'"); + return prefixes.get(prefix); + } + + + private ConceptMapRelationship readRelationship(FHIRLexer lexer) throws FHIRLexerException { + String token = lexer.take(); + if (token.equals("-")) + return ConceptMapRelationship.RELATEDTO; + if (token.equals("=")) // temporary + return ConceptMapRelationship.RELATEDTO; + if (token.equals("==")) + return ConceptMapRelationship.EQUIVALENT; + if (token.equals("!=")) + return ConceptMapRelationship.NOTRELATEDTO; + if (token.equals("<=")) + return ConceptMapRelationship.SOURCEISNARROWERTHANTARGET; + if (token.equals(">=")) + return ConceptMapRelationship.SOURCEISBROADERTHANTARGET; + throw lexer.error("Unknown relationship token '" + token + "'"); + } + + private void parseUses(Element result, FHIRLexer lexer) throws FHIRException { + lexer.token("uses"); + Element st = result.addElement("structure"); + st.makeElement("url").markLocation(lexer.getCurrentLocation()).setValue(lexer.readConstant("url")); + if (lexer.hasToken("alias")) { + lexer.token("alias"); + st.makeElement("alias").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + } + lexer.token("as"); + st.makeElement("mode").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + lexer.skipToken(";"); + if (lexer.hasComments()) { + st.makeElement("documentation").markLocation(lexer.getCommentLocation()).setValue(lexer.getFirstComment()); + } + } + + + private void parseImports(Element result, FHIRLexer lexer) throws FHIRException { + lexer.token("imports"); + result.addElement("import").markLocation(lexer.getCurrentLocation()).setValue(lexer.readConstant("url")); + lexer.skipToken(";"); + } + + private void parseGroup(Element result, FHIRLexer lexer) throws FHIRException { + SourceLocation commLoc = lexer.getCommentLocation(); + String comment = lexer.getAllComments(); + lexer.token("group"); + Element group = result.addElement("group").markLocation(lexer.getCurrentLocation()); + if (!Utilities.noString(comment)) { + group.makeElement("documentation").markLocation(commLoc).setValue(comment); + } + boolean newFmt = false; + if (lexer.hasToken("for")) { + lexer.token("for"); + SourceLocation loc = lexer.getCurrentLocation(); + if ("type".equals(lexer.getCurrent())) { + lexer.token("type"); + lexer.token("+"); + lexer.token("types"); + group.makeElement("typeMode").markLocation(loc).setValue(StructureMapGroupTypeMode.TYPEANDTYPES.toCode()); + } else { + lexer.token("types"); + group.makeElement("typeMode").markLocation(loc).setValue(StructureMapGroupTypeMode.TYPES.toCode()); + } + } + group.makeElement("name").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + if (lexer.hasToken("(")) { + newFmt = true; + lexer.take(); + while (!lexer.hasToken(")")) { + parseInput(group, lexer, true); + if (lexer.hasToken(",")) + lexer.token(","); + } + lexer.take(); + } + if (lexer.hasToken("extends")) { + lexer.next(); + group.makeElement("extends").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + } + if (newFmt) { + if (lexer.hasToken("<")) { + lexer.token("<"); + lexer.token("<"); + if (lexer.hasToken("types")) { + group.makeElement("typeMode").markLocation(lexer.getCurrentLocation()).setValue(StructureMapGroupTypeMode.TYPES.toCode()); + lexer.token("types"); + } else { + group.makeElement("typeMode").markLocation(lexer.getCurrentLocation()).setValue(StructureMapGroupTypeMode.TYPEANDTYPES.toCode()); + lexer.token("type"); + lexer.token("+"); + } + lexer.token(">"); + lexer.token(">"); + } + lexer.token("{"); + } + if (newFmt) { + while (!lexer.hasToken("}")) { + if (lexer.done()) + throw lexer.error("premature termination expecting 'endgroup'"); + parseRule(result, group, lexer, true); + } + } else { + while (lexer.hasToken("input")) + parseInput(group, lexer, false); + while (!lexer.hasToken("endgroup")) { + if (lexer.done()) + throw lexer.error("premature termination expecting 'endgroup'"); + parseRule(result, group, lexer, false); + } + } + lexer.next(); + if (newFmt && lexer.hasToken(";")) + lexer.next(); + } + + + private void parseRule(Element map, Element context, FHIRLexer lexer, boolean newFmt) throws FHIRException { + Element rule = context.addElement("rule").markLocation(lexer.getCurrentLocation()); + if (!newFmt) { + rule.makeElement("name").markLocation(lexer.getCurrentLocation()).setValue(lexer.takeDottedToken()); + lexer.token(":"); + lexer.token("for"); + } else { + if (lexer.hasComments()) { + rule.makeElement("documentation").markLocation(lexer.getCommentLocation()).setValue(lexer.getFirstComment()); + } + } + + boolean done = false; + while (!done) { + parseSource(rule, lexer); + done = !lexer.hasToken(","); + if (!done) + lexer.next(); + } + if ((newFmt && lexer.hasToken("->")) || (!newFmt && lexer.hasToken("make"))) { + lexer.token(newFmt ? "->" : "make"); + done = false; + while (!done) { + parseTarget(rule, lexer); + done = !lexer.hasToken(","); + if (!done) + lexer.next(); + } + } + if (lexer.hasToken("then")) { + lexer.token("then"); + if (lexer.hasToken("{")) { + lexer.token("{"); + while (!lexer.hasToken("}")) { + if (lexer.done()) + throw lexer.error("premature termination expecting '}' in nested group"); + parseRule(map, rule, lexer, newFmt); + } + lexer.token("}"); + } else { + done = false; + while (!done) { + parseRuleReference(rule, lexer); + done = !lexer.hasToken(","); + if (!done) + lexer.next(); + } + } + } + if (!rule.hasChild("documentation") && lexer.hasComments()) { + rule.makeElement("documentation").markLocation(lexer.getCommentLocation()).setValue(lexer.getFirstComment()); + } + + if (isSimpleSyntax(rule)) { + rule.forceElement("source").makeElement("variable").setValue(StructureMapUtilities.AUTO_VAR_NAME); + rule.forceElement("target").makeElement("variable").setValue(StructureMapUtilities.AUTO_VAR_NAME); + rule.forceElement("target").makeElement("transform").setValue(StructureMapTransform.CREATE.toCode()); + Element dep = rule.forceElement("dependent").markLocation(rule); + dep.makeElement("name").markLocation(rule).setValue(StructureMapUtilities.DEF_GROUP_NAME); + dep.addElement("parameter").markLocation(dep).makeElement("valueId").markLocation(dep).setValue(StructureMapUtilities.AUTO_VAR_NAME); + dep.addElement("parameter").markLocation(dep).makeElement("valueId").markLocation(dep).setValue(StructureMapUtilities.AUTO_VAR_NAME); + // no dependencies - imply what is to be done based on types + } + if (newFmt) { + if (lexer.isConstant()) { + if (lexer.isStringConstant()) { + rule.makeElement("name").markLocation(lexer.getCurrentLocation()).setValue(fixName(lexer.readConstant("ruleName"))); + } else { + rule.makeElement("name").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + } + } else { + if (rule.getChildrenByName("source").size() != 1 || !rule.getChildrenByName("source").get(0).hasChild("element")) + throw lexer.error("Complex rules must have an explicit name"); + if (rule.getChildrenByName("source").get(0).hasChild("type")) + rule.makeElement("name").setValue(rule.getChildrenByName("source").get(0).getNamedChildValue("element") + Utilities.capitalize(rule.getChildrenByName("source").get(0).getNamedChildValue("type"))); + else + rule.makeElement("name").setValue(rule.getChildrenByName("source").get(0).getNamedChildValue("element")); + } + lexer.token(";"); + } + } + + private String fixName(String c) { + return c.replace("-", ""); + } + + private void parseRuleReference(Element rule, FHIRLexer lexer) throws FHIRLexerException { + Element ref = rule.addElement("dependent").markLocation(lexer.getCurrentLocation()); + ref.makeElement("name").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + lexer.token("("); + boolean done = false; + while (!done) { + parseParameter(ref, lexer, false); + done = !lexer.hasToken(","); + if (!done) + lexer.next(); + } + lexer.token(")"); + } + + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 + private String removeQuotedOrBacktick(String token) { + if (token.startsWith("`") && token.endsWith("`")) { + return token.substring(1,token.length()-1); + } + if (token.startsWith("\"") && token.endsWith("\"")) { + return token.substring(1,token.length()-1); + } + return token; + } + + private void parseSource(Element rule, FHIRLexer lexer) throws FHIRException { + Element source = rule.addElement("source").markLocation(lexer.getCurrentLocation()); + source.makeElement("context").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + if (source.getChildValue("context").equals("search") && lexer.hasToken("(")) { + source.makeElement("context").markLocation(lexer.getCurrentLocation()).setValue("@search"); + lexer.take(); + SourceLocation loc = lexer.getCurrentLocation(); + ExpressionNode node = fpe.parse(lexer); + source.setUserData(StructureMapUtilities.MAP_SEARCH_EXPRESSION, node); + source.makeElement("element").markLocation(loc).setValue(node.toString()); + lexer.token(")"); + } else if (lexer.hasToken(".")) { + lexer.token("."); + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 + source.makeElement("element").markLocation(lexer.getCurrentLocation()).setValue(removeQuotedOrBacktick(lexer.take())); + } + if (lexer.hasToken(":")) { + // type and cardinality + lexer.token(":"); + source.makeElement("type").markLocation(lexer.getCurrentLocation()).setValue(lexer.takeDottedToken()); + } + if (Utilities.isInteger(lexer.getCurrent())) { + source.makeElement("min").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + lexer.token(".."); + source.makeElement("max").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + } + if (lexer.hasToken("default")) { + lexer.token("default"); + source.makeElement("defaultValue").markLocation(lexer.getCurrentLocation()).setValue(lexer.readConstant("default value")); + } + if (Utilities.existsInList(lexer.getCurrent(), "first", "last", "not_first", "not_last", "only_one")) { + source.makeElement("listMode").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + } + + if (lexer.hasToken("as")) { + lexer.take(); + source.makeElement("variable").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + } + if (lexer.hasToken("where")) { + lexer.take(); + SourceLocation loc = lexer.getCurrentLocation(); + ExpressionNode node = fpe.parse(lexer); + source.setUserData(StructureMapUtilities.MAP_WHERE_EXPRESSION, node); + source.makeElement("condition").markLocation(loc).setValue(node.toString()); + } + if (lexer.hasToken("check")) { + lexer.take(); + SourceLocation loc = lexer.getCurrentLocation(); + ExpressionNode node = fpe.parse(lexer); + source.setUserData(StructureMapUtilities.MAP_WHERE_CHECK, node); + source.makeElement("check").markLocation(loc).setValue(node.toString()); + } + if (lexer.hasToken("log")) { + lexer.take(); + SourceLocation loc = lexer.getCurrentLocation(); + ExpressionNode node = fpe.parse(lexer); + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 + source.setUserData(StructureMapUtilities.MAP_WHERE_LOG, node); + source.makeElement("logMessage").markLocation(loc).setValue(node.toString()); + } + } + + private void parseTarget(Element rule, FHIRLexer lexer) throws FHIRException { + Element target = rule.addElement("target").markLocation(lexer.getCurrentLocation()); + SourceLocation loc = lexer.getCurrentLocation(); + String start = lexer.take(); + if (lexer.hasToken(".")) { + target.makeElement("context").markLocation(loc).setValue(start); + start = null; + lexer.token("."); + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 + target.makeElement("element").markLocation(lexer.getCurrentLocation()).setValue(removeQuotedOrBacktick(lexer.take())); + } + String name; + boolean isConstant = false; + if (lexer.hasToken("=")) { + if (start != null) { + target.makeElement("context").markLocation(loc).setValue(start); + } + lexer.token("="); + isConstant = lexer.isConstant(); + loc = lexer.getCurrentLocation(); + name = lexer.take(); + } else { + loc = lexer.getCurrentLocation(); + name = start; + } + + if ("(".equals(name)) { + // inline fluentpath expression + target.makeElement("transform").markLocation(lexer.getCurrentLocation()).setValue(StructureMapTransform.EVALUATE.toCode()); + loc = lexer.getCurrentLocation(); + ExpressionNode node = fpe.parse(lexer); + target.setUserData(StructureMapUtilities.MAP_EXPRESSION, node); + target.addElement("parameter").markLocation(loc).makeElement("valueString").setValue(node.toString()); + lexer.token(")"); + } else if (lexer.hasToken("(")) { + target.makeElement("transform").markLocation(loc).setValue(name); + lexer.token("("); + if (target.getChildValue("transform").equals(StructureMapTransform.EVALUATE.toCode())) { + parseParameter(target, lexer, true); + lexer.token(","); + loc = lexer.getCurrentLocation(); + ExpressionNode node = fpe.parse(lexer); + target.setUserData(StructureMapUtilities.MAP_EXPRESSION, node); + target.addElement("parameter").markLocation(loc).makeElement("valueString").setValue(node.toString()); + } else { + while (!lexer.hasToken(")")) { + parseParameter(target, lexer, true); + if (!lexer.hasToken(")")) + lexer.token(","); + } + } + lexer.token(")"); + } else if (name != null) { + target.makeElement("transform").markLocation(loc).setValue(StructureMapTransform.COPY.toCode()); + if (!isConstant) { + loc = lexer.getCurrentLocation(); + String id = name; + while (lexer.hasToken(".")) { + id = id + lexer.take() + lexer.take(); + } + target.addElement("parameter").markLocation(loc).makeElement("valueId").setValue(id); + } else { + target.addElement("parameter").markLocation(lexer.getCurrentLocation()).makeElement("valueString").setValue(readConstant(name, lexer)); + } + } + if (lexer.hasToken("as")) { + lexer.take(); + target.makeElement("variable").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + } + while (Utilities.existsInList(lexer.getCurrent(), "first", "last", "share", "collate")) { + if (lexer.getCurrent().equals("share")) { + target.makeElement("listMode").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + target.makeElement("listRuleId").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + } else { + target.makeElement("listMode").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + } + } + } + + private void parseParameter(Element ref, FHIRLexer lexer, boolean isTarget) throws FHIRLexerException, FHIRFormatError { + boolean r5 = VersionUtilities.isR5Plus(context.getVersion()); + String name = r5 || isTarget ? "parameter" : "variable"; + if (ref.hasChildren(name) && !ref.getChildByName(name).isList()) { + throw lexer.error("variable on target is not a list, so can't add an element"); + } else if (!lexer.isConstant()) { + ref.addElement(name).markLocation(lexer.getCurrentLocation()).makeElement(r5 ? "valueId" : "value").setValue(lexer.take()); + } else if (lexer.isStringConstant()) + ref.addElement(name).markLocation(lexer.getCurrentLocation()).makeElement(r5 ? "valueString" : "value").setValue(lexer.readConstant("??")); + else { + ref.addElement(name).markLocation(lexer.getCurrentLocation()).makeElement(r5 ? "valueString" : "value").setValue(readConstant(lexer.take(), lexer)); + } + } + + private void parseInput(Element group, FHIRLexer lexer, boolean newFmt) throws FHIRException { + Element input = group.addElement("input").markLocation(lexer.getCurrentLocation()); + if (newFmt) { + input.makeElement("mode").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + } else + lexer.token("input"); + input.makeElement("name").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + if (lexer.hasToken(":")) { + lexer.token(":"); + input.makeElement("type").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + } + if (!newFmt) { + lexer.token("as"); + input.makeElement("mode").markLocation(lexer.getCurrentLocation()).setValue(lexer.take()); + if (lexer.hasComments()) { + input.makeElement("documentation").markLocation(lexer.getCommentLocation()).setValue(lexer.getFirstComment()); + } + lexer.skipToken(";"); + } + } + + private boolean isSimpleSyntax(Element rule) { + return + (rule.getChildren("source").size() == 1 && rule.getChildren("source").get(0).hasChild("context") && rule.getChildren("source").get(0).hasChild("element") && !rule.getChildren("source").get(0).hasChild("variable")) && + (rule.getChildren("target").size() == 1 && rule.getChildren("target").get(0).hasChild("context") && rule.getChildren("target").get(0).hasChild("element") && !rule.getChildren("target").get(0).hasChild("variable") && + !rule.getChildren("target").get(0).hasChild("parameter")) && + (rule.getChildren("dependent").size() == 0 && rule.getChildren("rule").size() == 0); + } + + private String readConstant(String s, FHIRLexer lexer) throws FHIRLexerException { + if (Utilities.isInteger(s)) + return s; + else if (Utilities.isDecimal(s, false)) + return s; + else if (Utilities.existsInList(s, "true", "false")) + return s; + else + return lexer.processConstant(s); + } + + +} diff --git a/matchbox-engine/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java b/matchbox-engine/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java index 4ecc686bf69..cb3b0a92421 100644 --- a/matchbox-engine/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java +++ b/matchbox-engine/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java @@ -35,24 +35,23 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.r5.conformance.profile.ProfileKnowledgeProvider; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; import org.hl7.fhir.r5.context.ContextUtilities; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.elementmodel.FmlParser; import org.hl7.fhir.r5.elementmodel.Manager; import org.hl7.fhir.r5.elementmodel.Property; +import org.hl7.fhir.r5.elementmodel.ParserBase.ValidationPolicy; import org.hl7.fhir.r5.fhirpath.ExpressionNode; -import org.hl7.fhir.r5.fhirpath.FHIRLexer; import org.hl7.fhir.r5.fhirpath.FHIRPathEngine; import org.hl7.fhir.r5.fhirpath.TypeDetails; import org.hl7.fhir.r5.fhirpath.ExpressionNode.CollectionStatus; -import org.hl7.fhir.r5.fhirpath.FHIRLexer.FHIRLexerException; import org.hl7.fhir.r5.fhirpath.TypeDetails.ProfiledType; +import org.hl7.fhir.r5.formats.IParser; import org.hl7.fhir.r5.model.*; import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupComponent; -import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupUnmappedMode; import org.hl7.fhir.r5.model.ConceptMap.SourceElementComponent; import org.hl7.fhir.r5.model.ConceptMap.TargetElementComponent; import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionMappingComponent; @@ -73,10 +72,15 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.FhirPublication; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationOptions; import org.hl7.fhir.utilities.xhtml.NodeType; import org.hl7.fhir.utilities.xhtml.XhtmlNode; +import net.sourceforge.plantuml.utils.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.*; @@ -149,8 +153,9 @@ public static String render(StructureMap map) { b.append("/// status = \""+map.getStatus().toCode()+"\"\r\n"); b.append("\r\n"); if (map.getDescription() != null) { - renderMultilineDoco(b, map.getDescription(), 0); - b.append("\r\n"); + b.append("/// description = \"\"\"\r\n"); + renderMultilineDoco(b, map.getDescription(), 0, false); + b.append("\"\"\"\r\n"); } renderConceptMaps(b, map); renderUses(b, map); @@ -290,7 +295,7 @@ public static String groupToString(StructureMapGroupComponent g) { private static void renderGroup(StringBuilder b, StructureMapGroupComponent g) { if (g.hasDocumentation()) { - renderMultilineDoco(b, g.getDocumentation(), 0); + renderMultilineDoco(b, g.getDocumentation(), 0, true); } b.append("group "); b.append(g.getName()); @@ -340,8 +345,9 @@ public static String ruleToString(StructureMapGroupRuleComponent r) { } private static void renderRule(StringBuilder b, StructureMapGroupRuleComponent r, int indent) { - if (r.hasFormatCommentPre()) { - renderMultilineDoco(b, r.getFormatCommentsPre(), indent); + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 + if (r.hasDocumentation()) { + renderMultilineDoco(b, r.getDocumentation(), indent, true); } for (int i = 0; i < indent; i++) b.append(' '); @@ -589,37 +595,35 @@ else if (rtp.hasValueIntegerType()) } private static void renderDoco(StringBuilder b, String doco) { + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 + renderDoco(b, doco, true); + } + + private static void renderDoco(StringBuilder b, String doco, boolean addComment) { if (Utilities.noString(doco)) return; if (b != null && b.length() > 1 && b.charAt(b.length() - 1) != '\n' && b.charAt(b.length() - 1) != ' ') { b.append(" "); } - b.append("// "); + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 + if (addComment) { + b.append("// "); + } b.append(doco.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")); } - private static void renderMultilineDoco(StringBuilder b, String doco, int indent) { + private static void renderMultilineDoco(StringBuilder b, String doco, int indent, boolean addComment) { if (Utilities.noString(doco)) return; String[] lines = doco.split("\\r?\\n"); for (String line : lines) { for (int i = 0; i < indent; i++) b.append(' '); - renderDoco(b, line); + renderDoco(b, line, addComment); b.append("\r\n"); } } - private static void renderMultilineDoco(StringBuilder b, List doco, int indent) { - if (doco == null || doco.isEmpty()) - return; - for (String line : doco) { - for (int i = 0; i < indent; i++) - b.append(' '); - renderDoco(b, line); - b.append("\r\n"); - } - } - + public ITransformerServices getServices() { return services; } @@ -628,599 +632,32 @@ public IWorkerContext getWorker() { return worker; } + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 public StructureMap parse(String text, String srcName) throws FHIRException { - FHIRLexer lexer = new FHIRLexer(Utilities.stripBOM(text), srcName, true, true); - if (lexer.done()) - throw lexer.error("Map Input cannot be empty"); - StructureMap result = new StructureMap(); - if (lexer.hasToken("map")) { - lexer.token("map"); - result.setUrl(lexer.readConstant("url")); - lexer.token("="); - result.setName(lexer.readConstant("name")); - result.setDescription(lexer.getAllComments()); - result.setStatus(PublicationStatus.DRAFT); - } - while (lexer.hasToken("///")) { - lexer.next(); - String fid = lexer.takeDottedToken(); - lexer.token("="); - switch (fid) { - case "url" : - result.setUrl(lexer.readConstant("url")); - break; - case "name" : - result.setName(lexer.readConstant("name")); - break; - case "title" : - result.setTitle(lexer.readConstant("title")); - break; - case "description" : - result.setTitle(lexer.readConstant("description")); - break; - case "status" : - result.setStatus(PublicationStatus.fromCode(lexer.readConstant("status"))); - break; - default: - lexer.readConstant("nothing"); - // nothing - } - } - if (!result.hasId() && result.hasName()) { - String id = Utilities.makeId(result.getName()); - if (!Utilities.noString(id)) { - result.setId(id); - } - } - if (!result.hasStatus()) { - result.setStatus(PublicationStatus.DRAFT); - } - if (!result.hasDescription() && result.hasTitle()) { - result.setDescription(result.getTitle()); - } - - while (lexer.hasToken("conceptmap")) - parseConceptMap(result, lexer); - - while (lexer.hasToken("uses")) - parseUses(result, lexer); - while (lexer.hasToken("imports")) - parseImports(result, lexer); - - while (lexer.hasToken("conceptmap")) - parseConceptMap(result, lexer); - - while (!lexer.done()) { - parseGroup(result, lexer); - } - - return result; - } - - - private void parseConceptMap(StructureMap result, FHIRLexer lexer) throws FHIRLexerException { - ConceptMap map = new ConceptMap(); - map.addFormatCommentsPre(lexer.getComments()); - lexer.token("conceptmap"); - String id = lexer.readConstant("map id"); - if (id.startsWith("#")) - throw lexer.error("Concept Map identifier must start with #"); - map.setId(id); - map.setStatus(PublicationStatus.DRAFT); // todo: how to add this to the text format - result.getContained().add(map); - lexer.token("{"); - // lexer.token("source"); - // map.setSource(new UriType(lexer.readConstant("source"))); - // lexer.token("target"); - // map.setSource(new UriType(lexer.readConstant("target"))); - Map prefixes = new HashMap(); - while (lexer.hasToken("prefix")) { - lexer.token("prefix"); - String n = lexer.take(); - lexer.token("="); - String v = lexer.readConstant("prefix url"); - prefixes.put(n, v); - } - while (lexer.hasToken("unmapped")) { - List comments = lexer.cloneComments(); - lexer.token("unmapped"); - lexer.token("for"); - String n = readPrefix(prefixes, lexer); - ConceptMapGroupComponent g = getGroup(map, n, null); - g.addFormatCommentsPre(comments); - lexer.token("="); - String v = lexer.take(); - if (v.equals("provided")) { - g.getUnmapped().setMode(ConceptMapGroupUnmappedMode.USESOURCECODE); - } else - throw lexer.error("Only unmapped mode PROVIDED is supported at this time"); - } - while (!lexer.hasToken("}")) { - List comments = lexer.cloneComments(); - String srcs = readPrefix(prefixes, lexer); - lexer.token(":"); - String sc = lexer.getCurrent().startsWith("\"") ? lexer.readConstant("code") : lexer.take(); - ConceptMapRelationship rel = readRelationship(lexer); - String tgts = readPrefix(prefixes, lexer); - ConceptMapGroupComponent g = getGroup(map, srcs, tgts); - SourceElementComponent e = g.addElement(); - e.addFormatCommentsPre(comments); - e.setCode(sc); - if (e.getCode().startsWith("\"")) { - e.setCode(lexer.processConstant(e.getCode())); - } - TargetElementComponent tgt = e.addTarget(); - tgt.setRelationship(rel); - lexer.token(":"); - tgt.setCode(lexer.take()); - if (tgt.getCode().startsWith("\"")) { - tgt.setCode(lexer.processConstant(tgt.getCode())); - } - // tgt.setComment(lexer.getAllComments()); - } - map.addFormatCommentsPost(lexer.getComments()); - lexer.token("}"); - } - - - - private ConceptMapGroupComponent getGroup(ConceptMap map, String srcs, String tgts) { - for (ConceptMapGroupComponent grp : map.getGroup()) { - if (grp.getSource().equals(srcs)) - if (!grp.hasTarget() || tgts == null || tgts.equals(grp.getTarget())) { - if (!grp.hasTarget() && tgts != null) - grp.setTarget(tgts); - return grp; - } - } - ConceptMapGroupComponent grp = map.addGroup(); - grp.setSource(srcs); - grp.setTarget(tgts); - return grp; - } - - - - private String readPrefix(Map prefixes, FHIRLexer lexer) throws FHIRLexerException { - String prefix = lexer.take(); - if (!prefixes.containsKey(prefix)) - throw lexer.error("Unknown prefix '" + prefix + "'"); - return prefixes.get(prefix); - } - - - private ConceptMapRelationship readRelationship(FHIRLexer lexer) throws FHIRLexerException { - String token = lexer.take(); - if (token.equals("-")) - return ConceptMapRelationship.RELATEDTO; - if (token.equals("==")) - return ConceptMapRelationship.EQUIVALENT; - if (token.equals("!=")) - return ConceptMapRelationship.NOTRELATEDTO; - if (token.equals("<=")) - return ConceptMapRelationship.SOURCEISNARROWERTHANTARGET; - if (token.equals(">=")) - return ConceptMapRelationship.SOURCEISBROADERTHANTARGET; - throw lexer.error("Unknown relationship token '" + token + "'"); - } - - - private void parseUses(StructureMap result, FHIRLexer lexer) throws FHIRException { - lexer.token("uses"); - StructureMapStructureComponent st = result.addStructure(); - st.setUrl(lexer.readConstant("url")); - if (lexer.hasToken("alias")) { - lexer.token("alias"); - st.setAlias(lexer.take()); - } - lexer.token("as"); - String doco; - if (lexer.getCurrent().equals("source")) { - st.setMode(StructureMapModelMode.SOURCE); - doco = lexer.tokenWithTrailingComment("source"); - } else if (lexer.getCurrent().equals("target")) { - st.setMode(StructureMapModelMode.TARGET); - doco = lexer.tokenWithTrailingComment("target"); - } else { - throw lexer.error("Found '"+lexer.getCurrent()+"' expecting 'source' or 'target'"); - } - if (lexer.hasToken(";")) { - doco = lexer.tokenWithTrailingComment(";"); - } - st.setDocumentation(doco); - } - - - - private void parseImports(StructureMap result, FHIRLexer lexer) throws FHIRException { - lexer.token("imports"); - result.addImport(lexer.readConstant("url")); - lexer.skipToken(";"); - } - - private void parseGroup(StructureMap result, FHIRLexer lexer) throws FHIRException { - String comment = lexer.getAllComments(); - lexer.token("group"); - StructureMapGroupComponent group = result.addGroup(); - if (comment != null) { - group.setDocumentation(comment); - } - boolean newFmt = false; - if (lexer.hasToken("for")) { - lexer.token("for"); - if ("type".equals(lexer.getCurrent())) { - lexer.token("type"); - lexer.token("+"); - lexer.token("types"); - group.setTypeMode(StructureMapGroupTypeMode.TYPEANDTYPES); - } else { - lexer.token("types"); - group.setTypeMode(StructureMapGroupTypeMode.TYPES); - } - } - group.setName(lexer.take()); - if (lexer.hasToken("(")) { - newFmt = true; - lexer.take(); - while (!lexer.hasToken(")")) { - parseInput(group, lexer, true); - if (lexer.hasToken(",")) - lexer.token(","); - } - lexer.take(); - } - if (lexer.hasToken("extends")) { - lexer.next(); - group.setExtends(lexer.take()); - } - if (newFmt) { - if (lexer.hasToken("<")) { - lexer.token("<"); - lexer.token("<"); - if (lexer.hasToken("types")) { - group.setTypeMode(StructureMapGroupTypeMode.TYPES); - lexer.token("types"); - } else { - lexer.token("type"); - lexer.token("+"); - group.setTypeMode(StructureMapGroupTypeMode.TYPEANDTYPES); - } - lexer.token(">"); - lexer.token(">"); - } - lexer.token("{"); - } - if (newFmt) { - while (!lexer.hasToken("}")) { - if (lexer.done()) - throw lexer.error("premature termination expecting 'endgroup'"); - parseRule(result, group.getRule(), lexer, true); - } - } else { - while (lexer.hasToken("input")) - parseInput(group, lexer, false); - while (!lexer.hasToken("endgroup")) { - if (lexer.done()) - throw lexer.error("premature termination expecting 'endgroup'"); - parseRule(result, group.getRule(), lexer, false); - } - } - group.addFormatCommentsPost(lexer.getComments()); - lexer.next(); - if (newFmt && lexer.hasToken(";")) - lexer.next(); - } - - - - private void parseInput(StructureMapGroupComponent group, FHIRLexer lexer, boolean newFmt) throws FHIRException { - StructureMapGroupInputComponent input = group.addInput(); - if (newFmt) { - input.setMode(StructureMapInputMode.fromCode(lexer.take())); - } else - lexer.token("input"); - input.setName(lexer.take()); - if (lexer.hasToken(":")) { - lexer.token(":"); - input.setType(lexer.take()); - } - if (!newFmt) { - lexer.token("as"); - input.setMode(StructureMapInputMode.fromCode(lexer.take())); - input.setDocumentation(lexer.getAllComments()); - lexer.skipToken(";"); - } - } - - - - private void parseRule(StructureMap map, List list, FHIRLexer lexer, boolean newFmt) throws FHIRException { - StructureMapGroupRuleComponent rule = new StructureMapGroupRuleComponent(); - if (!newFmt) { - rule.setName(lexer.takeDottedToken()); - lexer.token(":"); - lexer.token("for"); - } else { - rule.addFormatCommentsPre(lexer.getComments()); - } - list.add(rule); - boolean done = false; - while (!done) { - parseSource(rule, lexer); - done = !lexer.hasToken(","); - if (!done) - lexer.next(); - } - if ((newFmt && lexer.hasToken("->")) || (!newFmt && lexer.hasToken("make"))) { - lexer.token(newFmt ? "->" : "make"); - done = false; - while (!done) { - parseTarget(rule, lexer); - done = !lexer.hasToken(","); - if (!done) - lexer.next(); - } - } - if (lexer.hasToken("then")) { - lexer.token("then"); - if (lexer.hasToken("{")) { - lexer.token("{"); - while (!lexer.hasToken("}")) { - if (lexer.done()) - throw lexer.error("premature termination expecting '}' in nested group"); - parseRule(map, rule.getRule(), lexer, newFmt); - } - lexer.token("}"); - } else { - done = false; - while (!done) { - parseRuleReference(rule, lexer); - done = !lexer.hasToken(","); - if (!done) - lexer.next(); - } - } - } - if (isSimpleSyntax(rule)) { - rule.getSourceFirstRep().setVariable(AUTO_VAR_NAME); - rule.getTargetFirstRep().setVariable(AUTO_VAR_NAME); - rule.getTargetFirstRep().setTransform(StructureMapTransform.CREATE); // with no parameter - e.g. imply what is to be created - var dependent = rule.addDependent(); - dependent.setName(StructureMapUtilities.DEF_GROUP_NAME); - dependent.addParameter().setValue(new IdType(AUTO_VAR_NAME)); - dependent.addParameter().setValue(new IdType(AUTO_VAR_NAME)); - - // FMLParser - // Element dep = rule.forceElement("dependent").markLocation(rule); - // dep.makeElement("name").markLocation(rule).setValue(StructureMapUtilities.DEF_GROUP_NAME); - // dep.addElement("parameter").markLocation(dep).makeElement("valueId").markLocation(dep).setValue(StructureMapUtilities.AUTO_VAR_NAME); - // dep.addElement("parameter").markLocation(dep).makeElement("valueId").markLocation(dep).setValue(StructureMapUtilities.AUTO_VAR_NAME); - - // no dependencies - imply what is to be done based on types - } - if (newFmt) { - if (lexer.isConstant()) { - if (lexer.isStringConstant()) { - rule.setName(fixName(lexer.readConstant("ruleName"))); - } else { - rule.setName(lexer.take()); - } - } else { - if (rule.getSource().size() != 1 || !rule.getSourceFirstRep().hasElement() && exceptionsForChecks ) - throw lexer.error("Complex rules must have an explicit name"); - if (rule.getSourceFirstRep().hasType()) - rule.setName(rule.getSourceFirstRep().getElement() + Utilities.capitalize(rule.getSourceFirstRep().getType())); - else - rule.setName(rule.getSourceFirstRep().getElement()); - } - String doco = lexer.tokenWithTrailingComment(";"); - if (doco != null) { - rule.setDocumentation(doco); - } - } - } - - private String fixName(String c) { - return c.replace("-", ""); - } - - private boolean isSimpleSyntax(StructureMapGroupRuleComponent rule) { - return - (rule.getSource().size() == 1 && rule.getSourceFirstRep().hasContext() && rule.getSourceFirstRep().hasElement() && !rule.getSourceFirstRep().hasVariable()) && - (rule.getTarget().size() == 1 && rule.getTargetFirstRep().hasContext() && rule.getTargetFirstRep().hasElement() && !rule.getTargetFirstRep().hasVariable() && !rule.getTargetFirstRep().hasParameter()) && - (rule.getDependent().size() == 0 && rule.getRule().size() == 0); - } - - - - private void parseRuleReference(StructureMapGroupRuleComponent rule, FHIRLexer lexer) throws FHIRLexerException { - StructureMapGroupRuleDependentComponent ref = rule.addDependent(); - ref.setName(lexer.take()); - lexer.token("("); - boolean done = false; - while (!done) { - parseParameter(ref, lexer); - done = !lexer.hasToken(","); - if (!done) - lexer.next(); - } - lexer.token(")"); - } - - - private void parseSource(StructureMapGroupRuleComponent rule, FHIRLexer lexer) throws FHIRException { - StructureMapGroupRuleSourceComponent source = rule.addSource(); - source.setContext(lexer.take()); - if (source.getContext().equals("search") && lexer.hasToken("(")) { - source.setContext("@search"); - lexer.take(); - ExpressionNode node = fpe.parse(lexer); - source.setUserData(MAP_SEARCH_EXPRESSION, node); - source.setElement(node.toString()); - lexer.token(")"); - } else if (lexer.hasToken(".")) { - lexer.token("."); - source.setElement(readAsStringOrProcessedConstant(lexer.take(), lexer)); - } - if (lexer.hasToken(":")) { - // type and cardinality - lexer.token(":"); - source.setType(lexer.takeDottedToken()); - } - if (Utilities.isInteger(lexer.getCurrent())) { - source.setMin(lexer.takeInt()); - lexer.token(".."); - source.setMax(lexer.take()); - } - if (lexer.hasToken("default")) { - lexer.token("default"); - source.setDefaultValue(lexer.readConstant("default value")); - } - if (Utilities.existsInList(lexer.getCurrent(), "first", "last", "not_first", "not_last", "only_one")) - source.setListMode(StructureMapSourceListMode.fromCode(lexer.take())); - - if (lexer.hasToken("as")) { - lexer.take(); - source.setVariable(lexer.take()); - } - if (lexer.hasToken("where")) { - lexer.take(); - ExpressionNode node = fpe.parse(lexer); - source.setUserData(MAP_WHERE_EXPRESSION, node); - source.setCondition(node.toString()); - } - if (lexer.hasToken("check")) { - lexer.take(); - ExpressionNode node = fpe.parse(lexer); - source.setUserData(MAP_WHERE_CHECK, node); - source.setCheck(node.toString()); - } - if (lexer.hasToken("log")) { - lexer.take(); - ExpressionNode node = fpe.parse(lexer); - source.setUserData(MAP_WHERE_LOG, node); - source.setLogMessage(node.toString()); - } - } - - private String readAsStringOrProcessedConstant(String s, FHIRLexer lexer) throws FHIRLexerException { - if (s.startsWith("\"") || s.startsWith("`")) - return lexer.processConstant(s); - else - return s; - } - - private void parseTarget(StructureMapGroupRuleComponent rule, FHIRLexer lexer) throws FHIRException { - StructureMapGroupRuleTargetComponent target = rule.addTarget(); - String start = lexer.take(); - if (lexer.hasToken(".")) { - target.setContext(start); - start = null; - lexer.token("."); - target.setElement(lexer.take()); - } - String name; - boolean isConstant = false; - if (lexer.hasToken("=")) { - if (start != null) - target.setContext(start); - lexer.token("="); - isConstant = lexer.isConstant(); - name = lexer.take(); - } else - name = start; - - if ("(".equals(name)) { - // inline fluentpath expression - target.setTransform(StructureMapTransform.EVALUATE); - ExpressionNode node = fpe.parse(lexer); - target.setUserData(MAP_EXPRESSION, node); - target.addParameter().setValue(new StringType(node.toString())); - lexer.token(")"); - } else if (lexer.hasToken("(")) { - target.setTransform(StructureMapTransform.fromCode(name)); - lexer.token("("); - if (target.getTransform() == StructureMapTransform.EVALUATE) { - parseParameter(target, lexer); - lexer.token(","); - ExpressionNode node = fpe.parse(lexer); - target.setUserData(MAP_EXPRESSION, node); - target.addParameter().setValue(new StringType(node.toString())); - } else { - while (!lexer.hasToken(")")) { - parseParameter(target, lexer); - if (!lexer.hasToken(")")) - lexer.token(","); - } - } - lexer.token(")"); - } else if (name != null) { - if (target.getContext() != null) { - target.setTransform(StructureMapTransform.COPY); - if (!isConstant) { - String id = name; - while (lexer.hasToken(".")) { - id = id + lexer.take() + lexer.take(); - } - target.addParameter().setValue(new IdType(id)); - } else - target.addParameter().setValue(readConstant(name, lexer)); - } else { - target.setContext(name); - } - } - if (lexer.hasToken("as")) { - lexer.take(); - target.setVariable(lexer.take()); - } - while (Utilities.existsInList(lexer.getCurrent(), "first", "last", "share", "collate")) { - if (lexer.getCurrent().equals("share")) { - target.addListMode(StructureMapTargetListMode.SHARE); - lexer.next(); - target.setListRuleId(lexer.take()); - } else { - if (lexer.getCurrent().equals("first")) - target.addListMode(StructureMapTargetListMode.FIRST); - else - target.addListMode(StructureMapTargetListMode.LAST); - lexer.next(); - } - } - } - - - private void parseParameter(StructureMapGroupRuleDependentComponent ref, FHIRLexer lexer) throws FHIRLexerException, FHIRFormatError { - if (!lexer.isConstant()) { - ref.addParameter().setValue(new IdType(lexer.take())); - } else if (lexer.isStringConstant()) - ref.addParameter().setValue(new StringType(lexer.readConstant("??"))); - else { - ref.addParameter().setValue(readConstant(lexer.take(), lexer)); + IWorkerContext context = this.getWorker(); + if (!(context.getVersion().equals("5.0.0"))) { + log("FHIR version needs to be 5.0.0"); + return null; } - } - - private void parseParameter(StructureMapGroupRuleTargetComponent target, FHIRLexer lexer) throws FHIRLexerException, FHIRFormatError { - if (!lexer.isConstant()) { - target.addParameter().setValue(new IdType(lexer.take())); - } else if (lexer.isStringConstant()) - target.addParameter().setValue(new StringType(lexer.readConstant("??"))); - else { - target.addParameter().setValue(readConstant(lexer.take(), lexer)); + FmlParser fp = new FmlParser(context); + fp.setupValidation(ValidationPolicy.EVERYTHING); + List errors = new ArrayList(); + Element res = fp.parse(errors, Utilities.stripBOM(text)); + if (res == null) { + Log.error(errors.toString()); + throw new FHIRException("Unable to parse Map Source for "+srcName + " Details "+errors.toString()); } + ByteArrayOutputStream boas = new ByteArrayOutputStream(); + try { + new org.hl7.fhir.r5.elementmodel.JsonParser(this.getWorker()).compose(res, boas, + IParser.OutputStyle.PRETTY, + null); + return (StructureMap) new org.hl7.fhir.r5.formats.JsonParser().parse( new ByteArrayInputStream(boas.toByteArray())); + } catch (IOException e) { + throw new FHIRException(e.getMessage(), e); + } } - - - private DataType readConstant(String s, FHIRLexer lexer) throws FHIRLexerException { - if (Utilities.isInteger(s)) - return new IntegerType(s); - else if (Utilities.isDecimal(s, false)) - return new DecimalType(s); - else if (Utilities.existsInList(s, "true", "false")) - return new BooleanType(s.equals("true")); - else - return new StringType(lexer.processConstant(s)); - } - + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 public StructureDefinition getTargetType(StructureMap map) throws FHIRException { boolean found = false; @@ -1340,13 +777,13 @@ private void executeRule(String indent, TransformContext context, StructureMap m for (StructureMapGroupRuleComponent childrule : rule.getRule()) { executeRule(indent + " ", context, map, v, group, childrule, false); } - // matchbox patch #265 for simple rules + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 } else if (rule.hasDependent() && !checkisSimple(rule)) { for (StructureMapGroupRuleDependentComponent dependent : rule.getDependent()) { executeDependency(indent + " ", context, map, v, group, dependent); } - // matchbox patch #265 for simple rules - } else if (checkisSimple(rule) || (rule.getSource().size() == 1 && rule.getSourceFirstRep().hasVariable() && rule.getTarget().size() == 1 && rule.getTargetFirstRep().hasVariable() && rule.getTargetFirstRep().getTransform() == StructureMapTransform.CREATE && !rule.getTargetFirstRep().hasParameter())) { + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 + } else if (checkisSimple(rule)) { // simple inferred, map by type if (debug) { log(v.summary()); @@ -1666,9 +1103,11 @@ private List processSource(String ruleId, TransformContext context, V varsForSource.add(VariableMode.INPUT, src.getVariable(), item); } if (!fpe.evaluateToBoolean(varsForSource, null, null, item, expr)) { + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 log(indent + " condition [" + src.getCondition() + "] for " + item.toString() + (src.hasVariable() ? " with variable "+ src.getVariable(): "" ) + " : false"); remove.add(item); } else + // matchbox pr https://github.com/hapifhir/org.hl7.fhir.core/issues/1777 log(indent + " condition [" + src.getCondition() + "] for " + item.toString() + (src.hasVariable() ? " with variable "+ src.getVariable(): "" ) + " : true"); } items.removeAll(remove); diff --git a/matchbox-engine/src/test/java/ch/ahdis/matchbox/engine/tests/FhirMappingLanguageTests.java b/matchbox-engine/src/test/java/ch/ahdis/matchbox/engine/tests/FhirMappingLanguageTests.java index 4da5e6bdc24..34e59b1b12f 100644 --- a/matchbox-engine/src/test/java/ch/ahdis/matchbox/engine/tests/FhirMappingLanguageTests.java +++ b/matchbox-engine/src/test/java/ch/ahdis/matchbox/engine/tests/FhirMappingLanguageTests.java @@ -321,7 +321,6 @@ void testTutorialStep1Json() throws FHIRException, IOException { } @Test - @Disabled void testTutorialStep1bJson() throws FHIRException, IOException { // from rule 'rule_a_short' // 1b org.hl7.fhir.exceptions.FHIRException: No matches found for rule for diff --git a/matchbox-engine/src/test/resources/mapping-language/tutorial/step1/map/step1.xml b/matchbox-engine/src/test/resources/mapping-language/tutorial/step1/map/step1.xml index 86a858a9484..365912840a7 100644 --- a/matchbox-engine/src/test/resources/mapping-language/tutorial/step1/map/step1.xml +++ b/matchbox-engine/src/test/resources/mapping-language/tutorial/step1/map/step1.xml @@ -1,6 +1,30 @@ - - + + + + + + +

+
/// url = "http://hl7.org/fhir/StructureMap/tutorial-step1"
+/// name = "tutorial-step1"
+/// title = "Tutorial Step 1"
+/// status = "draft"
+
+// Tutorial Step 1
+
+uses "http://hl7.org/fhir/StructureDefinition/tutorial-left-1" alias TLeft as source
+uses "http://hl7.org/fhir/StructureDefinition/tutorial-right-1" alias TRight as target
+
+group tutorial(source src : TLeft, target tgt : TRight) {
+  src.a as a -> tgt.a = a "rule_a";
+}
+
+
+
+ @@ -47,4 +71,4 @@ </target> </rule> </group> -</StructureMap> +</StructureMap> \ No newline at end of file diff --git a/matchbox-engine/src/test/resources/mapping-language/tutorial/step1/map/step1b.map b/matchbox-engine/src/test/resources/mapping-language/tutorial/step1/map/step1b.map index 41a7d2ea555..6b59cb75e6e 100644 --- a/matchbox-engine/src/test/resources/mapping-language/tutorial/step1/map/step1b.map +++ b/matchbox-engine/src/test/resources/mapping-language/tutorial/step1/map/step1b.map @@ -5,6 +5,13 @@ uses "http://hl7.org/fhir/StructureDefinition/tutorial-left-1" alias TLeft as source uses "http://hl7.org/fhir/StructureDefinition/tutorial-right-1" alias TRight as target -group tutorial(source src : TLeft, target tgt : TRight) { +// uses "http://hl7.org/fhir/StructureDefinition/string" alias string as source +// uses "http://hl7.org/fhir/StructureDefinition/string" alias string as target + +group tutorial(source src : TLeft, target tgt : TRight) <<type+>> { src.a -> tgt.a "rule_a_short"; +} + +group string(source src : string, target tgt : string) <<type+>> { + src.value as v -> tgt.value = v "stringValue"; } \ No newline at end of file diff --git a/matchbox-engine/src/test/resources/mapping-language/tutorial/step1/map/step1b.xml b/matchbox-engine/src/test/resources/mapping-language/tutorial/step1/map/step1b.xml index 960f7b3921f..e734fb46152 100644 --- a/matchbox-engine/src/test/resources/mapping-language/tutorial/step1/map/step1b.xml +++ b/matchbox-engine/src/test/resources/mapping-language/tutorial/step1/map/step1b.xml @@ -1,6 +1,36 @@ -<?xml version="1.0"?> -<StructureMap xmlns="http://hl7.org/fhir"> +<StructureMap + xmlns="http://hl7.org/fhir"> <id value="tutorial-step1b"/> + <meta> + <lastUpdated value="2024-10-10T11:44:21.716+00:00"/> + </meta> + <text> + <status value="generated"/> + <div + xmlns="http://www.w3.org/1999/xhtml"> + <pre>/// url = "http://hl7.org/fhir/StructureMap/tutorial-step1b" +/// name = "tutorial-step1b" +/// title = "Tutorial Step 1b" +/// status = "draft" + +// Tutorial Step 1b + +uses "http://hl7.org/fhir/StructureDefinition/tutorial-left-1" alias TLeft as source +uses "http://hl7.org/fhir/StructureDefinition/tutorial-right-1" alias TRight as target + +// uses "http://hl7.org/fhir/StructureDefinition/string" alias string as source +// uses "http://hl7.org/fhir/StructureDefinition/string" alias string as target +group tutorial(source src : TLeft, target tgt : TRight) <<type+>> { + src.a -> tgt.a "rule_a_short"; +} + +group string(source src : string, target tgt : string) <<type+>> { + src.value as v -> tgt.value = v "stringValue"; +} + +</pre> + </div> + </text> <url value="http://hl7.org/fhir/StructureMap/tutorial-step1b"/> <name value="tutorial-step1b"/> <title value="Tutorial Step 1b"/> @@ -18,7 +48,8 @@ </structure> <group> <name value="tutorial"/> - <typeMode value="none"/> + <typeMode value="type-and-types"/> + <documentation value="uses "http://hl7.org/fhir/StructureDefinition/string" alias string as source uses "http://hl7.org/fhir/StructureDefinition/string" alias string as target"/> <input> <name value="src"/> <type value="TLeft"/> @@ -45,4 +76,36 @@ </target> </rule> </group> + <!-- this group should be created on demand if we have to equivalent types --> + <group> + <name value="string"/> + <typeMode value="type-and-types"/> + <input> + <name value="src"/> + <type value="string"/> + <mode value="source"/> + </input> + <input> + <name value="tgt"/> + <type value="string"/> + <mode value="target"/> + </input> + <rule> + <name value="stringValue"/> + <source> + <context value="src"/> + <element value="value"/> + <variable value="v"/> + </source> + <target> + <context value="tgt"/> + <contextType value="variable"/> + <element value="value"/> + <transform value="copy"/> + <parameter> + <valueId value="v"/> + </parameter> + </target> + </rule> + </group> </StructureMap> diff --git a/matchbox-engine/src/test/resources/mapping-language/tutorial/step3/map/step3b.map b/matchbox-engine/src/test/resources/mapping-language/tutorial/step3/map/step3b.map index 52bbd6a89ca..f5ef634fe07 100644 --- a/matchbox-engine/src/test/resources/mapping-language/tutorial/step3/map/step3b.map +++ b/matchbox-engine/src/test/resources/mapping-language/tutorial/step3/map/step3b.map @@ -7,6 +7,6 @@ uses "http://hl7.org/fhir/StructureDefinition/tutorial-right-3" alias TRight as group tutorial(source src : TLeft, target tgt : TRight) { // src.a2 as a where a2.length <= 20 -> tgt.a2 = a "rule_a20b"; see https://github.com/hapifhir/org.hl7.fhir.core/issues/1748 - src.a2 as a where a.length() <= 20 -> tgt.a2 = a "rule_a20b"; + src.a2 as a where (a.length() <= 20) -> tgt.a2 = a "rule_a20b"; } diff --git a/matchbox-engine/src/test/resources/mapping-language/tutorial/step3/map/step3c.map b/matchbox-engine/src/test/resources/mapping-language/tutorial/step3/map/step3c.map index 7c1a7750287..6c506f48b3a 100644 --- a/matchbox-engine/src/test/resources/mapping-language/tutorial/step3/map/step3c.map +++ b/matchbox-engine/src/test/resources/mapping-language/tutorial/step3/map/step3c.map @@ -7,6 +7,6 @@ uses "http://hl7.org/fhir/StructureDefinition/tutorial-right-3" alias TRight as group tutorial(source src : TLeft, target tgt : TRight) { // src.a2 as a check a2.length <= 20 -> tgt.a2 = a "rule_a20c"; see https://github.com/hapifhir/org.hl7.fhir.core/issues/1748 - src.a2 as a check a.length() <= 20 -> tgt.a2 = a "rule_a20c"; + src.a2 as a check (a.length() <= 20) -> tgt.a2 = a "rule_a20c"; } diff --git a/updatehapi.sh b/updatehapi.sh index 07bd11eae23..1965629d378 100755 --- a/updatehapi.sh +++ b/updatehapi.sh @@ -1,5 +1,6 @@ cp ../org.hl7.fhir.core/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/npm/NpmPackage.java matchbox-engine/src/main/java/org/hl7/fhir/utilities/npm cp ../org.hl7.fhir.core/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/XmlParser.java matchbox-engine/src/main/java/org/hl7/fhir/r5/elementmodel +cp ../org.hl7.fhir.core/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/FmlParser.java matchbox-engine/src/main/java/org/hl7/fhir/r5/elementmodel cp ../org.hl7.fhir.core/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/FHIRPathHostServices.java matchbox-engine/src/main/java/org/hl7/fhir/r5/utils/structuremap/ cp ../org.hl7.fhir.core/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java matchbox-engine/src/main/java/org/hl7/fhir/r5/utils/structuremap/ cp ../org.hl7.fhir.core/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java matchbox-engine/src/main/java/org/hl7/fhir/r5/context/ From 86433ad9228b0c65eebe8da1886071d638b1be97 Mon Sep 17 00:00:00 2001 From: oliveregger <oliver.egger@ahdis.ch> Date: Sat, 12 Oct 2024 14:19:46 +0200 Subject: [PATCH 5/6] update changelog --- docs/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.md b/docs/changelog.md index e44805e368c..afaa3de2020 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,4 +1,5 @@ Unreleased + - Tutorial for validation FHIR resources with [matchbox](https://ahdis.github.io/matchbox/validation-tutorial/) - Gazelle reports: add test to ensure https://gazelle.ihe.net/jira/browse/EHS-831 is fixed - Allow validating a resource through the GUI with URL search parameters [#288](https://github.com/ahdis/matchbox/issues/288) From 671ba092b95ff5c952bee59dc4794eec64ca7bd4 Mon Sep 17 00:00:00 2001 From: oliveregger <oliver.egger@ahdis.ch> Date: Sun, 13 Oct 2024 11:17:37 +0200 Subject: [PATCH 6/6] 6.3.31 --- .gitignore | 2 ++ docs/changelog.md | 2 +- docs/validation.md | 3 +++ .../main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java | 4 ++++ matchbox-server/with-elm/application.yaml | 4 ++++ pom.xml | 2 +- 6 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 463b0fd0f76..c39873f1e3a 100644 --- a/.gitignore +++ b/.gitignore @@ -229,3 +229,5 @@ database/h2.trace.db .apt_generated_tests matchbox-server/tx.html + +matchbox-server/test.html diff --git a/docs/changelog.md b/docs/changelog.md index afaa3de2020..2a54b237a83 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,7 +4,7 @@ Unreleased - Gazelle reports: add test to ensure https://gazelle.ihe.net/jira/browse/EHS-831 is fixed - Allow validating a resource through the GUI with URL search parameters [#288](https://github.com/ahdis/matchbox/issues/288) - Terminology: support CodeableConcept in ValueSet/$validate operation [#291](https://github.com/ahdis/matchbox/issues/291) -- Upgrade hapifhir org.hl7.fhir.core to 6.3.30 +- Upgrade hapifhir org.hl7.fhir.core to 6.3.31 - FML: Use FMLParser in StructureMapUtilities and support for identity transform [#289](https://github.com/ahdis/matchbox/issues/289) 2024/10/07 Release 3.9.3 diff --git a/docs/validation.md b/docs/validation.md index ed884da8d52..e523501d247 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -75,6 +75,9 @@ matchbox: |----------------------|-------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | implementationguides | 0..\* | the Implementation Guide and version which with which matchbox will be configured, you can provide by classpath, file, http address, if none is specified the FHIR package servers will be used (need to be online) | | txServer | 0..1 | txServer to use, n/a if none (default), http://localhost:8080/fhir for internal (if server.port in application.yaml is 8080), http://tx.fhir.org for hl7 validator | +| txUseEcosystem | 0..1 | boolean true if none (default), if true asks tx servers for which CodeSystem they are authorative | +| txServerCache | 0..1 | boolean if respones are cached, true if none (default) | +| txLog | 0..1 | string indicating file location of log (end either in .txt or .html, default no logging | | igsPreloaded | 0..\* | For each mentioned ImplementationGuide (comma separated) an engine will be created, which will be cached in memory as long the application is running. Other IG's will created on demand and will be cached for an hour for subsequent calls. Tradeoff between memory consumption and first response time (creating of engine might have duration of half a minute). Default no igs are preloaded. | | onlyOneEngine | 0..1 | Implementation Guides can have multiple versions with different dependencies. Matchbox creates for transformation and validation an own engine for each Implementation Guide and its dependencies (default setting). You can switch this behavior, e.g. if you are using it in development and want to create and update resources or transform maps. Set the setting for onlyOneEngine to true. The changes are however not persisted and will be lost if matchbox is restarted. | | httpReadOnly | 0..1 | Whether to allow creating, modifying or deleting resources on the server via the HTTP API or not. If `true`, IGs can only be loaded through the configuration. | diff --git a/matchbox-engine/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java b/matchbox-engine/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java index 2f20b8e73af..a1d07ea3d5b 100644 --- a/matchbox-engine/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java +++ b/matchbox-engine/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java @@ -1771,6 +1771,10 @@ protected void addServerValidationParameters(TerminologyClientContext terminolog } else if (options.getVsAsUrl()){ pin.addParameter().setName("url").setValue(new UriType(vs.getUrl())); } else { + if (vs.hasCompose() && vs.hasExpansion()) { + vs = vs.copy(); + vs.setExpansion(null); + } pin.addParameter().setName("valueSet").setResource(vs); if (vs.getUrl() != null) { terminologyClientContext.getCached().add(vs.getUrl()+"|"+ vs.getVersion()); diff --git a/matchbox-server/with-elm/application.yaml b/matchbox-server/with-elm/application.yaml index 3663a966680..0b15bf2f839 100644 --- a/matchbox-server/with-elm/application.yaml +++ b/matchbox-server/with-elm/application.yaml @@ -26,7 +26,11 @@ matchbox: fhir: context: fhirVersion: 4.0.1 +# txServer: http://tx.fhir.org txServer: http://localhost:${server.port}/matchboxv3/fhir + txUseEcosystem: false + txLog: test.html + txServerCache: false suppressWarnInfo: hl7.fhir.r4.core#4.0.1: - "Constraint failed: dom-6:" diff --git a/pom.xml b/pom.xml index 3ccf2d61470..fa69c3dc569 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> - <fhir.core.version>6.3.30</fhir.core.version> + <fhir.core.version>6.3.31</fhir.core.version> <hapi.fhir.version>7.4.0</hapi.fhir.version> <!-- Dependency Versions --> <validator_test_case_version>1.5.5</validator_test_case_version>