
001package org.hl7.fhir.r4.context; 002 003import java.io.FileNotFoundException; 004import java.io.IOException; 005import java.util.ArrayList; 006import java.util.Date; 007import java.util.HashMap; 008import java.util.HashSet; 009import java.util.List; 010import java.util.Locale; 011import java.util.Map; 012import java.util.ResourceBundle; 013import java.util.Set; 014 015import org.apache.commons.lang3.StringUtils; 016import org.fhir.ucum.UcumService; 017import org.hl7.fhir.exceptions.DefinitionException; 018import org.hl7.fhir.exceptions.FHIRException; 019import org.hl7.fhir.exceptions.TerminologyServiceException; 020import org.hl7.fhir.r4.conformance.ProfileUtilities; 021import org.hl7.fhir.r4.context.TerminologyCache.CacheToken; 022import org.hl7.fhir.r4.model.BooleanType; 023import org.hl7.fhir.r4.model.CapabilityStatement; 024import org.hl7.fhir.r4.model.CodeSystem; 025import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode; 026import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent; 027import org.hl7.fhir.r4.model.CodeableConcept; 028import org.hl7.fhir.r4.model.Coding; 029import org.hl7.fhir.r4.model.ConceptMap; 030import org.hl7.fhir.r4.model.Constants; 031import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionBindingComponent; 032import org.hl7.fhir.r4.model.ImplementationGuide; 033import org.hl7.fhir.r4.model.MetadataResource; 034import org.hl7.fhir.r4.model.NamingSystem; 035import org.hl7.fhir.r4.model.NamingSystem.NamingSystemIdentifierType; 036import org.hl7.fhir.r4.model.NamingSystem.NamingSystemUniqueIdComponent; 037import org.hl7.fhir.r4.model.OperationDefinition; 038import org.hl7.fhir.r4.model.Parameters; 039import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; 040import org.hl7.fhir.r4.model.PlanDefinition; 041import org.hl7.fhir.r4.model.Questionnaire; 042import org.hl7.fhir.r4.model.Reference; 043import org.hl7.fhir.r4.model.Resource; 044import org.hl7.fhir.r4.model.SearchParameter; 045import org.hl7.fhir.r4.model.StringType; 046import org.hl7.fhir.r4.model.StructureDefinition; 047import org.hl7.fhir.r4.model.StructureMap; 048import org.hl7.fhir.r4.model.TerminologyCapabilities; 049import org.hl7.fhir.r4.model.TerminologyCapabilities.TerminologyCapabilitiesCodeSystemComponent; 050import org.hl7.fhir.r4.model.ValueSet; 051import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; 052import org.hl7.fhir.r4.model.ValueSet.ValueSetComposeComponent; 053import org.hl7.fhir.r4.terminologies.TerminologyClient; 054import org.hl7.fhir.r4.terminologies.ValueSetCheckerSimple; 055import org.hl7.fhir.r4.terminologies.ValueSetExpander.TerminologyServiceErrorClass; 056import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 057import org.hl7.fhir.r4.terminologies.ValueSetExpanderSimple; 058import org.hl7.fhir.r4.utils.ToolingExtensions; 059import org.hl7.fhir.utilities.OIDUtils; 060import org.hl7.fhir.utilities.TranslationServices; 061import org.hl7.fhir.utilities.Utilities; 062import org.hl7.fhir.utilities.i18n.I18nBase; 063import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 064import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 065import org.hl7.fhir.utilities.validation.ValidationOptions; 066 067/* 068 Copyright (c) 2011+, HL7, Inc. 069 All rights reserved. 070 071 Redistribution and use in source and binary forms, with or without modification, 072 are permitted provided that the following conditions are met: 073 074 * Redistributions of source code must retain the above copyright notice, this 075 list of conditions and the following disclaimer. 076 * Redistributions in binary form must reproduce the above copyright notice, 077 this list of conditions and the following disclaimer in the documentation 078 and/or other materials provided with the distribution. 079 * Neither the name of HL7 nor the names of its contributors may be used to 080 endorse or promote products derived from this software without specific 081 prior written permission. 082 083 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 084 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 085 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 086 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 087 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 088 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 089 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 090 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 091 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 092 POSSIBILITY OF SUCH DAMAGE. 093 094 */ 095 096import com.google.gson.JsonObject; 097 098public abstract class BaseWorkerContext extends I18nBase implements IWorkerContext { 099 100 private Object lock = new Object(); // used as a lock for the data that follows 101 102 private Map<String, Map<String, Resource>> allResourcesById = new HashMap<String, Map<String, Resource>>(); 103 // all maps are to the full URI 104 private Map<String, CodeSystem> codeSystems = new HashMap<String, CodeSystem>(); 105 private Set<String> supportedCodeSystems = new HashSet<String>(); 106 private Map<String, ValueSet> valueSets = new HashMap<String, ValueSet>(); 107 private Map<String, ConceptMap> maps = new HashMap<String, ConceptMap>(); 108 protected Map<String, StructureMap> transforms = new HashMap<String, StructureMap>(); 109 private Map<String, StructureDefinition> structures = new HashMap<String, StructureDefinition>(); 110 private Map<String, ImplementationGuide> guides = new HashMap<String, ImplementationGuide>(); 111 private Map<String, CapabilityStatement> capstmts = new HashMap<String, CapabilityStatement>(); 112 private Map<String, SearchParameter> searchParameters = new HashMap<String, SearchParameter>(); 113 private Map<String, Questionnaire> questionnaires = new HashMap<String, Questionnaire>(); 114 private Map<String, OperationDefinition> operations = new HashMap<String, OperationDefinition>(); 115 private Map<String, PlanDefinition> plans = new HashMap<String, PlanDefinition>(); 116 private List<NamingSystem> systems = new ArrayList<NamingSystem>(); 117 private UcumService ucumService; 118 119 protected Map<String, Map<String, ValidationResult>> validationCache = new HashMap<String, Map<String, ValidationResult>>(); 120 protected String tsServer; 121 protected String name; 122 private boolean allowLoadingDuplicates; 123 124 protected TerminologyClient txClient; 125 protected HTMLClientLogger txLog; 126 private TerminologyCapabilities txcaps; 127 private boolean canRunWithoutTerminology; 128 protected boolean noTerminologyServer; 129 private int expandCodesLimit = 1000; 130 protected ILoggingService logger; 131 protected Parameters expParameters; 132 private TranslationServices translator = new NullTranslator(); 133 protected TerminologyCache txCache; 134 135 private boolean tlogging = true; 136 private Locale locale; 137 private ResourceBundle i18Nmessages; 138 139 public BaseWorkerContext() throws FileNotFoundException, IOException, FHIRException { 140 super(); 141 txCache = new TerminologyCache(lock, null); 142 } 143 144 public BaseWorkerContext(Map<String, CodeSystem> codeSystems, Map<String, ValueSet> valueSets, 145 Map<String, ConceptMap> maps, Map<String, StructureDefinition> profiles, Map<String, ImplementationGuide> guides) 146 throws FileNotFoundException, IOException, FHIRException { 147 super(); 148 this.codeSystems = codeSystems; 149 this.valueSets = valueSets; 150 this.maps = maps; 151 this.structures = profiles; 152 this.guides = guides; 153 txCache = new TerminologyCache(lock, null); 154 } 155 156 protected void copy(BaseWorkerContext other) { 157 synchronized (other.lock) { // tricky, because you need to lock this as well, but it's really not in use yet 158 allResourcesById.putAll(other.allResourcesById); 159 translator = other.translator; 160 codeSystems.putAll(other.codeSystems); 161 txcaps = other.txcaps; 162 valueSets.putAll(other.valueSets); 163 maps.putAll(other.maps); 164 transforms.putAll(other.transforms); 165 structures.putAll(other.structures); 166 searchParameters.putAll(other.searchParameters); 167 plans.putAll(other.plans); 168 questionnaires.putAll(other.questionnaires); 169 operations.putAll(other.operations); 170 systems.addAll(other.systems); 171 guides.putAll(other.guides); 172 capstmts.putAll(other.capstmts); 173 174 allowLoadingDuplicates = other.allowLoadingDuplicates; 175 tsServer = other.tsServer; 176 name = other.name; 177 txClient = other.txClient; 178 txLog = other.txLog; 179 txcaps = other.txcaps; 180 canRunWithoutTerminology = other.canRunWithoutTerminology; 181 noTerminologyServer = other.noTerminologyServer; 182 if (other.txCache != null) 183 txCache = other.txCache.copy(); 184 expandCodesLimit = other.expandCodesLimit; 185 logger = other.logger; 186 expParameters = other.expParameters; 187 } 188 } 189 190 public void cacheResource(Resource r) throws FHIRException { 191 synchronized (lock) { 192 Map<String, Resource> map = allResourcesById.get(r.fhirType()); 193 if (map == null) { 194 map = new HashMap<String, Resource>(); 195 allResourcesById.put(r.fhirType(), map); 196 } 197 map.put(r.getId(), r); 198 199 if (r instanceof MetadataResource) { 200 MetadataResource m = (MetadataResource) r; 201 String url = m.getUrl(); 202 if (!allowLoadingDuplicates && hasResource(r.getClass(), url)) 203 throw new DefinitionException("Duplicate Resource " + url); 204 if (r instanceof StructureDefinition) 205 seeMetadataResource((StructureDefinition) m, structures, false); 206 else if (r instanceof ValueSet) 207 seeMetadataResource((ValueSet) m, valueSets, false); 208 else if (r instanceof CodeSystem) 209 seeMetadataResource((CodeSystem) m, codeSystems, false); 210 else if (r instanceof ImplementationGuide) 211 seeMetadataResource((ImplementationGuide) m, guides, false); 212 else if (r instanceof CapabilityStatement) 213 seeMetadataResource((CapabilityStatement) m, capstmts, false); 214 else if (r instanceof SearchParameter) 215 seeMetadataResource((SearchParameter) m, searchParameters, false); 216 else if (r instanceof PlanDefinition) 217 seeMetadataResource((PlanDefinition) m, plans, false); 218 else if (r instanceof OperationDefinition) 219 seeMetadataResource((OperationDefinition) m, operations, false); 220 else if (r instanceof Questionnaire) 221 seeMetadataResource((Questionnaire) m, questionnaires, true); 222 else if (r instanceof ConceptMap) 223 seeMetadataResource((ConceptMap) m, maps, false); 224 else if (r instanceof StructureMap) 225 seeMetadataResource((StructureMap) m, transforms, false); 226 else if (r instanceof NamingSystem) 227 systems.add((NamingSystem) r); 228 } 229 } 230 } 231 232 /* 233 * Compare business versions, returning "true" if the candidate newer version is 234 * in fact newer than the oldVersion Comparison will work for strictly numeric 235 * versions as well as multi-level versions separated by ., -, _, : or space 236 * Failing that, it will do unicode-based character ordering. E.g. 1.5.3 < 237 * 1.14.3 2017-3-10 < 2017-12-7 A3 < T2 238 */ 239 private boolean laterVersion(String newVersion, String oldVersion) { 240 // Compare business versions, retur 241 newVersion = newVersion.trim(); 242 oldVersion = oldVersion.trim(); 243 if (StringUtils.isNumeric(newVersion) && StringUtils.isNumeric(oldVersion)) 244 return Double.parseDouble(newVersion) > Double.parseDouble(oldVersion); 245 else if (hasDelimiter(newVersion, oldVersion, ".")) 246 return laterDelimitedVersion(newVersion, oldVersion, "\\."); 247 else if (hasDelimiter(newVersion, oldVersion, "-")) 248 return laterDelimitedVersion(newVersion, oldVersion, "\\-"); 249 else if (hasDelimiter(newVersion, oldVersion, "_")) 250 return laterDelimitedVersion(newVersion, oldVersion, "\\_"); 251 else if (hasDelimiter(newVersion, oldVersion, ":")) 252 return laterDelimitedVersion(newVersion, oldVersion, "\\:"); 253 else if (hasDelimiter(newVersion, oldVersion, " ")) 254 return laterDelimitedVersion(newVersion, oldVersion, "\\ "); 255 else { 256 return newVersion.compareTo(oldVersion) > 0; 257 } 258 } 259 260 /* 261 * Returns true if both strings include the delimiter and have the same number 262 * of occurrences of it 263 */ 264 private boolean hasDelimiter(String s1, String s2, String delimiter) { 265 return s1.contains(delimiter) && s2.contains(delimiter) && s1.split(delimiter).length == s2.split(delimiter).length; 266 } 267 268 private boolean laterDelimitedVersion(String newVersion, String oldVersion, String delimiter) { 269 String[] newParts = newVersion.split(delimiter); 270 String[] oldParts = oldVersion.split(delimiter); 271 for (int i = 0; i < newParts.length; i++) { 272 if (!newParts[i].equals(oldParts[i])) 273 return laterVersion(newParts[i], oldParts[i]); 274 } 275 // This should never happen 276 throw new Error( 277 "Delimited versions have exact match for delimiter '" + delimiter + "' : " + newParts + " vs " + oldParts); 278 } 279 280 protected <T extends MetadataResource> void seeMetadataResource(T r, Map<String, T> map, boolean addId) 281 throws FHIRException { 282 if (addId) 283 map.put(r.getId(), r); // todo: why? 284 if (!map.containsKey(r.getUrl())) 285 map.put(r.getUrl(), r); 286 else { 287 // If this resource already exists, see if it's the newest business version. The 288 // default resource to return if not qualified is the most recent business 289 // version 290 MetadataResource existingResource = (MetadataResource) map.get(r.getUrl()); 291 if (r.hasVersion() && existingResource.hasVersion() && !r.getVersion().equals(existingResource.getVersion())) { 292 if (laterVersion(r.getVersion(), existingResource.getVersion())) { 293 map.remove(r.getUrl()); 294 map.put(r.getUrl(), r); 295 } 296 } else 297 map.remove(r.getUrl()); 298 map.put(r.getUrl(), r); 299// throw new FHIRException("Multiple declarations of resource with same canonical URL (" + r.getUrl() + ") and version (" + (r.hasVersion() ? r.getVersion() : "" ) + ")"); 300 } 301 if (r.hasVersion()) 302 map.put(r.getUrl() + "|" + r.getVersion(), r); 303 } 304 305 @Override 306 public CodeSystem fetchCodeSystem(String system) { 307 synchronized (lock) { 308 return codeSystems.get(system); 309 } 310 } 311 312 @Override 313 public boolean supportsSystem(String system) throws TerminologyServiceException { 314 synchronized (lock) { 315 if (codeSystems.containsKey(system) && codeSystems.get(system).getContent() != CodeSystemContentMode.NOTPRESENT) 316 return true; 317 else if (supportedCodeSystems.contains(system)) 318 return true; 319 else if (system.startsWith("http://example.org") || system.startsWith("http://acme.com") 320 || system.startsWith("http://hl7.org/fhir/valueset-") || system.startsWith("urn:oid:")) 321 return false; 322 else { 323 if (noTerminologyServer) 324 return false; 325 if (txcaps == null) { 326 try { 327 log("Terminology server: Check for supported code systems for " + system); 328 txcaps = txClient.getTerminologyCapabilities(); 329 } catch (Exception e) { 330 if (canRunWithoutTerminology) { 331 noTerminologyServer = true; 332 log("==============!! Running without terminology server !! =============="); 333 if (txClient != null) { 334 log("txServer = " + txClient.getAddress()); 335 log("Error = " + e.getMessage() + ""); 336 } 337 log("====================================================================="); 338 return false; 339 } else 340 throw new TerminologyServiceException(e); 341 } 342 if (txcaps != null) { 343 for (TerminologyCapabilitiesCodeSystemComponent tccs : txcaps.getCodeSystem()) { 344 supportedCodeSystems.add(tccs.getUri()); 345 } 346 } 347 if (supportedCodeSystems.contains(system)) 348 return true; 349 } 350 } 351 return false; 352 } 353 } 354 355 private void log(String message) { 356 if (logger != null) 357 logger.logMessage(message); 358 else 359 System.out.println(message); 360 } 361 362 protected void tlog(String msg) { 363 if (tlogging) 364 System.out.println("-tx cache miss: " + msg); 365 } 366 367 // --- expansion support 368 // ------------------------------------------------------------------------------------------------------------ 369 370 public int getExpandCodesLimit() { 371 return expandCodesLimit; 372 } 373 374 public void setExpandCodesLimit(int expandCodesLimit) { 375 this.expandCodesLimit = expandCodesLimit; 376 } 377 378 @Override 379 public ValueSetExpansionOutcome expandVS(ElementDefinitionBindingComponent binding, boolean cacheOk, 380 boolean heirarchical) throws FHIRException { 381 ValueSet vs = null; 382 vs = fetchResource(ValueSet.class, binding.getValueSet()); 383 if (vs == null) 384 throw new FHIRException("Unable to resolve value Set " + binding.getValueSet()); 385 return expandVS(vs, cacheOk, heirarchical); 386 } 387 388 @Override 389 public ValueSetExpansionOutcome expandVS(ConceptSetComponent inc, boolean heirachical) 390 throws TerminologyServiceException { 391 ValueSet vs = new ValueSet(); 392 vs.setCompose(new ValueSetComposeComponent()); 393 vs.getCompose().getInclude().add(inc); 394 CacheToken cacheToken = txCache.generateExpandToken(vs, heirachical); 395 ValueSetExpansionOutcome res; 396 res = txCache.getExpansion(cacheToken); 397 if (res != null) 398 return res; 399 Parameters p = expParameters.copy(); 400 p.setParameter("includeDefinition", false); 401 p.setParameter("excludeNested", !heirachical); 402 403 if (noTerminologyServer) 404 return new ValueSetExpansionOutcome("Error expanding ValueSet: running without terminology services", 405 TerminologyServiceErrorClass.NOSERVICE); 406 Map<String, String> params = new HashMap<String, String>(); 407 params.put("_limit", Integer.toString(expandCodesLimit)); 408 params.put("_incomplete", "true"); 409 tlog("$expand on " + txCache.summary(vs)); 410 try { 411 ValueSet result = txClient.expandValueset(vs, p, params); 412 res = new ValueSetExpansionOutcome(result).setTxLink(txLog.getLastId()); 413 } catch (Exception e) { 414 res = new ValueSetExpansionOutcome(e.getMessage() == null ? e.getClass().getName() : e.getMessage(), 415 TerminologyServiceErrorClass.UNKNOWN); 416 if (txLog != null) 417 res.setTxLink(txLog.getLastId()); 418 } 419 txCache.cacheExpansion(cacheToken, res, TerminologyCache.PERMANENT); 420 return res; 421 422 } 423 424 @Override 425 public ValueSetExpansionOutcome expandVS(ValueSet vs, boolean cacheOk, boolean heirarchical) { 426 if (expParameters == null) 427 throw new Error("No Expansion Parameters provided"); 428 Parameters p = expParameters.copy(); 429 return expandVS(vs, cacheOk, heirarchical, p); 430 } 431 432 public ValueSetExpansionOutcome expandVS(ValueSet vs, boolean cacheOk, boolean heirarchical, Parameters p) { 433 if (p == null) 434 throw new Error("No Parameters provided to expandVS"); 435 if (vs.hasExpansion()) { 436 return new ValueSetExpansionOutcome(vs.copy()); 437 } 438 if (!vs.hasUrl()) 439 throw new Error("no value set"); 440 441 CacheToken cacheToken = txCache.generateExpandToken(vs, heirarchical); 442 ValueSetExpansionOutcome res; 443 if (cacheOk) { 444 res = txCache.getExpansion(cacheToken); 445 if (res != null) 446 return res; 447 } 448 p.setParameter("includeDefinition", false); 449 p.setParameter("excludeNested", !heirarchical); 450 451 // ok, first we try to expand locally 452 try { 453 ValueSetExpanderSimple vse = new ValueSetExpanderSimple(this); 454 res = vse.doExpand(vs, p); 455 if (!res.getValueset().hasUrl()) 456 throw new Error("no url in expand value set"); 457 txCache.cacheExpansion(cacheToken, res, TerminologyCache.TRANSIENT); 458 return res; 459 } catch (Exception e) { 460 } 461 462 // if that failed, we try to expand on the server 463 if (noTerminologyServer) 464 return new ValueSetExpansionOutcome("Error expanding ValueSet: running without terminology services", 465 TerminologyServiceErrorClass.NOSERVICE); 466 Map<String, String> params = new HashMap<String, String>(); 467 params.put("_limit", Integer.toString(expandCodesLimit)); 468 params.put("_incomplete", "true"); 469 tlog("$expand on " + txCache.summary(vs)); 470 try { 471 ValueSet result = txClient.expandValueset(vs, p, params); 472 if (!result.hasUrl()) 473 result.setUrl(vs.getUrl()); 474 if (!result.hasUrl()) 475 throw new Error("no url in expand value set 2"); 476 res = new ValueSetExpansionOutcome(result).setTxLink(txLog.getLastId()); 477 } catch (Exception e) { 478 res = new ValueSetExpansionOutcome(e.getMessage() == null ? e.getClass().getName() : e.getMessage(), 479 TerminologyServiceErrorClass.UNKNOWN).setTxLink(txLog == null ? null : txLog.getLastId()); 480 } 481 txCache.cacheExpansion(cacheToken, res, TerminologyCache.PERMANENT); 482 return res; 483 } 484 485 private boolean hasTooCostlyExpansion(ValueSet valueset) { 486 return valueset != null && valueset.hasExpansion() && ToolingExtensions.hasExtension(valueset.getExpansion(), 487 "http://hl7.org/fhir/StructureDefinition/valueset-toocostly"); 488 } 489 // --- validate code 490 // ------------------------------------------------------------------------------- 491 492 @Override 493 public ValidationResult validateCode(ValidationOptions options, String system, String code, String display) { 494 Coding c = new Coding(system, code, display); 495 return validateCode(options, c, null); 496 } 497 498 @Override 499 public ValidationResult validateCode(ValidationOptions options, String system, String code, String display, 500 ValueSet vs) { 501 Coding c = new Coding(system, code, display); 502 return validateCode(options, c, vs); 503 } 504 505 @Override 506 public ValidationResult validateCode(ValidationOptions options, String code, ValueSet vs) { 507 Coding c = new Coding(null, code, null); 508 return doValidateCode(options, c, vs, true); 509 } 510 511 @Override 512 public ValidationResult validateCode(ValidationOptions options, String system, String code, String display, 513 ConceptSetComponent vsi) { 514 Coding c = new Coding(system, code, display); 515 ValueSet vs = new ValueSet(); 516 vs.setUrl(Utilities.makeUuidUrn()); 517 vs.getCompose().addInclude(vsi); 518 return validateCode(options, c, vs); 519 } 520 521 @Override 522 public ValidationResult validateCode(ValidationOptions options, Coding code, ValueSet vs) { 523 return doValidateCode(options, code, vs, false); 524 } 525 526 public ValidationResult doValidateCode(ValidationOptions options, Coding code, ValueSet vs, boolean inferSystem) { 527 CacheToken cacheToken = txCache != null ? txCache.generateValidationToken(options, code, vs) : null; 528 ValidationResult res = null; 529 if (txCache != null) 530 res = txCache.getValidation(cacheToken); 531 if (res != null) 532 return res; 533 534 // ok, first we try to validate locally 535 try { 536 ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, this); 537 res = vsc.validateCode(code); 538 if (txCache != null) 539 txCache.cacheValidation(cacheToken, res, TerminologyCache.TRANSIENT); 540 return res; 541 } catch (Exception e) { 542 } 543 544 // if that failed, we try to validate on the server 545 if (noTerminologyServer) 546 return new ValidationResult(IssueSeverity.ERROR, "Error validating code: running without terminology services", 547 TerminologyServiceErrorClass.NOSERVICE); 548 String csumm = txCache != null ? txCache.summary(code) : null; 549 if (txCache != null) 550 tlog("$validate " + csumm + " for " + txCache.summary(vs)); 551 else 552 tlog("$validate " + csumm + " before cache exists"); 553 try { 554 Parameters pIn = new Parameters(); 555 pIn.addParameter().setName("coding").setValue(code); 556 if (inferSystem) 557 pIn.addParameter().setName("inferSystem").setValue(new BooleanType(true)); 558 if (options != null) 559 setTerminologyOptions(options, pIn); 560 res = validateOnServer(vs, pIn); 561 } catch (Exception e) { 562 res = new ValidationResult(IssueSeverity.ERROR, e.getMessage() == null ? e.getClass().getName() : e.getMessage()) 563 .setTxLink(txLog == null ? null : txLog.getLastId()); 564 } 565 if (txCache != null) 566 txCache.cacheValidation(cacheToken, res, TerminologyCache.PERMANENT); 567 return res; 568 } 569 570 private void setTerminologyOptions(ValidationOptions options, Parameters pIn) { 571 if (options != null && options.hasLanguages()) { 572 pIn.addParameter("displayLanguage", options.getLanguages().toString()); 573 } 574 } 575 576 @Override 577 public ValidationResult validateCode(ValidationOptions options, CodeableConcept code, ValueSet vs) { 578 CacheToken cacheToken = txCache.generateValidationToken(options, code, vs); 579 ValidationResult res = txCache.getValidation(cacheToken); 580 if (res != null) 581 return res; 582 583 // ok, first we try to validate locally 584 try { 585 ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, this); 586 res = vsc.validateCode(code); 587 txCache.cacheValidation(cacheToken, res, TerminologyCache.TRANSIENT); 588 return res; 589 } catch (Exception e) { 590 } 591 592 // if that failed, we try to validate on the server 593 if (noTerminologyServer) 594 return new ValidationResult(IssueSeverity.ERROR, "Error validating code: running without terminology services", 595 TerminologyServiceErrorClass.NOSERVICE); 596 tlog("$validate " + txCache.summary(code) + " for " + txCache.summary(vs)); 597 try { 598 Parameters pIn = new Parameters(); 599 pIn.addParameter().setName("codeableConcept").setValue(code); 600 if (options != null) 601 setTerminologyOptions(options, pIn); 602 res = validateOnServer(vs, pIn); 603 } catch (Exception e) { 604 res = new ValidationResult(IssueSeverity.ERROR, e.getMessage() == null ? e.getClass().getName() : e.getMessage()) 605 .setTxLink(txLog.getLastId()); 606 } 607 txCache.cacheValidation(cacheToken, res, TerminologyCache.PERMANENT); 608 return res; 609 } 610 611 private ValidationResult validateOnServer(ValueSet vs, Parameters pin) throws FHIRException { 612 if (vs != null) 613 pin.addParameter().setName("valueSet").setResource(vs); 614 for (ParametersParameterComponent pp : pin.getParameter()) 615 if (pp.getName().equals("profile")) 616 throw new Error("Can only specify profile in the context"); 617 if (expParameters == null) 618 throw new Error("No ExpansionProfile provided"); 619 pin.addParameter().setName("profile").setResource(expParameters); 620 txLog.clearLastId(); 621 Parameters pOut; 622 if (vs == null) 623 pOut = txClient.validateCS(pin); 624 else 625 pOut = txClient.validateVS(pin); 626 boolean ok = false; 627 String message = "No Message returned"; 628 String display = null; 629 TerminologyServiceErrorClass err = TerminologyServiceErrorClass.UNKNOWN; 630 for (ParametersParameterComponent p : pOut.getParameter()) { 631 if (p.getName().equals("result")) 632 ok = ((BooleanType) p.getValue()).getValue().booleanValue(); 633 else if (p.getName().equals("message")) 634 message = ((StringType) p.getValue()).getValue(); 635 else if (p.getName().equals("display")) 636 display = ((StringType) p.getValue()).getValue(); 637 else if (p.getName().equals("cause")) { 638 try { 639 IssueType it = IssueType.fromCode(((StringType) p.getValue()).getValue()); 640 if (it == IssueType.UNKNOWN) 641 err = TerminologyServiceErrorClass.UNKNOWN; 642 else if (it == IssueType.NOTSUPPORTED) 643 err = TerminologyServiceErrorClass.VALUESET_UNSUPPORTED; 644 } catch (FHIRException e) { 645 } 646 } 647 } 648 if (!ok) 649 return new ValidationResult(IssueSeverity.ERROR, message, err).setTxLink(txLog.getLastId()) 650 .setTxLink(txLog.getLastId()); 651 else if (message != null && !message.equals("No Message returned")) 652 return new ValidationResult(IssueSeverity.WARNING, message, new ConceptDefinitionComponent().setDisplay(display)) 653 .setTxLink(txLog.getLastId()).setTxLink(txLog.getLastId()); 654 else if (display != null) 655 return new ValidationResult(new ConceptDefinitionComponent().setDisplay(display)).setTxLink(txLog.getLastId()) 656 .setTxLink(txLog.getLastId()); 657 else 658 return new ValidationResult(new ConceptDefinitionComponent()).setTxLink(txLog.getLastId()) 659 .setTxLink(txLog.getLastId()); 660 } 661 662 // -------------------------------------------------------------------------------------------------------------------------------------------------------- 663 664 public void initTS(String cachePath) throws Exception { 665 txCache = new TerminologyCache(lock, cachePath); 666 } 667 668 @Override 669 public List<ConceptMap> findMapsForSource(String url) throws FHIRException { 670 synchronized (lock) { 671 List<ConceptMap> res = new ArrayList<ConceptMap>(); 672 for (ConceptMap map : maps.values()) 673 if (((Reference) map.getSource()).getReference().equals(url)) 674 res.add(map); 675 return res; 676 } 677 } 678 679 public boolean isCanRunWithoutTerminology() { 680 return canRunWithoutTerminology; 681 } 682 683 public void setCanRunWithoutTerminology(boolean canRunWithoutTerminology) { 684 this.canRunWithoutTerminology = canRunWithoutTerminology; 685 } 686 687 public void setLogger(ILoggingService logger) { 688 this.logger = logger; 689 } 690 691 public Parameters getExpansionParameters() { 692 return expParameters; 693 } 694 695 public void setExpansionProfile(Parameters expParameters) { 696 this.expParameters = expParameters; 697 } 698 699 @Override 700 public boolean isNoTerminologyServer() { 701 return noTerminologyServer; 702 } 703 704 public String getName() { 705 return name; 706 } 707 708 public void setName(String name) { 709 this.name = name; 710 } 711 712 @Override 713 public Set<String> getResourceNamesAsSet() { 714 Set<String> res = new HashSet<String>(); 715 res.addAll(getResourceNames()); 716 return res; 717 } 718 719 public boolean isAllowLoadingDuplicates() { 720 return allowLoadingDuplicates; 721 } 722 723 public void setAllowLoadingDuplicates(boolean allowLoadingDuplicates) { 724 this.allowLoadingDuplicates = allowLoadingDuplicates; 725 } 726 727 @SuppressWarnings("unchecked") 728 @Override 729 public <T extends Resource> T fetchResourceWithException(Class<T> class_, String uri) throws FHIRException { 730 if (class_ == StructureDefinition.class) 731 uri = ProfileUtilities.sdNs(uri, getOverrideVersionNs()); 732 synchronized (lock) { 733 734 if (uri.startsWith("http:") || uri.startsWith("https:")) { 735 String version = null; 736 if (uri.contains("|")) { 737 version = uri.substring(uri.lastIndexOf("|") + 1); 738 uri = uri.substring(0, uri.lastIndexOf("|")); 739 } 740 if (uri.contains("#")) 741 uri = uri.substring(0, uri.indexOf("#")); 742 if (class_ == Resource.class || class_ == null) { 743 if (structures.containsKey(uri)) 744 return (T) structures.get(uri); 745 if (guides.containsKey(uri)) 746 return (T) guides.get(uri); 747 if (capstmts.containsKey(uri)) 748 return (T) capstmts.get(uri); 749 if (valueSets.containsKey(uri)) 750 return (T) valueSets.get(uri); 751 if (codeSystems.containsKey(uri)) 752 return (T) codeSystems.get(uri); 753 if (operations.containsKey(uri)) 754 return (T) operations.get(uri); 755 if (searchParameters.containsKey(uri)) 756 return (T) searchParameters.get(uri); 757 if (plans.containsKey(uri)) 758 return (T) plans.get(uri); 759 if (maps.containsKey(uri)) 760 return (T) maps.get(uri); 761 if (transforms.containsKey(uri)) 762 return (T) transforms.get(uri); 763 if (questionnaires.containsKey(uri)) 764 return (T) questionnaires.get(uri); 765 for (Map<String, Resource> rt : allResourcesById.values()) { 766 for (Resource r : rt.values()) { 767 if (r instanceof MetadataResource) { 768 MetadataResource mr = (MetadataResource) r; 769 if (uri.equals(mr.getUrl())) 770 return (T) mr; 771 } 772 } 773 } 774 return null; 775 } else if (class_ == ImplementationGuide.class) { 776 return (T) guides.get(uri); 777 } else if (class_ == CapabilityStatement.class) { 778 return (T) capstmts.get(uri); 779 } else if (class_ == StructureDefinition.class) { 780 return (T) structures.get(uri); 781 } else if (class_ == StructureMap.class) { 782 return (T) transforms.get(uri); 783 } else if (class_ == ValueSet.class) { 784 if (valueSets.containsKey(uri + "|" + version)) 785 return (T) valueSets.get(uri + "|" + version); 786 else 787 return (T) valueSets.get(uri); 788 } else if (class_ == CodeSystem.class) { 789 if (codeSystems.containsKey(uri + "|" + version)) 790 return (T) codeSystems.get(uri + "|" + version); 791 else 792 return (T) codeSystems.get(uri); 793 } else if (class_ == ConceptMap.class) { 794 return (T) maps.get(uri); 795 } else if (class_ == PlanDefinition.class) { 796 return (T) plans.get(uri); 797 } else if (class_ == OperationDefinition.class) { 798 OperationDefinition od = operations.get(uri); 799 return (T) od; 800 } else if (class_ == SearchParameter.class) { 801 SearchParameter res = searchParameters.get(uri); 802 if (res == null) { 803 StringBuilder b = new StringBuilder(); 804 for (String s : searchParameters.keySet()) { 805 b.append(s); 806 b.append("\r\n"); 807 } 808 } 809 return (T) res; 810 } 811 } 812 if (class_ == CodeSystem.class && codeSystems.containsKey(uri)) 813 return (T) codeSystems.get(uri); 814 815 if (class_ == Questionnaire.class) 816 return (T) questionnaires.get(uri); 817 if (class_ == null) { 818 if (uri.matches(Constants.URI_REGEX) && !uri.contains("ValueSet")) 819 return null; 820 821 // it might be a special URL. 822 if (Utilities.isAbsoluteUrl(uri) || uri.startsWith("ValueSet/")) { 823 Resource res = null; // findTxValueSet(uri); 824 if (res != null) 825 return (T) res; 826 } 827 return null; 828 } 829 if (supportedCodeSystems.contains(uri)) 830 return null; 831 throw new FHIRException("not done yet: can't fetch " + uri); 832 } 833 } 834 835 private Set<String> notCanonical = new HashSet<String>(); 836 837 private String overrideVersionNs; 838 839// private MetadataResource findTxValueSet(String uri) { 840// MetadataResource res = expansionCache.getStoredResource(uri); 841// if (res != null) 842// return res; 843// synchronized (lock) { 844// if (notCanonical.contains(uri)) 845// return null; 846// } 847// try { 848// tlog("get canonical "+uri); 849// res = txServer.getCanonical(ValueSet.class, uri); 850// } catch (Exception e) { 851// synchronized (lock) { 852// notCanonical.add(uri); 853// } 854// return null; 855// } 856// if (res != null) 857// try { 858// expansionCache.storeResource(res); 859// } catch (IOException e) { 860// } 861// return res; 862// } 863 864 @Override 865 public Resource fetchResourceById(String type, String uri) { 866 synchronized (lock) { 867 String[] parts = uri.split("\\/"); 868 if (!Utilities.noString(type) && parts.length == 1) { 869 if (allResourcesById.containsKey(type)) 870 return allResourcesById.get(type).get(parts[0]); 871 else 872 return null; 873 } 874 if (parts.length >= 2) { 875 if (!Utilities.noString(type)) 876 if (!type.equals(parts[parts.length - 2])) 877 throw new Error("Resource type mismatch for " + type + " / " + uri); 878 return allResourcesById.get(parts[parts.length - 2]).get(parts[parts.length - 1]); 879 } else 880 throw new Error("Unable to process request for resource for " + type + " / " + uri); 881 } 882 } 883 884 public <T extends Resource> T fetchResource(Class<T> class_, String uri) { 885 try { 886 return fetchResourceWithException(class_, uri); 887 } catch (FHIRException e) { 888 throw new Error(e); 889 } 890 } 891 892 @Override 893 public <T extends Resource> boolean hasResource(Class<T> class_, String uri) { 894 try { 895 return fetchResourceWithException(class_, uri) != null; 896 } catch (Exception e) { 897 return false; 898 } 899 } 900 901 public TranslationServices translator() { 902 return translator; 903 } 904 905 public void setTranslator(TranslationServices translator) { 906 this.translator = translator; 907 } 908 909 public class NullTranslator implements TranslationServices { 910 911 @Override 912 public String translate(String context, String value, String targetLang) { 913 return value; 914 } 915 916 @Override 917 public String translate(String context, String value) { 918 return value; 919 } 920 921 @Override 922 public String toStr(float value) { 923 return null; 924 } 925 926 @Override 927 public String toStr(Date value) { 928 return null; 929 } 930 931 @Override 932 public String translateAndFormat(String contest, String lang, String value, Object... args) { 933 return String.format(value, args); 934 } 935 936 @Override 937 public Map<String, String> translations(String value) { 938 // TODO Auto-generated method stub 939 return null; 940 } 941 942 @Override 943 public Set<String> listTranslations(String category) { 944 // TODO Auto-generated method stub 945 return null; 946 } 947 948 } 949 950 public void reportStatus(JsonObject json) { 951 synchronized (lock) { 952 json.addProperty("codeystem-count", codeSystems.size()); 953 json.addProperty("valueset-count", valueSets.size()); 954 json.addProperty("conceptmap-count", maps.size()); 955 json.addProperty("transforms-count", transforms.size()); 956 json.addProperty("structures-count", structures.size()); 957 json.addProperty("guides-count", guides.size()); 958 json.addProperty("statements-count", capstmts.size()); 959 } 960 } 961 962 public void dropResource(Resource r) throws FHIRException { 963 dropResource(r.fhirType(), r.getId()); 964 } 965 966 public void dropResource(String fhirType, String id) { 967 synchronized (lock) { 968 969 Map<String, Resource> map = allResourcesById.get(fhirType); 970 if (map == null) { 971 map = new HashMap<String, Resource>(); 972 allResourcesById.put(fhirType, map); 973 } 974 if (map.containsKey(id)) 975 map.remove(id); 976 977 if (fhirType.equals("StructureDefinition")) 978 dropMetadataResource(structures, id); 979 else if (fhirType.equals("ImplementationGuide")) 980 dropMetadataResource(guides, id); 981 else if (fhirType.equals("CapabilityStatement")) 982 dropMetadataResource(capstmts, id); 983 else if (fhirType.equals("ValueSet")) 984 dropMetadataResource(valueSets, id); 985 else if (fhirType.equals("CodeSystem")) 986 dropMetadataResource(codeSystems, id); 987 else if (fhirType.equals("OperationDefinition")) 988 dropMetadataResource(operations, id); 989 else if (fhirType.equals("Questionnaire")) 990 dropMetadataResource(questionnaires, id); 991 else if (fhirType.equals("ConceptMap")) 992 dropMetadataResource(maps, id); 993 else if (fhirType.equals("StructureMap")) 994 dropMetadataResource(transforms, id); 995 else if (fhirType.equals("NamingSystem")) 996 for (int i = systems.size() - 1; i >= 0; i--) { 997 if (systems.get(i).getId().equals(id)) 998 systems.remove(i); 999 } 1000 } 1001 } 1002 1003 private <T extends MetadataResource> void dropMetadataResource(Map<String, T> map, String id) { 1004 T res = map.get(id); 1005 if (res != null) { 1006 map.remove(id); 1007 if (map.containsKey(res.getUrl())) 1008 map.remove(res.getUrl()); 1009 if (res.getVersion() != null) 1010 if (map.containsKey(res.getUrl() + "|" + res.getVersion())) 1011 map.remove(res.getUrl() + "|" + res.getVersion()); 1012 } 1013 } 1014 1015 @Override 1016 public List<MetadataResource> allConformanceResources() { 1017 synchronized (lock) { 1018 List<MetadataResource> result = new ArrayList<MetadataResource>(); 1019 result.addAll(structures.values()); 1020 result.addAll(guides.values()); 1021 result.addAll(capstmts.values()); 1022 result.addAll(codeSystems.values()); 1023 result.addAll(valueSets.values()); 1024 result.addAll(maps.values()); 1025 result.addAll(transforms.values()); 1026 result.addAll(plans.values()); 1027 result.addAll(questionnaires.values()); 1028 return result; 1029 } 1030 } 1031 1032 public String listSupportedSystems() { 1033 synchronized (lock) { 1034 String sl = null; 1035 for (String s : supportedCodeSystems) 1036 sl = sl == null ? s : sl + "\r\n" + s; 1037 return sl; 1038 } 1039 } 1040 1041 public int totalCount() { 1042 synchronized (lock) { 1043 return valueSets.size() + maps.size() + structures.size() + transforms.size(); 1044 } 1045 } 1046 1047 public List<ConceptMap> listMaps() { 1048 List<ConceptMap> m = new ArrayList<ConceptMap>(); 1049 synchronized (lock) { 1050 m.addAll(maps.values()); 1051 } 1052 return m; 1053 } 1054 1055 public List<StructureMap> listTransforms() { 1056 List<StructureMap> m = new ArrayList<StructureMap>(); 1057 synchronized (lock) { 1058 m.addAll(transforms.values()); 1059 } 1060 return m; 1061 } 1062 1063 public StructureMap getTransform(String code) { 1064 synchronized (lock) { 1065 return transforms.get(code); 1066 } 1067 } 1068 1069 public List<StructureDefinition> listStructures() { 1070 List<StructureDefinition> m = new ArrayList<StructureDefinition>(); 1071 synchronized (lock) { 1072 m.addAll(structures.values()); 1073 } 1074 return m; 1075 } 1076 1077 public StructureDefinition getStructure(String code) { 1078 synchronized (lock) { 1079 return structures.get(code); 1080 } 1081 } 1082 1083 @Override 1084 public String oid2Uri(String oid) { 1085 synchronized (lock) { 1086 String uri = OIDUtils.getUriForOid(oid); 1087 if (uri != null) 1088 return uri; 1089 for (NamingSystem ns : systems) { 1090 if (hasOid(ns, oid)) { 1091 uri = getUri(ns); 1092 if (uri != null) 1093 return null; 1094 } 1095 } 1096 } 1097 return null; 1098 } 1099 1100 private String getUri(NamingSystem ns) { 1101 for (NamingSystemUniqueIdComponent id : ns.getUniqueId()) { 1102 if (id.getType() == NamingSystemIdentifierType.URI) 1103 return id.getValue(); 1104 } 1105 return null; 1106 } 1107 1108 private boolean hasOid(NamingSystem ns, String oid) { 1109 for (NamingSystemUniqueIdComponent id : ns.getUniqueId()) { 1110 if (id.getType() == NamingSystemIdentifierType.OID && id.getValue().equals(oid)) 1111 return true; 1112 } 1113 return false; 1114 } 1115 1116 public void cacheVS(JsonObject json, Map<String, ValidationResult> t) { 1117 synchronized (lock) { 1118 validationCache.put(json.get("url").getAsString(), t); 1119 } 1120 } 1121 1122 public SearchParameter getSearchParameter(String code) { 1123 synchronized (lock) { 1124 return searchParameters.get(code); 1125 } 1126 } 1127 1128 @Override 1129 public String getOverrideVersionNs() { 1130 return overrideVersionNs; 1131 } 1132 1133 @Override 1134 public void setOverrideVersionNs(String value) { 1135 overrideVersionNs = value; 1136 } 1137 1138 @Override 1139 public ILoggingService getLogger() { 1140 return logger; 1141 } 1142 1143 @Override 1144 public StructureDefinition fetchTypeDefinition(String typeName) { 1145 if (Utilities.isAbsoluteUrl(typeName)) 1146 return fetchResource(StructureDefinition.class, typeName); 1147 else 1148 return fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + typeName); 1149 } 1150 1151 public boolean isTlogging() { 1152 return tlogging; 1153 } 1154 1155 public void setTlogging(boolean tlogging) { 1156 this.tlogging = tlogging; 1157 } 1158 1159 public UcumService getUcumService() { 1160 return ucumService; 1161 } 1162 1163 public void setUcumService(UcumService ucumService) { 1164 this.ucumService = ucumService; 1165 } 1166 1167 @Override 1168 public List<StructureDefinition> getStructures() { 1169 List<StructureDefinition> res = new ArrayList<>(); 1170 synchronized (lock) { // tricky, because you need to lock this as well, but it's really not in use yet 1171 res.addAll(structures.values()); 1172 } 1173 return res; 1174 } 1175 1176 public String getLinkForUrl(String corePath, String url) { 1177 for (CodeSystem r : codeSystems.values()) 1178 if (url.equals(r.getUrl())) 1179 return r.getUserString("path"); 1180 1181 for (ValueSet r : valueSets.values()) 1182 if (url.equals(r.getUrl())) 1183 return r.getUserString("path"); 1184 1185 for (ConceptMap r : maps.values()) 1186 if (url.equals(r.getUrl())) 1187 return r.getUserString("path"); 1188 1189 for (StructureMap r : transforms.values()) 1190 if (url.equals(r.getUrl())) 1191 return r.getUserString("path"); 1192 1193 for (StructureDefinition r : structures.values()) 1194 if (url.equals(r.getUrl())) 1195 return r.getUserString("path"); 1196 1197 for (ImplementationGuide r : guides.values()) 1198 if (url.equals(r.getUrl())) 1199 return r.getUserString("path"); 1200 1201 for (CapabilityStatement r : capstmts.values()) 1202 if (url.equals(r.getUrl())) 1203 return r.getUserString("path"); 1204 1205 for (SearchParameter r : searchParameters.values()) 1206 if (url.equals(r.getUrl())) 1207 return r.getUserString("path"); 1208 1209 for (Questionnaire r : questionnaires.values()) 1210 if (url.equals(r.getUrl())) 1211 return r.getUserString("path"); 1212 1213 for (OperationDefinition r : operations.values()) 1214 if (url.equals(r.getUrl())) 1215 return r.getUserString("path"); 1216 1217 for (PlanDefinition r : plans.values()) 1218 if (url.equals(r.getUrl())) 1219 return r.getUserString("path"); 1220 1221 if (url.equals("http://loinc.org")) 1222 return corePath + "loinc.html"; 1223 if (url.equals("http://unitsofmeasure.org")) 1224 return corePath + "ucum.html"; 1225 if (url.equals("http://snomed.info/sct")) 1226 return corePath + "snomed.html"; 1227 return null; 1228 } 1229}