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 97c434943..13d21dd32 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 @@ -1017,6 +1017,106 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte } } + @Override + public void validateCodeBatchByRef(ValidationOptions options, List codes, String vsUrl) { + if (options == null) { + options = ValidationOptions.defaults(); + } + // 1st pass: what is in the cache? + // 2nd pass: What can we do internally + // 3rd pass: hit the server + for (CodingValidationRequest t : codes) { + t.setCacheToken(txCache != null ? txCache.generateValidationToken(options, t.getCoding(), vsUrl, expParameters) : null); + if (t.getCoding().hasSystem()) { + codeSystemsUsed.add(t.getCoding().getSystem()); + } + if (txCache != null) { + t.setResult(txCache.getValidation(t.getCacheToken())); + } + } + if (options.isUseClient()) { + ValueSet vs = fetchResource(ValueSet.class, vsUrl); + if (vs != null) { + for (CodingValidationRequest t : codes) { + if (!t.hasResult()) { + try { + ValueSetValidator vsc = constructValueSetCheckerSimple(options, vs); + vsc.setThrowToServer(options.isUseServer() && tcc.getClient() != null); + ValidationResult res = vsc.validateCode("Coding", t.getCoding()); + if (txCache != null) { + txCache.cacheValidation(t.getCacheToken(), res, TerminologyCache.TRANSIENT); + } + t.setResult(res); + } catch (Exception e) { + } + } + } + } + } + + for (CodingValidationRequest t : codes) { + if (!t.hasResult()) { + String codeKey = t.getCoding().hasVersion() ? t.getCoding().getSystem()+"|"+t.getCoding().getVersion() : t.getCoding().getSystem(); + if (!options.isUseServer()) { + t.setResult(new ValidationResult(IssueSeverity.WARNING,formatMessage(I18nConstants.UNABLE_TO_VALIDATE_CODE_WITHOUT_USING_SERVER), TerminologyServiceErrorClass.BLOCKED_BY_OPTIONS, null)); + } else if (unsupportedCodeSystems.contains(codeKey)) { + t.setResult(new ValidationResult(IssueSeverity.ERROR,formatMessage(I18nConstants.TERMINOLOGY_TX_SYSTEM_NOTKNOWN, t.getCoding().getSystem()), TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED, null)); + } else if (noTerminologyServer) { + t.setResult(new ValidationResult(IssueSeverity.ERROR,formatMessage(I18nConstants.ERROR_VALIDATING_CODE_RUNNING_WITHOUT_TERMINOLOGY_SERVICES), TerminologyServiceErrorClass.NOSERVICE, null)); + } + } + } + + if (expParameters == null) + throw new Error(formatMessage(I18nConstants.NO_EXPANSIONPROFILE_PROVIDED)); + // for those that that failed, we try to validate on the server + Bundle batch = new Bundle(); + batch.setType(BundleType.BATCH); + Set systems = new HashSet<>(); + for (CodingValidationRequest codingValidationRequest : codes) { + if (!codingValidationRequest.hasResult()) { + Parameters pIn = constructParameters(options, codingValidationRequest, vsUrl); + setTerminologyOptions(options, pIn); + BundleEntryComponent be = batch.addEntry(); + be.setResource(pIn); + be.getRequest().setMethod(HTTPVerb.POST); + if (vsUrl != null) { + be.getRequest().setUrl("ValueSet/$validate-code"); + } else { + be.getRequest().setUrl("CodeSystem/$validate-code"); + } + be.setUserData("source", codingValidationRequest); + systems.add(codingValidationRequest.getCoding().getSystem()); + } + } + if (batch.getEntry().size() > 0) { + txLog("$batch validate for "+batch.getEntry().size()+" codes on systems "+systems.toString()); + if (tcc.getClient() == null) { + throw new FHIRException(formatMessage(I18nConstants.ATTEMPT_TO_USE_TERMINOLOGY_SERVER_WHEN_NO_TERMINOLOGY_SERVER_IS_AVAILABLE)); + } + if (txLog != null) { + txLog.clearLastId(); + } + Bundle resp = tcc.getClient().validateBatch(batch); + if (resp == null) { + throw new FHIRException(formatMessage(I18nConstants.TX_SERVER_NO_BATCH_RESPONSE)); + } + for (int i = 0; i < batch.getEntry().size(); i++) { + CodingValidationRequest t = (CodingValidationRequest) batch.getEntry().get(i).getUserData("source"); + BundleEntryComponent r = resp.getEntry().get(i); + + if (r.getResource() instanceof Parameters) { + t.setResult(processValidationResult((Parameters) r.getResource())); + if (txCache != null) { + txCache.cacheValidation(t.getCacheToken(), t.getResult(), TerminologyCache.PERMANENT); + } + } else { + t.setResult(new ValidationResult(IssueSeverity.ERROR, getResponseText(r.getResource()), null).setTxLink(txLog == null ? null : txLog.getLastId())); + } + } + } + } + private String getResponseText(Resource resource) { if (resource instanceof OperationOutcome) { return OperationOutcomeRenderer.toString((OperationOutcome) resource); @@ -1197,6 +1297,19 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte return pIn; } + protected Parameters constructParameters(ValidationOptions options, CodingValidationRequest codingValidationRequest, String vsUrl) { + Parameters pIn = new Parameters(); + pIn.addParameter().setName("coding").setValue(codingValidationRequest.getCoding()); + if (options.isGuessSystem()) { + pIn.addParameter().setName("implySystem").setValue(new BooleanType(true)); + } + if (vsUrl != null) { + pIn.addParameter().setName("url").setValue(new CanonicalType(vsUrl)); + } + pIn.addParameter().setName("profile").setResource(expParameters); + return pIn; + } + private void updateUnsupportedCodeSystems(ValidationResult res, Coding code, String codeKey) { if (res.getErrorClass() == TerminologyServiceErrorClass.CODESYSTEM_UNSUPPORTED && !code.hasVersion()) { unsupportedCodeSystems.add(codeKey); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/IWorkerContext.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/IWorkerContext.java index 0a75abb28..1bec15484 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/IWorkerContext.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/IWorkerContext.java @@ -321,12 +321,23 @@ public interface IWorkerContext { private Coding coding; private ValidationResult result; private CacheToken cacheToken; + private String vs; public CodingValidationRequest(Coding coding) { super(); this.coding = coding; } + public CodingValidationRequest(Coding coding, String vs) { + super(); + this.coding = coding; + this.vs = vs; + } + + public String getVs() { + return vs; + } + public ValidationResult getResult() { return result; } @@ -826,6 +837,7 @@ public interface IWorkerContext { * @param vs */ public void validateCodeBatch(ValidationOptions options, List codes, ValueSet vs); + public void validateCodeBatchByRef(ValidationOptions options, List codes, String vsUrl); // todo: figure these out diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/TerminologyCache.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/TerminologyCache.java index f9338044e..7254b80f6 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/TerminologyCache.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/TerminologyCache.java @@ -278,6 +278,28 @@ public class TerminologyCache { } } + public CacheToken generateValidationToken(ValidationOptions options, Coding code, String vsUrl, Parameters expParameters) { + try { + CacheToken ct = new CacheToken(); + if (code.hasSystem()) { + ct.setName(code.getSystem()); + ct.hasVersion = code.hasVersion(); + } + else + ct.name = NAME_FOR_NO_SYSTEM; + ct.setName(vsUrl); + JsonParser json = new JsonParser(); + json.setOutputStyle(OutputStyle.PRETTY); + String expJS = json.composeString(expParameters); + + ct.request = "{\"code\" : "+json.composeString(code, "code")+", \"valueSet\" :"+(vsUrl == null ? "null" : vsUrl)+(options == null ? "" : ", "+options.toJson())+", \"profile\": "+expJS+"}"; + ct.key = String.valueOf(hashJson(ct.request)); + return ct; + } catch (IOException e) { + throw new Error(e); + } + } + public String extracted(JsonParser json, ValueSet vsc) throws IOException { String s = null; if (vsc.getExpansion().getContains().size() > 1000 || vsc.getCompose().getIncludeFirstRep().getConcept().size() > 1000) { 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 3e3097977..a0e331ad9 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 @@ -225,6 +225,7 @@ public class I18nConstants { public static final String MEASURE_MR_GRP_POP_NO_COUNT = "MEASURE_MR_GRP_POP_NO_COUNT"; public static final String MEASURE_MR_GRP_POP_NO_SUBJECTS = "MEASURE_MR_GRP_POP_NO_SUBJECTS"; public static final String MEASURE_MR_GRP_POP_UNK_CODE = "MEASURE_MR_GRP_POP_UNK_CODE"; + public static final String MEASURE_MR_GRPST_POP_UNK_CODE = "MEASURE_MR_GRPST_POP_UNK_CODE"; public static final String MEASURE_MR_GRP_UNK_CODE = "MEASURE_MR_GRP_UNK_CODE"; public static final String MEASURE_MR_M_NONE = "Measure_MR_M_None"; public static final String MEASURE_MR_M_NOTFOUND = "Measure_MR_M_NotFound"; @@ -924,6 +925,7 @@ public class I18nConstants { public static final String SD_ED_TYPE_PROFILE_WRONG_TYPE = "SD_ED_TYPE_PROFILE_WRONG_TYPE"; public static final String VALUESET_CONCEPT_DISPLAY_PRESENCE_MIXED = "VALUESET_CONCEPT_DISPLAY_PRESENCE_MIXED"; public static final String VALUESET_CONCEPT_DISPLAY_SCT_TAG_MIXED = "VALUESET_CONCEPT_DISPLAY_SCT_TAG_MIXED"; + public static final String CS_SCT_IPS_NOT_IPS = "CS_SCT_IPS_NOT_IPS"; } diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 3ed046162..58d7768db 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -501,6 +501,7 @@ DUPLICATE_ID = Duplicate id value ''{0}'' TERMINOLOGY_TX_SYSTEM_NO_CODE = A code with no system has no defined meaning. A system should be provided MEASURE_MR_GRP_POP_NO_CODE = Group should have a code that matches the group population definition in the measure MEASURE_MR_GRP_POP_UNK_CODE = The code for this group population has no match in the measure definition +MEASURE_MR_GRPST_POP_UNK_CODE = The code for this group stratifier has no match in the measure definition MEASURE_MR_GRP_POP_DUPL_CODE = The code for this group population is duplicated with another group MEASURE_MR_GRP_POP_MISSING_BY_CODE = The MeasureReport does not include a population group for the population group {0} MEASURE_MR_GRP_POP_COUNT_MISMATCH = Mismatch between count {0} and number of subjects {1} @@ -979,3 +980,4 @@ SD_ED_TYPE_WRONG_TYPE_one = The element has a type {0} which is different to the SD_ED_TYPE_WRONG_TYPE_other = The element has a type {0} which is not in the types {1} on the base profile {2} VALUESET_CONCEPT_DISPLAY_PRESENCE_MIXED = This include has some concepts with displays and some without - check that this is what is intended VALUESET_CONCEPT_DISPLAY_SCT_TAG_MIXED = This SNOMED-CT based include has some concepts with semantic tags (FSN terms) and some without (preferred terms) - check that this is what is intended +CS_SCT_IPS_NOT_IPS = The Snomed CT code {0} ({1}) is not a member of the IPS free set 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 81e1b68c5..26bb827f9 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 @@ -212,6 +212,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IValidationP @Getter @Setter private boolean doImplicitFHIRPathStringConversion; @Getter @Setter private HtmlInMarkdownCheck htmlInMarkdownCheck; @Getter @Setter private boolean allowDoubleQuotesInFHIRPath; + @Getter @Setter private boolean checkIPSCodes; @Getter @Setter private Locale locale; @Getter @Setter private List igs = new ArrayList<>(); @Getter @Setter private List extensionDomains = new ArrayList<>(); @@ -264,6 +265,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IValidationP doImplicitFHIRPathStringConversion = other.doImplicitFHIRPathStringConversion; htmlInMarkdownCheck = other.htmlInMarkdownCheck; allowDoubleQuotesInFHIRPath = other.allowDoubleQuotesInFHIRPath; + checkIPSCodes = other.checkIPSCodes; locale = other.locale; igs.addAll(other.igs); extensionDomains.addAll(other.extensionDomains); @@ -853,6 +855,7 @@ public class ValidationEngine implements IValidatorResourceFetcher, IValidationP validator.setAllowDoubleQuotesInFHIRPath(allowDoubleQuotesInFHIRPath); validator.setNoUnicodeBiDiControlChars(noUnicodeBiDiControlChars); validator.setDoImplicitFHIRPathStringConversion(doImplicitFHIRPathStringConversion); + validator.setCheckIPSCodes(checkIPSCodes); 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/ValidatorCli.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java index dcda1d3db..ee5eb9221 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 @@ -141,7 +141,6 @@ public class ValidatorCli { TimeTracker tt = new TimeTracker(); TimeTracker.Session tts = tt.start("Loading"); - args = addAdditionalParamsForIpsParam(args); setJavaSystemProxyParamsFromParams(args); Display.displayVersion(System.out); @@ -200,6 +199,8 @@ public class ValidatorCli { public static void main(String[] args) throws Exception { final ValidatorCli validatorCli = new ValidatorCli(validationService); + + args = addAdditionalParamsForIpsParam(args); final CliContext cliContext = Params.loadCliContext(args); validatorCli.readParamsAndExecuteTask(cliContext, args); } @@ -259,6 +260,7 @@ public class ValidatorCli { if (a.equals("-ips")) { res.add("-version"); res.add("4.0"); + res.add("-check-ips-codes"); res.add("-ig"); res.add("hl7.fhir.uv.ips#1.1.0"); res.add("-profile"); @@ -266,6 +268,7 @@ public class ValidatorCli { } else if (a.equals("-ips#")) { res.add("-version"); res.add("4.0"); + res.add("-check-ips-codes"); res.add("-ig"); res.add("hl7.fhir.uv.ips#"+a.substring(5)); res.add("-profile"); @@ -273,6 +276,7 @@ public class ValidatorCli { } else if (a.startsWith("-ips$")) { res.add("-version"); res.add("4.0"); + res.add("-check-ips-codes"); res.add("-ig"); res.add("hl7.fhir.uv.ips#current$"+a.substring(5)); res.add("-profile"); 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 60955bf29..d7e028cdc 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 @@ -56,6 +56,8 @@ public class CliContext { private HtmlInMarkdownCheck htmlInMarkdownCheck = HtmlInMarkdownCheck.WARNING; @JsonProperty("allowDoubleQuotesInFHIRPath") private boolean allowDoubleQuotesInFHIRPath = false; + @JsonProperty("checkIPSCodes") + private boolean checkIPSCodes; @JsonProperty("langTransform") private String langTransform = null; @JsonProperty("map") @@ -315,6 +317,14 @@ public class CliContext { this.allowDoubleQuotesInFHIRPath = allowDoubleQuotesInFHIRPath; } + public boolean isCheckIPSCodes() { + return checkIPSCodes; + } + + public void setCheckIPSCodes(boolean checkIPSCodes) { + this.checkIPSCodes = checkIPSCodes; + } + @JsonProperty("locale") public String getLanguageCode() { return locale; @@ -727,6 +737,7 @@ public class CliContext { displayWarnings == that.displayWarnings && wantInvariantsInMessages == that.wantInvariantsInMessages && allowDoubleQuotesInFHIRPath == that.allowDoubleQuotesInFHIRPath && + checkIPSCodes == that.checkIPSCodes && Objects.equals(extensions, that.extensions) && Objects.equals(map, that.map) && Objects.equals(htmlInMarkdownCheck, that.htmlInMarkdownCheck) && @@ -768,7 +779,7 @@ public class CliContext { return Objects.hash(doNative, extensions, hintAboutNonMustSupport, recursive, doDebug, assumeValidRestReferences, canDoNative, noInternalCaching, noExtensibleBindingMessages, noInvariants, displayWarnings, wantInvariantsInMessages, map, output, outputSuffix, htmlOutput, txServer, sv, txLog, txCache, mapLog, lang, srcLang, tgtLang, fhirpath, snomedCT, targetVer, igs, questionnaireMode, level, profiles, sources, inputs, mode, locale, locations, crumbTrails, forPublication, showTimes, allowExampleUrls, outputStyle, jurisdiction, noUnicodeBiDiControlChars, watchMode, watchScanDelay, watchSettleTime, - htmlInMarkdownCheck, allowDoubleQuotesInFHIRPath); + htmlInMarkdownCheck, allowDoubleQuotesInFHIRPath, checkIPSCodes); } @Override @@ -821,6 +832,7 @@ public class CliContext { ", bundleValidationRules=" + bundleValidationRules + ", htmlInMarkdownCheck=" + htmlInMarkdownCheck + ", allowDoubleQuotesInFHIRPath=" + allowDoubleQuotesInFHIRPath + + ", checkIPSCodes=" + checkIPSCodes + ", watchMode=" + watchMode + ", watchSettleTime=" + watchSettleTime + ", watchScanDelay=" + watchScanDelay + 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 06513a0de..dde168775 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 @@ -476,6 +476,7 @@ public class ValidationService { validationEngine.setNoUnicodeBiDiControlChars(cliContext.isNoUnicodeBiDiControlChars()); validationEngine.setNoInvariantChecks(cliContext.isNoInvariants()); validationEngine.setDisplayWarnings(cliContext.isDisplayWarnings()); + validationEngine.setCheckIPSCodes(cliContext.isCheckIPSCodes()); validationEngine.setWantInvariantInMessage(cliContext.isWantInvariantsInMessages()); validationEngine.setSecurityChecks(cliContext.isSecurityChecks()); validationEngine.setCrumbTrails(cliContext.isCrumbTrails()); 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 013c528e0..0f73603c2 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 @@ -86,6 +86,7 @@ public class Params { public static final String SRC_LANG = "-src-lang"; public static final String TGT_LANG = "-tgt-lang"; public static final String ALLOW_DOUBLE_QUOTES = "-allow-double-quotes-in-fhirpath"; + public static final String CHECK_IPS_CODES = "-check-ips-codes"; public static final String RUN_TESTS = "-run-tests"; @@ -247,7 +248,9 @@ public class Params { } else if (args[i].equals(NO_EXTENSIBLE_BINDING_WARNINGS)) { cliContext.setNoExtensibleBindingMessages(true); } else if (args[i].equals(ALLOW_DOUBLE_QUOTES)) { - cliContext.setAllowDoubleQuotesInFHIRPath(true); + cliContext.setAllowDoubleQuotesInFHIRPath(true); + } else if (args[i].equals(CHECK_IPS_CODES)) { + cliContext.setCheckIPSCodes(true); } else if (args[i].equals(NO_UNICODE_BIDI_CONTROL_CHARS)) { cliContext.setNoUnicodeBiDiControlChars(true); } else if (args[i].equals(NO_INVARIANTS)) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/CodeSystemChecker.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/CodeSystemChecker.java new file mode 100644 index 000000000..9417547bb --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/CodeSystemChecker.java @@ -0,0 +1,38 @@ +package org.hl7.fhir.validation.codesystem; + +import java.util.List; + +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.utils.XVerExtensionManager; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.i18n.I18nConstants; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.validation.BaseValidator; +import org.hl7.fhir.validation.instance.utils.NodeStack; + +public class CodeSystemChecker extends BaseValidator { + + private boolean noDisplay = false; + private boolean hasDisplay = false; + protected List errors; + + protected CodeSystemChecker(IWorkerContext context, XVerExtensionManager xverManager, boolean debug, List errors) { + super(context, xverManager, debug); + this.errors = errors; + } + + public void checkConcept(String code, String display) { + if (Utilities.noString(display)) { + noDisplay = true; + } else { + hasDisplay = true; + } + } + + public void finish(Element inc, NodeStack stack) { + hint(errors, "2023-07-21", IssueType.BUSINESSRULE, inc.line(), inc.col(), stack.getLiteralPath(), !(noDisplay && hasDisplay), I18nConstants.VALUESET_CONCEPT_DISPLAY_PRESENCE_MIXED); + } + +} \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/CodeSystemValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/CodeSystemValidator.java deleted file mode 100644 index dc703e7e2..000000000 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/CodeSystemValidator.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.hl7.fhir.validation.codesystem; - -/* - Copyright (c) 2011+, HL7, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of HL7 nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. - - */ - - - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.hl7.fhir.r5.context.IWorkerContext; -import org.hl7.fhir.r5.model.CodeSystem; -import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; -import org.hl7.fhir.r5.utils.XVerExtensionManager; -import org.hl7.fhir.utilities.validation.ValidationMessage; -import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; -import org.hl7.fhir.validation.BaseValidator; - -public class CodeSystemValidator extends BaseValidator { - - public CodeSystemValidator(IWorkerContext context, XVerExtensionManager xverManager) { - super(context, xverManager, false); - } - - public List validate(CodeSystem cs, boolean forBuild) { - List errors = new ArrayList(); - - // this is an invariant on CodeSystem, but the invariant is wrong in R3, and doesn't work - checkCodesUnique(cs, errors); - return errors; - } - - private void checkCodesUnique(CodeSystem cs, List errors) { - Set codes = new HashSet(); - checkCodes(codes, cs.getConcept(), "CodeSystem.where(id = '"+cs.getId()+"')", errors); - } - - private void checkCodes(Set codes, List list, String path, List errors) { - for (ConceptDefinitionComponent cc : list) { - String npath = path+".concept.where(code = '"+cc.getCode()+"')"; - if (codes.contains(cc.getCode())) { - rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, npath, false, "Duplicate Code "+cc.getCode()); - } - codes.add(cc.getCode()); - checkCodes(codes, cc.getConcept(), npath, errors); - } - } -} \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/CodingsObserver.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/CodingsObserver.java new file mode 100644 index 000000000..f7533b1d5 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/CodingsObserver.java @@ -0,0 +1,109 @@ +package org.hl7.fhir.validation.codesystem; + +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; + +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.context.IWorkerContext.CodingValidationRequest; +import org.hl7.fhir.r5.model.CodeableConcept; +import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.utils.XVerExtensionManager; +import org.hl7.fhir.utilities.i18n.I18nConstants; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.validation.BaseValidator; +import org.hl7.fhir.validation.instance.utils.NodeStack; + +public class CodingsObserver extends BaseValidator { + + private class CodingUsage { + + private NodeStack stack; + private Coding c; + + public CodingUsage(NodeStack stack, Coding c) { + this.stack = stack; + this.c = c; + } + } + + public CodingsObserver(IWorkerContext context, XVerExtensionManager xverManager, boolean debug) { + super(context, xverManager, debug); + this.context = context; + } + + private IWorkerContext context; + private List list = new ArrayList<>(); + private boolean checkIPSCodes; + + public void seeCode(NodeStack stack, String system, String version, String code, String display) { + seeCode(stack, new Coding().setSystem(system).setCode(code).setVersion(version).setDisplay(display)); + } + + public boolean isCheckIPSCodes() { + return checkIPSCodes; + } + + public void setCheckIPSCodes(boolean checkIPSCodes) { + this.checkIPSCodes = checkIPSCodes; + } + + + public void seeCode(NodeStack stack, CodeableConcept cc) { + for (Coding c : cc.getCoding()) { + seeCode(stack, c); + } + } + + public void seeCode(NodeStack stack, Coding c) { + list.add(new CodingUsage(stack, c)); + + } + + public void finish(List errors, NodeStack rootStack) { + if (checkIPSCodes) { + System.out.println(""); + System.out.println("Checking SCT codes for IPS"); + + Set snomedCTCodes = new HashSet<>(); + for (CodingUsage c : list) { + if ("http://snomed.info/sct".equals(c.c.getSystem()) && c.c.getCode() != null) { + snomedCTCodes.add(c.c.getCode()); + } + } + if (!snomedCTCodes.isEmpty()) { + Map nonIPSCodes = checkSCTCodes(snomedCTCodes); + if (!nonIPSCodes.isEmpty()) { + for (String s : nonIPSCodes.keySet()) { + hint(errors, "2023-07-25", IssueType.BUSINESSRULE, rootStack, false, I18nConstants.CS_SCT_IPS_NOT_IPS, s, nonIPSCodes.get(s)); + } + } + } + System.out.println("Done Checking SCT codes for IPS"); + } + + } + + private Map checkSCTCodes(Set codes) { + List serverList = new ArrayList<>(); + for (String s : codes) { + serverList.add(new CodingValidationRequest(new Coding("http://snomed.info/sct", s, null))); + } + + context.validateCodeBatchByRef(null, serverList, "http://terminology.hl7.org/ValueSet/snomed-intl-ips"); + + Map results = new HashMap<>(); + + for (CodingValidationRequest vr : serverList) { + if (!vr.getResult().isOk()) { + results.put(vr.getCoding().getCode(), vr.getResult().getDisplay() != null ? vr.getResult().getDisplay() : vr.getCoding().getDisplay()); + } + } + return results; + } + +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/GeneralCodeSystemChecker.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/GeneralCodeSystemChecker.java new file mode 100644 index 000000000..d8ea86f15 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/GeneralCodeSystemChecker.java @@ -0,0 +1,17 @@ +package org.hl7.fhir.validation.codesystem; + +import java.util.List; + +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.utils.XVerExtensionManager; +import org.hl7.fhir.utilities.validation.ValidationMessage; + +public class GeneralCodeSystemChecker extends CodeSystemChecker { + + public GeneralCodeSystemChecker(IWorkerContext context, XVerExtensionManager xverManager, boolean debug, + List errors) { + super(context, xverManager, debug, errors); + // TODO Auto-generated constructor stub + } + +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/SnomedCTChecker.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/SnomedCTChecker.java new file mode 100644 index 000000000..046304535 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/codesystem/SnomedCTChecker.java @@ -0,0 +1,37 @@ +package org.hl7.fhir.validation.codesystem; + +import java.util.List; + +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.utils.XVerExtensionManager; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.i18n.I18nConstants; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.validation.instance.utils.NodeStack; + +public class SnomedCTChecker extends CodeSystemChecker { + private boolean noTag = false; + private boolean hasTag = false; + + public SnomedCTChecker(IWorkerContext context, XVerExtensionManager xverManager, boolean debug, List errors) { + super(context, xverManager, debug, errors); + } + + public void checkConcept(String code, String display) { + super.checkConcept(code, display); + if (!Utilities.noString(display)) { + boolean tagged = display.endsWith(")") && display.indexOf("(") > display.length() - 20; + if (tagged) { + hasTag = true; + } else { + noTag = true; + } + } + } + public void finish(Element inc, NodeStack stack) { + super.finish(inc, stack); + hint(errors, "2023-07-21", IssueType.BUSINESSRULE, inc.line(), inc.col(), stack.getLiteralPath(), !(noTag && hasTag), I18nConstants.VALUESET_CONCEPT_DISPLAY_SCT_TAG_MIXED); + } +} 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 9323a4efc..64d507b44 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 @@ -185,6 +185,7 @@ import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.hl7.fhir.validation.BaseValidator; import org.hl7.fhir.validation.cli.model.HtmlInMarkdownCheck; import org.hl7.fhir.validation.cli.utils.QuestionnaireMode; +import org.hl7.fhir.validation.codesystem.CodingsObserver; import org.hl7.fhir.validation.instance.type.BundleValidator; import org.hl7.fhir.validation.instance.type.CodeSystemValidator; import org.hl7.fhir.validation.instance.type.ConceptMapValidator; @@ -504,6 +505,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private ValidationOptions baseOptions = new ValidationOptions(); private Map crLookups = new HashMap<>(); private boolean logProgress; + private CodingsObserver codingObserver; public InstanceValidator(IWorkerContext theContext, IEvaluationContext hostServices, XVerExtensionManager xverManager) { super(theContext, xverManager, false); @@ -518,6 +520,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat source = Source.InstanceValidator; fpe.setDoNotEnforceAsSingletonRule(!VersionUtilities.isR5VerOrLater(theContext.getVersion())); fpe.setAllowDoubleQuotes(allowDoubleQuotesInFHIRPath); + codingObserver = new CodingsObserver(theContext, xverManager, debug); } @Override @@ -911,6 +914,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (hintAboutNonMustSupport) { checkElementUsage(errors, element, stack); } + codingObserver.finish(errors, stack); errors.removeAll(messagesToRemove); timeTracker.overall(t); if (DEBUG_ELEMENT) { @@ -6509,10 +6513,12 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (lang == null) { lang = "en"; // ubiquitious default languauge } + codingObserver.seeCode(stack, system, version, code, display); return context.validateCode(baseOptions.withLanguage(lang), system, version, code, checkDisplay ? display : null); } public ValidationResult checkCodeOnServer(NodeStack stack, ValueSet valueset, Coding c, boolean checkMembership) { + codingObserver.seeCode(stack, c); if (checkMembership) { return context.validateCode(baseOptions.withLanguage(stack.getWorkingLang()).withCheckValueSetOnly(), c, valueset); } else { @@ -6521,6 +6527,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } public ValidationResult checkCodeOnServer(NodeStack stack, ValueSet valueset, CodeableConcept cc, boolean vsOnly) { + codingObserver.seeCode(stack, cc); if (vsOnly) { return context.validateCode(baseOptions.withLanguage(stack.getWorkingLang()).withCheckValueSetOnly(), cc, valueset); } else { @@ -6657,6 +6664,14 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat baseOptions.setDisplayWarningMode(displayWarnings); } + public boolean isCheckIPSCodes() { + return codingObserver.isCheckIPSCodes(); + } + + public void setCheckIPSCodes(boolean checkIPSCodes) { + codingObserver.setCheckIPSCodes(checkIPSCodes); + } + public InstanceValidator setForPublication(boolean forPublication) { this.forPublication = forPublication; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureValidator.java index 9bdf04d29..e35cec9e0 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/MeasureValidator.java @@ -531,7 +531,7 @@ public class MeasureValidator extends BaseValidator { CodeableConcept cc = ObjectConverter.readAsCodeableConcept(mrgs.getNamedChild("code")); if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrgs.line(), mrgs.col(), ns.getLiteralPath(), cc != null, I18nConstants.MEASURE_MR_GRP_POP_NO_CODE)) { MeasureGroupStratifierComponent mgs = getGroupStratifierForCode(cc, mg); - if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), mgs != null, I18nConstants.MEASURE_MR_GRP_POP_UNK_CODE)) { + if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), mgs != null, I18nConstants.MEASURE_MR_GRPST_POP_UNK_CODE)) { if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, mrg.line(), mrg.col(), ns.getLiteralPath(), !strats.contains(mgs), I18nConstants.MEASURE_MR_GRP_POP_DUPL_CODE)) { strats.add(mgs); ok = validateMeasureReportGroupStratifier(hostContext, m, mgs, errors, mrgs, ns, inProgress) && ok; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/ValueSetValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/ValueSetValidator.java index 82bee6298..3afc8c4a5 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/ValueSetValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/ValueSetValidator.java @@ -22,76 +22,20 @@ import org.hl7.fhir.utilities.validation.ValidationOptions; import org.hl7.fhir.validation.BaseValidator; import org.hl7.fhir.validation.TimeTracker; import org.hl7.fhir.validation.instance.InstanceValidator; -import org.hl7.fhir.validation.instance.type.ValueSetValidator.SystemLevelValidator; import org.hl7.fhir.validation.instance.utils.NodeStack; +import org.hl7.fhir.validation.codesystem.CodeSystemChecker; +import org.hl7.fhir.validation.codesystem.GeneralCodeSystemChecker; +import org.hl7.fhir.validation.codesystem.SnomedCTChecker; public class ValueSetValidator extends BaseValidator { - public class SystemLevelValidator { - protected List errors; - protected Element inc; - protected NodeStack stack; - - private boolean noDisplay = false; - private boolean hasDisplay = false; - protected SystemLevelValidator(List errors, Element inc, NodeStack stack) { - super(); - this.errors = errors; - this.inc = inc; - this.stack = stack; - } - - public void checkConcept(String code, String display) { - if (Utilities.noString(display)) { - noDisplay = true; - } else { - hasDisplay = true; - } - } - - public void finish() { - hint(errors, "2023-07-21", IssueType.BUSINESSRULE, inc.line(), inc.col(), stack.getLiteralPath(), !(noDisplay && hasDisplay), I18nConstants.VALUESET_CONCEPT_DISPLAY_PRESENCE_MIXED); - } - } - - public class SnomedCTValidator extends SystemLevelValidator { - private boolean noTag = false; - private boolean hasTag = false; - - protected SnomedCTValidator(List errors, Element inc, NodeStack stack) { - super(errors, inc, stack); - } - public void checkConcept(String code, String display) { - super.checkConcept(code, display); - if (!Utilities.noString(display)) { - boolean tagged = display.endsWith(")") && display.indexOf("(") > display.length() - 20; - if (tagged) { - hasTag = true; - } else { - noTag = true; - } - } - } - public void finish() { - hint(errors, "2023-07-21", IssueType.BUSINESSRULE, inc.line(), inc.col(), stack.getLiteralPath(), !(noTag && hasTag), I18nConstants.VALUESET_CONCEPT_DISPLAY_SCT_TAG_MIXED); - } - } - - public class GeneralValidator extends SystemLevelValidator { - - protected GeneralValidator(List errors, Element inc, NodeStack stack) { - super(errors, inc, stack); - } - - } - - private SystemLevelValidator getSystemValidator(String system, List errors, Element inc, NodeStack stack) { + private CodeSystemChecker getSystemValidator(String system, List errors) { if (system == null) { - return new GeneralValidator(errors, inc, stack); + return new GeneralCodeSystemChecker(context, xverManager, debug, errors); } switch (system) { - case "http://snomed.info/sct" :return new SnomedCTValidator(errors, inc, stack); - default: return new GeneralValidator(errors, inc, stack); + case "http://snomed.info/sct" :return new SnomedCTChecker(context, xverManager, debug, errors); + default: return new GeneralCodeSystemChecker(context, xverManager, debug, errors); } } @@ -209,7 +153,7 @@ public class ValueSetValidator extends BaseValidator { List concepts = include.getChildrenByName("concept"); List filters = include.getChildrenByName("filter"); - SystemLevelValidator slv = getSystemValidator(system, errors, include, stack); + CodeSystemChecker slv = getSystemValidator(system, errors); if (!Utilities.noString(system)) { boolean systemOk = true; int cc = 0; @@ -256,7 +200,7 @@ public class ValueSetValidator extends BaseValidator { } cf++; } - slv.finish(); + slv.finish(include, stack); } else { warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), filters.size() == 0 && concepts.size() == 0, I18nConstants.VALUESET_NO_SYSTEM_WARNING); } @@ -264,7 +208,7 @@ public class ValueSetValidator extends BaseValidator { } - private boolean validateValueSetIncludeConcept(List errors, Element concept, NodeStack stackInc, NodeStack stack, String system, String version, SystemLevelValidator slv) { + private boolean validateValueSetIncludeConcept(List errors, Element concept, NodeStack stackInc, NodeStack stack, String system, String version, CodeSystemChecker slv) { String code = concept.getChildValue("code"); String display = concept.getChildValue("display"); slv.checkConcept(code, display); @@ -305,7 +249,7 @@ public class ValueSetValidator extends BaseValidator { return true; } - private VSCodingValidationRequest prepareValidateValueSetIncludeConcept(List errors, Element concept, NodeStack stack, String system, String version, SystemLevelValidator slv) { + private VSCodingValidationRequest prepareValidateValueSetIncludeConcept(List errors, Element concept, NodeStack stack, String system, String version, CodeSystemChecker slv) { String code = concept.getChildValue("code"); String display = concept.getChildValue("display"); slv.checkConcept(code, display); @@ -317,7 +261,7 @@ public class ValueSetValidator extends BaseValidator { return new VSCodingValidationRequest(stack, c); } - private boolean validateValueSetIncludeFilter(List errors, Element filter, NodeStack push, String system, String version, SystemLevelValidator slv) { + private boolean validateValueSetIncludeFilter(List errors, Element filter, NodeStack push, String system, String version, CodeSystemChecker slv) { // // String display = concept.getChildValue("display"); // slv.checkConcept(code, display);