
001package org.hl7.fhir.r5.terminologies.utilities; 002 003import java.util.ArrayList; 004import java.util.Collection; 005import java.util.List; 006 007import org.hl7.fhir.exceptions.FHIRException; 008import org.hl7.fhir.exceptions.TerminologyServiceException; 009import org.hl7.fhir.r5.context.BaseWorkerContext; 010import org.hl7.fhir.r5.context.ContextUtilities; 011import org.hl7.fhir.r5.context.IWorkerContext; 012import org.hl7.fhir.r5.extensions.ExtensionDefinitions; 013import org.hl7.fhir.r5.extensions.ExtensionUtilities; 014import org.hl7.fhir.r5.model.BooleanType; 015import org.hl7.fhir.r5.model.CanonicalResource; 016import org.hl7.fhir.r5.model.CodeSystem; 017import org.hl7.fhir.r5.model.DataType; 018import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; 019import org.hl7.fhir.r5.model.OperationOutcome.IssueType; 020import org.hl7.fhir.r5.model.OperationOutcome.OperationOutcomeIssueComponent; 021import org.hl7.fhir.r5.model.Extension; 022import org.hl7.fhir.r5.model.Parameters; 023import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent; 024import org.hl7.fhir.r5.model.StringType; 025import org.hl7.fhir.r5.model.UriType; 026import org.hl7.fhir.r5.model.UrlType; 027import org.hl7.fhir.r5.model.ValueSet; 028import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionComponent; 029 030import org.hl7.fhir.r5.terminologies.expansion.OperationIsTooCostly; 031import org.hl7.fhir.r5.terminologies.validation.VSCheckerException; 032import org.hl7.fhir.r5.utils.UserDataNames; 033import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage; 034import org.hl7.fhir.utilities.StandardsStatus; 035import org.hl7.fhir.utilities.Utilities; 036import org.hl7.fhir.utilities.VersionUtilities; 037import org.hl7.fhir.utilities.i18n.I18nConstants; 038import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 039 040import javax.annotation.Nonnull; 041 042@MarkedToMoveToAdjunctPackage 043public class ValueSetProcessBase { 044 045 public class UnknownValueSetException extends FHIRException { 046 047 protected UnknownValueSetException() { 048 super(); 049 } 050 051 protected UnknownValueSetException(String message, Throwable cause) { 052 super(message, cause); 053 } 054 055 protected UnknownValueSetException(String message) { 056 super(message); 057 } 058 059 protected UnknownValueSetException(Throwable cause) { 060 super(cause); 061 } 062 } 063 064 public static class TerminologyOperationDetails { 065 066 private List<String> supplements; 067 068 public TerminologyOperationDetails(List<String> supplements) { 069 super(); 070 this.supplements = supplements; 071 } 072 073 public void seeSupplement(CodeSystem supp) { 074 supplements.remove(supp.getUrl()); 075 supplements.remove(supp.getVersionedUrl()); 076 } 077 } 078 079 public enum OpIssueCode { 080 NotInVS, ThisNotInVS, InvalidCode, Display, DisplayComment, NotFound, CodeRule, VSProcessing, InferFailed, StatusCheck, InvalidData, CodeComment, VersionError; 081 082 public String toCode() { 083 switch (this) { 084 case CodeRule: return "code-rule"; 085 case Display: return "invalid-display"; 086 case DisplayComment: return "display-comment"; 087 case InferFailed: return "cannot-infer"; 088 case InvalidCode: return "invalid-code"; 089 case NotFound: return "not-found"; 090 case NotInVS: return "not-in-vs"; 091 case InvalidData: return "invalid-data"; 092 case StatusCheck: return "status-check"; 093 case ThisNotInVS: return "this-code-not-in-vs"; 094 case VSProcessing: return "vs-invalid"; 095 case CodeComment: return "code-comment"; 096 case VersionError: return "version-error"; 097 default: 098 return "??"; 099 } 100 } 101 } 102 protected BaseWorkerContext context; 103 private ContextUtilities cu; 104 protected TerminologyOperationContext opContext; 105 protected List<String> requiredSupplements = new ArrayList<>(); 106 protected List<String> allErrors = new ArrayList<>(); 107 108 protected ValueSetProcessBase(BaseWorkerContext context, TerminologyOperationContext opContext) { 109 super(); 110 this.context = context; 111 this.opContext = opContext; 112 } 113 public static class AlternateCodesProcessingRules { 114 private boolean all; 115 private List<String> uses = new ArrayList<>(); 116 117 public AlternateCodesProcessingRules(boolean b) { 118 all = b; 119 } 120 121 private void seeParameter(DataType value) { 122 if (value != null) { 123 if (value instanceof BooleanType) { 124 all = ((BooleanType) value).booleanValue(); 125 uses.clear(); 126 } else if (value.isPrimitive()) { 127 String s = value.primitiveValue(); 128 if (!Utilities.noString(s)) { 129 uses.add(s); 130 } 131 } 132 } 133 } 134 135 public void seeParameters(Parameters pp) { 136 for (ParametersParameterComponent p : pp.getParameter()) { 137 String name = p.getName(); 138 if ("includeAlternateCodes".equals(name)) { 139 DataType value = p.getValue(); 140 seeParameter(value); 141 } 142 } 143 } 144 145 public void seeValueSet(ValueSet vs) { 146 if (vs != null) { 147 for (Extension ext : vs.getCompose().getExtension()) { 148 if (Utilities.existsInList(ext.getUrl(), ExtensionDefinitions.EXT_VS_EXP_PARAM_NEW, ExtensionDefinitions.EXT_VS_EXP_PARAM_OLD)) { 149 String name = ext.getExtensionString("name"); 150 Extension value = ext.getExtensionByUrl("value"); 151 if ("includeAlternateCodes".equals(name) && value != null && value.hasValue()) { 152 seeParameter(value.getValue()); 153 } 154 } 155 } 156 } 157 } 158 159 public boolean passes(List<Extension> extensions) { 160 if (all) { 161 return true; 162 } 163 164 for (Extension ext : extensions) { 165 if (ExtensionDefinitions.EXT_CS_ALTERNATE_USE.equals(ext.getUrl())) { 166 if (ext.hasValueCoding() && Utilities.existsInList(ext.getValueCoding().getCode(), uses)) { 167 return true; 168 } 169 } 170 } 171 return false; 172 } 173 } 174 175 protected List<OperationOutcomeIssueComponent> makeIssue(IssueSeverity level, IssueType type, String location, String message, OpIssueCode code, String server, String msgId) { 176 OperationOutcomeIssueComponent result = new OperationOutcomeIssueComponent(); 177 switch (level) { 178 case ERROR: 179 result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.ERROR); 180 break; 181 case FATAL: 182 result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.FATAL); 183 break; 184 case INFORMATION: 185 result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.INFORMATION); 186 break; 187 case WARNING: 188 result.setSeverity(org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity.WARNING); 189 break; 190 } 191 result.setCode(type); 192 if (location != null) { 193 result.addLocation(location); 194 result.addExpression(location); 195 } 196 result.getDetails().setText(message); 197 if (code != null) { 198 result.getDetails().addCoding("http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", code.toCode(), null); 199 } 200 if (server != null) { 201 result.addExtension(ExtensionDefinitions.EXT_ISSUE_SERVER, new UrlType(server)); 202 } 203 if (msgId != null) { 204 result.addExtension(ExtensionDefinitions.EXT_ISSUE_MSG_ID, new StringType(msgId)); 205 } 206 ArrayList<OperationOutcomeIssueComponent> list = new ArrayList<>(); 207 list.add(result); 208 return list; 209 } 210 211 public void checkCanonical(List<OperationOutcomeIssueComponent> issues, String path, CanonicalResource resource, CanonicalResource source) { 212 if (resource != null) { 213 StandardsStatus standardsStatus = ExtensionUtilities.getStandardsStatus(resource); 214 if (standardsStatus == StandardsStatus.DEPRECATED) { 215 addToIssues(issues, makeStatusIssue(path, "deprecated", I18nConstants.MSG_DEPRECATED, resource)); 216 } else if (standardsStatus == StandardsStatus.WITHDRAWN) { 217 addToIssues(issues, makeStatusIssue(path, "withdrawn", I18nConstants.MSG_WITHDRAWN, resource)); 218 } else if (resource.getStatus() == PublicationStatus.RETIRED) { 219 addToIssues(issues, makeStatusIssue(path, "retired", I18nConstants.MSG_RETIRED, resource)); 220 } else if (source != null) { 221 if (resource.getExperimental() && !source.getExperimental()) { 222 addToIssues(issues, makeStatusIssue(path, "experimental", I18nConstants.MSG_EXPERIMENTAL, resource)); 223 } else if ((resource.getStatus() == PublicationStatus.DRAFT || standardsStatus == StandardsStatus.DRAFT) 224 && !(source.getStatus() == PublicationStatus.DRAFT || ExtensionUtilities.getStandardsStatus(source) == StandardsStatus.DRAFT)) { 225 addToIssues(issues, makeStatusIssue(path, "draft", I18nConstants.MSG_DRAFT, resource)); 226 } 227 } else { 228 if (resource.getExperimental()) { 229 addToIssues(issues, makeStatusIssue(path, "experimental", I18nConstants.MSG_EXPERIMENTAL, resource)); 230 } else if ((resource.getStatus() == PublicationStatus.DRAFT || standardsStatus == StandardsStatus.DRAFT)) { 231 addToIssues(issues, makeStatusIssue(path, "draft", I18nConstants.MSG_DRAFT, resource)); 232 } 233 } 234 } 235 } 236 237 private List<OperationOutcomeIssueComponent> makeStatusIssue(String path, String id, String msg, CanonicalResource resource) { 238 List<OperationOutcomeIssueComponent> iss = makeIssue(IssueSeverity.INFORMATION, IssueType.BUSINESSRULE, null, 239 context.formatMessage(msg, resource.getVersionedUrl(), null, resource.fhirType()), OpIssueCode.StatusCheck, null, msg); 240 241 // this is a testing hack - see TerminologyServiceTests 242 iss.get(0).setUserData(UserDataNames.tx_status_msg_name, "warning-"+id); 243 iss.get(0).setUserData(UserDataNames.tx_status_msg_value, new UriType(resource.getVersionedUrl())); 244 ExtensionUtilities.setStringExtension(iss.get(0), ExtensionDefinitions.EXT_ISSUE_MSG_ID, msg); 245 246 return iss; 247 } 248 249 private void addToIssues(List<OperationOutcomeIssueComponent> issues, List<OperationOutcomeIssueComponent> toAdd) { 250 for (OperationOutcomeIssueComponent t : toAdd) { 251 boolean found = false; 252 for (OperationOutcomeIssueComponent i : issues) { 253 if (i.getSeverity() == t.getSeverity() && i.getCode() == t.getCode() && i.getDetails().getText().equals(t.getDetails().getText())) { // ignore location 254 found = true; 255 } 256 } 257 if (!found) { 258 issues.add(t); 259 } 260 } 261 } 262 263 public void checkCanonical(ValueSetExpansionComponent params, CanonicalResource resource, ValueSet source) { 264 if (resource != null) { 265 StandardsStatus standardsStatus = ExtensionUtilities.getStandardsStatus(resource); 266 if (standardsStatus == StandardsStatus.DEPRECATED) { 267 if (!params.hasParameterValue("warning-deprecated", resource.getVersionedUrl())) { 268 params.addParameter("warning-deprecated", new UriType(resource.getVersionedUrl())); 269 } 270 } else if (standardsStatus == StandardsStatus.WITHDRAWN) { 271 if (!params.hasParameterValue("warning-withdrawn", resource.getVersionedUrl())) { 272 params.addParameter("warning-withdrawn", new UriType(resource.getVersionedUrl())); 273 } 274 } else if (resource.getStatus() == PublicationStatus.RETIRED) { 275 if (!params.hasParameterValue("warning-retired", resource.getVersionedUrl())) { 276 params.addParameter("warning-retired", new UriType(resource.getVersionedUrl())); 277 } 278 } else if (resource.getExperimental() && !source.getExperimental()) { 279 if (!params.hasParameterValue("warning-experimental", resource.getVersionedUrl())) { 280 params.addParameter("warning-experimental", new UriType(resource.getVersionedUrl())); 281 } 282 } else if ((resource.getStatus() == PublicationStatus.DRAFT || standardsStatus == StandardsStatus.DRAFT) 283 && !(source.getStatus() == PublicationStatus.DRAFT || ExtensionUtilities.getStandardsStatus(source) == StandardsStatus.DRAFT)) { 284 if (!params.hasParameterValue("warning-draft", resource.getVersionedUrl())) { 285 params.addParameter("warning-draft", new UriType(resource.getVersionedUrl())); 286 } 287 } 288 } 289 } 290 291 public TerminologyOperationContext getOpContext() { 292 return opContext; 293 } 294 295 296 public ContextUtilities getCu() { 297 if (cu == null) { 298 cu = new ContextUtilities(context); 299 } 300 return cu; 301 } 302 303 304 public String removeSupplement(String s) { 305 requiredSupplements.remove(s); 306 if (s.contains("|")) { 307 s = s.substring(0, s.indexOf("|")); 308 requiredSupplements.remove(s); 309 } 310 return s; 311 } 312 313 protected boolean versionsMatch(@Nonnull String system, @Nonnull String candidate, @Nonnull String criteria) { 314 if (system == null || candidate == null || criteria == null) { 315 return false; 316 } 317 CodeSystem cs = context.fetchCodeSystem(system); 318 VersionAlgorithm va = cs == null ? VersionAlgorithm.Unknown : VersionAlgorithm.fromType(cs.getVersionAlgorithm()); 319 if (va == VersionAlgorithm.Unknown) { 320 va = VersionAlgorithm.guessFormat(candidate); 321 } 322 switch (va) { 323 case Unknown: return candidate.startsWith(criteria); 324 case SemVer: return VersionUtilities.isSemVer(candidate) ? VersionUtilities.versionMatches(criteria, candidate) : false; 325 case Integer: return candidate.equals(criteria); 326 case Alpha: return candidate.startsWith(criteria); 327 case Date:return candidate.startsWith(criteria); 328 case Natural: return candidate.startsWith(criteria); 329 default: return candidate.startsWith(criteria); 330 } 331 } 332 333 protected FHIRException failWithIssue(IssueType type, OpIssueCode code, String path, String msgId, Object... params) { 334 String msg = context.formatMessage(msgId, params); 335 List<OperationOutcomeIssueComponent> issues = new ArrayList<>(); 336 issues.addAll(makeIssue(IssueSeverity.ERROR, type, path, msg, code, null, msgId)); 337 throw new VSCheckerException(msg, issues, TerminologyServiceErrorClass.PROCESSING); 338 } 339 340 protected FHIRException fail(String msgId, Object... params) { 341 String msg = context.formatMessage(msgId, params); 342 allErrors.add(msg); 343 return new FHIRException(msg); 344 } 345 346 protected ValueSetProcessBase.UnknownValueSetException failWithUnknownVSException(String msgId, boolean check, Object... params) { 347 String msg = context.formatMessage(msgId, params); 348 allErrors.add(msg); 349 return new ValueSetProcessBase.UnknownValueSetException(msg); 350 } 351 352 protected OperationIsTooCostly failAsTooCostly(String msg) { 353 allErrors.add(msg); 354 return new OperationIsTooCostly(msg); 355 } 356 357 protected TerminologyServiceException failTSE(String msg) { 358 allErrors.add(msg); 359 return new TerminologyServiceException(msg); 360 } 361 362 public Collection<? extends String> getAllErrors() { 363 return allErrors; 364 } 365 366 protected AlternateCodesProcessingRules altCodeParams = new AlternateCodesProcessingRules(false); 367 protected AlternateCodesProcessingRules allAltCodes = new AlternateCodesProcessingRules(true); 368}