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 c16d9f6b4..5aee1c730 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 @@ -113,6 +113,8 @@ import org.hl7.fhir.r5.renderers.OperationOutcomeRenderer; import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpander; import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome; +import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext; +import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext.TerminologyServiceProtectionException; import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass; import org.hl7.fhir.r5.terminologies.validation.VSCheckerException; import org.hl7.fhir.r5.terminologies.validation.ValueSetValidator; @@ -1185,12 +1187,17 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte } catch (VSCheckerException e) { if (e.isWarning()) { localWarning = e.getMessage(); - } else { + } else { localError = e.getMessage(); } if (e.getIssues() != null) { issues.addAll(e.getIssues()); } + } catch (TerminologyServiceProtectionException e) { + OperationOutcomeIssueComponent iss = new OperationOutcomeIssueComponent(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.ERROR, e.getType()); + iss.getDetails().setText(e.getMessage()); + issues.add(iss); + return new ValidationResult(IssueSeverity.ERROR, e.getMessage(), e.getError(), issues); } catch (Exception e) { // e.printStackTrace(); localError = e.getMessage(); @@ -1246,15 +1253,15 @@ public abstract class BaseWorkerContext extends I18nBase implements IWorkerConte } protected ValueSetExpander constructValueSetExpanderSimple() { - return new ValueSetExpander(this); + return new ValueSetExpander(this, new TerminologyOperationContext(this)); } protected ValueSetValidator constructValueSetCheckerSimple( ValidationOptions options, ValueSet vs, ValidationContextCarrier ctxt) { - return new ValueSetValidator(options, vs, this, ctxt, expParameters, tcc.getTxcaps()); + return new ValueSetValidator(this, new TerminologyOperationContext(this), options, vs, ctxt, expParameters, tcc.getTxcaps()); } protected ValueSetValidator constructValueSetCheckerSimple( ValidationOptions options, ValueSet vs) { - return new ValueSetValidator(options, vs, this, expParameters, tcc.getTxcaps()); + return new ValueSetValidator(this, new TerminologyOperationContext(this), options, vs, expParameters, tcc.getTxcaps()); } protected Parameters constructParameters(ValueSet vs, boolean hierarchical) { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/CanonicalResourceManager.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/CanonicalResourceManager.java index e5b84f0a9..469859583 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/CanonicalResourceManager.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/CanonicalResourceManager.java @@ -594,7 +594,7 @@ public class CanonicalResourceManager { if (list != null) { for (CanonicalResourceManager.CachedCanonicalResource t : list) { possibleMatches = true; - if (pvlist == null || pvlist.contains(t.getPackageInfo().getVID())) { + if (pvlist == null || t.getPackageInfo() == null || pvlist.contains(t.getPackageInfo().getVID())) { res.add(t.getResource()); } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/expansion/ValueSetExpander.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/expansion/ValueSetExpander.java index e17618eaa..763fa7c3b 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/expansion/ValueSetExpander.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/expansion/ValueSetExpander.java @@ -114,9 +114,12 @@ import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; import org.hl7.fhir.r5.terminologies.ValueSetUtilities; import org.hl7.fhir.r5.terminologies.providers.CodeSystemProvider; import org.hl7.fhir.r5.terminologies.providers.CodeSystemProviderExtension; +import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext; +import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext.TerminologyServiceProtectionException; import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass; import org.hl7.fhir.r5.terminologies.utilities.ValueSetProcessBase; import org.hl7.fhir.r5.utils.ToolingExtensions; +import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.i18n.I18nConstants; @@ -127,20 +130,18 @@ public class ValueSetExpander extends ValueSetProcessBase { private ValueSet focus; private List allErrors = new ArrayList<>(); - private List requiredSupplements = new ArrayList<>(); private int maxExpansionSize = 1000; private WorkingContext dwc = new WorkingContext(); private boolean checkCodesWhenExpanding; private boolean includeAbstract = true; - public ValueSetExpander(IWorkerContext context) { - this.context = context; + public ValueSetExpander(IWorkerContext context, TerminologyOperationContext opContext) { + super(context, opContext); } - public ValueSetExpander(IWorkerContext context, List allErrors) { - super(); - this.context = context; + public ValueSetExpander(IWorkerContext context, TerminologyOperationContext opContext, List allErrors) { + super(context, opContext); this.allErrors = allErrors; } @@ -151,7 +152,8 @@ public class ValueSetExpander extends ValueSetProcessBase { private ValueSetExpansionContainsComponent addCode(WorkingContext wc, String system, String code, String display, String dispLang, ValueSetExpansionContainsComponent parent, List designations, Parameters expParams, boolean isAbstract, boolean inactive, List filters, boolean noInactive, boolean deprecated, List vsProp, List csProps, List expProps, List csExtList, List vsExtList, ValueSetExpansionComponent exp) throws ETooCostly { - + opContext.deadCheck(); + if (filters != null && !filters.isEmpty() && !filterContainsCode(filters, system, code, exp)) return null; if (noInactive && inactive) { @@ -311,6 +313,7 @@ public class ValueSetExpander extends ValueSetProcessBase { } private void addCodeAndDescendents(WorkingContext wc, ValueSetExpansionContainsComponent focus, ValueSetExpansionContainsComponent parent, Parameters expParams, List filters, boolean noInactive, List vsProps, ValueSet vsSrc, ValueSetExpansionComponent exp) throws FHIRException, ETooCostly { + opContext.deadCheck(); focus.checkNoModifiers("Expansion.contains", "expanding"); ValueSetExpansionContainsComponent np = null; for (String code : getCodesForConcept(focus, expParams)) { @@ -360,6 +363,7 @@ public class ValueSetExpander extends ValueSetProcessBase { private void addCodeAndDescendents(WorkingContext wc, CodeSystem cs, String system, ConceptDefinitionComponent def, ValueSetExpansionContainsComponent parent, Parameters expParams, List filters, ConceptDefinitionComponent exclusion, ConceptFilter filterFunc, boolean noInactive, List vsProps, List otherFilters, ValueSetExpansionComponent exp) throws FHIRException, ETooCostly { + opContext.deadCheck(); def.checkNoModifiers("Code in Code System", "expanding"); if (exclusion != null) { if (exclusion.getCode().equals(def.getCode())) @@ -432,6 +436,7 @@ public class ValueSetExpander extends ValueSetProcessBase { } private void excludeCodes(WorkingContext wc, ConceptSetComponent exc, List params, String ctxt) throws FHIRException { + opContext.deadCheck(); exc.checkNoModifiers("Compose.exclude", "expanding"); if (exc.hasSystem() && exc.getConcept().size() == 0 && exc.getFilter().size() == 0) { wc.getExcludeSystems().add(exc.getSystem()); @@ -460,6 +465,7 @@ public class ValueSetExpander extends ValueSetProcessBase { } private void excludeCodes(WorkingContext wc, ValueSetExpansionComponent expand, List params) { + opContext.deadCheck(); for (ValueSetExpansionContainsComponent c : expand.getContains()) { excludeCode(wc, c.getSystem(), c.getCode()); } @@ -475,8 +481,11 @@ public class ValueSetExpander extends ValueSetProcessBase { } public ValueSetExpansionOutcome expand(ValueSet source, Parameters expParams) { + allErrors.clear(); try { + opContext.seeContext(source.getVersionedUrl()); + return expandInternal(source, expParams); } catch (NoTerminologyServiceException e) { // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set @@ -486,6 +495,12 @@ public class ValueSetExpander extends ValueSetProcessBase { // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set // that might fail too, but it might not, later. return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.INTERNAL_ERROR, allErrors); + } catch (TerminologyServiceProtectionException e) { + if (opContext.isOriginal()) { + return new ValueSetExpansionOutcome(e.getMessage(), e.getError(), allErrors); + } else { + throw e; + } } catch (ETooCostly e) { return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.TOO_COSTLY, allErrors); } catch (Exception e) { @@ -574,8 +589,8 @@ public class ValueSetExpander extends ValueSetProcessBase { if (dwc.getTotal() >= 0) { focus.getExpansion().setTotal(dwc.getTotal()); } - if (!requiredSupplements.isEmpty()) { - return new ValueSetExpansionOutcome("Required supplements not found: "+requiredSupplements.toString(), TerminologyServiceErrorClass.BUSINESS_RULE, allErrors); + if (!requiredSupplements.isEmpty()) { + return new ValueSetExpansionOutcome(context.formatMessagePlural(requiredSupplements.size(), I18nConstants.VALUESET_SUPPLEMENT_MISSING, CommaSeparatedStringBuilder.build(requiredSupplements)), TerminologyServiceErrorClass.BUSINESS_RULE, allErrors); } if (!expParams.hasParameter("includeDefinition") || !expParams.getParameterBool("includeDefinition")) { focus.setCompose(null); @@ -656,7 +671,7 @@ public class ValueSetExpander extends ValueSetProcessBase { expParams = expParams.copy(); expParams.addParameter("activeOnly", true); } - ValueSetExpansionOutcome vso = new ValueSetExpander(context, allErrors).expand(vs, expParams); + ValueSetExpansionOutcome vso = new ValueSetExpander(context, opContext.copy(), allErrors).expand(vs, expParams); if (vso.getError() != null) { addErrors(vso.getAllErrors()); throw fail("Unable to expand imported value set "+vs.getUrl()+": " + vso.getError()); @@ -697,6 +712,7 @@ public class ValueSetExpander extends ValueSetProcessBase { } public void copyExpansion(WorkingContext wc,List list) { + opContext.deadCheck(); for (ValueSetExpansionContainsComponent cc : list) { ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent(); n.setSystem(cc.getSystem()); @@ -725,6 +741,7 @@ public class ValueSetExpander extends ValueSetProcessBase { } private void copyImportContains(List list, ValueSetExpansionContainsComponent parent, Parameters expParams, List filter, boolean noInactive, List vsProps, ValueSet vsSrc, ValueSetExpansionComponent exp) throws FHIRException, ETooCostly { + opContext.deadCheck(); for (ValueSetExpansionContainsComponent c : list) { c.checkNoModifiers("Imported Expansion in Code System", "expanding"); ValueSetExpansionContainsComponent np = addCode(dwc, c.getSystem(), c.getCode(), c.getDisplay(), vsSrc.getLanguage(), parent, null, expParams, c.getAbstract(), c.getInactive(), @@ -734,6 +751,7 @@ public class ValueSetExpander extends ValueSetProcessBase { } private void includeCodes(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, boolean heirarchical, boolean noInactive, List extensions, ValueSet valueSet) throws ETooCostly, FileNotFoundException, IOException, FHIRException, CodeSystemProviderExtension { + opContext.deadCheck(); inc.checkNoModifiers("Compose.include", "expanding"); List imports = new ArrayList(); for (CanonicalType imp : inc.getValueSet()) { @@ -764,6 +782,7 @@ public class ValueSetExpander extends ValueSetProcessBase { } private void doServerIncludeCodes(ConceptSetComponent inc, boolean heirarchical, ValueSetExpansionComponent exp, List imports, Parameters expParams, List extensions, boolean noInactive, List vsProps) throws FHIRException, CodeSystemProviderExtension, ETooCostly { + opContext.deadCheck(); CodeSystemProvider csp = CodeSystemProvider.factory(inc.getSystem()); if (csp != null) { csp.includeCodes(inc, heirarchical, exp, imports, expParams, extensions, noInactive, vsProps); @@ -800,6 +819,7 @@ public class ValueSetExpander extends ValueSetProcessBase { public void doInternalIncludeCodes(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, List imports, CodeSystem cs, boolean noInactive, Resource vsSrc) throws NoTerminologyServiceException, TerminologyServiceException, FHIRException, ETooCostly { + opContext.deadCheck(); if (cs == null) { if (context.isNoTerminologyServer()) throw failTSE("Unable to find code system " + inc.getSystem().toString()); @@ -884,6 +904,7 @@ public class ValueSetExpander extends ValueSetProcessBase { private void processFilter(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, List imports, CodeSystem cs, boolean noInactive, ConceptSetFilterComponent fc, WorkingContext wc, List filters) throws ETooCostly { + opContext.deadCheck(); if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISA) { // special: all codes in the target code system under the value ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue()); @@ -919,6 +940,7 @@ public class ValueSetExpander extends ValueSetProcessBase { if (isNotBlank(def.getDisplay()) && isNotBlank(fc.getValue())) { if (def.getDisplay().contains(fc.getValue()) && passesOtherFilters(filters, cs, def.getCode())) { for (String code : getCodesForConcept(def, expParams)) { + opContext.deadCheck(); ValueSetExpansionContainsComponent t = addCode(wc, inc.getSystem(), code, def.getDisplay(), cs.getLanguage(), null, def.getDesignation(), expParams, CodeSystemUtilities.isNotSelectable(cs, def), CodeSystemUtilities.isInactive(cs, def), imports, noInactive, false, exp.getProperty(), makeCSProps(def.getDefinition(), def.getProperty()), null, def.getExtension(), null, exp); } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyOperationContext.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyOperationContext.java new file mode 100644 index 000000000..de31e7465 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyOperationContext.java @@ -0,0 +1,86 @@ +package org.hl7.fhir.r5.terminologies.utilities; + +import java.util.List; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.TerminologyServiceException; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.model.OperationOutcome.IssueType; +import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext.TerminologyServiceProtectionException; +import org.hl7.fhir.utilities.i18n.I18nConstants; + +import java.util.ArrayList; + +public class TerminologyOperationContext { + + public class TerminologyServiceProtectionException extends FHIRException { + + private TerminologyServiceErrorClass error; + private IssueType type; + + protected TerminologyServiceProtectionException(String message, TerminologyServiceErrorClass error, IssueType type) { + super(message); + this.error = error; + this.type = type; + } + + public TerminologyServiceErrorClass getError() { + return error; + } + + public IssueType getType() { + return type; + } + + } + + public static boolean debugging = java.lang.management.ManagementFactory.getRuntimeMXBean().getInputArguments().toString().indexOf("-agentlib:jdwp") > 0; + private static final int EXPANSION_DEAD_TIME_SECS = 60; + private long deadTime; + private List contexts = new ArrayList<>(); + private IWorkerContext worker; + private boolean original; + + public TerminologyOperationContext(IWorkerContext worker) { + super(); + this.worker = worker; + this.original = true; + + if (EXPANSION_DEAD_TIME_SECS == 0 || debugging) { + deadTime = 0; + } else { + deadTime = System.currentTimeMillis() + (EXPANSION_DEAD_TIME_SECS * 1000); + } + } + + private TerminologyOperationContext() { + super(); + } + + public TerminologyOperationContext copy() { + TerminologyOperationContext ret = new TerminologyOperationContext(); + ret.worker = worker; + ret.contexts.addAll(contexts); + ret.deadTime = deadTime; + return ret; + } + + public void deadCheck() { + if (deadTime != 0 && System.currentTimeMillis() > deadTime) { + throw new TerminologyServiceProtectionException(worker.formatMessage(I18nConstants.VALUESET_TOO_COSTLY_TIME, contexts.get(0), EXPANSION_DEAD_TIME_SECS), TerminologyServiceErrorClass.TOO_COSTLY, IssueType.TOOCOSTLY); + } + } + + public void seeContext(String context) { + if (contexts.contains(context)) { + throw new TerminologyServiceProtectionException(worker.formatMessage(I18nConstants.VALUESET_CIRCULAR_REFERENCE, context, contexts.toString()), TerminologyServiceErrorClass.BUSINESS_RULE, IssueType.BUSINESSRULE); + } + contexts.add(context); + } + + public boolean isOriginal() { + return original; + } + + +} 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 551711e65..e1e8fbd0c 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 @@ -25,7 +25,14 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; public class ValueSetProcessBase { protected IWorkerContext context; - + protected TerminologyOperationContext opContext; + protected List requiredSupplements = new ArrayList<>(); + + protected ValueSetProcessBase(IWorkerContext context, TerminologyOperationContext opContext) { + super(); + this.context = context; + this.opContext = opContext; + } public static class AlternateCodesProcessingRules { private boolean all; private List uses = new ArrayList<>(); @@ -106,7 +113,9 @@ public class ValueSetProcessBase { break; } result.setCode(type); - result.addLocation(location); + if (location != null) { + result.addLocation(location); + } result.getDetails().setText(message); ArrayList list = new ArrayList<>(); list.add(result); 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 7d2779098..55bf7eebb 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 @@ -47,6 +47,7 @@ import org.hl7.fhir.exceptions.NoTerminologyServiceException; import org.hl7.fhir.r5.context.ContextUtilities; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult; +import org.hl7.fhir.r5.extensions.ExtensionConstants; import org.hl7.fhir.r5.model.CanonicalType; import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.Enumerations.CodeSystemContentMode; @@ -79,6 +80,7 @@ import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome; import org.hl7.fhir.r5.terminologies.providers.CodeSystemProvider; import org.hl7.fhir.r5.terminologies.providers.SpecialCodeSystem; import org.hl7.fhir.r5.terminologies.providers.URICodeSystem; +import org.hl7.fhir.r5.terminologies.utilities.TerminologyOperationContext; import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass; import org.hl7.fhir.r5.terminologies.utilities.ValueSetProcessBase; import org.hl7.fhir.r5.utils.ToolingExtensions; @@ -106,18 +108,18 @@ public class ValueSetValidator extends ValueSetProcessBase { private Set unknownSystems; private boolean throwToServer; - public ValueSetValidator(ValidationOptions options, ValueSet source, IWorkerContext context, Parameters expansionProfile, TerminologyCapabilities txCaps) { + public ValueSetValidator(IWorkerContext context, TerminologyOperationContext opContext, ValidationOptions options, ValueSet source, Parameters expansionProfile, TerminologyCapabilities txCaps) { + super(context, opContext); this.valueset = source; - this.context = context; this.options = options; this.expansionProfile = expansionProfile; this.txCaps = txCaps; analyseValueSet(); } - public ValueSetValidator(ValidationOptions options, ValueSet source, IWorkerContext context, ValidationContextCarrier ctxt, Parameters expansionProfile, TerminologyCapabilities txCaps) { + public ValueSetValidator(IWorkerContext context, TerminologyOperationContext opContext, ValidationOptions options, ValueSet source, ValidationContextCarrier ctxt, Parameters expansionProfile, TerminologyCapabilities txCaps) { + super(context, opContext); this.valueset = source; - this.context = context; this.options = options.copy(); this.options.setEnglishOk(true); this.localContext = ctxt; @@ -143,6 +145,13 @@ public class ValueSetValidator extends ValueSetProcessBase { } private void analyseValueSet() { + if (valueset != null) { + opContext.seeContext(valueset.getVersionedUrl()); + for (Extension s : valueset.getExtensionsByUrl(ExtensionConstants.EXT_VSSUPPLEMENT)) { + requiredSupplements.add(s.getValue().primitiveValue()); + } + } + altCodeParams.seeParameters(expansionProfile); altCodeParams.seeValueSet(valueset); if (localContext != null) { @@ -158,6 +167,7 @@ public class ValueSetValidator extends ValueSetProcessBase { } private void analyseComponent(ConceptSetComponent i) { + opContext.deadCheck(); if (i.getSystemElement().hasExtension(ToolingExtensions.EXT_VALUESET_SYSTEM)) { String ref = i.getSystemElement().getExtensionString(ToolingExtensions.EXT_VALUESET_SYSTEM); if (ref.startsWith("#")) { @@ -179,6 +189,8 @@ public class ValueSetValidator extends ValueSetProcessBase { } public ValidationResult validateCode(String path, CodeableConcept code) throws FHIRException { + opContext.deadCheck(); + // first, we validate the codings themselves ValidationProcessInfo info = new ValidationProcessInfo(); @@ -250,7 +262,9 @@ public class ValueSetValidator extends ValueSetProcessBase { info.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.CODEINVALID, path, msg)); } } - if (info.hasErrors()) { + if (!checkRequiredSupplements(info)) { + return new ValidationResult(IssueSeverity.ERROR, info.getIssues().get(info.getIssues().size()-1).getDetails().getText(), info.getIssues()); + } else if (info.hasErrors()) { ValidationResult res = new ValidationResult(IssueSeverity.ERROR, info.summary(), info.getIssues()); if (foundCoding != null) { ConceptDefinitionComponent cd = new ConceptDefinitionComponent(foundCoding.getCode()); @@ -279,6 +293,13 @@ public class ValueSetValidator extends ValueSetProcessBase { } } + private boolean checkRequiredSupplements(ValidationProcessInfo info) { + if (!requiredSupplements.isEmpty()) { + info.getIssues().addAll(makeIssue(IssueSeverity.ERROR, IssueType.NOTFOUND, null, context.formatMessagePlural(requiredSupplements.size(), I18nConstants.VALUESET_SUPPLEMENT_MISSING, CommaSeparatedStringBuilder.build(requiredSupplements)))); + } + return requiredSupplements.isEmpty(); + } + private boolean valueSetDependsOn(String system, String version) { for (ConceptSetComponent inc : valueset.getCompose().getInclude()) { if (system.equals(inc.getSystem()) && (version == null || inc.getVersion() == null || version.equals(inc.getVersion()))) { @@ -315,7 +336,7 @@ public class ValueSetValidator extends ValueSetProcessBase { return t; } } - CodeSystem cs = context.fetchCodeSystem(system, version); + CodeSystem cs = context.fetchSupplementedCodeSystem(system, version); if (cs == null) { cs = findSpecialCodeSystem(system, version); } @@ -342,6 +363,7 @@ public class ValueSetValidator extends ValueSetProcessBase { } public ValidationResult validateCode(String path, Coding code) throws FHIRException { + opContext.deadCheck(); String warningMessage = null; // first, we validate the concept itself @@ -460,6 +482,9 @@ public class ValueSetValidator extends ValueSetProcessBase { inInclude = checkInclude(code, vi); } String wv = vi.getVersion(system, code.getVersion()); + if (!checkRequiredSupplements(info)) { + return new ValidationResult(IssueSeverity.ERROR, issues.get(issues.size()-1).getDetails().getText(), issues); + } // then, if we have a value set, we check it's in the value set @@ -597,6 +622,7 @@ public class ValueSetValidator extends ValueSetProcessBase { private ValidationResult findCodeInExpansion(Coding code, List contains) { for (ValueSetExpansionContainsComponent containsComponent: contains) { + opContext.deadCheck(); if (containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { ConceptDefinitionComponent ccd = new ConceptDefinitionComponent(); ccd.setCode(containsComponent.getCode()); @@ -623,6 +649,7 @@ public class ValueSetValidator extends ValueSetProcessBase { private boolean checkExpansion(Coding code, List contains, VersionInfo vi) { for (ValueSetExpansionContainsComponent containsComponent: contains) { + opContext.deadCheck(); if (containsComponent.hasSystem() && containsComponent.hasCode() && containsComponent.getSystem().equals(code.getSystem()) && containsComponent.getCode().equals(code.getCode())) { vi.setExpansionVersion(containsComponent.getVersion()); return true; @@ -667,6 +694,7 @@ public class ValueSetValidator extends ValueSetProcessBase { } for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) { + opContext.deadCheck(); if (isOkLanguage(ds.getLanguage())) { b.append("'"+ds.getValue()+"'"); if (code.getDisplay().equalsIgnoreCase(ds.getValue())) { @@ -688,6 +716,7 @@ public class ValueSetValidator extends ValueSetProcessBase { } } for (ConceptReferenceDesignationComponent ds : vs.getCc().getDesignation()) { + opContext.deadCheck(); if (isOkLanguage(ds.getLanguage())) { b.append("'"+ds.getValue()+"'"); if (code.getDisplay().equalsIgnoreCase(ds.getValue())) { @@ -732,6 +761,7 @@ public class ValueSetValidator extends ValueSetProcessBase { return null; // if it has an expansion for (ValueSetExpansionContainsComponent exp : valueset.getExpansion().getContains()) { + opContext.deadCheck(); if (system.equals(exp.getSystem()) && code.equals(exp.getCode())) { ConceptReferenceComponent cc = new ConceptReferenceComponent(); cc.setDisplay(exp.getDisplay()); @@ -810,6 +840,7 @@ public class ValueSetValidator extends ValueSetProcessBase { } private ConceptDefinitionComponent findCodeInConcept(ConceptDefinitionComponent concept, String code, AlternateCodesProcessingRules altCodeRules) { + opContext.deadCheck(); if (code.equals(concept.getCode())) { return concept; } @@ -879,6 +910,7 @@ public class ValueSetValidator extends ValueSetProcessBase { int i = 0; for (ConceptSetComponent vsi : valueset.getCompose().getInclude()) { + opContext.deadCheck(); if (vsi.hasValueSet()) { for (CanonicalType u : vsi.getValueSet()) { if (!checkForCodeInValueSet(code, u.getValue(), sys, problems)) { @@ -963,6 +995,7 @@ public class ValueSetValidator extends ValueSetProcessBase { */ private boolean checkSystems(List contains, String code, Set systems, List problems) { for (ValueSetExpansionContainsComponent c: contains) { + opContext.deadCheck(); if (c.getCode().equals(code)) { systems.add(c.getSystem()); } @@ -976,6 +1009,7 @@ public class ValueSetValidator extends ValueSetProcessBase { if (valueset == null) { return false; } + opContext.deadCheck(); checkCanonical(info.getIssues(), path, valueset, valueset); Boolean result = false; VersionInfo vi = new VersionInfo(this); @@ -1010,6 +1044,7 @@ public class ValueSetValidator extends ValueSetProcessBase { } private Boolean inComponent(String path, ConceptSetComponent vsi, int vsiIndex, String system, String version, String code, boolean only, ValidationProcessInfo info) throws FHIRException { + opContext.deadCheck(); boolean ok = true; if (vsi.hasValueSet()) { @@ -1189,6 +1224,7 @@ public class ValueSetValidator extends ValueSetProcessBase { } public boolean validateCodeInConceptList(String code, CodeSystem def, List list, AlternateCodesProcessingRules altCodeRules) { + opContext.deadCheck(); if (def.getCaseSensitive()) { for (ConceptDefinitionComponent cc : list) { if (cc.getCode().equals(code)) { @@ -1219,7 +1255,7 @@ public class ValueSetValidator extends ValueSetProcessBase { return inner.get(url); } ValueSet vs = context.fetchResource(ValueSet.class, url, valueset); - ValueSetValidator vsc = new ValueSetValidator(options, vs, context, localContext, expansionProfile, txCaps); + ValueSetValidator vsc = new ValueSetValidator(context, opContext.copy(), options, vs, localContext, expansionProfile, txCaps); vsc.setThrowToServer(throwToServer); inner.put(url, vsc); return vsc; diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/CommaSeparatedStringBuilder.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/CommaSeparatedStringBuilder.java index ed1d3031d..57ac3959e 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/CommaSeparatedStringBuilder.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/CommaSeparatedStringBuilder.java @@ -117,4 +117,12 @@ public class CommaSeparatedStringBuilder { } return b.toString(); } + + public static String build(List list) { + CommaSeparatedStringBuilder self = new CommaSeparatedStringBuilder(); + for (String s : list) { + self.append(s); + } + return self.toString(); + } } \ No newline at end of file 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 a14be8625..5168dd1d1 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 @@ -866,6 +866,7 @@ public class I18nConstants { public static final String UNKNOWN_CODESYSTEM = "UNKNOWN_CODESYSTEM"; public static final String UNKNOWN_CODESYSTEM_VERSION = "UNKNOWN_CODESYSTEM_VERSION"; public static final String VALUESET_TOO_COSTLY = "VALUESET_TOO_COSTLY"; + public static final String VALUESET_TOO_COSTLY_TIME = "VALUESET_TOO_COSTLY_TIME"; public static final String NO_VALID_DISPLAY_FOUND = "NO_VALID_DISPLAY_FOUND"; public static final String SD_NO_CONTEXT_WHEN_NOT_EXTENSION = "SD_NO_CONTEXT_WHEN_NOT_EXTENSION"; public static final String SD_CONTEXT_SHOULD_NOT_BE_ELEMENT = "SD_CONTEXT_SHOULD_NOT_BE_ELEMENT"; @@ -977,6 +978,8 @@ public class I18nConstants { public static final String CODESYSTEM_CS_COUNT_NOTPRESENT_ZERO = "CODESYSTEM_CS_COUNT_NOTPRESENT_ZERO"; public static final String CODESYSTEM_CS_COUNT_SUPPLEMENT_WRONG = "CODESYSTEM_CS_COUNT_SUPPLEMENT_WRONG"; public static final String CODESYSTEM_CS_COUNT_NO_CONTENT_ALLOWED = "CODESYSTEM_CS_COUNT_NO_CONTENT_ALLOWED"; + public static final String VALUESET_CIRCULAR_REFERENCE = "VALUESET_CIRCULAR_REFERENCE"; + public static final String VALUESET_SUPPLEMENT_MISSING = "VALUESET_SUPPLEMENT_MISSING"; } diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index cbd93e2fc..7f1205a11 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -925,8 +925,9 @@ UNKNOWN_CODESYSTEM = The CodeSystem {0} is unknown UNKNOWN_CODESYSTEM_VERSION = The CodeSystem {0} version {1} is unknown. Valid versions: {2} UNABLE_TO_INFER_CODESYSTEM = The System URI could not be determined for the code {0} in the ValueSet {1} VALUESET_TOO_COSTLY = The value set {0} has too many codes to display ({1}) -NO_VALID_DISPLAY_FOUND_one = No valid Display Names found for {0}#{1} in the language {3} -NO_VALID_DISPLAY_FOUND_other = No valid Display Names found for {0}#{1} in the languages {3} +VALUESET_TOO_COSTLY_TIME = The value set {0} took too long to process (>{1}sec) +NO_VALID_DISPLAY_FOUND_one = No valid Display Names found for {1}#{2} in the language {4} +NO_VALID_DISPLAY_FOUND_other = No valid Display Names found for {1}#{2} in the languages {4} SD_NO_CONTEXT_WHEN_NOT_EXTENSION = The type is {0} so an extension context should not be specified SD_NO_CONTEXT_INV_WHEN_NOT_EXTENSION = The type is {0} so an extension context invariants should not be specified SD_CONTEXT_SHOULD_NOT_BE_ELEMENT = Review the extension type: extensions should not have a context of {0} unless it''s really intended that they can be used anywhere @@ -1036,5 +1037,7 @@ CODESYSTEM_CS_COUNT_FRAGMENT_WRONG = The code system is a fragment/example, but CODESYSTEM_CS_COUNT_NOTPRESENT_ZERO = The code system has no content, but the exceeds the stated total number is 0 concepts - check that this isn't a complete code system that has no concepts, or update/remove the stated count CODESYSTEM_CS_COUNT_SUPPLEMENT_WRONG = The code system supplement states the total number of concepts as {1}, but this is different to the underlying code system that states a value of {0} CODESYSTEM_CS_COUNT_NO_CONTENT_ALLOWED = The code system says it has no content present, but concepts are found - +VALUESET_CIRCULAR_REFERENCE = Found a circularity pointing to {0} processing ValueSet with pathway {1} +VALUESET_SUPPLEMENT_MISSING_one = Required supplement not found: {1} +VALUESET_SUPPLEMENT_MISSING_other = Required supplements not found: {1} \ No newline at end of file diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages_es.properties b/org.hl7.fhir.utilities/src/main/resources/Messages_es.properties index b30bbce6f..3485b2106 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages_es.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages_es.properties @@ -838,3 +838,6 @@ Display_Name_WS_for__should_be_one_of__instead_of_other = SD_ED_TYPE_PROFILE_WRONG_TYPE_one = SD_ED_TYPE_PROFILE_WRONG_TYPE_many = SD_ED_TYPE_PROFILE_WRONG_TYPE_other = +VALUESET_SUPPLEMENT_MISSING_one = +VALUESET_SUPPLEMENT_MISSING_many = +VALUESET_SUPPLEMENT_MISSING_other = diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/terminology/tests/ExternalTerminologyServiceTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/terminology/tests/ExternalTerminologyServiceTests.java index 00c90d9fd..f7af6afda 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/terminology/tests/ExternalTerminologyServiceTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/terminology/tests/ExternalTerminologyServiceTests.java @@ -46,8 +46,8 @@ public class ExternalTerminologyServiceTests implements ITxTesterLoader { private JsonObject test; } -// private static final String SERVER = FhirSettings.getTxFhirDevelopment(); - private static final String SERVER = FhirSettings.getTxFhirLocal(); + private static final String SERVER = FhirSettings.getTxFhirDevelopment(); +// private static final String SERVER = FhirSettings.getTxFhirLocal(); // private static final String SERVER = "https://r4.ontoserver.csiro.au/fhir";