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