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}