001package org.hl7.fhir.r4.context; 002 003import java.io.ByteArrayInputStream; 004import java.io.File; 005import java.io.FileInputStream; 006import java.io.FileNotFoundException; 007import java.io.IOException; 008import java.io.InputStream; 009import java.net.URISyntaxException; 010import java.util.ArrayList; 011import java.util.Arrays; 012import java.util.Collections; 013import java.util.HashMap; 014import java.util.HashSet; 015import java.util.List; 016import java.util.Map; 017import java.util.Set; 018import java.util.zip.ZipEntry; 019import java.util.zip.ZipInputStream; 020 021import org.apache.commons.io.IOUtils; 022import org.fhir.ucum.UcumService; 023import org.hl7.fhir.r4.conformance.ProfileUtilities; 024import org.hl7.fhir.r4.conformance.ProfileUtilities.ProfileKnowledgeProvider; 025import org.hl7.fhir.r4.context.IWorkerContext.ILoggingService.LogCategory; 026import org.hl7.fhir.r4.formats.IParser; 027import org.hl7.fhir.r4.formats.JsonParser; 028import org.hl7.fhir.r4.formats.ParserType; 029import org.hl7.fhir.r4.formats.XmlParser; 030import org.hl7.fhir.r4.model.Bundle; 031import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; 032import org.hl7.fhir.r4.model.CodeSystem; 033import org.hl7.fhir.r4.model.ConceptMap; 034import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionBindingComponent; 035import org.hl7.fhir.r4.model.MetadataResource; 036import org.hl7.fhir.r4.model.NamingSystem; 037import org.hl7.fhir.r4.model.NamingSystem.NamingSystemIdentifierType; 038import org.hl7.fhir.r4.model.NamingSystem.NamingSystemUniqueIdComponent; 039import org.hl7.fhir.r4.model.OperationDefinition; 040import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; 041import org.hl7.fhir.r4.model.Questionnaire; 042import org.hl7.fhir.r4.model.Resource; 043import org.hl7.fhir.r4.model.ResourceType; 044import org.hl7.fhir.r4.model.SearchParameter; 045import org.hl7.fhir.r4.model.StructureDefinition; 046import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind; 047import org.hl7.fhir.r4.model.StructureDefinition.TypeDerivationRule; 048import org.hl7.fhir.r4.model.StructureMap; 049import org.hl7.fhir.r4.model.StructureMap.StructureMapModelMode; 050import org.hl7.fhir.r4.model.StructureMap.StructureMapStructureComponent; 051import org.hl7.fhir.r4.model.ValueSet; 052import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 053import org.hl7.fhir.r4.terminologies.ValueSetExpansionCache; 054import org.hl7.fhir.r4.utils.INarrativeGenerator; 055import org.hl7.fhir.r4.utils.IResourceValidator; 056import org.hl7.fhir.r4.utils.NarrativeGenerator; 057import org.hl7.fhir.r4.utils.client.FHIRToolingClient; 058import org.hl7.fhir.exceptions.DefinitionException; 059import org.hl7.fhir.exceptions.FHIRException; 060import org.hl7.fhir.exceptions.FHIRFormatError; 061import org.hl7.fhir.utilities.CSFileInputStream; 062import org.hl7.fhir.utilities.OIDUtils; 063import org.hl7.fhir.utilities.Utilities; 064import org.hl7.fhir.utilities.cache.NpmPackage; 065import org.hl7.fhir.utilities.validation.ValidationMessage; 066import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 067import org.hl7.fhir.utilities.validation.ValidationMessage.Source; 068 069import com.google.gson.JsonObject; 070 071import ca.uhn.fhir.parser.DataFormatException; 072 073/* 074 * This is a stand alone implementation of worker context for use inside a tool. 075 * It loads from the validation package (validation-min.xml.zip), and has a 076 * very light client to connect to an open unauthenticated terminology service 077 */ 078 079public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerContext, ProfileKnowledgeProvider { 080 081 public interface IContextResourceLoader { 082 Bundle loadBundle(InputStream stream, boolean isJson) throws FHIRException, IOException; 083 } 084 085 public interface IValidatorFactory { 086 IResourceValidator makeValidator(IWorkerContext ctxts) throws FHIRException; 087 } 088 089 private Questionnaire questionnaire; 090 private Map<String, byte[]> binaries = new HashMap<String, byte[]>(); 091 private String version; 092 private String revision; 093 private String date; 094 private IValidatorFactory validatorFactory; 095 private UcumService ucumService; 096 private boolean ignoreProfileErrors; 097 098 public SimpleWorkerContext() throws FileNotFoundException, IOException, FHIRException { 099 super(); 100 } 101 102 public SimpleWorkerContext(SimpleWorkerContext other) throws FileNotFoundException, IOException, FHIRException { 103 super(); 104 copy(other); 105 } 106 107 protected void copy(SimpleWorkerContext other) { 108 super.copy(other); 109 questionnaire = other.questionnaire; 110 binaries.putAll(other.binaries); 111 version = other.version; 112 revision = other.revision; 113 date = other.date; 114 validatorFactory = other.validatorFactory; 115 } 116 117 // -- Initializations 118 /** 119 * Load the working context from the validation pack 120 * 121 * @param path 122 * filename of the validation pack 123 * @return 124 * @throws IOException 125 * @throws FileNotFoundException 126 * @throws FHIRException 127 * @throws Exception 128 */ 129 public static SimpleWorkerContext fromPack(String path) throws FileNotFoundException, IOException, FHIRException { 130 SimpleWorkerContext res = new SimpleWorkerContext(); 131 res.loadFromPack(path, null); 132 return res; 133 } 134 135 public static SimpleWorkerContext fromPackage(NpmPackage pi, boolean allowDuplicates) throws FileNotFoundException, IOException, FHIRException { 136 SimpleWorkerContext res = new SimpleWorkerContext(); 137 res.setAllowLoadingDuplicates(allowDuplicates); 138 res.loadFromPackage(pi, null); 139 return res; 140 } 141 142 public static SimpleWorkerContext fromPackage(NpmPackage pi) throws FileNotFoundException, IOException, FHIRException { 143 SimpleWorkerContext res = new SimpleWorkerContext(); 144 res.loadFromPackage(pi, null); 145 return res; 146 } 147 148 public static SimpleWorkerContext fromPackage(NpmPackage pi, IContextResourceLoader loader) throws FileNotFoundException, IOException, FHIRException { 149 SimpleWorkerContext res = new SimpleWorkerContext(); 150 res.setAllowLoadingDuplicates(true); 151 res.loadFromPackage(pi, loader); 152 return res; 153 } 154 155 public static SimpleWorkerContext fromPack(String path, boolean allowDuplicates) throws FileNotFoundException, IOException, FHIRException { 156 SimpleWorkerContext res = new SimpleWorkerContext(); 157 res.setAllowLoadingDuplicates(allowDuplicates); 158 res.loadFromPack(path, null); 159 return res; 160 } 161 162 public static SimpleWorkerContext fromPack(String path, IContextResourceLoader loader) throws FileNotFoundException, IOException, FHIRException { 163 SimpleWorkerContext res = new SimpleWorkerContext(); 164 res.loadFromPack(path, loader); 165 return res; 166 } 167 168 public static SimpleWorkerContext fromClassPath() throws IOException, FHIRException { 169 SimpleWorkerContext res = new SimpleWorkerContext(); 170 res.loadFromStream(SimpleWorkerContext.class.getResourceAsStream("validation.json.zip"), null); 171 return res; 172 } 173 174 public static SimpleWorkerContext fromClassPath(String name) throws IOException, FHIRException { 175 InputStream s = SimpleWorkerContext.class.getResourceAsStream("/"+name); 176 SimpleWorkerContext res = new SimpleWorkerContext(); 177 res.loadFromStream(s, null); 178 return res; 179 } 180 181 public static SimpleWorkerContext fromDefinitions(Map<String, byte[]> source) throws IOException, FHIRException { 182 SimpleWorkerContext res = new SimpleWorkerContext(); 183 for (String name : source.keySet()) { 184 res.loadDefinitionItem(name, new ByteArrayInputStream(source.get(name)), null); 185 } 186 return res; 187 } 188 189 public static SimpleWorkerContext fromDefinitions(Map<String, byte[]> source, IContextResourceLoader loader) throws IOException, FHIRException { 190 SimpleWorkerContext res = new SimpleWorkerContext(); 191 for (String name : source.keySet()) { 192 res.loadDefinitionItem(name, new ByteArrayInputStream(source.get(name)), loader); 193 } 194 return res; 195 } 196 private void loadDefinitionItem(String name, InputStream stream, IContextResourceLoader loader) throws IOException, FHIRException { 197 if (name.endsWith(".xml")) 198 loadFromFile(stream, name, loader); 199 else if (name.endsWith(".json")) 200 loadFromFileJson(stream, name, loader); 201 else if (name.equals("version.info")) 202 readVersionInfo(stream); 203 else 204 loadBytes(name, stream); 205 } 206 207 public String connectToTSServer(String url) throws URISyntaxException { 208 tlog("Connect to "+url); 209 txServer = new FHIRToolingClient(url); 210 txServer.setTimeout(30000); 211 return txServer.getCapabilitiesStatementQuick().getSoftware().getVersion(); 212 } 213 214 public String connectToTSServer(FHIRToolingClient client) throws URISyntaxException { 215 tlog("Connect to "+client.getAddress()); 216 txServer = client; 217 return txServer.getCapabilitiesStatementQuick().getSoftware().getVersion(); 218 } 219 220 public void loadFromFile(InputStream stream, String name, IContextResourceLoader loader) throws IOException, FHIRException { 221 Resource f; 222 try { 223 if (loader != null) 224 f = loader.loadBundle(stream, false); 225 else { 226 XmlParser xml = new XmlParser(); 227 f = xml.parse(stream); 228 } 229 } catch (DataFormatException e1) { 230 throw new org.hl7.fhir.exceptions.FHIRFormatError("Error parsing "+name+":" +e1.getMessage(), e1); 231 } catch (Exception e1) { 232 throw new org.hl7.fhir.exceptions.FHIRFormatError("Error parsing "+name+":" +e1.getMessage(), e1); 233 } 234 if (f instanceof Bundle) { 235 Bundle bnd = (Bundle) f; 236 for (BundleEntryComponent e : bnd.getEntry()) { 237 if (e.getFullUrl() == null) { 238 logger.logDebugMessage(LogCategory.CONTEXT, "unidentified resource in " + name+" (no fullUrl)"); 239 } 240 cacheResource(e.getResource()); 241 } 242 } else if (f instanceof MetadataResource) { 243 MetadataResource m = (MetadataResource) f; 244 cacheResource(m); 245 } 246 } 247 248 private void loadFromFileJson(InputStream stream, String name, IContextResourceLoader loader) throws IOException, FHIRException { 249 Bundle f = null; 250 try { 251 if (loader != null) 252 f = loader.loadBundle(stream, true); 253 else { 254 JsonParser json = new JsonParser(); 255 Resource r = json.parse(stream); 256 if (r instanceof Bundle) 257 f = (Bundle) r; 258 else 259 cacheResource(r); 260 } 261 } catch (FHIRFormatError e1) { 262 throw new org.hl7.fhir.exceptions.FHIRFormatError(e1.getMessage(), e1); 263 } 264 if (f != null) 265 for (BundleEntryComponent e : f.getEntry()) { 266 cacheResource(e.getResource()); 267 } 268 } 269 270 private void loadFromPack(String path, IContextResourceLoader loader) throws FileNotFoundException, IOException, FHIRException { 271 loadFromStream(new CSFileInputStream(path), loader); 272 } 273 274 public void loadFromPackage(NpmPackage pi, IContextResourceLoader loader, String... types) throws FileNotFoundException, IOException, FHIRException { 275 if (types.length == 0) 276 types = new String[] { "StructureDefinition", "ValueSet", "CodeSystem", "SearchParameter", "OperationDefinition", "Questionnaire","ConceptMap","StructureMap", "NamingSystem"}; 277 for (String s : pi.list("package")) { 278 if (s.contains("-") && s.endsWith(".json")) { 279 String t = s.substring(0, s.indexOf("-")); 280 if (Utilities.existsInList(t, types)) { 281 loadDefinitionItem(s, pi.load("package", s), loader); 282 } 283 } 284 } 285 } 286 287 public void loadFromFile(String file, IContextResourceLoader loader) throws IOException, FHIRException { 288 loadDefinitionItem(file, new CSFileInputStream(file), loader); 289 } 290 291 private void loadFromStream(InputStream stream, IContextResourceLoader loader) throws IOException, FHIRException { 292 ZipInputStream zip = new ZipInputStream(stream); 293 ZipEntry ze; 294 while ((ze = zip.getNextEntry()) != null) { 295 loadDefinitionItem(ze.getName(), zip, loader); 296 zip.closeEntry(); 297 } 298 zip.close(); 299 } 300 301 private void readVersionInfo(InputStream stream) throws IOException, DefinitionException { 302 byte[] bytes = IOUtils.toByteArray(stream); 303 binaries.put("version.info", bytes); 304 305 String[] vi = new String(bytes).split("\\r?\\n"); 306 for (String s : vi) { 307 if (s.startsWith("version=")) { 308 if (version == null) 309 version = s.substring(8); 310 else if (!version.equals(s.substring(8))) 311 throw new DefinitionException("Version mismatch. The context has version "+version+" loaded, and the new content being loaded is version "+s.substring(8)); 312 } 313 if (s.startsWith("revision=")) 314 revision = s.substring(9); 315 if (s.startsWith("date=")) 316 date = s.substring(5); 317 } 318 } 319 320 private void loadBytes(String name, InputStream stream) throws IOException { 321 byte[] bytes = IOUtils.toByteArray(stream); 322 binaries.put(name, bytes); 323 } 324 325 @Override 326 public IParser getParser(ParserType type) { 327 switch (type) { 328 case JSON: return newJsonParser(); 329 case XML: return newXmlParser(); 330 default: 331 throw new Error("Parser Type "+type.toString()+" not supported"); 332 } 333 } 334 335 @Override 336 public IParser getParser(String type) { 337 if (type.equalsIgnoreCase("JSON")) 338 return new JsonParser(); 339 if (type.equalsIgnoreCase("XML")) 340 return new XmlParser(); 341 throw new Error("Parser Type "+type.toString()+" not supported"); 342 } 343 344 @Override 345 public IParser newJsonParser() { 346 return new JsonParser(); 347 } 348 @Override 349 public IParser newXmlParser() { 350 return new XmlParser(); 351 } 352 353 @Override 354 public INarrativeGenerator getNarrativeGenerator(String prefix, String basePath) { 355 return new NarrativeGenerator(prefix, basePath, this); 356 } 357 358 @Override 359 public IResourceValidator newValidator() throws FHIRException { 360 if (validatorFactory == null) 361 throw new Error("No validator configured"); 362 return validatorFactory.makeValidator(this); 363 } 364 365 366 367 368 @Override 369 public List<String> getResourceNames() { 370 List<String> result = new ArrayList<String>(); 371 for (StructureDefinition sd : listStructures()) { 372 if (sd.getKind() == StructureDefinitionKind.RESOURCE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) 373 result.add(sd.getName()); 374 } 375 Collections.sort(result); 376 return result; 377 } 378 379 @Override 380 public List<String> getTypeNames() { 381 List<String> result = new ArrayList<String>(); 382 for (StructureDefinition sd : listStructures()) { 383 if (sd.getKind() != StructureDefinitionKind.LOGICAL && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) 384 result.add(sd.getName()); 385 } 386 Collections.sort(result); 387 return result; 388 } 389 390 @Override 391 public String getAbbreviation(String name) { 392 return "xxx"; 393 } 394 395 @Override 396 public boolean isDatatype(String typeSimple) { 397 // TODO Auto-generated method stub 398 return false; 399 } 400 401 @Override 402 public boolean isResource(String t) { 403 StructureDefinition sd; 404 try { 405 sd = fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+t); 406 } catch (Exception e) { 407 return false; 408 } 409 if (sd == null) 410 return false; 411 if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) 412 return false; 413 return sd.getKind() == StructureDefinitionKind.RESOURCE; 414 } 415 416 @Override 417 public boolean hasLinkFor(String typeSimple) { 418 return false; 419 } 420 421 @Override 422 public String getLinkFor(String corePath, String typeSimple) { 423 return null; 424 } 425 426 @Override 427 public BindingResolution resolveBinding(StructureDefinition profile, ElementDefinitionBindingComponent binding, String path) { 428 return null; 429 } 430 431 @Override 432 public String getLinkForProfile(StructureDefinition profile, String url) { 433 return null; 434 } 435 436 public Questionnaire getQuestionnaire() { 437 return questionnaire; 438 } 439 440 public void setQuestionnaire(Questionnaire questionnaire) { 441 this.questionnaire = questionnaire; 442 } 443 444 @Override 445 public Set<String> typeTails() { 446 return new HashSet<String>(Arrays.asList("Integer","UnsignedInt","PositiveInt","Decimal","DateTime","Date","Time","Instant","String","Uri","Url","Canonical","Oid","Uuid","Id","Boolean","Code","Markdown","Base64Binary","Coding","CodeableConcept","Attachment","Identifier","Quantity","SampledData","Range","Period","Ratio","HumanName","Address","ContactPoint","Timing","Reference","Annotation","Signature","Meta")); 447 } 448 449 @Override 450 public List<StructureDefinition> allStructures() { 451 List<StructureDefinition> result = new ArrayList<StructureDefinition>(); 452 Set<StructureDefinition> set = new HashSet<StructureDefinition>(); 453 for (StructureDefinition sd : listStructures()) { 454 if (!set.contains(sd)) { 455 result.add(sd); 456 set.add(sd); 457 } 458 } 459 return result; 460 } 461 462 public void loadBinariesFromFolder(String folder) throws FileNotFoundException, Exception { 463 for (String n : new File(folder).list()) { 464 loadBytes(n, new FileInputStream(Utilities.path(folder, n))); 465 } 466 } 467 468 public void loadBinariesFromFolder(NpmPackage pi) throws FileNotFoundException, Exception { 469 for (String n : pi.list("other")) { 470 loadBytes(n, pi.load("other", n)); 471 } 472 } 473 474 public void loadFromFolder(String folder) throws FileNotFoundException, Exception { 475 for (String n : new File(folder).list()) { 476 if (n.endsWith(".json")) 477 loadFromFile(Utilities.path(folder, n), new JsonParser()); 478 else if (n.endsWith(".xml")) 479 loadFromFile(Utilities.path(folder, n), new XmlParser()); 480 } 481 } 482 483 private void loadFromFile(String filename, IParser p) throws FileNotFoundException, Exception { 484 Resource r; 485 try { 486 r = p.parse(new FileInputStream(filename)); 487 if (r.getResourceType() == ResourceType.Bundle) { 488 for (BundleEntryComponent e : ((Bundle) r).getEntry()) { 489 cacheResource(e.getResource()); 490 } 491 } else { 492 cacheResource(r); 493 } 494 } catch (Exception e) { 495 return; 496 } 497 } 498 499 public Map<String, byte[]> getBinaries() { 500 return binaries; 501 } 502 503 @Override 504 public boolean prependLinks() { 505 return false; 506 } 507 508 @Override 509 public boolean hasCache() { 510 return false; 511 } 512 513 @Override 514 public String getVersion() { 515 return version+"-"+revision; 516 } 517 518 519 public List<StructureMap> findTransformsforSource(String url) { 520 List<StructureMap> res = new ArrayList<StructureMap>(); 521 for (StructureMap map : listTransforms()) { 522 boolean match = false; 523 boolean ok = true; 524 for (StructureMapStructureComponent t : map.getStructure()) { 525 if (t.getMode() == StructureMapModelMode.SOURCE) { 526 match = match || t.getUrl().equals(url); 527 ok = ok && t.getUrl().equals(url); 528 } 529 } 530 if (match && ok) 531 res.add(map); 532 } 533 return res; 534 } 535 536 public IValidatorFactory getValidatorFactory() { 537 return validatorFactory; 538 } 539 540 public void setValidatorFactory(IValidatorFactory validatorFactory) { 541 this.validatorFactory = validatorFactory; 542 } 543 544 @Override 545 protected void seeMetadataResource(MetadataResource r, Map map, boolean addId) throws FHIRException { 546 if (r instanceof StructureDefinition) { 547 StructureDefinition p = (StructureDefinition)r; 548 549 if (!p.hasSnapshot() && p.getKind() != StructureDefinitionKind.LOGICAL) { 550 if (!p.hasBaseDefinition()) 551 throw new DefinitionException("Profile "+p.getName()+" ("+p.getUrl()+") has no base and no snapshot"); 552 StructureDefinition sd = fetchResource(StructureDefinition.class, p.getBaseDefinition()); 553 if (sd == null) 554 throw new DefinitionException("Profile "+p.getName()+" ("+p.getUrl()+") base "+p.getBaseDefinition()+" could not be resolved"); 555 List<ValidationMessage> msgs = new ArrayList<ValidationMessage>(); 556 List<String> errors = new ArrayList<String>(); 557 ProfileUtilities pu = new ProfileUtilities(this, msgs, this); 558 pu.setThrowException(false); 559 pu.sortDifferential(sd, p, p.getUrl(), errors); 560 for (String err : errors) 561 msgs.add(new ValidationMessage(Source.ProfileValidator, IssueType.EXCEPTION, p.getUserString("path"), "Error sorting Differential: "+err, ValidationMessage.IssueSeverity.ERROR)); 562 pu.generateSnapshot(sd, p, p.getUrl(), p.getName()); 563 for (ValidationMessage msg : msgs) { 564 if ((!ignoreProfileErrors && msg.getLevel() == ValidationMessage.IssueSeverity.ERROR) || msg.getLevel() == ValidationMessage.IssueSeverity.FATAL) 565 throw new DefinitionException("Profile "+p.getName()+" ("+p.getUrl()+"). Error generating snapshot: "+msg.getMessage()); 566 } 567 if (!p.hasSnapshot()) 568 throw new FHIRException("Profile "+p.getName()+" ("+p.getUrl()+"). Error generating snapshot"); 569 pu = null; 570 } 571 } 572 super.seeMetadataResource(r, map, addId); 573 } 574 575 public UcumService getUcumService() { 576 return ucumService; 577 } 578 579 public void setUcumService(UcumService ucumService) { 580 this.ucumService = ucumService; 581 } 582 583 public boolean isIgnoreProfileErrors() { 584 return ignoreProfileErrors; 585 } 586 587 public void setIgnoreProfileErrors(boolean ignoreProfileErrors) { 588 this.ignoreProfileErrors = ignoreProfileErrors; 589 } 590 591 592 593 594}