From ef8ee528b922c2ab47aad9948a7c75ecee3aaf3c Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Mon, 13 Jan 2025 23:00:26 +1100 Subject: [PATCH 01/13] Fix NPW generating snapshots --- .../org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java index 1f374e0a71..511269e868 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java @@ -2853,7 +2853,7 @@ else if (ToolingExtensions.hasExtension(expBase.getValueset().getExpansion(), To } else { boolean ok = true; for (ValueSetExpansionContainsComponent cc : expDerived.getValueset().getExpansion().getContains()) { - ValidationResult vr = context.validateCode(null, cc.getSystem(), cc.getVersion(), cc.getCode(), null, baseVs); + ValidationResult vr = context.validateCode(new ValidationOptions(), cc.getSystem(), cc.getVersion(), cc.getCode(), null, baseVs); if (!vr.isOk()) { ok = false; break; From 769fe6cb311adfa367e479e87479b1c1c2e7c722 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Mon, 13 Jan 2025 23:00:46 +1100 Subject: [PATCH 02/13] use 2.0.0 for IPS, not an old copy --- .../org/hl7/fhir/utilities/PathBuilder.java | 20 ++++++++++++------- .../org/hl7/fhir/validation/ValidatorCli.java | 2 +- .../cli/services/ValidationService.java | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/PathBuilder.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/PathBuilder.java index 2a0d237d74..2f9d4fc45a 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/PathBuilder.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/PathBuilder.java @@ -169,19 +169,25 @@ private void checkNonNullNonEmptyFirstEntry(String[] args) { private String replaceVariables(String a) throws IOException { if ("[tmp]".equals(a)) { - if (hasCTempDir()) { - return Utilities.C_TEMP_DIR; - } else if (FhirSettings.hasTempPath()) { - return FhirSettings.getTempPath(); - } else { - return System.getProperty("java.io.tmpdir"); - } + return getTempDir(); + } else if (a.startsWith("[tmp]")) { + return getTempDir()+a.substring(5); } else if ("[user]".equals(a)) { return System.getProperty("user.home"); } return a; } + private String getTempDir() throws IOException { + if (hasCTempDir()) { + return Utilities.C_TEMP_DIR; + } else if (FhirSettings.hasTempPath()) { + return FhirSettings.getTempPath(); + } else { + return System.getProperty("java.io.tmpdir"); + } + } + protected static boolean hasCTempDir() throws IOException { if (!System.getProperty("os.name").toLowerCase().contains("win")) { return false; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java index 902846d134..d63bb4090c 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java @@ -276,7 +276,7 @@ private static String[] addAdditionalParamsForIpsParam(String[] args) { res.add("4.0"); res.add("-check-ips-codes"); res.add("-ig"); - res.add("hl7.fhir.uv.ips#1.1.0"); + res.add("hl7.fhir.uv.ips#2.0.0"); res.add("-profile"); res.add("http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips"); res.add("-extension"); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java index 540276a226..4178f0a253 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java @@ -280,7 +280,7 @@ public void validateSources(CliContext cliContext, ValidationEngine validator, V if (cliContext.getOutput() == null) { dst = System.out; } else { - dst = new PrintStream(ManagedFileAccess.outStream(cliContext.getOutput())); + dst = new PrintStream(ManagedFileAccess.outStream(Utilities.path(cliContext.getOutput()))); } renderer.setOutput(dst); } else { From c9082f37d366fa1a6d07663bad2ebc9fdc83a5a4 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Tue, 14 Jan 2025 15:10:31 +1100 Subject: [PATCH 03/13] fix typo --- .../java/org/hl7/fhir/r4b/elementmodel/ObjectConverter.java | 2 +- .../main/java/org/hl7/fhir/r5/elementmodel/ObjectConverter.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/elementmodel/ObjectConverter.java b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/elementmodel/ObjectConverter.java index 4336d48733..529cc5ac78 100644 --- a/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/elementmodel/ObjectConverter.java +++ b/org.hl7.fhir.r4b/src/main/java/org/hl7/fhir/r4b/elementmodel/ObjectConverter.java @@ -72,7 +72,7 @@ public Element convert(Resource ig) throws IOException, FHIRException { ByteArrayInputStream bi = new ByteArrayInputStream(bs.toByteArray()); List list = new JsonParser(context).parse(bi); if (list.size() != 1) { - throw new FHIRException("Unable to convert because the source contains multieple resources"); + throw new FHIRException("Unable to convert because the source contains multiple resources"); } return list.get(0).getElement(); } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ObjectConverter.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ObjectConverter.java index 6d366baaab..fc126e6330 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ObjectConverter.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ObjectConverter.java @@ -73,7 +73,7 @@ public Element convert(Resource ig) throws IOException, FHIRException { ByteArrayInputStream bi = new ByteArrayInputStream(bs.toByteArray()); List list = new JsonParser(context).parse(bi); if (list.size() != 1) { - throw new FHIRException("Unable to convert because the source contains multieple resources"); + throw new FHIRException("Unable to convert because the source contains multiple resources"); } return list.get(0).getElement(); } From 99697351a09359d33feb56cde841bd1ba6354863 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Tue, 14 Jan 2025 15:17:46 +1100 Subject: [PATCH 04/13] Correct grammar in language about resource not being valid against a profile --- .../src/main/resources/Messages.properties | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index abe7a46b7f..588c07d6ba 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -556,8 +556,8 @@ RESOURCETYPE_PROPERTY_WRONG_TYPE = The JSON element ''resourceType'' has the wro Reference_REF_Aggregation = Reference is {0} which isn''t supported by the specified aggregation mode(s) for the reference ({1}) Reference_REF_BadTargetType = Invalid Resource target type. Found {0}, but expected one of ({1}) Reference_REF_BadTargetType2 = The type ''{0}'' implied by the reference URL {1} is not a valid Target for this element (must be one of {2}) -Reference_REF_CantMatchChoice = Unable to find a match for profile {0} among choices: {1} -Reference_REF_CantMatchType = Unable to find a match for profile {0} (by type) among choices: {1} +Reference_REF_CantMatchChoice = Unable to find a profile match for {0} among choices: {1} +Reference_REF_CantMatchType = Unable to find a profile match for {0} (by type) among choices: {1} Reference_REF_CantResolve = Unable to resolve resource with reference ''{0}'' Reference_REF_CantResolveProfile = Unable to resolve the profile reference ''{0}'' Reference_REF_Format1 = Relative URLs must be of the format [ResourceName]/[id], or a search URL is allowed ([type]?parameters. Encountered {0}) @@ -1216,4 +1216,5 @@ CODESYSTEM_PROPERTY_CODE_DEFAULT_WARNING = The type of property ''{0}'' is ''cod CODESYSTEM_PROPERTY_VALUESET_NOT_FOUND = The ValueSet {0} is unknown, so the property codes cannot be validated CODESYSTEM_PROPERTY_BAD_INTERNAL_REFERENCE = The code ''{0}'' is not a valid code in this code system CODESYSTEM_PROPERTY_BAD_PROPERTY_CODE = The code ''{0}'' is not a valid code in the value set ''{1}'' -CODESYSTEM_DUPLICATE_CODE = The code ''{0}'' has already been defined \ No newline at end of file +CODESYSTEM_DUPLICATE_CODE = The code ''{0}'' has already been defined +REFERENCE_RESOLUTION_FAILED = Fetching ''{0}'' failed. System details: {1}: {2} From 8ed34e2604276afbbc1de77011c8030af826a1d3 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Tue, 14 Jan 2025 15:51:54 +1100 Subject: [PATCH 05/13] Add validator support for -check-references and -resolution-context --- .../filesystem/ManagedFileAccess.java | 16 ++ .../fhir/utilities/i18n/I18nConstants.java | 1 + .../hl7/fhir/validation/BaseValidator.java | 6 +- .../hl7/fhir/validation/ValidationEngine.java | 10 +- .../fhir/validation/cli/model/CliContext.java | 40 ++++- .../services/StandAloneValidatorFetcher.java | 137 ++++++++++++++-- .../cli/services/ValidationService.java | 6 + .../hl7/fhir/validation/cli/utils/Params.java | 6 + .../instance/InstanceValidator.java | 37 +++-- .../BasePolicyAdvisorForFullValidation.java | 8 + .../validation/instance/utils/NodeStack.java | 7 +- .../tests/ValidationEngineTests.java | 154 ++++++++++++++++++ 12 files changed, 393 insertions(+), 35 deletions(-) diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/filesystem/ManagedFileAccess.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/filesystem/ManagedFileAccess.java index 0a5caae356..9c6490cdcf 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/filesystem/ManagedFileAccess.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/filesystem/ManagedFileAccess.java @@ -129,6 +129,22 @@ public static File file(String path, String filepath) throws IOException { throw new IOException("Internal Error"); } } + + public static File file(File root, String filepath) throws IOException { + switch (accessPolicy) { + case DIRECT: + if (!inAllowedPaths(root.getAbsolutePath())) { + throw new IOException("The path '"+root.getAbsolutePath()+"' cannot be accessed by policy"); + } + return new File(root.getAbsolutePath(), filepath); + case MANAGED: + return accessor.file(Utilities.path(root.getAbsolutePath(), filepath)); + case PROHIBITED: + throw new IOException("Access to files is not allowed by local security policy"); + default: + throw new IOException("Internal Error"); + } + } /** * Open a FileInputStream, conforming to local security policy **/ diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java index 7e16141cd8..9edc4b03a6 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java @@ -1184,4 +1184,5 @@ public class I18nConstants { public static final String CODESYSTEM_PROPERTY_BAD_INTERNAL_REFERENCE = "CODESYSTEM_PROPERTY_BAD_INTERNAL_REFERENCE"; public static final String CODESYSTEM_PROPERTY_BAD_PROPERTY_CODE = "CODESYSTEM_PROPERTY_BAD_PROPERTY_CODE"; public static final String CODESYSTEM_DUPLICATE_CODE = "CODESYSTEM_DUPLICATE_CODE"; + public static final String REFERENCE_RESOLUTION_FAILED = "REFERENCE_RESOLUTION_FAILED"; } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java index 12ab270289..42d41b794d 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java @@ -343,7 +343,8 @@ protected boolean hint(List errors, String ruleDate, IssueTyp protected boolean slicingHint(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, boolean isCritical, String msg, String html, String[] text) { if (!thePass && doingHints()) { - addValidationMessage(errors, ruleDate, type, line, col, path, msg, IssueSeverity.INFORMATION, null).setSlicingHint(true).setSliceHtml(html, text).setCriticalSignpost(isCritical); + addValidationMessage(errors, ruleDate, type, line, col, path, msg, IssueSeverity.INFORMATION, null) + .setMessageId(I18nConstants.DETAILS_FOR__MATCHING_AGAINST_PROFILE_).setSlicingHint(true).setSliceHtml(html, text).setCriticalSignpost(isCritical); } return thePass; } @@ -356,7 +357,8 @@ protected boolean slicingHint(List errors, String ruleDate, I */ protected boolean slicingHint(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, boolean isCritical, String msg, String html, String[] text, List sliceInfo, String id) { if (!thePass && doingHints()) { - addValidationMessage(errors, ruleDate, type, line, col, path, msg, IssueSeverity.INFORMATION, id).setSlicingHint(true).setSliceHtml(html, text).setCriticalSignpost(isCritical).setSliceInfo(sliceInfo); + addValidationMessage(errors, ruleDate, type, line, col, path, msg, IssueSeverity.INFORMATION, id) + .setMessageId(I18nConstants.DETAILS_FOR__MATCHING_AGAINST_PROFILE_).setSlicingHint(true).setSliceHtml(html, text).setCriticalSignpost(isCritical).setSliceInfo(sliceInfo); } return thePass; } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java index f89c9a7abe..7bf3b27573 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java @@ -35,8 +35,12 @@ import org.hl7.fhir.r5.context.IWorkerContextManager; import org.hl7.fhir.r5.context.SimpleWorkerContext; import org.hl7.fhir.r5.context.SystemOutLoggingService; -import org.hl7.fhir.r5.elementmodel.*; +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.elementmodel.Manager; import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; +import org.hl7.fhir.r5.elementmodel.ObjectConverter; +import org.hl7.fhir.r5.elementmodel.ParserBase; +import org.hl7.fhir.r5.elementmodel.SHCParser; import org.hl7.fhir.r5.fhirpath.ExpressionNode; import org.hl7.fhir.r5.fhirpath.FHIRPathEngine; import org.hl7.fhir.r5.formats.FormatUtilities; @@ -61,9 +65,9 @@ import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.renderers.RendererFactory; import org.hl7.fhir.r5.renderers.utils.RenderingContext; -import org.hl7.fhir.r5.renderers.utils.ResourceWrapper; import org.hl7.fhir.r5.renderers.utils.RenderingContext.GenerationRules; import org.hl7.fhir.r5.renderers.utils.RenderingContext.ResourceRendererMode; +import org.hl7.fhir.r5.renderers.utils.ResourceWrapper; import org.hl7.fhir.r5.utils.EOperationOutcome; import org.hl7.fhir.r5.utils.ToolingExtensions; import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities; @@ -79,6 +83,7 @@ import org.hl7.fhir.r5.utils.validation.constants.ContainedReferenceValidationPolicy; import org.hl7.fhir.r5.utils.validation.constants.IdStatus; import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; +import org.hl7.fhir.utilities.ByteProvider; import org.hl7.fhir.utilities.FhirPublication; import org.hl7.fhir.utilities.IniFile; import org.hl7.fhir.utilities.SIDUtilities; @@ -107,7 +112,6 @@ import org.hl7.fhir.validation.instance.InstanceValidator; import org.hl7.fhir.validation.instance.advisor.BasePolicyAdvisorForFullValidation; import org.hl7.fhir.validation.instance.utils.ValidationContext; -import org.hl7.fhir.utilities.ByteProvider; import org.xml.sax.SAXException; import lombok.Getter; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java index 57100e9578..e8dae20e1e 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java @@ -59,6 +59,15 @@ public class CliContext { @SerializedName("assumeValidRestReferences") private boolean assumeValidRestReferences = false; + @JsonProperty("checkReferences") + @SerializedName("checkReferences") + private + boolean checkReferences = false; + @JsonProperty("resolutionContext") + @SerializedName("resolutionContext") + private + String resolutionContext = null; + @JsonProperty("canDoNative") @SerializedName("canDoNative") private @@ -381,6 +390,18 @@ public CliContext setSource(String source) { return this; } + @SerializedName("resolutionContext") + @JsonProperty("resolutionContext") + public String getResolutionContext() { + return resolutionContext; + } + + @SerializedName("resolutionContext") + @JsonProperty("resolutionContext") + public CliContext setResolutionContext(String resolutionContext) { + this.resolutionContext = resolutionContext; + return this; + } @SerializedName("langTransform") @JsonProperty("langTransform") @@ -967,6 +988,19 @@ public CliContext setAssumeValidRestReferences(boolean assumeValidRestReferences return this; } + @SerializedName("checkReferences") + @JsonProperty("checkReferences") + public boolean isCheckReferences() { + return checkReferences; + } + + @SerializedName("checkReferences") + @JsonProperty("checkReferences") + public CliContext setCheckReferences(boolean checkReferences) { + this.checkReferences = checkReferences; + return this; + } + @SerializedName("noInternalCaching") @JsonProperty("noInternalCaching") public boolean isNoInternalCaching() { @@ -1150,6 +1184,7 @@ public boolean equals(Object o) { recursive == that.recursive && doDebug == that.doDebug && assumeValidRestReferences == that.assumeValidRestReferences && + checkReferences == that.checkReferences && canDoNative == that.canDoNative && noInternalCaching == that.noInternalCaching && noExtensibleBindingMessages == that.noExtensibleBindingMessages && @@ -1161,6 +1196,7 @@ public boolean equals(Object o) { checkIPSCodes == that.checkIPSCodes && Objects.equals(extensions, that.extensions) && Objects.equals(map, that.map) && + Objects.equals(resolutionContext, that.resolutionContext) && Objects.equals(htmlInMarkdownCheck, that.htmlInMarkdownCheck) && Objects.equals(output, that.output) && Objects.equals(outputSuffix, that.outputSuffix) && @@ -1206,7 +1242,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(baseEngine, doNative, extensions, hintAboutNonMustSupport, recursive, doDebug, assumeValidRestReferences, canDoNative, noInternalCaching, + return Objects.hash(baseEngine, doNative, extensions, hintAboutNonMustSupport, recursive, doDebug, assumeValidRestReferences, checkReferences,canDoNative, noInternalCaching, resolutionContext, noExtensibleBindingMessages, noInvariants, displayWarnings, wantInvariantsInMessages, map, output, outputSuffix, htmlOutput, txServer, sv, txLog, txCache, mapLog, lang, srcLang, tgtLang, fhirpath, snomedCT, targetVer, packageName, igs, questionnaireMode, level, profiles, options, sources, inputs, mode, locale, locations, crumbTrails, showMessageIds, forPublication, showTimes, allowExampleUrls, outputStyle, jurisdiction, noUnicodeBiDiControlChars, watchMode, watchScanDelay, watchSettleTime, bestPracticeLevel, unknownCodeSystemsCauseErrors, noExperimentalContent, advisorFile, expansionParameters, format, htmlInMarkdownCheck, allowDoubleQuotesInFHIRPath, checkIPSCodes); @@ -1222,6 +1258,7 @@ public String toString() { ", recursive=" + recursive + ", doDebug=" + doDebug + ", assumeValidRestReferences=" + assumeValidRestReferences + + ", checkReferences=" + checkReferences + ", canDoNative=" + canDoNative + ", noInternalCaching=" + noInternalCaching + ", noExtensibleBindingMessages=" + noExtensibleBindingMessages + @@ -1238,6 +1275,7 @@ public String toString() { ", txLog='" + txLog + '\'' + ", txCache='" + txCache + '\'' + ", mapLog='" + mapLog + '\'' + + ", resolutionContext='" + resolutionContext + '\'' + ", lang='" + lang + '\'' + ", srcLang='" + srcLang + '\'' + ", tgtLang='" + tgtLang + '\'' + diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/StandAloneValidatorFetcher.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/StandAloneValidatorFetcher.java index 8fb1972efe..8b3805d322 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/StandAloneValidatorFetcher.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/StandAloneValidatorFetcher.java @@ -1,9 +1,14 @@ package org.hl7.fhir.validation.cli.services; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; @@ -12,12 +17,18 @@ import java.util.Map; import java.util.Set; +import javax.annotation.Nonnull; + import org.hl7.fhir.convertors.txClient.TerminologyClientFactory; +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.context.IWorkerContextManager; import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.elementmodel.Element.SpecialElement; +import org.hl7.fhir.r5.elementmodel.Manager; +import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; import org.hl7.fhir.r5.model.CanonicalResource; import org.hl7.fhir.r5.model.ElementDefinition; import org.hl7.fhir.r5.model.Resource; @@ -28,17 +39,16 @@ import org.hl7.fhir.r5.utils.validation.IResourceValidator; import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; -import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor.AdditionalBindingPurpose; -import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor.CodedContentValidationAction; -import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor.ElementValidationAction; -import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor.ResourceValidationAction; import org.hl7.fhir.r5.utils.validation.constants.BindingKind; -import org.hl7.fhir.r5.utils.validation.constants.CodedContentValidationPolicy; import org.hl7.fhir.r5.utils.validation.constants.ContainedReferenceValidationPolicy; import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; +import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.VersionUtilities; import org.hl7.fhir.utilities.VersionUtilities.VersionURLInfo; +import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; +import org.hl7.fhir.utilities.http.HTTPResult; +import org.hl7.fhir.utilities.http.ManagedWebAccess; import org.hl7.fhir.utilities.json.model.JsonObject; import org.hl7.fhir.utilities.json.parser.JsonParser; import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; @@ -47,8 +57,6 @@ import org.hl7.fhir.validation.cli.utils.Common; import org.hl7.fhir.validation.instance.advisor.BasePolicyAdvisorForFullValidation; -import javax.annotation.Nonnull; - public class StandAloneValidatorFetcher implements IValidatorResourceFetcher, IValidationPolicyAdvisor, IWorkerContextManager.ICanonicalResourceLocator { @@ -60,6 +68,9 @@ public class StandAloneValidatorFetcher implements IValidatorResourceFetcher, IV private Map pidList = new HashMap<>(); private Map pidMap = new HashMap<>(); private IValidationPolicyAdvisor policyAdvisor; + private String resolutionContext; + private Map knownFiles = new HashMap<>(); + public StandAloneValidatorFetcher(FilesystemPackageCacheManager pcm, IWorkerContext context, IPackageInstaller installer) { this.pcm = pcm; @@ -69,8 +80,98 @@ public StandAloneValidatorFetcher(FilesystemPackageCacheManager pcm, IWorkerCont } @Override - public Element fetch(IResourceValidator validator, Object appContext, String url) throws FHIRException { - throw new FHIRException("The URL '" + url + "' is not known to the FHIR validator, and has not been provided as part of the setup / parameters"); + public Element fetch(IResourceValidator validator, Object appContext, String url) throws FHIRException, IOException { + if (!Utilities.isAbsoluteUrl(url) && Utilities.startsWithInList(resolutionContext, "http:", "https:")) { + url = Utilities.pathURL(resolutionContext, url); + } + + if (Utilities.isAbsoluteUrl(url)) { + HTTPResult cnt = null; + try { + cnt = ManagedWebAccess.get(Arrays.asList("web"), url, "application/json"); + cnt.checkThrowException(); + + } catch (Exception e) { + cnt = ManagedWebAccess.get(Arrays.asList("web"), url, "application/fhir+xml"); + cnt.checkThrowException(); + } + if (cnt.getContentType() != null && cnt.getContentType().contains("xml")) { + return Manager.parse(context, new ByteArrayInputStream(cnt.getContent()), FhirFormat.XML).get(0).getElement(); + } else { + return Manager.parse(context, new ByteArrayInputStream(cnt.getContent()), FhirFormat.JSON).get(0).getElement(); + } + } else if (resolutionContext == null) { + throw new FHIRException("The URL '" + url + "' is not known to the FHIR validator, and a resolution context has not been provided as part of the setup / parameters"); + } else if (resolutionContext.startsWith("file:")) { + File rc = ManagedFileAccess.file(resolutionContext.substring(5)); + if (!rc.exists()) { + throw new FHIRException("The URL '" + url + "' is not known to the FHIR validator, and a resolution context has not been provided as part of the setup / parameters"); + } + // first we look for the file by several different patterns + File tgt = ManagedFileAccess.file(rc, url); + if (tgt.exists()) { + return see(tgt, loadFile(tgt)); + } + tgt = ManagedFileAccess.file(rc, url+".json"); + if (tgt.exists()) { + return see(tgt, loadFile(tgt)); + } + tgt = ManagedFileAccess.file(rc, url+".xml"); + if (tgt.exists()) { + return see(tgt, loadFile(tgt)); + } + String[] p = url.split("\\/"); + if (p.length != 2) { + throw new FHIRException("The URL '" + url + "' was not understood - expecting type/id"); + } + if (knownFiles.containsKey(p[0]+"/"+p[1])) { + tgt = ManagedFileAccess.file(knownFiles.get(p[0]+"/"+p[1])); + return loadFile(tgt); + } + tgt = ManagedFileAccess.file(rc, p[0]+"-"+p[1]+".json"); + if (tgt.exists()) { + return see(tgt, loadFile(tgt)); + } + tgt = ManagedFileAccess.file(rc, p[0]+"-"+p[1]+".xml"); + if (tgt.exists()) { + return see(tgt, loadFile(tgt)); + } + // didn't find it? now scan... + for (File f : ManagedFileAccess.listFiles(rc)) { + if (isPossibleMatch(f, p[0], p[1])) { + Element e = see(f, loadFile(f)); + if (p[0].equals(e.fhirType()) && p[1].equals(e.getIdBase())) { + return e; + } + } + } + return null; + } else { + throw new FHIRException("The resolution context '"+resolutionContext+"' was not understood"); + + } + } + + private Element see(File f, Element e) { + knownFiles.put(e.fhirType()+"/"+e.getIdBase(), f.getAbsolutePath()); + return e; + } + + private boolean isPossibleMatch(File f, String rt, String id) throws FileNotFoundException, IOException { + String src = TextFile.fileToString(f); + if (f.getName().endsWith(".xml")) { + return src.contains("<"+rt) && src.contains("\""+id+"\""); + } else { + return src.contains("\""+rt+"\"") && src.contains("\""+id+"\""); + } + } + + private Element loadFile(File tgt) throws FHIRFormatError, DefinitionException, FHIRException, FileNotFoundException, IOException { + if (tgt.getName().endsWith(".xml")) { + return Manager.parse(context, new FileInputStream(tgt), FhirFormat.XML).get(0).getElement(); + } else { + return Manager.parse(context, new FileInputStream(tgt), FhirFormat.JSON).get(0).getElement(); + } } @Override @@ -78,7 +179,7 @@ public ReferenceValidationPolicy policyForReference(IResourceValidator validator Object appContext, String path, String url) { - return ReferenceValidationPolicy.IGNORE; + return policyAdvisor.policyForReference(validator, appContext, path, url); } @Override @@ -344,6 +445,14 @@ public ReferenceValidationPolicy getReferencePolicy() { return policyAdvisor.getReferencePolicy(); } + public void setReferencePolicy(ReferenceValidationPolicy policy) { + if (policyAdvisor instanceof BasePolicyAdvisorForFullValidation) { + ((BasePolicyAdvisorForFullValidation) policyAdvisor).setRefpol(policy); + } else { + throw new Error("Cannot set reference policy on a "+policy.getClass().getName()); + } + } + public IValidationPolicyAdvisor getPolicyAdvisor() { return policyAdvisor; } @@ -353,4 +462,12 @@ public IValidationPolicyAdvisor setPolicyAdvisor(IValidationPolicyAdvisor policy return this; } + public String getResolutionContext() { + return resolutionContext; + } + + public void setResolutionContext(String resolutionContext) { + this.resolutionContext = resolutionContext; + } + } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java index 4178f0a253..639f182072 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java @@ -636,6 +636,12 @@ protected ValidationEngine buildValidationEngine(CliContext cliContext, String d validationEngine.setFetcher(fetcher); validationEngine.getContext().setLocator(fetcher); validationEngine.setPolicyAdvisor(fetcher); + if (cliContext.isCheckReferences()) { + fetcher.setReferencePolicy(ReferenceValidationPolicy.CHECK_VALID); + } else { + fetcher.setReferencePolicy(ReferenceValidationPolicy.IGNORE); + } + fetcher.setResolutionContext(cliContext.getResolutionContext()); } else { DisabledValidationPolicyAdvisor fetcher = new DisabledValidationPolicyAdvisor(); validationEngine.setPolicyAdvisor(fetcher); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java index 61d69fb0fe..fc6ee9879d 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java @@ -38,6 +38,8 @@ public class Params { public static final String QUESTIONNAIRE = "-questionnaire"; public static final String NATIVE = "-native"; public static final String ASSUME_VALID_REST_REF = "-assumeValidRestReferences"; + public static final String CHECK_REFERENCES = "-check-references"; + public static final String RESOLUTION_CONTEXT = "-resolution-context"; public static final String DEBUG = "-debug"; public static final String SCT = "-sct"; public static final String RECURSE = "-recurse"; @@ -273,6 +275,10 @@ else if (args[i].equals(HTML_OUTPUT)) { cliContext.setDoNative(true); } else if (args[i].equals(ASSUME_VALID_REST_REF)) { cliContext.setAssumeValidRestReferences(true); + } else if (args[i].equals(CHECK_REFERENCES)) { + cliContext.setCheckReferences(true); + } else if (args[i].equals(RESOLUTION_CONTEXT)) { + cliContext.setResolutionContext(args[++i]); } else if (args[i].equals(DEBUG)) { cliContext.setDoDebug(true); } else if (args[i].equals(SCT)) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index 14f9ae2bb0..46925d023f 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -2023,21 +2023,6 @@ else if (strength == BindingStrength.EXTENSIBLE) { return ok; } - private Set getUnknownSystems(ValidationResult vr) { - if (vr == null) { - return null; - } - if (vr.getUnknownSystems() != null && !vr.getUnknownSystems().isEmpty()) { - return vr.getUnknownSystems(); - } - if (vr.getSystem() != null) { - Set set = new HashSet(); - set.add(vr.getSystem()); - return set; - } - return null; - } - private boolean convertCDACodeToCodeableConcept(List errors, String path, Element element, StructureDefinition logical, CodeableConcept cc) { boolean ok = true; cc.setText(element.getNamedChildValue("originalText", false)); @@ -2525,7 +2510,7 @@ private boolean checkExtensionContext(Object appContext, List boolean ok = false; CommaSeparatedStringBuilder contexts = new CommaSeparatedStringBuilder(); List plist = new ArrayList<>(); - plist.add(stripIndexes(stack.getLiteralPath())); + plist.add(stripIndexes(stripRefs(stack.getLiteralPath()))); for (String s : stack.getLogicalPaths()) { String p = stripIndexes(s); // all extensions are always allowed in ElementDefinition.example.value, and in fixed and pattern values. TODO: determine the logical paths from the path stated in the element definition.... @@ -2683,6 +2668,17 @@ private boolean checkExtensionContext(Object appContext, List } } + private String stripRefs(String literalPath) { + if (literalPath.contains(".resolve().ofType(")) { + String s = literalPath.substring(literalPath.lastIndexOf(".resolve().")+18); + int i = s.indexOf(")"); + s = s.substring(0, i)+s.substring(i+1); + return s; + } else { + return literalPath; + } + } + private boolean containsAny(Set set, List list) { for (String p : list) { if (set.contains(p)) { @@ -4182,9 +4178,14 @@ private boolean checkReference(ValidationContext valContext, } else { try { ext = fetcher.fetch(this, valContext.getAppContext(), ref); - } catch (IOException e) { + } catch (Exception e) { if (STACK_TRACE) e.printStackTrace(); - throw new FHIRException(e); + ext = null; + + // it's probably an error, but here we're just giving the user information about why resolution failed + hint(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, element.line(), element.col(), path, + false, I18nConstants.REFERENCE_RESOLUTION_FAILED, ref, e.getClass().getName(), e.getMessage()); + } if (ext != null) { setParents(ext); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/BasePolicyAdvisorForFullValidation.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/BasePolicyAdvisorForFullValidation.java index 4ad4164d79..0413bbface 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/BasePolicyAdvisorForFullValidation.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/BasePolicyAdvisorForFullValidation.java @@ -39,6 +39,14 @@ public BasePolicyAdvisorForFullValidation(ReferenceValidationPolicy refpol) { this.refpol = refpol; } + public ReferenceValidationPolicy getRefpol() { + return refpol; + } + + public void setRefpol(ReferenceValidationPolicy refpol) { + this.refpol = refpol; + } + @Override public ReferenceValidationPolicy policyForReference(IResourceValidator validator, Object appContext, String path, String url) { return refpol; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java index 687e8e90f4..18024a332e 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/utils/NodeStack.java @@ -64,7 +64,12 @@ public NodeStack(IWorkerContext context, Element element, String refPath, String this.context = context; ids = new HashMap<>(); this.element = element; - literalPath = refPath + "->" + element.getName(); + int i = element.getName().indexOf("."); + if (i == -1) { + literalPath = refPath+".resolve().ofType(" + element.getName()+")"; + } else { + literalPath = refPath+".resolve().ofType(" + element.getName().substring(0, i)+")"+element.getName().substring(i); + } workingLang = validationLanguage; } diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java index 4d7770ec68..1a4fd22803 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java @@ -3,21 +3,30 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.IOException; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.List; +import org.hl7.fhir.r5.formats.JsonParser; import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; import org.hl7.fhir.r5.model.OperationOutcome; import org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent; import org.hl7.fhir.r5.test.utils.TestingUtilities; import org.hl7.fhir.r5.utils.OperationOutcomeUtilities; +import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.FhirPublication; +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; import org.hl7.fhir.utilities.settings.FhirSettings; import org.hl7.fhir.utilities.tests.CacheVerificationLogger; import org.hl7.fhir.validation.IgLoader; import org.hl7.fhir.validation.ValidationEngine; +import org.hl7.fhir.validation.cli.services.StandAloneValidatorFetcher; import org.hl7.fhir.validation.tests.utilities.TestUtilities; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; @@ -285,4 +294,149 @@ public static void execute() throws Exception { System.out.println("Finished"); } + + @Test + public void testResolveRelativeFileValid() throws Exception { + String folder = setupFolder(); + try { + ValidationEngine ve = TestUtilities.getValidationEngine("hl7.fhir.r4.core#4.0.1", DEF_TX, FhirPublication.R4, "4.0.1"); + StandAloneValidatorFetcher fetcher = new StandAloneValidatorFetcher(ve.getPcm(), ve.getContext(), ve); + ve.setFetcher(fetcher); + ve.getContext().setLocator(fetcher); + ve.setPolicyAdvisor(fetcher); + fetcher.setReferencePolicy(ReferenceValidationPolicy.CHECK_VALID); + fetcher.setResolutionContext("file:"+folder); + ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Observation.json"))); + ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Patient.json"))); + OperationOutcome op = ve.validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", "resolution", "relative-url-valid.json"), null); + Assertions.assertTrue(checkOutcomes("testResolveRelativeFileValid", op, + "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have a performer\n"+ + "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have an effective[x] ()\n"+ + "Observation null warning/invariant: Constraint failed: dom-6: 'A resource should have narrative for robust management' (defined in http://hl7.org/fhir/StructureDefinition/DomainResource) (Best Practice Recommendation)")); + } finally { + Utilities.clearDirectory(folder); + ManagedFileAccess.file(folder).delete(); + } + } + + @Test + public void testResolveRelativeFileInvalid() throws Exception { + String folder = setupFolder(); + try { + ValidationEngine ve = TestUtilities.getValidationEngine("hl7.fhir.r4.core#4.0.1", DEF_TX, FhirPublication.R4, "4.0.1"); + StandAloneValidatorFetcher fetcher = new StandAloneValidatorFetcher(ve.getPcm(), ve.getContext(), ve); + ve.setFetcher(fetcher); + ve.getContext().setLocator(fetcher); + ve.setPolicyAdvisor(fetcher); + fetcher.setReferencePolicy(ReferenceValidationPolicy.CHECK_VALID); + fetcher.setResolutionContext("file:"+folder); + ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Observation.json"))); + ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Patient.json"))); + OperationOutcome op = ve.validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", "resolution", "relative-url-invalid.json"), null); + Assertions.assertTrue(checkOutcomes("testResolveRelativeFileValid", op, + "Observation.subject null error/structure: Unable to find a profile match for Patient/example-newborn among choices: http://hl7.org/fhir/test/StructureDefinition/PatientRule\n"+ + "Observation.subject null information/structure: Details for Patient/example-newborn matching against profile http://hl7.org/fhir/test/StructureDefinition/PatientRule|0.1.0\n"+ + "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have a performer\n"+ + "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have an effective[x] ()\n"+ + "Observation null warning/invariant: Constraint failed: dom-6: 'A resource should have narrative for robust management' (defined in http://hl7.org/fhir/StructureDefinition/DomainResource) (Best Practice Recommendation)")); + } finally { + Utilities.clearDirectory(folder); + ManagedFileAccess.file(folder).delete(); + } + } + + @Test + public void testResolveRelativeFileError() throws Exception { + String folder = setupFolder(); + try { + ValidationEngine ve = TestUtilities.getValidationEngine("hl7.fhir.r4.core#4.0.1", DEF_TX, FhirPublication.R4, "4.0.1"); + StandAloneValidatorFetcher fetcher = new StandAloneValidatorFetcher(ve.getPcm(), ve.getContext(), ve); + ve.setFetcher(fetcher); + ve.getContext().setLocator(fetcher); + ve.setPolicyAdvisor(fetcher); + fetcher.setReferencePolicy(ReferenceValidationPolicy.CHECK_VALID); + fetcher.setResolutionContext("file:"+folder); + ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Observation.json"))); + ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Patient.json"))); + OperationOutcome op = ve.validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", "resolution", "relative-url-error.json"), null); + Assertions.assertTrue(checkOutcomes("testResolveRelativeFileValid", op, + "Observation.subject null error/structure: Unable to resolve resource with reference 'patient/example-newborn-x'\n"+ + "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have a performer\n"+ + "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have an effective[x] ()\n"+ + "Observation null warning/invariant: Constraint failed: dom-6: 'A resource should have narrative for robust management' (defined in http://hl7.org/fhir/StructureDefinition/DomainResource) (Best Practice Recommendation)")); + } finally { + Utilities.clearDirectory(folder); + ManagedFileAccess.file(folder).delete(); + } + } + + + @Test + public void testResolveAbsoluteValid() throws Exception { + ValidationEngine ve = TestUtilities.getValidationEngine("hl7.fhir.r4.core#4.0.1", DEF_TX, FhirPublication.R4, "4.0.1"); + StandAloneValidatorFetcher fetcher = new StandAloneValidatorFetcher(ve.getPcm(), ve.getContext(), ve); + ve.setFetcher(fetcher); + ve.getContext().setLocator(fetcher); + ve.setPolicyAdvisor(fetcher); + ve.setShowMessagesFromReferences(true); + fetcher.setReferencePolicy(ReferenceValidationPolicy.CHECK_VALID); + ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Observation.json"))); + ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Patient.json"))); + OperationOutcome op = ve.validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", "resolution", "absolute-url-valid.json"), null); + Assertions.assertTrue(checkOutcomes("testResolveRelativeFileValid", op, + "Observation.subject.resolve().ofType(Patient).managingOrganization null error/structure: Unable to resolve resource with reference 'Organization/1'\n"+ + "Observation.subject.resolve().ofType(Patient).managingOrganization null information/informational: Fetching 'Organization/1' failed. System details: org.hl7.fhir.exceptions.FHIRException: The URL 'Organization/1' is not known to the FHIR validator, and a resolution context has not been provided as part of the setup / parameters\n"+ + "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have a performer\n"+ + "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have an effective[x] ()\n"+ + "Observation null warning/invariant: Constraint failed: dom-6: 'A resource should have narrative for robust management' (defined in http://hl7.org/fhir/StructureDefinition/DomainResource) (Best Practice Recommendation)")); + } + + @Test + public void testResolveAbsoluteInvalid() throws Exception { + ValidationEngine ve = TestUtilities.getValidationEngine("hl7.fhir.r4.core#4.0.1", DEF_TX, FhirPublication.R4, "4.0.1"); + StandAloneValidatorFetcher fetcher = new StandAloneValidatorFetcher(ve.getPcm(), ve.getContext(), ve); + ve.setFetcher(fetcher); + ve.getContext().setLocator(fetcher); + ve.setPolicyAdvisor(fetcher); + fetcher.setReferencePolicy(ReferenceValidationPolicy.CHECK_VALID); + ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Observation.json"))); + ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Patient.json"))); + OperationOutcome op = ve.validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", "resolution", "absolute-url-invalid.json"), null); + Assertions.assertTrue(checkOutcomes("testResolveRelativeFileValid", op, + "Observation.subject null error/structure: Unable to find a profile match for https://hl7.org/fhir/R4/patient-example-newborn.json among choices: http://hl7.org/fhir/test/StructureDefinition/PatientRule\n"+ + "Observation.subject null information/structure: Details for https://hl7.org/fhir/R4/patient-example-newborn.json matching against profile http://hl7.org/fhir/test/StructureDefinition/PatientRule|0.1.0\n"+ + "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have a performer\n"+ + "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have an effective[x] ()\n"+ + "Observation null warning/invariant: Constraint failed: dom-6: 'A resource should have narrative for robust management' (defined in http://hl7.org/fhir/StructureDefinition/DomainResource) (Best Practice Recommendation)")); + } + + @Test + public void testResolveAbsoluteError() throws Exception { + ValidationEngine ve = TestUtilities.getValidationEngine("hl7.fhir.r4.core#4.0.1", DEF_TX, FhirPublication.R4, "4.0.1"); + StandAloneValidatorFetcher fetcher = new StandAloneValidatorFetcher(ve.getPcm(), ve.getContext(), ve); + ve.setFetcher(fetcher); + ve.getContext().setLocator(fetcher); + ve.setPolicyAdvisor(fetcher); + fetcher.setReferencePolicy(ReferenceValidationPolicy.CHECK_VALID); + ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Observation.json"))); + ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Patient.json"))); + OperationOutcome op = ve.validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", "resolution", "absolute-url-error.json"), null); + Assertions.assertTrue(checkOutcomes("testResolveRelativeFileValid", op, + "Observation.subject null error/structure: Unable to resolve resource with reference 'http://hl7x.org/fhir/R4/Patient/Patient/example-newborn'\n"+ + "Observation.subject null information/informational: Fetching 'http://hl7x.org/fhir/R4/Patient/Patient/example-newborn' failed. System details: java.net.UnknownHostException: hl7x.org\n"+ + "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have a performer\n"+ + "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have an effective[x] ()\n"+ + "Observation null warning/invariant: Constraint failed: dom-6: 'A resource should have narrative for robust management' (defined in http://hl7.org/fhir/StructureDefinition/DomainResource) (Best Practice Recommendation)")); + } + + private String setupFolder() throws IOException { + String now = new SimpleDateFormat("yyyymmddhhMMss").format(new Date()); + String folder = Utilities.path("[tmp]", "validator-resolution", now); + Utilities.createDirectory(folder); + for (String s : Utilities.strings("Organization-first.xml", "Patient-example-newborn.json", "patient-example.json")) { + TextFile.bytesToFile( TestingUtilities.loadTestResourceBytes("validator", "resolution", s), Utilities.path(folder, s)); + } + return folder; + } + } \ No newline at end of file From 54c6299b3b4bb31d66a187bf1a25e8ecf335e0e2 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 15 Jan 2025 15:14:44 +1100 Subject: [PATCH 06/13] Add server headers to fhir-settings.json --- .../main/java/org/hl7/fhir/utilities/http/HTTPResult.java | 2 +- .../org/hl7/fhir/utilities/http/ManagedFhirWebAccessor.java | 1 - .../java/org/hl7/fhir/utilities/http/ManagedWebAccessor.java | 5 +++++ .../java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java | 2 +- .../org/hl7/fhir/utilities/settings/ServerDetailsPOJO.java | 3 +++ 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPResult.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPResult.java index 79bd513fd5..382fd0f03b 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPResult.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/HTTPResult.java @@ -40,11 +40,11 @@ public HTTPResult(String source, int code, String message, String contentType, b public void checkThrowException() throws IOException { if (code >= 300) { - String filename = Utilities.path("[tmp]", "http-log", "fhir-http-"+(SimpleHTTPClient.nextCounter())+".log"); if (content == null || content.length == 0) { HTTPResultException exception = new HTTPResultException(code, message, source, null); throw new IOException(exception.message, exception); } else { + String filename = Utilities.path("[tmp]", "http-log", "fhir-http-"+(SimpleHTTPClient.nextCounter())+".log"); Utilities.createDirectory(Utilities.path("[tmp]", "http-log")); TextFile.bytesToFile(content, filename); HTTPResultException exception = new HTTPResultException(code, message, source, filename); diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedFhirWebAccessor.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedFhirWebAccessor.java index cdc1989e1f..5dc2a84c34 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedFhirWebAccessor.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedFhirWebAccessor.java @@ -44,7 +44,6 @@ public ManagedFhirWebAccessor withLogger(ToolingClientLogger logger) { return this; } - public ManagedFhirWebAccessor(String userAgent, List serverAuthDetails) { super(Arrays.asList("fhir"), userAgent, serverAuthDetails); this.timeout = 5000; diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessor.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessor.java index 10fdb5d1e5..16d4de18be 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessor.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/ManagedWebAccessor.java @@ -83,6 +83,11 @@ private SimpleHTTPClient setupClient(String url) throws IOException { client.setAuthenticationMode(HTTPAuthenticationMode.APIKEY); break; } + if (settings.getHeaders() != null) { + for (String n : settings.getHeaders().keySet()) { + client.addHeader(n, settings.getHeaders().get(n)); + } + } } } if (getUsername() != null || getToken() != null) { diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java index bf35ee9061..0f205ce121 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/http/SimpleHTTPClient.java @@ -132,7 +132,7 @@ public HTTPResult post(String url, String contentType, byte[] content, String ac c.setDoOutput(true); c.setDoInput(true); c.setRequestMethod("POST"); - c.setRequestProperty("Content-type", contentType); + c.setRequestProperty("Content-Type", contentType); if (accept != null) { c.setRequestProperty("Accept", accept); } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/settings/ServerDetailsPOJO.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/settings/ServerDetailsPOJO.java index 160d16c631..3b2b28124a 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/settings/ServerDetailsPOJO.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/settings/ServerDetailsPOJO.java @@ -1,5 +1,7 @@ package org.hl7.fhir.utilities.settings; +import java.util.Map; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -38,4 +40,5 @@ public class ServerDetailsPOJO { String apikey; + Map headers; } From 6ce59520e9562bcf60abde36658b442b812c0063 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Wed, 15 Jan 2025 15:16:04 +1100 Subject: [PATCH 07/13] Add AI Support for code/text validation --- .../fhir/utilities/i18n/I18nConstants.java | 3 + .../fhir/utilities/json/model/JsonArray.java | 6 + .../fhir/utilities/json/model/JsonNumber.java | 8 ++ .../fhir/utilities/json/model/JsonObject.java | 14 +++ .../src/main/resources/Messages.properties | 4 + .../http/ManagedWebAccessAuthTests.java | 6 +- .../hl7/fhir/validation/ValidationEngine.java | 4 + .../org/hl7/fhir/validation/ai/AIAPI.java | 20 +++ .../hl7/fhir/validation/ai/ChatGPTAPI.java | 64 ++++++++++ .../org/hl7/fhir/validation/ai/ClaudeAPI.java | 69 ++++++++++ .../ai/CodeAndTextValidationRequest.java | 39 ++++++ .../ai/CodeAndTextValidationResult.java | 28 +++++ .../validation/ai/CodeAndTextValidator.java | 119 ++++++++++++++++++ .../fhir/validation/cli/model/CliContext.java | 17 ++- .../cli/services/ValidationService.java | 1 + .../hl7/fhir/validation/cli/utils/Params.java | 3 + .../instance/InstanceValidator.java | 82 +++++++++++- 17 files changed, 482 insertions(+), 5 deletions(-) create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/AIAPI.java create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ChatGPTAPI.java create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ClaudeAPI.java create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationRequest.java create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationResult.java create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidator.java diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java index 9edc4b03a6..9f392a2c48 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java @@ -1185,4 +1185,7 @@ public class I18nConstants { public static final String CODESYSTEM_PROPERTY_BAD_PROPERTY_CODE = "CODESYSTEM_PROPERTY_BAD_PROPERTY_CODE"; public static final String CODESYSTEM_DUPLICATE_CODE = "CODESYSTEM_DUPLICATE_CODE"; public static final String REFERENCE_RESOLUTION_FAILED = "REFERENCE_RESOLUTION_FAILED"; + public static final String VALIDATION_AI_TEXT_CODE = "VALIDATION_AI_TEXT_CODE"; + public static final String VALIDATION_AI_FAILED = "VALIDATION_AI_FAILED"; + public static final String VALIDATION_AI_FAILED_LOG = "VALIDATION_AI_FAILED_LOG"; } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/model/JsonArray.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/model/JsonArray.java index 6ca5a0a99c..79b70b8c6e 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/model/JsonArray.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/model/JsonArray.java @@ -58,6 +58,12 @@ public JsonArray add(String value) throws JsonException { items.add(new JsonString(value)); return this; } + + public JsonObject addObject() { + JsonObject res = new JsonObject(); + add(res); + return res; + } public Integer size() { return items.size(); diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/model/JsonNumber.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/model/JsonNumber.java index bb552e6349..6ae726d5ae 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/model/JsonNumber.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/model/JsonNumber.java @@ -42,6 +42,14 @@ public Integer getInteger() { return null; } } + + public Double getDouble() { + if (Utilities.isDecimal(value, false)) { + return Double.parseDouble(value); + } else { + return null; + } + } @Override protected JsonElement copy(JsonElement other) { diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/model/JsonObject.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/model/JsonObject.java index 31ee4859eb..855a6e76b1 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/model/JsonObject.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/json/model/JsonObject.java @@ -287,6 +287,20 @@ public Integer asInteger(String name) { } return null; } + + public Double asDouble(String name) { + if (hasNumber(name)) { + return ((JsonNumber) get(name)).getDouble(); + } + if (hasPrimitive(name)) { + String s = asString(name); + if (Utilities.isDecimal(s, false)) { + return Double.parseDouble(s); + } + } + return null; + } + public String asString(String name) { return hasPrimitive(name) ? ((JsonPrimitive) get(name)).getValue() : null; diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 588c07d6ba..7f6114fbc9 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -1218,3 +1218,7 @@ CODESYSTEM_PROPERTY_BAD_INTERNAL_REFERENCE = The code ''{0}'' is not a valid cod CODESYSTEM_PROPERTY_BAD_PROPERTY_CODE = The code ''{0}'' is not a valid code in the value set ''{1}'' CODESYSTEM_DUPLICATE_CODE = The code ''{0}'' has already been defined REFERENCE_RESOLUTION_FAILED = Fetching ''{0}'' failed. System details: {1}: {2} +VALIDATION_AI_TEXT_CODE = Apparent mis-match between code ''{0}'' and text ''{1}'': {3} ({2} confidence) +VALIDATION_AI_FAILED = Consulting AI failed: {0} +VALIDATION_AI_FAILED_LOG = Consulting AI failed: {0} (see {1} for further details) + diff --git a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/ManagedWebAccessAuthTests.java b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/ManagedWebAccessAuthTests.java index ced8783b2b..c67f65d3a5 100644 --- a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/ManagedWebAccessAuthTests.java +++ b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/http/ManagedWebAccessAuthTests.java @@ -167,7 +167,7 @@ private ServerDetailsPOJO getBasicAuthServerPojo() { "fhir", DUMMY_USERNAME, DUMMY_PASSWORD, - null, null); + null, null, null); } @Test @@ -186,7 +186,7 @@ private ServerDetailsPOJO getTokenAuthServerPojo() { "fhir", null, null, - DUMMY_TOKEN, null); + DUMMY_TOKEN, null, null); } @Test @@ -205,7 +205,7 @@ private ServerDetailsPOJO getApiKeyAuthServerPojo() { "fhir", null, null, - null, DUMMY_API_KEY); + null, DUMMY_API_KEY, null); } @Test diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java index 7bf3b27573..615042f493 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java @@ -222,6 +222,7 @@ public interface IValidationEngineLoader { @Getter @Setter private boolean crumbTrails; @Getter @Setter private boolean showMessageIds; @Getter @Setter private boolean forPublication; + @Getter @Setter private String aiService; @Getter @Setter private boolean allowExampleUrls; @Getter @Setter private boolean showMessagesFromReferences; @Getter @Setter private boolean doImplicitFHIRPathStringConversion; @@ -278,6 +279,7 @@ public ValidationEngine(ValidationEngine other) throws FHIRException, IOExceptio securityChecks = other.securityChecks; crumbTrails = other.crumbTrails; forPublication = other.forPublication; + aiService = other.aiService; allowExampleUrls = other.allowExampleUrls; showMessagesFromReferences = other.showMessagesFromReferences; doImplicitFHIRPathStringConversion = other.doImplicitFHIRPathStringConversion; @@ -904,6 +906,8 @@ public InstanceValidator getValidator(FhirFormat format) throws FHIRException, I validator.setNoUnicodeBiDiControlChars(noUnicodeBiDiControlChars); validator.setDoImplicitFHIRPathStringConversion(doImplicitFHIRPathStringConversion); validator.setCheckIPSCodes(checkIPSCodes); + validator.setAIService(aiService); + validator.setCacheFolder(context.getTxCache().getFolder()); if (format == FhirFormat.SHC) { igLoader.loadIg(getIgs(), getBinaries(), SHCParser.CURRENT_PACKAGE, true); } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/AIAPI.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/AIAPI.java new file mode 100644 index 0000000000..bcbecb9122 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/AIAPI.java @@ -0,0 +1,20 @@ +package org.hl7.fhir.validation.ai; + +import java.io.IOException; +import java.util.List; + +public abstract class AIAPI { + + public abstract List validateCodings(List requests) throws IOException; + + + protected String getSystemName(String system) { + switch (system) { + case "http://snomed.info/sct": return "SNOMED CT"; + case "http://loinc.org": return "LOINC"; + case "http://www.nlm.nih.gov/research/umls/rxnorm": return "RxNorm"; + default : return system; + } + } + +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ChatGPTAPI.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ChatGPTAPI.java new file mode 100644 index 0000000000..69b19ae0c1 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ChatGPTAPI.java @@ -0,0 +1,64 @@ +package org.hl7.fhir.validation.ai; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.http.HTTPResult; +import org.hl7.fhir.utilities.http.ManagedWebAccess; +import org.hl7.fhir.utilities.json.model.JsonArray; +import org.hl7.fhir.utilities.json.model.JsonElement; +import org.hl7.fhir.utilities.json.model.JsonObject; +import org.hl7.fhir.utilities.json.parser.JsonParser; + +public class ChatGPTAPI extends AIAPI { + private static final String API_URL = "https://api.openai.com/v1/chat/completions"; + private static final String MODEL = "gpt-4o-mini"; + + @Override + public List validateCodings(List requests) throws IOException { + StringBuilder prompt = new StringBuilder(); + prompt.append("For each of the following cases, determine if the text appropriately matches the code. "); + prompt.append("Respond in JSON format with an array of objects containing 'index', 'isValid', 'explanation', and 'confidence'.\n\n"); + + for (int i = 0; i < requests.size(); i++) { + CodeAndTextValidationRequest req = requests.get(i); + prompt.append(String.format("%d. Is '%s' a reasonable text to associate with the %s code %s (display '%s')\n", + i + 1, req.getText(), getSystemName(req.getSystem()), req.getCode(), req.getDisplay())); + } + + String systemPrompt = "You are a medical terminology expert. Evaluate whether text descriptions match their\n"+ + "associated clinical codes. Provide detailed explanations for any mismatches. "+ + "Express your confidence level based on how certain you are of the relationship."; + + JsonArray json = getResponse(prompt.toString(), systemPrompt); + + return parseValidationResponse(json, requests); + } + + public JsonArray getResponse(String prompt, String systemPrompt) throws IOException { + JsonObject json = new JsonObject(); + json.add("model", MODEL); + json.forceArray("messages").addObject().add("role", "system").add("content", systemPrompt); + json.forceArray("messages").addObject().add("role", "user").add("content", prompt); + + HTTPResult response = ManagedWebAccess.post(Utilities.strings("web") , API_URL, JsonParser.composeBytes(json), + "application/json", "application/json"); + response.checkThrowException(); + json = JsonParser.parseObject(response.getContentAsString()); + String text = json.getJsonArray("choices").get(0).asJsonObject().getJsonObject("message").asString("content"); + text = text.replace("```", "").substring(4); + return (JsonArray) JsonParser.parse(text); + } + + private List parseValidationResponse(JsonArray json, List requests) { + List res = new ArrayList<>(); + for (JsonObject o : json.asJsonObjects()) { + CodeAndTextValidationRequest request = requests.get(o.asInteger("index")-1); + res.add(new CodeAndTextValidationResult(request, o.asBoolean("isValid"), o.asString("explanation"), o.asString("confidence"))); + } + return res; + } +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ClaudeAPI.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ClaudeAPI.java new file mode 100644 index 0000000000..b25c210b48 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ClaudeAPI.java @@ -0,0 +1,69 @@ +package org.hl7.fhir.validation.ai; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.http.HTTPResult; +import org.hl7.fhir.utilities.http.ManagedWebAccess; +import org.hl7.fhir.utilities.http.ManagedWebAccessor; +import org.hl7.fhir.utilities.json.model.JsonObject; +import org.hl7.fhir.utilities.json.parser.JsonParser; + +public class ClaudeAPI extends AIAPI { + + private static final String API_URL = "https://api.anthropic.com/v1/messages"; + private static final String MODEL = "claude-3-5-sonnet-20241022"; + + @Override + public List validateCodings(List requests) throws IOException { + StringBuilder prompt = new StringBuilder(); + prompt.append("For each of the following cases, determine if the text appropriately matches the code. "); + prompt.append("Respond in JSON format with an array of objects containing 'index', 'isValid', 'explanation', and 'confidence'.\n\n"); + + for (int i = 0; i < requests.size(); i++) { + CodeAndTextValidationRequest req = requests.get(i); + prompt.append(String.format("%d. Is '%s' a reasonable text to associate with the %s code %s (display = %s)?\n", + i + 1, req.getText(), getSystemName(req.getSystem()), req.getCode(), req.getDisplay())); + } + + String systemPrompt = "You are a medical terminology expert. Evaluate whether text descriptions match their\n"+ + "associated clinical codes. Provide detailed explanations for any mismatches. "+ + "Express your confidence level based on how certain you are of the relationship."; + + JsonObject json = getResponse(prompt.toString(), systemPrompt); + + return parseValidationResponse(json, requests); + } + + + public JsonObject getResponse(String prompt, String systemPrompt) throws IOException { + JsonObject j = new JsonObject(); + j.add("model", "claude-3-5-sonnet-20241022"); + j.add("system", systemPrompt); + j.add("max_tokens", 1024); + j.forceArray("messages").addObject().add("role", "user").add("content", prompt); + + ManagedWebAccessor web = ManagedWebAccess.accessor(Utilities.strings("web")); + web.getHeaders().put("anthropic-version", "2023-06-01"); + HTTPResult response = web.post(API_URL, JsonParser.composeBytes(j), "application/json", "application/json"); + response.checkThrowException(); + JsonObject json = JsonParser.parseObject(response.getContentAsString()); + String text = json.getJsonArray("content").get(0).asJsonObject().asString("text"); + return JsonParser.parseObject(text); + } + + + + private List parseValidationResponse(JsonObject json, List requests) { + List res = new ArrayList<>(); + for (JsonObject o : json.getProperties().get(0).getValue().asJsonArray().asJsonObjects()) { + CodeAndTextValidationRequest request = requests.get(o.asInteger("index")-1); + res.add(new CodeAndTextValidationResult(request, o.asBoolean("isValid"), o.asString("explanation"), o.asString("confidence"))); + } + return res; + } + +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationRequest.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationRequest.java new file mode 100644 index 0000000000..d2b9833b64 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationRequest.java @@ -0,0 +1,39 @@ +package org.hl7.fhir.validation.ai; + +import org.hl7.fhir.validation.instance.utils.NodeStack; + +public class CodeAndTextValidationRequest { + private NodeStack location; + private String lang; + private String system; + private String code; + private String display; + private String text; + public CodeAndTextValidationRequest(NodeStack location, String lang, String system, String code, String display, String text) { + super(); + this.location = location; + this.lang = lang == null ? "en" : lang; + this.system = system; + this.code = code; + this.display = display; + this.text = text; + } + public NodeStack getLocation() { + return location; + } + public String getSystem() { + return system; + } + public String getCode() { + return code; + } + public String getText() { + return text; + } + public String getDisplay() { + return display; + } + public String getLang() { + return lang; + } +} \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationResult.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationResult.java new file mode 100644 index 0000000000..62d58e33bc --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationResult.java @@ -0,0 +1,28 @@ +package org.hl7.fhir.validation.ai; + +public class CodeAndTextValidationResult { + private CodeAndTextValidationRequest request; + private boolean isValid; + private String explanation; + private String confidence; + protected CodeAndTextValidationResult(CodeAndTextValidationRequest request, boolean isValid, String explanation, String confidence) { + super(); + this.request = request; + this.isValid = isValid; + this.explanation = explanation; + this.confidence = confidence; + } + public CodeAndTextValidationRequest getRequest() { + return request; + } + public boolean isValid() { + return isValid; + } + public String getExplanation() { + return explanation; + } + public String getConfidence() { + return confidence; + } + +} \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidator.java new file mode 100644 index 0000000000..425d843138 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidator.java @@ -0,0 +1,119 @@ +package org.hl7.fhir.validation.ai; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.utilities.Utilities; + +public class CodeAndTextValidator { + + private String aiService; + private Connection db; + private PreparedStatement select; + private PreparedStatement insert; + + public CodeAndTextValidator(String cacheFolder, String aiService) throws FHIRException { + this.aiService = aiService; + try { + String filename = Utilities.path(cacheFolder, "ai-"+aiService+".db"); + db = DriverManager.getConnection("jdbc:sqlite:"+filename); + DatabaseMetaData meta = db.getMetaData(); + ResultSet rs = meta.getTables(null, null, "CodeAndText", new String[] {"TABLE"}); + boolean exists = rs.next(); + if (!exists) { + Statement stmt = db.createStatement(); + stmt.execute("CREATE TABLE CodeAndText (\r\n"+ + "System nvarchar NOT NULL,\r\n"+ + "Code nvarchar NOT NULL,\r\n"+ + "Lang nvarchar NOT NULL,\r\n"+ + "Text nvarchar NOT NULL,\r\n"+ + "Valid integer NOT NULL,\r\n"+ + "Explanation nvarchar NOT NULL,\r\n"+ + "Confidence nvarchar NOT NULL,\r\n"+ + "PRIMARY KEY (System,Code,Text))\r\n"); + } + select = db.prepareStatement("Select Valid, Explanation, Confidence from CodeAndText where System = ? and Code = ? and Lang = ? and Text = ?"); + insert = db.prepareStatement("Insert into CodeAndText (System, Code, Lang, Text, Valid , Explanation, Confidence) values (?, ?, ?, ?, ?, ?, ?)"); + } catch (Exception e) { + throw new FHIRException("Exception opening AI Cache: "+e.getMessage(), e); + } + } + + public List validateCodings(List requests) throws IOException { + try { + // first, split the list by cache + List results = new ArrayList(); + List query = new ArrayList(); + + for (CodeAndTextValidationRequest req : requests) { + CodeAndTextValidationResult cached = findExistingResult(req); + if (cached != null) { + results.add(cached); + } else { + query.add(req); + } + } + List outcomes = null; + if (query.size() > 0) { + switch (aiService.toLowerCase()) { + case "claude" : + System.out.println("Consulting Claude about "+query.size()+" code/text combinations"); + outcomes = new ClaudeAPI().validateCodings(query); + break; + case "chatgpt" : + System.out.println("Consulting ChatGPT about "+query.size()+" code/text combinations"); + outcomes = new ChatGPTAPI().validateCodings(query); + break; + default: + throw new FHIRException("Unknown AI Service "+aiService); + } + for (CodeAndTextValidationResult o : outcomes) { + results.add(o); + storeResult(o); + } + } + return results; + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new FHIRException(e); + } + } + + + private CodeAndTextValidationResult findExistingResult(CodeAndTextValidationRequest req) throws SQLException { + select.setString(1, req.getSystem()); + select.setString(2, req.getCode()); + select.setString(3, req.getLang()); + select.setString(4, req.getText()); + ResultSet rs = select.executeQuery(); + if (rs.next()) { + return new CodeAndTextValidationResult(req, rs.getInt(1) == 1, rs.getString(2), rs.getString(3)); + } else { + return null; + } + } + + private void storeResult(CodeAndTextValidationResult o) throws SQLException { + insert.setString(1, o.getRequest().getSystem()); + insert.setString(2, o.getRequest().getCode()); + insert.setString(3, o.getRequest().getLang()); + insert.setString(4, o.getRequest().getText()); + insert.setInt(5, o.isValid() ? 1 : 0); + insert.setString(6, o.getExplanation()); + insert.setString(7, o.getConfidence()); + insert.execute(); + } + + +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java index e8dae20e1e..d99d3c0a4a 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java @@ -248,6 +248,11 @@ public class CliContext { private boolean forPublication = false; + @JsonProperty("aiService") + @SerializedName("aiService") + private + String aiService; + @JsonProperty("allowExampleUrls") @SerializedName("allowExampleUrls") private @@ -1099,6 +1104,14 @@ public boolean isForPublication() { public void setForPublication(boolean forPublication) { this.forPublication = forPublication; } + + public String getAIService() { + return aiService; + } + + public void setAIService(String aiService) { + this.aiService = aiService; + } public boolean isAllowExampleUrls() { return allowExampleUrls; @@ -1222,6 +1235,7 @@ public boolean equals(Object o) { Objects.equals(crumbTrails, that.crumbTrails) && Objects.equals(showMessageIds, that.showMessageIds) && Objects.equals(forPublication, that.forPublication) && + Objects.equals(aiService, that.aiService) && Objects.equals(allowExampleUrls, that.allowExampleUrls) && Objects.equals(showTimes, that.showTimes) && mode == that.mode && @@ -1242,7 +1256,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(baseEngine, doNative, extensions, hintAboutNonMustSupport, recursive, doDebug, assumeValidRestReferences, checkReferences,canDoNative, noInternalCaching, resolutionContext, + return Objects.hash(baseEngine, doNative, extensions, hintAboutNonMustSupport, recursive, doDebug, assumeValidRestReferences, checkReferences,canDoNative, noInternalCaching, resolutionContext, aiService, noExtensibleBindingMessages, noInvariants, displayWarnings, wantInvariantsInMessages, map, output, outputSuffix, htmlOutput, txServer, sv, txLog, txCache, mapLog, lang, srcLang, tgtLang, fhirpath, snomedCT, targetVer, packageName, igs, questionnaireMode, level, profiles, options, sources, inputs, mode, locale, locations, crumbTrails, showMessageIds, forPublication, showTimes, allowExampleUrls, outputStyle, jurisdiction, noUnicodeBiDiControlChars, watchMode, watchScanDelay, watchSettleTime, bestPracticeLevel, unknownCodeSystemsCauseErrors, noExperimentalContent, advisorFile, expansionParameters, format, htmlInMarkdownCheck, allowDoubleQuotesInFHIRPath, checkIPSCodes); @@ -1295,6 +1309,7 @@ public String toString() { ", crumbTrails=" + crumbTrails + ", showMessageIds=" + showMessageIds + ", forPublication=" + forPublication + + ", aiService=" + aiService + ", outputStyle=" + outputStyle + ", jurisdiction=" + jurisdiction + ", allowExampleUrls=" + allowExampleUrls + diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java index 639f182072..de3199ce93 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java @@ -630,6 +630,7 @@ protected ValidationEngine buildValidationEngine(CliContext cliContext, String d validationEngine.setForPublication(cliContext.isForPublication()); validationEngine.setShowTimes(cliContext.isShowTimes()); validationEngine.setAllowExampleUrls(cliContext.isAllowExampleUrls()); + validationEngine.setAiService(cliContext.getAIService()); ReferenceValidationPolicy refpol = ReferenceValidationPolicy.CHECK_VALID; if (!cliContext.isDisableDefaultResourceFetcher()) { StandAloneValidatorFetcher fetcher = new StandAloneValidatorFetcher(validationEngine.getPcm(), validationEngine.getContext(), validationEngine); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java index fc6ee9879d..11e7541930 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java @@ -96,6 +96,7 @@ public class Params { public static final String CRUMB_TRAIL = "-crumb-trails"; public static final String SHOW_MESSAGE_IDS = "-show-message-ids"; public static final String FOR_PUBLICATION = "-forPublication"; + public static final String AI_SERVICE = "-ai-service"; public static final String VERBOSE = "-verbose"; public static final String SHOW_TIMES = "-show-times"; public static final String ALLOW_EXAMPLE_URLS = "-allow-example-urls"; @@ -385,6 +386,8 @@ else if (args[i].equals(HTML_OUTPUT)) { cliContext.setShowMessageIds(true); } else if (args[i].equals(FOR_PUBLICATION)) { cliContext.setForPublication(true); + } else if (args[i].equals(AI_SERVICE)) { + cliContext.setAIService(args[++i]); } else if (args[i].equals(UNKNOWN_CODESYSTEMS_CAUSE_ERROR)) { cliContext.setUnknownCodeSystemsCauseErrors(true); } else if (args[i].equals(NO_EXPERIMENTAL_CONTENT)) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index 46925d023f..0cf52dac5e 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -179,6 +179,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.utilities.VersionUtilities; import org.hl7.fhir.utilities.VersionUtilities.VersionURLInfo; import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; +import org.hl7.fhir.utilities.http.HTTPResultException; import org.hl7.fhir.utilities.i18n.I18nConstants; import org.hl7.fhir.utilities.json.model.JsonObject; import org.hl7.fhir.utilities.validation.IDigitalSignatureServices; @@ -190,6 +191,9 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.utilities.xhtml.NodeType; import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.hl7.fhir.validation.BaseValidator; +import org.hl7.fhir.validation.ai.CodeAndTextValidationRequest; +import org.hl7.fhir.validation.ai.CodeAndTextValidationResult; +import org.hl7.fhir.validation.ai.CodeAndTextValidator; import org.hl7.fhir.validation.cli.model.HtmlInMarkdownCheck; import org.hl7.fhir.validation.cli.utils.QuestionnaireMode; import org.hl7.fhir.validation.codesystem.CodingsObserver; @@ -619,6 +623,10 @@ public FHIRPathEngine getFHIRPathEngine() { private IDigitalSignatureServices signatureServices; private boolean unknownCodeSystemsCauseErrors; private boolean noExperimentalContent; + private List textsToCheck = new ArrayList<>(); + private String aiService; + private Set textsToCheckKeys = new HashSet<>(); + private String cacheFolder; public InstanceValidator(@Nonnull IWorkerContext theContext, @Nonnull IEvaluationContext hostServices, @Nonnull XVerExtensionManager xverManager, ValidatorSession session) { super(theContext, xverManager, false, session); @@ -1015,6 +1023,30 @@ public void validate(Object appContext, List errors, String p codingObserver.finish(errors, stack); errors.removeAll(messagesToRemove); timeTracker.overall(t); + if (aiService != null && !textsToCheck.isEmpty()) { + CodeAndTextValidator ctv = new CodeAndTextValidator(cacheFolder, aiService); + List results = null; + try { + results = ctv.validateCodings(textsToCheck); + } catch (Exception e) { + if (e.getCause() != null && e.getCause() instanceof HTTPResultException) { + warning(errors, "2025-01-14", IssueType.EXCEPTION, stack, false, + I18nConstants.VALIDATION_AI_FAILED_LOG, e.getMessage(), ((HTTPResultException)e.getCause()).logPath); + } else { + warning(errors, "2025-01-14", IssueType.EXCEPTION, stack, false, + I18nConstants.VALIDATION_AI_FAILED, e.getMessage()); + } + } + if (results != null) { + for (CodeAndTextValidationResult vr : results) { + if (!vr.isValid()) { + warning(errors, "2025-01-14", IssueType.BUSINESSRULE, vr.getRequest().getLocation().line(), vr.getRequest().getLocation().col(), vr.getRequest().getLocation().getLiteralPath(), false, + I18nConstants.VALIDATION_AI_TEXT_CODE, vr.getRequest().getCode(), vr.getRequest().getText(), vr.getConfidence(), vr.getExplanation()); + } + } + } + } + if (DEBUG_ELEMENT) { element.printToOutput(); } @@ -1354,6 +1386,11 @@ private boolean checkCodeableConcept(List errors, String path if (warning(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, I18nConstants.TERMINOLOGY_TX_BINDING_MISSING, path)) { try { CodeableConcept cc = ObjectConverter.readAsCodeableConcept(element); + if (cc.hasText() && cc.hasCoding()) { + for (Coding c : cc.getCoding()) { + recordCodeTextCombo(stack, c, cc.getText()); + } + } if (binding.hasValueSet()) { String vsRef = binding.getValueSet(); ValueSet valueset = resolveBindingReference(profile, vsRef, profile.getUrl(), profile); @@ -1385,6 +1422,11 @@ private boolean checkCodeableConcept(List errors, String path if (!noTerminologyChecks && theElementCntext != null && !checked.ok()) { // no binding check, so we just check the CodeableConcept generally try { CodeableConcept cc = ObjectConverter.readAsCodeableConcept(element); + if (cc.hasText() && cc.hasCoding()) { + for (Coding c : cc.getCoding()) { + recordCodeTextCombo(stack, c, cc.getText()); + } + } if (cc.hasCoding()) { long t = System.nanoTime(); ValidationResult vr = checkCodeOnServer(stack, null, cc); @@ -1399,6 +1441,20 @@ private boolean checkCodeableConcept(List errors, String path return checkDisp; } + private void recordCodeTextCombo(NodeStack node, Coding c, String text) { + if (!c.hasDisplay() || !c.getDisplay().equals(text)) { + ValidationResult vr = context.validateCode(baseOptions.setDisplayWarningMode(false) + .setLanguages(node.getWorkingLang()), c.getSystem(), c.getVersion(), c.getCode(), text); + if (!vr.isOk()) { + int key = (c.getSystem()+"||"+c.getCode()+"||"+text).hashCode(); + if (!textsToCheckKeys.contains(key)) { + textsToCheckKeys.add(key); + textsToCheck.add(new CodeAndTextValidationRequest(node, node.getWorkingLang() == null ? context.getLocale().toLanguageTag() : node.getWorkingLang(), c.getSystem(), c.getCode(), vr.getDisplay(), text)); + } + } + } + } + private boolean isInScope(ElementDefinitionBindingAdditionalComponent ab, StructureDefinition profile, Element resource, StringBuilder b) { if (ab.getUsage().isEmpty()) { return true; @@ -1869,6 +1925,11 @@ private boolean checkCDACodeableConcept(List errors, String p try { CodeableConcept cc = new CodeableConcept(); ok.see(convertCDACodeToCodeableConcept(errors, path, element, logical, cc)); + if (cc.hasText() && cc.hasCoding()) { + for (Coding c : cc.getCoding()) { + recordCodeTextCombo(stack, c, cc.getText()); + } + } ElementDefinitionBindingComponent binding = theElementCntext.getBinding(); if (warning(errors, NO_RULE_DATE, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, I18nConstants.TERMINOLOGY_TX_BINDING_MISSING, path)) { if (binding.hasValueSet()) { @@ -8170,6 +8231,25 @@ public void setNoExperimentalContent(boolean noExperimentalContent) { public void resetTimes() { timeTracker.reset(); } - + + public List getTextsToCheck() { + return textsToCheck; + } + + public String getAIService() { + return aiService; + } + + public void setAIService(String aiService) { + this.aiService = aiService; + } + + public String getCacheFolder() { + return cacheFolder; + } + + public void setCacheFolder(String cacheFolder) { + this.cacheFolder = cacheFolder; + } } From 652cc8fba608a151c428cf53857e9b70ef9e07af Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Fri, 17 Jan 2025 06:39:59 +1100 Subject: [PATCH 08/13] Better handle failure to find imported value sets when expanding, and track & report expansion source And render supplement dependencies + correct error handling for expansion failure --- .../r4/terminologies/CodeSystemUtilities.java | 2 +- .../fhir/r5/context/BaseWorkerContext.java | 21 +++- .../fhir/r5/renderers/ValueSetRenderer.java | 31 +++++- .../r5/terminologies/ValueSetUtilities.java | 96 +++++++++++++++++++ .../client/TerminologyClientContext.java | 11 +++ .../expansion/ValueSetExpander.java | 66 +++++++++++-- .../utilities/TerminologyCache.java | 17 +++- .../TerminologyServiceErrorClass.java | 2 +- .../org/hl7/fhir/r5/utils/UserDataNames.java | 2 +- .../utilities/i18n/RenderingI18nContext.java | 4 + .../src/main/resources/Messages.properties | 6 +- .../resources/rendering-phrases.properties | 4 + .../tests/TerminologyServiceTests.java | 3 + 13 files changed, 242 insertions(+), 23 deletions(-) diff --git a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/terminologies/CodeSystemUtilities.java b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/terminologies/CodeSystemUtilities.java index ba72ce5062..79a2e27d66 100644 --- a/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/terminologies/CodeSystemUtilities.java +++ b/org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/terminologies/CodeSystemUtilities.java @@ -240,7 +240,7 @@ public static String getOID(CodeSystem cs) { return null; } - private static ConceptDefinitionComponent findCode(List list, String code) { + public static ConceptDefinitionComponent findCode(List list, String code) { for (ConceptDefinitionComponent c : list) { if (c.getCode().equals(code)) return c; diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java index 5b08b68032..65895c07de 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java @@ -942,7 +942,10 @@ public ValueSetExpansionOutcome expandVS(ITerminologyOperationDetails opCtxt, Co try { ValueSet result = tc.getClient().expandValueset(vs, p); - res = new ValueSetExpansionOutcome(result).setTxLink(txLog.getLastId()); + res = new ValueSetExpansionOutcome(result).setTxLink(txLog.getLastId()); + if (res != null && res.getValueset() != null) { + res.getValueset().setUserData(UserDataNames.VS_EXPANSION_SOURCE, tc.getHost()); + } } catch (Exception e) { res = new ValueSetExpansionOutcome(e.getMessage() == null ? e.getClass().getName() : e.getMessage(), TerminologyServiceErrorClass.UNKNOWN, true); if (txLog != null) { @@ -1012,7 +1015,10 @@ public ValueSetExpansionOutcome expandVS(String url, boolean cacheOk, boolean hi throw new Error(formatMessage(I18nConstants.NO_URL_IN_EXPAND_VALUE_SET_2)); } } - res = new ValueSetExpansionOutcome(result).setTxLink(txLog.getLastId()); + res = new ValueSetExpansionOutcome(result).setTxLink(txLog.getLastId()); + if (res != null && res.getValueset() != null) { + res.getValueset().setUserData(UserDataNames.VS_EXPANSION_SOURCE, tc.getHost()); + } } catch (Exception e) { res = new ValueSetExpansionOutcome((e.getMessage() == null ? e.getClass().getName() : e.getMessage()), TerminologyServiceErrorClass.UNKNOWN, allErrors, true).setTxLink(txLog == null ? null : txLog.getLastId()); } @@ -1085,6 +1091,9 @@ public ValueSetExpansionOutcome expandVS(ValueSet vs, boolean cacheOk, boolean h res = null; try { res = vse.expand(vs, p); + if (res != null && res.getValueset() != null) { + res.getValueset().setUserData(UserDataNames.VS_EXPANSION_SOURCE, vse.getSource()); + } } catch (Exception e) { allErrors.addAll(vse.getAllErrors()); e.printStackTrace(); @@ -1098,7 +1107,7 @@ public ValueSetExpansionOutcome expandVS(ValueSet vs, boolean cacheOk, boolean h txCache.cacheExpansion(cacheToken, res, TerminologyCache.TRANSIENT); return res; } - if (res.getErrorClass() == TerminologyServiceErrorClass.INTERNAL_ERROR || isNoTerminologyServer()) { // this class is created specifically to say: don't consult the server + if (res.getErrorClass() == TerminologyServiceErrorClass.INTERNAL_ERROR || isNoTerminologyServer() || res.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNKNOWN) { // this class is created specifically to say: don't consult the server return new ValueSetExpansionOutcome(res.getError(), res.getErrorClass(), false); } @@ -1133,6 +1142,9 @@ public ValueSetExpansionOutcome expandVS(ValueSet vs, boolean cacheOk, boolean h res = new ValueSetExpansionOutcome((e.getMessage() == null ? e.getClass().getName() : e.getMessage()), TerminologyServiceErrorClass.UNKNOWN, allErrors, true).setTxLink(txLog == null ? null : txLog.getLastId()); } } + if (res != null && res.getValueset() != null) { + res.getValueset().setUserData(UserDataNames.VS_EXPANSION_SOURCE, tc.getHost()); + } txCache.cacheExpansion(cacheToken, res, TerminologyCache.PERMANENT); return res; } @@ -1698,6 +1710,9 @@ public ValidationResult validateCode(ValidationOptions options, CodeableConcept } } Set unknownSystems = new HashSet<>(); + if ("8517006".equals(code.getCodingFirstRep().getCode())) { + DebugUtilities.breakpoint(); + } List issues = new ArrayList<>(); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ValueSetRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ValueSetRenderer.java index e525538f0f..050091a5eb 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ValueSetRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ValueSetRenderer.java @@ -90,7 +90,20 @@ public void buildNarrative(RenderingStatus status, XhtmlNode x, ResourceWrapper if (vs.hasCopyright()) generateCopyright(x, r); } - + if (vs.hasExtension(ToolingExtensions.EXT_VS_CS_SUPPL_NEEDED)) { + var p = x.para(); + p.tx("This ValueSet requires the Code system Supplement "); + String u = ToolingExtensions.readStringExtension(vs, ToolingExtensions.EXT_VS_CS_SUPPL_NEEDED); + CodeSystem cs = context.getContext().fetchResource(CodeSystem.class, u); + if (cs == null) { + p.code().tx(u); + } else if (!cs.hasWebPath()) { + p.ah(u).tx(cs.present()); + } else { + p.ah(cs.getWebPath()).tx(cs.present()); + } + p.tx("."); + } if (vs.hasExpansion()) { // for now, we just accept an expansion if there is one generateExpansion(status, r, x, vs, false, maps); @@ -498,14 +511,26 @@ private void generateVersionNotice(XhtmlNode x, ValueSetExpansionComponent expan if (versions.size() == 1 && versions.get(s).size() == 1) { for (String v : versions.get(s)) { // though there'll only be one XhtmlNode p = x.para().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px"); - p.tx(context.formatPhrase(RenderingContext.VALUE_SET_EXPANSION)+" "); + if (!vs.hasUserData(UserDataNames.VS_EXPANSION_SOURCE)) { + p.tx(context.formatPhrase(RenderingContext.VALUE_SET_EXPANSION)+" "); + } else if ("internal".equals(vs.getUserString(UserDataNames.VS_EXPANSION_SOURCE))) { + p.tx(context.formatPhrase(RenderingContext.VALUE_SET_EXPANSION_INTERNAL)+" "); + } else { + p.tx(context.formatPhrase(RenderingContext.VALUE_SET_EXPANSION_SRVR, vs.getUserString(UserDataNames.VS_EXPANSION_SOURCE))+" "); + } expRef(p, s, v, vs); } } else { for (String v : versions.get(s)) { if (first) { div = x.div().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px"); - div.para().tx(context.formatPhrase(RenderingContext.VALUE_SET_EXPANSIONS)); + if (!vs.hasUserData(UserDataNames.VS_EXPANSION_SOURCE)) { + div.para().tx(context.formatPhrase(RenderingContext.VALUE_SET_EXPANSIONS)); + } else if ("internal".equals(vs.getUserString(UserDataNames.VS_EXPANSION_SOURCE))) { + div.para().tx(context.formatPhrase(RenderingContext.VALUE_SET_EXPANSIONS_INTERNAL)); + } else { + div.para().tx(context.formatPhrase(RenderingContext.VALUE_SET_EXPANSIONS_SRVR, vs.getUserString(UserDataNames.VS_EXPANSION_SOURCE))); + } ul = div.ul(); first = false; } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetUtilities.java index d6a4ba06b0..15410631d3 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/ValueSetUtilities.java @@ -48,8 +48,10 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.r5.model.IntegerType; import org.hl7.fhir.r5.model.Enumerations.FilterOperator; import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r5.model.Identifier; import org.hl7.fhir.r5.model.Meta; +import org.hl7.fhir.r5.model.Parameters; import org.hl7.fhir.r5.model.UriType; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; @@ -65,6 +67,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionPropertyComponent; import org.hl7.fhir.r5.terminologies.CodeSystemUtilities.ConceptDefinitionComponentSorter; import org.hl7.fhir.r5.terminologies.CodeSystemUtilities.ConceptStatus; +import org.hl7.fhir.r5.terminologies.utilities.TerminologyCache.SourcedValueSet; import org.hl7.fhir.r5.utils.CanonicalResourceUtilities; import org.hl7.fhir.r5.utils.ToolingExtensions; import org.hl7.fhir.r5.utils.UserDataNames; @@ -501,4 +504,97 @@ private static void addCodes(Set res, ConceptSetComponent inc, List sources = new HashSet<>(); private AcceptLanguageHeader langs; private List designations = new ArrayList<>(); @@ -612,6 +636,9 @@ private void excludeCodes(WorkingContext wc, ConceptSetComponent exc, Parameters if ((cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) && context.supportsSystem(exc.getSystem(), opContext.getOptions().getFhirVersion())) { ValueSetExpansionOutcome vse = context.expandVS(new TerminologyOperationDetails(requiredSupplements), exc, false, false); ValueSet valueset = vse.getValueset(); + if (valueset.hasUserData(UserDataNames.VS_EXPANSION_SOURCE)) { + sources.add(valueset.getUserString(UserDataNames.VS_EXPANSION_SOURCE)); + } if (valueset == null) throw failTSE("Error Expanding ValueSet: "+vse.getError()); excludeCodes(wc, valueset.getExpansion()); @@ -677,6 +704,8 @@ public ValueSetExpansionOutcome expand(ValueSet source, Parameters expParams) { } } catch (ETooCostly e) { return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.TOO_COSTLY, allErrors, false); + } catch (UnknownValueSetException e) { + return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.VALUESET_UNKNOWN, allErrors, false); } catch (Exception e) { if (debug) { e.printStackTrace(); @@ -870,9 +899,9 @@ private ValueSet importValueSet(WorkingContext wc, String value, ValueSetExpansi boolean pinned = !url.equals(value); String ver = pinned ? url.substring(value.length()+1) : null; if (context.fetchResource(CodeSystem.class, url, valueSet) != null) { - throw fail(pinned ? I18nConstants.VS_EXP_IMPORT_CS_PINNED : I18nConstants.VS_EXP_IMPORT_CS, true, value, ver); + throw failUnk(pinned ? I18nConstants.VS_EXP_IMPORT_CS_PINNED : I18nConstants.VS_EXP_IMPORT_CS, true, value, ver); } else { - throw fail(pinned ? I18nConstants.VS_EXP_IMPORT_UNK_PINNED : I18nConstants.VS_EXP_IMPORT_UNK, true, value, ver); + throw failUnk(pinned ? I18nConstants.VS_EXP_IMPORT_UNK_PINNED : I18nConstants.VS_EXP_IMPORT_UNK, true, value, ver); } } checkCanonical(exp, vs, focus); @@ -880,13 +909,19 @@ private ValueSet importValueSet(WorkingContext wc, String value, ValueSetExpansi expParams = expParams.copy(); expParams.addParameter("activeOnly", true); } - ValueSetExpansionOutcome vso = new ValueSetExpander(context, opContext.copy(), allErrors).expand(vs, expParams); + ValueSetExpander expander = new ValueSetExpander(context, opContext.copy(), allErrors); + ValueSetExpansionOutcome vso = expander.expand(vs, expParams); if (vso.getError() != null) { addErrors(vso.getAllErrors()); - throw fail(I18nConstants.VS_EXP_IMPORT_ERROR, true, vs.getUrl(), vso.getError()); + if (vso.getErrorClass() == TerminologyServiceErrorClass.VALUESET_UNKNOWN) { + throw failUnk(I18nConstants.VS_EXP_IMPORT_ERROR, true, vs.getUrl(), vso.getError()); + } else { + throw fail(I18nConstants.VS_EXP_IMPORT_ERROR, true, vs.getUrl(), vso.getError()); + } } else if (vso.getValueset() == null) { throw fail(I18nConstants.VS_EXP_IMPORT_FAIL, true, vs.getUrl()); } + sources.addAll(expander.sources); if (vs.hasVersion() || REPORT_VERSION_ANYWAY) { UriType u = new UriType(vs.getUrl() + (vs.hasVersion() ? "|"+vs.getVersion() : "")); if (!existsInParams(exp.getParameter(), "used-valueset", u)) @@ -938,10 +973,12 @@ private ValueSet importValueSetForExclude(WorkingContext wc, String value, Value expParams = expParams.copy(); expParams.addParameter("activeOnly", true); } - ValueSetExpansionOutcome vso = new ValueSetExpander(context, opContext.copy(), allErrors).expand(vs, expParams); + ValueSetExpander expander = new ValueSetExpander(context, opContext.copy(), allErrors); + ValueSetExpansionOutcome vso = expander.expand(vs, expParams); + sources.addAll(expander.sources); if (vso.getError() != null) { addErrors(vso.getAllErrors()); - throw fail(I18nConstants.VS_EXP_IMPORT_ERROR_X, true, vs.getUrl(), vso.getError()); + throw fail(I18nConstants.VS_EXP_IMPORT_ERROR, true, vs.getUrl(), vso.getError()); } else if (vso.getValueset() == null) { throw fail(I18nConstants.VS_EXP_IMPORT_FAIL_X, true, vs.getUrl()); } @@ -1055,6 +1092,9 @@ private void doServerIncludeCodes(ConceptSetComponent inc, boolean heirarchical, throw failTSE("Unable to expand imported value set: " + vso.getError()); } ValueSet vs = vso.getValueset(); + if (vs.hasUserData(UserDataNames.VS_EXPANSION_SOURCE)) { + sources.add(vs.getUserString(UserDataNames.VS_EXPANSION_SOURCE)); + } if (vs.hasVersion() || REPORT_VERSION_ANYWAY) { UriType u = new UriType(vs.getUrl() + (vs.hasVersion() ? "|"+vs.getVersion() : "")); if (!existsInParams(exp.getParameter(), "used-valueset", u)) { @@ -1328,6 +1368,12 @@ private FHIRException fail(String msgId, boolean check, Object... params) { return new FHIRException(msg); } + private UnknownValueSetException failUnk(String msgId, boolean check, Object... params) { + String msg = context.formatMessage(msgId, params); + allErrors.add(msg); + return new UnknownValueSetException(msg); + } + private ETooCostly failCostly(String msg) { allErrors.add(msg); return new ETooCostly(msg); @@ -1371,6 +1417,14 @@ public ValueSetExpander setDebug(boolean debug) { this.debug = debug; return this; } + + public String getSource() { + if (sources.isEmpty()) { + return "internal"; + } else { + return CommaSeparatedStringBuilder.join(", ", Utilities.sorted(sources)); + } + } } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyCache.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyCache.java index eb31066c84..0ca3a520eb 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyCache.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyCache.java @@ -56,6 +56,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome; import org.hl7.fhir.r5.terminologies.utilities.TerminologyCache.SourcedValueSet; +import org.hl7.fhir.r5.utils.UserDataNames; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.IniFile; import org.hl7.fhir.utilities.StringPair; @@ -419,7 +420,7 @@ public CacheToken generateValidationToken(ValidationOptions options, Coding code nameCacheToken(vs, ct); JsonParser json = new JsonParser(); json.setOutputStyle(OutputStyle.PRETTY); - String expJS = json.composeString(expParameters); + String expJS = expParameters == null ? "" : json.composeString(expParameters); if (vs != null && vs.hasUrl() && vs.hasVersion()) { ct.request = "{\"code\" : "+json.composeString(code, "codeableConcept")+", \"url\": \""+Utilities.escapeJson(vs.getUrl()) @@ -699,8 +700,12 @@ private void save(NamedCache nc) { sw.write("e: {\r\n"); if (ce.e.isFromServer()) sw.write(" \"from-server\" : true,\r\n"); - if (ce.e.getValueset() != null) + if (ce.e.getValueset() != null) { + if (ce.e.getValueset().hasUserData(UserDataNames.VS_EXPANSION_SOURCE)) { + sw.write(" \"source\" : "+Utilities.escapeJson(ce.e.getValueset().getUserString(UserDataNames.VS_EXPANSION_SOURCE)).trim()+",\r\n"); + } sw.write(" \"valueSet\" : "+json.composeString(ce.e.getValueset()).trim()+",\r\n"); + } sw.write(" \"error\" : \""+Utilities.escapeJson(ce.e.getError()).trim()+"\"\r\n}\r\n"); } else if (ce.s != null) { sw.write("s: {\r\n"); @@ -820,10 +825,14 @@ private CacheEntry getCacheEntry(String request, String resultString) throws IOE JsonObject o = (JsonObject) new com.google.gson.JsonParser().parse(resultString); String error = loadJS(o.get("error")); if (e == 'e') { - if (o.has("valueSet")) + if (o.has("valueSet")) { ce.e = new ValueSetExpansionOutcome((ValueSet) new JsonParser().parse(o.getAsJsonObject("valueSet")), error, TerminologyServiceErrorClass.UNKNOWN, o.has("from-server")); - else + if (o.has("source")) { + ce.e.getValueset().setUserData(UserDataNames.VS_EXPANSION_SOURCE, o.get("source").getAsString()); + } + } else { ce.e = new ValueSetExpansionOutcome(error, TerminologyServiceErrorClass.UNKNOWN, o.has("from-server")); + } } else if (e == 's') { ce.s = new SubsumesResult(o.get("result").getAsBoolean()); } else { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyServiceErrorClass.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyServiceErrorClass.java index 5aeb0fff5e..7c83dd5aab 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyServiceErrorClass.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyServiceErrorClass.java @@ -1,7 +1,7 @@ package org.hl7.fhir.r5.terminologies.utilities; public enum TerminologyServiceErrorClass { - UNKNOWN, NOSERVICE, SERVER_ERROR, VALUESET_UNSUPPORTED, CODESYSTEM_UNSUPPORTED, CODESYSTEM_UNSUPPORTED_VERSION, BLOCKED_BY_OPTIONS, INTERNAL_ERROR, BUSINESS_RULE, TOO_COSTLY, PROCESSING; + UNKNOWN, NOSERVICE, SERVER_ERROR, VALUESET_UNSUPPORTED, CODESYSTEM_UNSUPPORTED, CODESYSTEM_UNSUPPORTED_VERSION, BLOCKED_BY_OPTIONS, INTERNAL_ERROR, BUSINESS_RULE, TOO_COSTLY, PROCESSING, VALUESET_UNKNOWN; public boolean isInfrastructure() { return this == NOSERVICE || this == SERVER_ERROR || this == VALUESET_UNSUPPORTED; diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/UserDataNames.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/UserDataNames.java index 0270edcda6..1c498caa29 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/UserDataNames.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/UserDataNames.java @@ -138,6 +138,6 @@ public class UserDataNames { public static final String kindling_ballot_package = "ballot.package"; public static final String archetypeSource = "archetype-source"; public static final String archetypeName = "archetype-name"; - + public static final String VS_EXPANSION_SOURCE = "VS_EXPANSION_SOURCE"; } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/RenderingI18nContext.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/RenderingI18nContext.java index 8d6c27d382..42b7c958bf 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/RenderingI18nContext.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/RenderingI18nContext.java @@ -865,6 +865,10 @@ public class RenderingI18nContext extends I18nBase { public static final String VALUE_SET_EXP = "VALUE_SET_EXP"; public static final String VALUE_SET_EXPANSION = "VALUE_SET_EXPANSION"; public static final String VALUE_SET_EXPANSIONS = "VALUE_SET_EXPANSIONS"; + public static final String VALUE_SET_EXPANSION_SRVR = "VALUE_SET_EXPANSION_SRVR"; + public static final String VALUE_SET_EXPANSIONS_SRVR = "VALUE_SET_EXPANSIONS_SRVR"; + public static final String VALUE_SET_EXPANSION_INTERNAL = "VALUE_SET_EXPANSION_INTERNAL"; + public static final String VALUE_SET_EXPANSIONS_INTERNAL = "VALUE_SET_EXPANSIONS_INTERNAL"; public static final String VALUE_SET_EXP_FRAG = "VALUE_SET_EXP_FRAG"; public static final String VALUE_SET_GENERALIZES = "VALUE_SET_GENERALIZES"; public static final String VALUE_SET_HAS = "VALUE_SET_HAS"; diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 7f6114fbc9..52544803d3 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -1191,15 +1191,13 @@ VS_EXP_IMPORT_UNK = Unable to find included value set ''{0}'' VS_EXP_IMPORT_UNK_PINNED = Unable to find included value set ''{0}'' version ''{1}'' VS_EXP_IMPORT_NULL = Unable to find included value set with no identity VS_EXP_IMPORT_ERROR = Unable to expand included value set ''{0}'': {1} -VS_EXP_IMPORT_ERROR = Unable to expand included value set ''{0}'', but no error +VS_EXP_IMPORT_ERROR_X = Unable to expand included value set ''{0}'', but no error +VS_EXP_IMPORT_ERROR_TOO_COSTLY = Unable to expand excluded value set ''{0}'': too costly VS_EXP_IMPORT_CS_X = Cannot exclude value set ''{0}'' because it's actually a code system VS_EXP_IMPORT_CS_PINNED_X = Cannot exclude value set ''{0}'' version ''{1}'' because it's actually a code system VS_EXP_IMPORT_UNK_X = Unable to find excluded value set ''{0}'' VS_EXP_IMPORT_UNK_PINNED_X = Unable to find excluded value set ''{0}'' version ''{1}'' VS_EXP_IMPORT_NUL_XL = Unable to find excluded value set with no identity -VS_EXP_IMPORT_ERROR_X = Unable to expand excluded value set ''{0}'': {1} -VS_EXP_IMPORT_ERROR_X = Unable to expand excluded value set ''{0}'', but no error -VS_EXP_IMPORT_ERROR_TOO_COSTLY = Unable to expand excluded value set ''{0}'': too costly VS_EXP_FILTER_UNK = ValueSet ''{0}'' Filter by property ''{1}'' and op ''{2}'' is not supported yet CONCEPTMAP_VS_NOT_A_VS = Reference must be to a ValueSet, but found a {0} instead SD_DERIVATION_NO_CONCRETE = {0} is labeled as an abstract type, but no concrete descendants were found (check definitions - this is usually an error unless concrete definitions are in some other package) diff --git a/org.hl7.fhir.utilities/src/main/resources/rendering-phrases.properties b/org.hl7.fhir.utilities/src/main/resources/rendering-phrases.properties index decd77d61c..98eced79a0 100644 --- a/org.hl7.fhir.utilities/src/main/resources/rendering-phrases.properties +++ b/org.hl7.fhir.utilities/src/main/resources/rendering-phrases.properties @@ -851,6 +851,10 @@ VALUE_SET_EXISTS = exists VALUE_SET_EXP = Expansion based on example code system VALUE_SET_EXPANSION = Expansion based on VALUE_SET_EXPANSIONS = Expansion based on: +VALUE_SET_EXPANSION_SRVR = Expansion from {0} based on +VALUE_SET_EXPANSIONS_SRVR = Expansion from {0} based on: +VALUE_SET_EXPANSION_INTERNAL = Expansion done internally based on +VALUE_SET_EXPANSIONS_INTERNAL = Expansion done internally based on: VALUE_SET_EXP_FRAG = Expansion based on code system fragment VALUE_SET_GENERALIZES = generalizes VALUE_SET_HAS = This value set has {0} codes in it. In order to keep the publication size manageable, only a selection ({1} codes) of the whole set of codes is shown. diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/terminology/tests/TerminologyServiceTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/terminology/tests/TerminologyServiceTests.java index bcddc4f698..dbc9f809d0 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/terminology/tests/TerminologyServiceTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/terminology/tests/TerminologyServiceTests.java @@ -184,6 +184,9 @@ private void expand(String id, ValidationEngine engine, Resource req, String res case UNKNOWN: e.setCode(IssueType.UNKNOWN); break; + case VALUESET_UNKNOWN: + e.setCode(IssueType.UNKNOWN); + break; case VALUESET_UNSUPPORTED: e.setCode(IssueType.NOTSUPPORTED); break; From c1089f234c2c245da27d453ceba38d2bb194c721 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Fri, 17 Jan 2025 06:40:33 +1100 Subject: [PATCH 09/13] fix issues with version dependent resolution of correct server for implicit value sets --- .../client/TerminologyClientManager.java | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/client/TerminologyClientManager.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/client/TerminologyClientManager.java index d9dd3077a2..a8e6ae5af8 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/client/TerminologyClientManager.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/client/TerminologyClientManager.java @@ -22,6 +22,7 @@ import org.hl7.fhir.r5.model.Parameters; import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r5.model.TerminologyCapabilities.TerminologyCapabilitiesCodeSystemComponent; +import org.hl7.fhir.r5.model.UriType; import org.hl7.fhir.r5.model.TerminologyCapabilities; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; @@ -562,7 +563,28 @@ public SourcedValueSet findValueSetOnServer(String canonical) { if (IGNORE_TX_REGISTRY || getMasterClient() == null) { return null; } - String request = Utilities.pathURL(monitorServiceURL, "resolve?fhirVersion="+factory.getVersion()+"&valueSet="+Utilities.URLEncode(canonical)); + String request = null; + boolean isImplicit = false; + String iVersion = null; + if (ValueSetUtilities.isImplicitSCTValueSet(canonical)) { + isImplicit = true; + iVersion = canonical.substring(0, canonical.indexOf("?fhir_vs")); + if ("http://snomed.info/sct".equals(iVersion) && canonical.contains("|")) { + iVersion = canonical.substring(canonical.indexOf("|")+1); + } + iVersion = ValueSetUtilities.versionFromExpansionParams(expParameters, "http://snomed.info/sct", iVersion); + request = Utilities.pathURL(monitorServiceURL, "resolve?fhirVersion="+factory.getVersion()+"&url="+Utilities.URLEncode("http://snomed.info/sct"+(iVersion == null ? "": "|"+iVersion))); + } else if (ValueSetUtilities.isImplicitLoincValueSet(canonical)) { + isImplicit = true; + iVersion = null; + if (canonical.contains("|")) { + iVersion = canonical.substring(canonical.indexOf("|")+1); + } + iVersion = ValueSetUtilities.versionFromExpansionParams(expParameters, "http://loinc.org", iVersion); + request = Utilities.pathURL(monitorServiceURL, "resolve?fhirVersion="+factory.getVersion()+"&url="+Utilities.URLEncode("http://loinc.org"+(iVersion == null ? "": "|"+iVersion))); + } else { + request = Utilities.pathURL(monitorServiceURL, "resolve?fhirVersion="+factory.getVersion()+"&valueSet="+Utilities.URLEncode(canonical)); + } String server = null; try { if (!useEcosystem) { @@ -628,7 +650,23 @@ public SourcedValueSet findValueSetOnServer(String canonical) { Bundle bnd = client.getClient().search("ValueSet", criteria); String rid = null; if (bnd.getEntry().size() == 0) { - return null; + if (isImplicit) { + // couldn't find it, but can we expand on it? + Parameters p= new Parameters(); + p.addParameter("url", new UriType(canonical)); + p.addParameter("count", 0); + p.addParameters(expParameters); + try { + ValueSet vs = client.getClient().expandValueset(null, p); + if (vs != null) { + return new SourcedValueSet(server, ValueSetUtilities.makeImplicitValueSet(canonical, iVersion)); + } + } catch (Exception e) { + return null; + } + } else { + return null; + } } else if (bnd.getEntry().size() > 1) { List vslist = new ArrayList<>(); for (BundleEntryComponent be : bnd.getEntry()) { @@ -659,7 +697,6 @@ public SourcedValueSet findValueSetOnServer(String canonical) { return null; } } - public SourcedCodeSystem findCodeSystemOnServer(String canonical) { if (IGNORE_TX_REGISTRY || getMasterClient() == null || !useEcosystem) { return null; From c6edf285e32548328103af7599b51a93b0c2951a Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Fri, 17 Jan 2025 06:41:00 +1100 Subject: [PATCH 10/13] Correctly handle supplements when validating based on expansions --- .../utilities/ValueSetProcessBase.java | 9 +++++++++ .../terminologies/validation/ValueSetValidator.java | 13 ++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/ValueSetProcessBase.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/ValueSetProcessBase.java index 6f4bca5621..b31ef5492f 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/ValueSetProcessBase.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/ValueSetProcessBase.java @@ -264,6 +264,15 @@ public ContextUtilities getCu() { } + public String removeSupplement(String s) { + requiredSupplements.remove(s); + if (s.contains("|")) { + s = s.substring(0, s.indexOf("|")); + requiredSupplements.remove(s); + } + return s; + } + protected AlternateCodesProcessingRules altCodeParams = new AlternateCodesProcessingRules(false); protected AlternateCodesProcessingRules allAltCodes = new AlternateCodesProcessingRules(true); } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/validation/ValueSetValidator.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/validation/ValueSetValidator.java index 9d87baafb1..08e79173b7 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/validation/ValueSetValidator.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/validation/ValueSetValidator.java @@ -77,6 +77,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; +import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionParameterComponent; import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; import org.hl7.fhir.r5.terminologies.client.TerminologyClientManager; import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome; @@ -518,17 +519,14 @@ public CodeSystem resolveCodeSystem(String system, String version) { if (cs != null) { if (cs.hasUserData("supplements.installed")) { for (String s : cs.getUserString("supplements.installed").split("\\,")) { - requiredSupplements.remove(s); - if (s.contains("|")) { - s = s.substring(0, s.indexOf("|")); - requiredSupplements.remove(s); - } + s = removeSupplement(s); } } } return cs; } + public List resolveCodeSystemVersions(String system) { List res = new ArrayList<>(); for (CodeSystem t : localSystems) { @@ -696,6 +694,11 @@ public ValidationResult validateCode(String path, Coding code) throws FHIRExcept res = validateCode(path, code, cs, null, info); res.setIssues(issues); } else if (cs == null && valueset.hasExpansion() && inExpansion) { + for (ValueSetExpansionParameterComponent p : valueset.getExpansion().getParameter()) { + if ("used-supplement".equals(p.getName())) { + removeSupplement(p.getValue().primitiveValue()); + } + } // we just take the value set as face value then res = new ValidationResult(system, wv, new ConceptDefinitionComponent().setCode(code.getCode()).setDisplay(code.getDisplay()), code.getDisplay()); if (!preferServerSide(system)) { From 1163ecad8f0625ed1ee1e76eacc0c5b77ceab919 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Fri, 17 Jan 2025 06:41:59 +1100 Subject: [PATCH 11/13] Update ai based code/text validation framework --- .../hl7/fhir/validation/ai/ChatGPTAPI.java | 49 ++++-- .../org/hl7/fhir/validation/ai/ClaudeAPI.java | 51 +++--- .../ai/CodeAndTextValidationRequest.java | 9 ++ .../ai/CodeAndTextValidationResult.java | 3 + .../validation/ai/CodeAndTextValidator.java | 12 +- .../org/hl7/fhir/validation/ai/Ollama.java | 115 ++++++++++++++ .../org/hl7/fhir/validation/ai/Scanner.java | 149 ++++++++++++++++++ 7 files changed, 351 insertions(+), 37 deletions(-) create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/Ollama.java create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/Scanner.java diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ChatGPTAPI.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ChatGPTAPI.java index 69b19ae0c1..d5460c497d 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ChatGPTAPI.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ChatGPTAPI.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; +import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.http.HTTPResult; import org.hl7.fhir.utilities.http.ManagedWebAccess; @@ -19,23 +20,37 @@ public class ChatGPTAPI extends AIAPI { @Override public List validateCodings(List requests) throws IOException { - StringBuilder prompt = new StringBuilder(); - prompt.append("For each of the following cases, determine if the text appropriately matches the code. "); - prompt.append("Respond in JSON format with an array of objects containing 'index', 'isValid', 'explanation', and 'confidence'.\n\n"); - - for (int i = 0; i < requests.size(); i++) { - CodeAndTextValidationRequest req = requests.get(i); - prompt.append(String.format("%d. Is '%s' a reasonable text to associate with the %s code %s (display '%s')\n", - i + 1, req.getText(), getSystemName(req.getSystem()), req.getCode(), req.getDisplay())); + // limit to 5 in a batch + List> chunks = new ArrayList<>(); + for (int i = 0; i < requests.size(); i += 4) { + chunks.add(requests.subList(i, Math.min(i + 4, requests.size()))); } + List results = new ArrayList(); + int c = 0; + System.out.print(" "); + for (List chunk : chunks) { + + StringBuilder prompt = new StringBuilder(); + prompt.append("For each of the following cases, determine if the text is not compatible with the code. The text may contain significantly more or less information than the code.\n\n"); + prompt.append("Respond in JSON format with an array of objects containing 'index', 'isCompatible', 'explanation', and 'confidence'.\n\n"); - String systemPrompt = "You are a medical terminology expert. Evaluate whether text descriptions match their\n"+ - "associated clinical codes. Provide detailed explanations for any mismatches. "+ - "Express your confidence level based on how certain you are of the relationship."; + for (int i = 0; i < chunk.size(); i++) { + CodeAndTextValidationRequest req = chunk.get(i); + prompt.append(String.format("%d. Is '%s' in conflict with the %s code %s (display '%s')\n", + i + 1, req.getText(), getSystemName(req.getSystem()), req.getCode(), req.getDisplay())); + } - JsonArray json = getResponse(prompt.toString(), systemPrompt); + String systemPrompt = "You are a medical terminology expert. Evaluate whether text descriptions match their\n"+ + "associated clinical codes. Provide detailed explanations for any mismatches. "+ + "Express your confidence level based on how certain you are of the relationship."; - return parseValidationResponse(json, requests); + System.out.print(""+c+" "); + JsonArray json = getResponse(prompt.toString(), systemPrompt); + + parseValidationResponse(json, chunk, results); + c += 4; + } + return results; } public JsonArray getResponse(String prompt, String systemPrompt) throws IOException { @@ -50,15 +65,15 @@ public JsonArray getResponse(String prompt, String systemPrompt) throws IOExcept json = JsonParser.parseObject(response.getContentAsString()); String text = json.getJsonArray("choices").get(0).asJsonObject().getJsonObject("message").asString("content"); text = text.replace("```", "").substring(4); + + TextFile.stringToFile(text, Utilities.path("[tmp]", "fhir-validator-chatgpt-response.json")); return (JsonArray) JsonParser.parse(text); } - private List parseValidationResponse(JsonArray json, List requests) { - List res = new ArrayList<>(); + private void parseValidationResponse(JsonArray json, List requests, List res) { for (JsonObject o : json.asJsonObjects()) { CodeAndTextValidationRequest request = requests.get(o.asInteger("index")-1); - res.add(new CodeAndTextValidationResult(request, o.asBoolean("isValid"), o.asString("explanation"), o.asString("confidence"))); + res.add(new CodeAndTextValidationResult(request, o.asBoolean("isCompatible"), o.asString("explanation"), o.asString("confidence"))); } - return res; } } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ClaudeAPI.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ClaudeAPI.java index b25c210b48..cbcc86e8e0 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ClaudeAPI.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/ClaudeAPI.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; +import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.http.HTTPResult; import org.hl7.fhir.utilities.http.ManagedWebAccess; @@ -19,29 +20,42 @@ public class ClaudeAPI extends AIAPI { @Override public List validateCodings(List requests) throws IOException { - StringBuilder prompt = new StringBuilder(); - prompt.append("For each of the following cases, determine if the text appropriately matches the code. "); - prompt.append("Respond in JSON format with an array of objects containing 'index', 'isValid', 'explanation', and 'confidence'.\n\n"); - - for (int i = 0; i < requests.size(); i++) { - CodeAndTextValidationRequest req = requests.get(i); - prompt.append(String.format("%d. Is '%s' a reasonable text to associate with the %s code %s (display = %s)?\n", - i + 1, req.getText(), getSystemName(req.getSystem()), req.getCode(), req.getDisplay())); + // limit to 5 in a batch + List> chunks = new ArrayList<>(); + for (int i = 0; i < requests.size(); i += 4) { + chunks.add(requests.subList(i, Math.min(i + 4, requests.size()))); } + List results = new ArrayList(); + int c = 0; + System.out.print(" "); + for (List chunk : chunks) { - String systemPrompt = "You are a medical terminology expert. Evaluate whether text descriptions match their\n"+ - "associated clinical codes. Provide detailed explanations for any mismatches. "+ - "Express your confidence level based on how certain you are of the relationship."; + StringBuilder prompt = new StringBuilder(); + prompt.append("For each of the following cases, determine if the text can't be a description of the same situation as the code. The text may contain significantly more or less information than the code.\n\n"); + prompt.append("Respond in JSON format with an array of objects containing 'index', 'isCompatible', 'explanation', and 'confidence'. Please evaluate all the items in a single go\n\n"); - JsonObject json = getResponse(prompt.toString(), systemPrompt); + for (int i = 0; i < chunk.size(); i++) { + CodeAndTextValidationRequest req = chunk.get(i); + prompt.append(String.format("%d. Is '%s' in conflict with the %s code %s (display = %s)?\n", + i + 1, req.getText(), getSystemName(req.getSystem()), req.getCode(), req.getDisplay())); + } - return parseValidationResponse(json, requests); - } + String systemPrompt = "You are a medical terminology expert. Evaluate whether text descriptions match their\n"+ + "associated clinical codes. Provide detailed explanations for any mismatches. "+ + "Express your confidence level based on how certain you are of the relationship."; + + System.out.print(""+c+" "); + JsonObject json = getResponse(prompt.toString(), systemPrompt); + parseValidationResponse(json, chunk, results); + c+= 4; + } + return results; + } public JsonObject getResponse(String prompt, String systemPrompt) throws IOException { JsonObject j = new JsonObject(); - j.add("model", "claude-3-5-sonnet-20241022"); + j.add("model", MODEL); j.add("system", systemPrompt); j.add("max_tokens", 1024); j.forceArray("messages").addObject().add("role", "user").add("content", prompt); @@ -52,18 +66,17 @@ public JsonObject getResponse(String prompt, String systemPrompt) throws IOExcep response.checkThrowException(); JsonObject json = JsonParser.parseObject(response.getContentAsString()); String text = json.getJsonArray("content").get(0).asJsonObject().asString("text"); + TextFile.stringToFile(text, Utilities.path("[tmp]", "fhir-validator-claude-response.json")); return JsonParser.parseObject(text); } - private List parseValidationResponse(JsonObject json, List requests) { - List res = new ArrayList<>(); + private void parseValidationResponse(JsonObject json, List requests, List res) { for (JsonObject o : json.getProperties().get(0).getValue().asJsonArray().asJsonObjects()) { CodeAndTextValidationRequest request = requests.get(o.asInteger("index")-1); - res.add(new CodeAndTextValidationResult(request, o.asBoolean("isValid"), o.asString("explanation"), o.asString("confidence"))); + res.add(new CodeAndTextValidationResult(request, o.asBoolean("isCompatible"), o.asString("explanation"), o.asString("confidence"))); } - return res; } } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationRequest.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationRequest.java index d2b9833b64..9cee746f3b 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationRequest.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationRequest.java @@ -3,6 +3,8 @@ import org.hl7.fhir.validation.instance.utils.NodeStack; public class CodeAndTextValidationRequest { + private Object data; + private NodeStack location; private String lang; private String system; @@ -36,4 +38,11 @@ public String getDisplay() { public String getLang() { return lang; } + public Object getData() { + return data; + } + public CodeAndTextValidationRequest setData(Object data) { + this.data = data; + return this; + } } \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationResult.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationResult.java index 62d58e33bc..953b3f667c 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationResult.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidationResult.java @@ -24,5 +24,8 @@ public String getExplanation() { public String getConfidence() { return confidence; } + public String summary() { + return (isValid ? "Valid" : "Invalid") +" ("+confidence+") : "+explanation; + } } \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidator.java index 425d843138..c0445ffd49 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/CodeAndTextValidator.java @@ -74,8 +74,18 @@ public List validateCodings(List validateCodings(List requests) throws IOException { + List res = new ArrayList<>(); + for ( CodeAndTextValidationRequest req : requests) { + StringBuilder prompt = new StringBuilder(); +// prompt.append("You are a medical terminology expert. Evaluate whether the text description '"+req.getText()+"' matches the\n"+ +// "clinical code '"+req.getCode()+"' from '"+getSystemName(req.getSystem())+"' which has a display of '"+req.getDisplay()+"'. Provide detailed explanations for any mismatches. "+ +// "It's ok if the text includes more details than the code. Express your confidence level based on how certain you are of the relationship.\n\n"); +// prompt.append("Respond in JSON format with an object containing 'isValid', 'explanation', and 'confidence'.\n\n"); + + prompt.append("Evaluate if B can't be a description of the same situation as the data presented in A.\r\n"); + prompt.append("\r\n"); + prompt.append("* B may be significantly more or less specific than A.\r\n"); + prompt.append("* Provide detailed explanations for your reasoning.\r\n"); + prompt.append("* It's ok if the text includes more or less information than the code.\r\n"); + prompt.append("* Respond in JSON format with an object containing a boolean property 'isCompatible', and string properties 'explanation' and 'confidence'\r\n"); + prompt.append("\r\n"); + prompt.append("A\r\n"); + prompt.append("Code: "+getSystemName(req.getSystem())+", '"+req.getCode()+"'\r\n"); + prompt.append("Text: '"+req.getDisplay()+"'\r\n"); + prompt.append("\r\n"); + prompt.append("B\r\n"); + prompt.append(req.getText()+"\r\n"); + + System.out.print("."); + JsonObject json = getResponse(prompt.toString()); + + res.add(parseValidationResponse(json, req)); + } + return res; + } + + + public JsonObject getResponse(String prompt) throws IOException { + JsonObject j = new JsonObject(); + j.add("model", model); + j.add("format", "json"); + j.add("stream", false); + j.add("prompt", prompt); + + ManagedWebAccessor web = ManagedWebAccess.accessor(Utilities.strings("web")); + HTTPResult response = web.post(url, JsonParser.composeBytes(j), "application/json", "application/json"); + response.checkThrowException(); + JsonObject json = JsonParser.parseObject(response.getContentAsString()); + String text = json.asString("response"); + TextFile.stringToFile(text, Utilities.path("[tmp]", "fhir-validator-ollama-response.json")); + return JsonParser.parseObject(text); + } + + + + private CodeAndTextValidationResult parseValidationResponse(JsonObject json, CodeAndTextValidationRequest request) throws IOException { + // what ollama returns is unpredictable + if (json.has("explanation") && json.has("isCompatible")) { + return parseItem(request, json); + } else { + throw new FHIRException("Unable to understand ollama's response json: see "+Utilities.path("[tmp]", "fhir-validator-ollama-response.json")); + } + } + + public CodeAndTextValidationResult parseItem(CodeAndTextValidationRequest request, JsonObject o) { + return new CodeAndTextValidationResult(request, o.asBoolean("isCompatible"), o.asString("explanation"), o.asString("confidence")); + } + + +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/Scanner.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/Scanner.java new file mode 100644 index 0000000000..d27e27cf83 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ai/Scanner.java @@ -0,0 +1,149 @@ +package org.hl7.fhir.validation.ai; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.hl7.fhir.r4.formats.JsonParser; +import org.hl7.fhir.r4.model.Base; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent; +import org.hl7.fhir.r4.model.Property; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.terminologies.CodeSystemUtilities; +import org.hl7.fhir.r5.context.SimpleWorkerContext; +import org.hl7.fhir.r5.terminologies.utilities.ValidationResult; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.json.model.JsonObject; +import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; +import org.hl7.fhir.utilities.npm.NpmPackage; +import org.hl7.fhir.utilities.validation.ValidationOptions; + +public class Scanner { + + public static void main(String[] args) throws IOException { + new Scanner().execute("/Users/grahamegrieve/web/www.hl7.org.fhir/us", "/Users/grahamegrieve/web/www.hl7.org.fhir/uv", "/Users/grahamegrieve/web/www.hl7.org.fhir/R4"); + } + + private Set combos = new HashSet<>(); + private Map codesystems = new HashMap<>(); + private SimpleWorkerContext ctxt; + + private void execute(String... paths) throws IOException { + System.out.println("loading"); + NpmPackage npm = new FilesystemPackageCacheManager.Builder().build().loadPackage("hl7.fhir.r5.core"); + ctxt = new SimpleWorkerContext.SimpleWorkerContextBuilder().withAllowLoadingDuplicates(true).fromPackage(npm); + for (String p : paths) { + execute(new File(p)); + } + System.out.println("saving"); + JsonObject j = new JsonObject(); + for (String s : Utilities.sorted(combos)) { + JsonObject o = processCombo(s); + if (o != null) { + j.forceArray("cases").add(o); + } + } + org.hl7.fhir.utilities.json.parser.JsonParser.compose(j, new File("/Users/grahamegrieve/temp/code-text-cases.json"), true); + System.out.println("done. see [tmp]/code-text-cases.json"); + } + + private JsonObject processCombo(String s) { + String path = s.substring(0, s.indexOf("`")+2).trim(); + s = s.substring(s.indexOf("`")+1).trim(); + String text = s.substring(s.indexOf("::")+2).trim(); + String uri = s.substring(0, s.indexOf("::")).trim(); + String system = uri.substring(0, uri.indexOf("#")).trim(); + String code = uri.substring(uri.indexOf("#")+1).trim(); + String display = getFromCodeSystem(system, code); + if (display != null && !display.toLowerCase().equals(text.toLowerCase())) { + JsonObject object = new JsonObject(); + object.add("path", path); + object.add("system", system); + object.add("code", code); + object.add("display", display); + object.add("lang", "en"); + object.add("text", text); + object.add("goal", "valid"); + return object; + } else { + return null; + } + } + + private String getFromCodeSystem(String system, String code) { + CodeSystem cs = codesystems.get(system); + if (cs != null) { + ConceptDefinitionComponent cd = CodeSystemUtilities.findCode(cs.getConcept(), code); + if (cd != null) { + return cd.getDisplay(); + } + } + ValidationResult vr = ctxt.validateCode(ValidationOptions.defaults(), system, null, code, null); + if (vr.isOk()) { + return vr.getDisplay(); + } + return null; + } + + private void execute(File folder) { + System.out.println(folder.getAbsolutePath()); + for (File f : folder.listFiles()) { + if (f.isDirectory()) { + execute(f); + } else if (f.getName().endsWith(".json")) { + try { + Resource r = new JsonParser().parse(new FileInputStream(f)); + if (r != null) { + if (r instanceof CodeSystem) { + CodeSystem cs = (CodeSystem) r; + codesystems.put(cs.getUrl(), cs); + } + scan(r.fhirType(), r); + } + + } catch (Exception e) { + // nothing + } catch (Error e) { + // nothing + } + } + } + } + + private void scan(String path, Base b) { + for (Property p : b.children()) { + for (Base v : p.getValues()) { + if (v.isResource() || Utilities.existsInList(v.fhirType(), "Element", "BackboneElement")) { + scan(path+"."+p.getName(), v); + } else if (v instanceof CodeableConcept) { + see(path+"."+p.getName(), (CodeableConcept) v); + } + } + } + + } + + private void see(String path, CodeableConcept cc) { + if (cc.hasText()) { + for (Coding c : cc.getCoding()) { + if (!c.hasDisplay() || !c.getDisplay().toLowerCase().equals(cc.getText().toLowerCase())) + see(path, c.getSystem(), c.getCode(), cc.getText()); + } + } + + } + + private void see(String path, String system, String code, String text) { + if (!system.contains("acme") && !system.contains("example")) { + combos.add(path+"`"+system+"#"+code+" :: "+text); + } + } + +} From f5081aeed22382739de9507844d19e060d1433d6 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Fri, 17 Jan 2025 06:42:19 +1100 Subject: [PATCH 12/13] Don't enforce code system property references to standard properties in R4 --- .../instance/type/CodeSystemValidator.java | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/CodeSystemValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/CodeSystemValidator.java index a5f11ba7e2..08bba70612 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/CodeSystemValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/CodeSystemValidator.java @@ -275,16 +275,29 @@ private boolean checkPropertyDefinition(List errors, Element } } } else { - ConceptDefinitionComponent cc = CodeSystemUtilities.findCode(pcs.getConcept(), pcode); - if (warning(errors, "2025-01-09", IssueType.INVALID, cs.line(), cs.col(), stack.getLiteralPath(), cc != null, I18nConstants.CODESYSTEM_PROPERTY_URI_INVALID, pcode, base, pcs.present(), uri, code)) { - foundPropDefn = true; - if ("code".equals(type)) { - ConceptPropertyComponent ccp = CodeSystemUtilities.getProperty(cc, "binding"); - if (ccp != null && ccp.hasValue() && ccp.getValue().hasPrimitiveValue()) { - ruleFromUri = CodeValidationRule.VS_ERROR; - valuesetFromUri = ccp.getValue().primitiveValue(); - } else { - ruleFromUri = CodeValidationRule.INTERNAL_CODE_WARNING; + ConceptDefinitionComponent cc = CodeSystemUtilities.findCode(pcs.getConcept(), pcode); + if (warning(errors, "2025-01-09", IssueType.INVALID, cs.line(), cs.col(), stack.getLiteralPath(), cc != null || isOfficialRef(uri), I18nConstants.CODESYSTEM_PROPERTY_URI_INVALID, pcode, base, pcs.present(), uri, code)) { + if (cc != null) { + foundPropDefn = true; + if ("code".equals(type)) { + ConceptPropertyComponent ccp = CodeSystemUtilities.getProperty(cc, "binding"); + if (ccp != null && ccp.hasValue() && ccp.getValue().hasPrimitiveValue()) { + ruleFromUri = CodeValidationRule.VS_ERROR; + valuesetFromUri = ccp.getValue().primitiveValue(); + } else { + ruleFromUri = CodeValidationRule.INTERNAL_CODE_WARNING; + } + } + } else { + switch (uri) { + case "http://hl7.org/fhir/concept-properties#status": + case "http://hl7.org/fhir/concept-properties#retirementDate": + case "http://hl7.org/fhir/concept-properties#deprecationDate": + case "http://hl7.org/fhir/concept-properties#parent": + case "http://hl7.org/fhir/concept-properties#child": + case "http://hl7.org/fhir/concept-properties#notSelectable": + default: + // do nothing for now } } } else { @@ -446,6 +459,15 @@ private boolean checkPropertyDefinition(List errors, Element return ok; } + private boolean isOfficialRef(String uri) { + if (VersionUtilities.isR5Plus(context.getVersion())) { + return false; + } else { + return Utilities.existsInList(uri, "http://hl7.org/fhir/concept-properties#status", "http://hl7.org/fhir/concept-properties#retirementDate", + "http://hl7.org/fhir/concept-properties#deprecationDate","http://hl7.org/fhir/concept-properties#parent","http://hl7.org/fhir/concept-properties#child","http://hl7.org/fhir/concept-properties#notSelectable"); + } + } + private ValueSet findVS(List errors, Element cs, NodeStack stack, String url, String message) { if (url == null) { return null; From 3d0a3c451e8f7aea07f343207f536d0e8fa152fe Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Fri, 17 Jan 2025 07:55:26 +1100 Subject: [PATCH 13/13] compile fixes and fixes for David --- .../org/hl7/fhir/r5/context/BaseWorkerContext.java | 3 --- .../utilities/filesystem/ManagedFileAccess.java | 14 +------------- .../hl7/fhir/validation/ValidationTimeTracker.java | 8 ++++++++ .../validation/instance/InstanceValidator.java | 6 +++++- .../validation/tests/ValidationEngineTests.java | 10 +++++----- 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java index 65895c07de..d782ead19a 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/BaseWorkerContext.java @@ -1710,9 +1710,6 @@ public ValidationResult validateCode(ValidationOptions options, CodeableConcept } } Set unknownSystems = new HashSet<>(); - if ("8517006".equals(code.getCodingFirstRep().getCode())) { - DebugUtilities.breakpoint(); - } List issues = new ArrayList<>(); diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/filesystem/ManagedFileAccess.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/filesystem/ManagedFileAccess.java index 9c6490cdcf..f8906e2010 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/filesystem/ManagedFileAccess.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/filesystem/ManagedFileAccess.java @@ -131,19 +131,7 @@ public static File file(String path, String filepath) throws IOException { } public static File file(File root, String filepath) throws IOException { - switch (accessPolicy) { - case DIRECT: - if (!inAllowedPaths(root.getAbsolutePath())) { - throw new IOException("The path '"+root.getAbsolutePath()+"' cannot be accessed by policy"); - } - return new File(root.getAbsolutePath(), filepath); - case MANAGED: - return accessor.file(Utilities.path(root.getAbsolutePath(), filepath)); - case PROHIBITED: - throw new IOException("Access to files is not allowed by local security policy"); - default: - throw new IOException("Internal Error"); - } + return file(root.getAbsolutePath(), filepath); } /** * Open a FileInputStream, conforming to local security policy diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationTimeTracker.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationTimeTracker.java index 9588962e04..dfd269696f 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationTimeTracker.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationTimeTracker.java @@ -7,6 +7,7 @@ public class ValidationTimeTracker { private long loadTime = 0; private long fpeTime = 0; private long specTime = 0; + private long aiTime = 0; public long getOverall() { return overall; @@ -28,6 +29,9 @@ public long getSpecTime() { return specTime; } + public long getAiTime() { + return aiTime; + } public void load(long start) { loadTime = loadTime + (System.nanoTime() - start); } @@ -35,6 +39,9 @@ public void load(long start) { public void overall(long start) { overall = overall + (System.nanoTime() - start); } + public void ai(long start) { + aiTime = aiTime + (System.nanoTime() - start); + } public void tx(long start, String s) { long ms = (System.nanoTime() - start) / 1000000; @@ -61,5 +68,6 @@ public void reset() { loadTime = 0; fpeTime = 0; specTime = 0; + aiTime = 0; } } \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index 0cf52dac5e..6fef4ca99b 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -1024,6 +1024,7 @@ public void validate(Object appContext, List errors, String p errors.removeAll(messagesToRemove); timeTracker.overall(t); if (aiService != null && !textsToCheck.isEmpty()) { + t = System.nanoTime(); CodeAndTextValidator ctv = new CodeAndTextValidator(cacheFolder, aiService); List results = null; try { @@ -1045,6 +1046,7 @@ public void validate(Object appContext, List errors, String p } } } + timeTracker.ai(t); } if (DEBUG_ELEMENT) { @@ -7815,7 +7817,9 @@ public long timeNoTX() { return (timeTracker.getOverall() - timeTracker.getTxTime()) / 1000000; } public String reportTimes() { - String s = String.format("Times (ms): overall = %d:4, tx = %d, sd = %d, load = %d, fpe = %d, spec = %d", timeTracker.getOverall() / 1000000, timeTracker.getTxTime() / 1000000, timeTracker.getSdTime() / 1000000, timeTracker.getLoadTime() / 1000000, timeTracker.getFpeTime() / 1000000, timeTracker.getSpecTime() / 1000000); + String s = String.format("Times (ms): overall = %d:4, tx = %d, sd = %d, load = %d, fpe = %d, spec = %d, ai = %d", + timeTracker.getOverall() / 1000000, timeTracker.getTxTime() / 1000000, timeTracker.getSdTime() / 1000000, + timeTracker.getLoadTime() / 1000000, timeTracker.getFpeTime() / 1000000, timeTracker.getSpecTime() / 1000000, timeTracker.getAiTime() / 1000000); timeTracker.reset(); return s; } diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java index 1a4fd22803..0831c58740 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java @@ -333,7 +333,7 @@ public void testResolveRelativeFileInvalid() throws Exception { ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Observation.json"))); ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Patient.json"))); OperationOutcome op = ve.validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", "resolution", "relative-url-invalid.json"), null); - Assertions.assertTrue(checkOutcomes("testResolveRelativeFileValid", op, + Assertions.assertTrue(checkOutcomes("testResolveRelativeFileInvalid", op, "Observation.subject null error/structure: Unable to find a profile match for Patient/example-newborn among choices: http://hl7.org/fhir/test/StructureDefinition/PatientRule\n"+ "Observation.subject null information/structure: Details for Patient/example-newborn matching against profile http://hl7.org/fhir/test/StructureDefinition/PatientRule|0.1.0\n"+ "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have a performer\n"+ @@ -359,7 +359,7 @@ public void testResolveRelativeFileError() throws Exception { ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Observation.json"))); ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Patient.json"))); OperationOutcome op = ve.validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", "resolution", "relative-url-error.json"), null); - Assertions.assertTrue(checkOutcomes("testResolveRelativeFileValid", op, + Assertions.assertTrue(checkOutcomes("testResolveRelativeFileError", op, "Observation.subject null error/structure: Unable to resolve resource with reference 'patient/example-newborn-x'\n"+ "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have a performer\n"+ "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have an effective[x] ()\n"+ @@ -383,7 +383,7 @@ public void testResolveAbsoluteValid() throws Exception { ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Observation.json"))); ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Patient.json"))); OperationOutcome op = ve.validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", "resolution", "absolute-url-valid.json"), null); - Assertions.assertTrue(checkOutcomes("testResolveRelativeFileValid", op, + Assertions.assertTrue(checkOutcomes("testResolveAbsoluteValid", op, "Observation.subject.resolve().ofType(Patient).managingOrganization null error/structure: Unable to resolve resource with reference 'Organization/1'\n"+ "Observation.subject.resolve().ofType(Patient).managingOrganization null information/informational: Fetching 'Organization/1' failed. System details: org.hl7.fhir.exceptions.FHIRException: The URL 'Organization/1' is not known to the FHIR validator, and a resolution context has not been provided as part of the setup / parameters\n"+ "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have a performer\n"+ @@ -402,7 +402,7 @@ public void testResolveAbsoluteInvalid() throws Exception { ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Observation.json"))); ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Patient.json"))); OperationOutcome op = ve.validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", "resolution", "absolute-url-invalid.json"), null); - Assertions.assertTrue(checkOutcomes("testResolveRelativeFileValid", op, + Assertions.assertTrue(checkOutcomes("testResolveAbsoluteInvalid", op, "Observation.subject null error/structure: Unable to find a profile match for https://hl7.org/fhir/R4/patient-example-newborn.json among choices: http://hl7.org/fhir/test/StructureDefinition/PatientRule\n"+ "Observation.subject null information/structure: Details for https://hl7.org/fhir/R4/patient-example-newborn.json matching against profile http://hl7.org/fhir/test/StructureDefinition/PatientRule|0.1.0\n"+ "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have a performer\n"+ @@ -421,7 +421,7 @@ public void testResolveAbsoluteError() throws Exception { ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Observation.json"))); ve.seeResource(new JsonParser().parse(TestingUtilities.loadTestResourceStream("validator", "resolution", "StructureDefinition-Patient.json"))); OperationOutcome op = ve.validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", "resolution", "absolute-url-error.json"), null); - Assertions.assertTrue(checkOutcomes("testResolveRelativeFileValid", op, + Assertions.assertTrue(checkOutcomes("testResolveAbsoluteError", op, "Observation.subject null error/structure: Unable to resolve resource with reference 'http://hl7x.org/fhir/R4/Patient/Patient/example-newborn'\n"+ "Observation.subject null information/informational: Fetching 'http://hl7x.org/fhir/R4/Patient/Patient/example-newborn' failed. System details: java.net.UnknownHostException: hl7x.org\n"+ "Observation null warning/invalid: Best Practice Recommendation: In general, all observations should have a performer\n"+