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