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