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}