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}