001package org.hl7.fhir.validation;
002
003import java.io.ByteArrayInputStream;
004import java.io.ByteArrayOutputStream;
005import java.io.File;
006import java.io.FileInputStream;
007import java.io.IOException;
008import java.io.InputStream;
009import java.util.ArrayList;
010import java.util.Arrays;
011import java.util.HashMap;
012import java.util.List;
013import java.util.Map;
014import java.util.zip.ZipEntry;
015import java.util.zip.ZipInputStream;
016
017import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_50;
018import org.hl7.fhir.convertors.factory.VersionConvertorFactory_14_50;
019import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_50;
020import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50;
021import org.hl7.fhir.convertors.factory.VersionConvertorFactory_43_50;
022import org.hl7.fhir.exceptions.FHIRException;
023import org.hl7.fhir.r5.context.SimpleWorkerContext;
024import org.hl7.fhir.r5.elementmodel.Manager;
025import org.hl7.fhir.r5.formats.JsonParser;
026import org.hl7.fhir.r5.formats.XmlParser;
027import org.hl7.fhir.r5.model.Constants;
028import org.hl7.fhir.r5.model.ImplementationGuide;
029import org.hl7.fhir.r5.model.Resource;
030import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities;
031import org.hl7.fhir.utilities.IniFile;
032import org.hl7.fhir.utilities.SimpleHTTPClient;
033import org.hl7.fhir.utilities.SimpleHTTPClient.HTTPResult;
034import org.hl7.fhir.utilities.TextFile;
035import org.hl7.fhir.utilities.Utilities;
036import org.hl7.fhir.utilities.VersionUtilities;
037import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
038import org.hl7.fhir.utilities.npm.NpmPackage;
039import org.hl7.fhir.utilities.turtle.Turtle;
040import org.hl7.fhir.validation.cli.utils.Common;
041import org.hl7.fhir.validation.cli.utils.VersionSourceInformation;
042
043import lombok.Getter;
044
045public class IgLoader {
046
047  private static final String[] IGNORED_EXTENSIONS = {"md", "css", "js", "png", "gif", "jpg", "html", "tgz", "pack", "zip"};
048  private static final String[] EXEMPT_FILES = {"spec.internals", "version.info", "schematron.zip", "package.json"};
049  private static final int SCAN_HEADER_SIZE = 2048;
050
051  @Getter private final FilesystemPackageCacheManager packageCacheManager;
052  @Getter private final SimpleWorkerContext context;
053  @Getter private final String version;
054  @Getter private final boolean isDebug;
055
056  public IgLoader(FilesystemPackageCacheManager packageCacheManager,
057                  SimpleWorkerContext context,
058                  String theVersion) {
059      this(packageCacheManager, context, theVersion, false);
060  }
061
062  public IgLoader(FilesystemPackageCacheManager packageCacheManager,
063                  SimpleWorkerContext context,
064                  String theVersion,
065                  boolean isDebug) {
066      this.packageCacheManager = packageCacheManager;
067      this.context = context;
068      this.version = theVersion;
069      this.isDebug = isDebug;
070  }
071
072  /**
073   *
074   * @param igs
075   * @param binaries
076   * @param src Source of the IG
077   *
078   * @param recursive
079   * @throws IOException
080   * @throws FHIRException
081   *
082   * @see IgLoader#loadIgSource(String, boolean, boolean) loadIgSource for detailed description of the src parameter
083   */
084  public void loadIg(List<ImplementationGuide> igs,
085                     Map<String, byte[]> binaries,
086                     String src,
087                     boolean recursive) throws IOException, FHIRException {
088
089    final String explicitFhirVersion;
090    final String srcPackage;
091    if (src.startsWith("[") && src.indexOf(']', 1) > 1) {
092      explicitFhirVersion = src.substring(1,src.indexOf(']', 1));
093      srcPackage = src.substring(src.indexOf(']',1) + 1);
094      if (!VersionUtilities.isSupportedVersion(explicitFhirVersion)) {
095        throw new FHIRException("Unsupported FHIR Version: " + explicitFhirVersion + " valid versions are " + VersionUtilities.listSupportedVersions());
096      }
097    } else {
098      explicitFhirVersion = null;
099      srcPackage = src;
100    }
101
102    NpmPackage npm = srcPackage.matches(FilesystemPackageCacheManager.PACKAGE_VERSION_REGEX_OPT) && !new File(srcPackage).exists() ? getPackageCacheManager().loadPackage(srcPackage, null) : null;
103    if (npm != null) {
104      for (String s : npm.dependencies()) {
105        if (!getContext().getLoadedPackages().contains(s)) {
106          if (!VersionUtilities.isCorePackage(s)) {
107            loadIg(igs, binaries, s, false);
108          }
109        }
110      }
111      System.out.print("  Load " + srcPackage);
112      if (!srcPackage.contains("#")) {
113        System.out.print("#" + npm.version());
114      }
115      int count = getContext().loadFromPackage(npm, ValidatorUtils.loaderForVersion(npm.fhirVersion()));
116      System.out.println(" - " + count + " resources (" + getContext().clock().milestone() + ")");
117    } else {
118      System.out.print("  Load " + srcPackage);
119      String canonical = null;
120      int count = 0;
121      Map<String, byte[]> source = loadIgSource(srcPackage, recursive, true);
122      String version = Constants.VERSION;
123      if (getVersion() != null) {
124        version = getVersion();
125      }
126      if (source.containsKey("version.info")) {
127        version = readInfoVersion(source.get("version.info"));
128      }
129      if (explicitFhirVersion != null) {
130        version = explicitFhirVersion;
131      }
132
133      for (Map.Entry<String, byte[]> t : source.entrySet()) {
134        String fn = t.getKey();
135        if (!exemptFile(fn)) {
136          Resource r = loadFileWithErrorChecking(version, t, fn);
137          if (r != null) {
138            count++;
139            getContext().cacheResource(r);
140            if (r instanceof ImplementationGuide) {
141              canonical = ((ImplementationGuide) r).getUrl();
142              igs.add((ImplementationGuide) r);
143              if (canonical.contains("/ImplementationGuide/")) {
144                Resource r2 = r.copy();
145                ((ImplementationGuide) r2).setUrl(canonical.substring(0, canonical.indexOf("/ImplementationGuide/")));
146                getContext().cacheResource(r2);
147              }
148            }
149          }
150        }
151      }
152      if (canonical != null) {
153        ValidatorUtils.grabNatives(binaries, source, canonical);
154      }
155      System.out.println(" - " + count + " resources (" + getContext().clock().milestone() + ")");
156    }
157  }
158
159  /**
160   *
161   * @param source
162   * @param opName
163   * @param asIg
164   * @return
165   * @throws FHIRException
166   * @throws IOException
167   *
168   *    * @see IgLoader#loadIgSource(String, boolean, boolean) loadIgSource for detailed description of the src parameter
169   */
170
171  public Content loadContent(String source, String opName, boolean asIg) throws FHIRException, IOException {
172    Map<String, byte[]> s = loadIgSource(source, false, asIg);
173    Content res = new Content();
174    if (s.size() != 1)
175      throw new FHIRException("Unable to find resource " + source + " to " + opName);
176    for (Map.Entry<String, byte[]> t : s.entrySet()) {
177      res.focus = t.getValue();
178      if (t.getKey().endsWith(".json"))
179        res.cntType = Manager.FhirFormat.JSON;
180      else if (t.getKey().endsWith(".xml"))
181        res.cntType = Manager.FhirFormat.XML;
182      else if (t.getKey().endsWith(".ttl"))
183        res.cntType = Manager.FhirFormat.TURTLE;
184      else if (t.getKey().endsWith(".shc"))
185        res.cntType = Manager.FhirFormat.SHC;
186      else if (t.getKey().endsWith(".txt") || t.getKey().endsWith(".map"))
187        res.cntType = Manager.FhirFormat.TEXT;
188      else
189        throw new FHIRException("Todo: Determining resource type is not yet done");
190    }
191    return res;
192  }
193
194  /**
195   *
196   * @param src can be one of the following:
197   *      <br> - a canonical url for an ig - this will be converted to a package id and loaded into the cache
198   *      <br> - a package id for an ig - this will be loaded into the cache
199   *      <br> - a direct reference to a package ("package.tgz") - this will be extracted by the cache manager, but not put in the cache
200   *      <br> - a folder containing resources - these will be loaded directly
201   * @param recursive if true and src resolves to a folder, recursively find and load IgSources from that directory
202   * @param explore should be true if we're trying to load an -ig parameter, and false if we're loading source
203   *
204   * @return
205   * @throws FHIRException
206   * @throws IOException
207   */
208  public Map<String, byte[]> loadIgSource(String src,
209                                          boolean recursive,
210                                          boolean explore) throws FHIRException, IOException {
211    //
212    if (Common.isNetworkPath(src)) {
213      String v = null;
214      if (src.contains("|")) {
215        v = src.substring(src.indexOf("|") + 1);
216        src = src.substring(0, src.indexOf("|"));
217      }
218      String pid = explore ? getPackageCacheManager().getPackageId(src) : null;
219      if (!Utilities.noString(pid))
220        return fetchByPackage(pid + (v == null ? "" : "#" + v), false);
221      else
222        return fetchFromUrl(src + (v == null ? "" : "|" + v), explore);
223    }
224
225    File f = new File(Utilities.path(src));
226    if (f.exists()) {
227      if (f.isDirectory() && new File(Utilities.path(src, "package.tgz")).exists()) {
228        FileInputStream stream = new FileInputStream(Utilities.path(src, "package.tgz"));
229        try {
230          return loadPackage(stream, Utilities.path(src, "package.tgz"), false);
231        } finally {
232          stream.close();
233        }
234      }
235      if (f.isDirectory() && new File(Utilities.path(src, "igpack.zip")).exists()) {
236        FileInputStream stream = new FileInputStream(Utilities.path(src, "igpack.zip"));
237        try {
238          return readZip(stream);
239        } finally {
240          stream.close();
241        }
242      }
243      if (f.isDirectory() && new File(Utilities.path(src, "validator.pack")).exists()) {
244        FileInputStream stream = new FileInputStream(Utilities.path(src, "validator.pack"));
245        try {
246          return readZip(stream);
247        } finally {
248          stream.close();
249        }
250      }
251      if (f.isDirectory()) {
252        return scanDirectory(f, recursive);
253      }
254      FileInputStream stream = new FileInputStream(src);
255      try {
256        if (src.endsWith(".tgz")) {
257          Map<String, byte[]> res = loadPackage(stream, src, false);
258          return res;
259        }
260        if (src.endsWith(".pack")) {
261          return readZip(stream);
262        }
263        if (src.endsWith("igpack.zip")) {
264          return readZip(stream);
265        }
266      } finally {
267        stream.close();
268      }
269
270      Manager.FhirFormat fmt = ResourceChecker.checkIsResource(getContext(), isDebug(), TextFile.fileToBytes(f), src, true);
271      if (fmt != null) {
272        Map<String, byte[]> res = new HashMap<String, byte[]>();
273        res.put(Utilities.changeFileExt(src, "." + fmt.getExtension()), TextFile.fileToBytesNCS(src));
274        return res;
275      }
276    } else if ((src.matches(FilesystemPackageCacheManager.PACKAGE_REGEX) || src.matches(FilesystemPackageCacheManager.PACKAGE_VERSION_REGEX)) && !src.endsWith(".zip") && !src.endsWith(".tgz")) {
277      return fetchByPackage(src, false);
278    }
279    throw new FHIRException("Unable to find/resolve/read " + (explore ? "-ig " : "") + src);
280  }
281
282  public void scanForIgVersion(String src,
283                               boolean recursive,
284                               VersionSourceInformation versions) throws Exception {
285    Map<String, byte[]> source = loadIgSourceForVersion(src, recursive, true, versions);
286    if (source != null) {
287      if (source.containsKey("version.info")) {
288        versions.see(readInfoVersion(source.get("version.info")), "version.info in " + src);
289      } else if (source.size() == 1) {
290        for (byte[] v : source.values()) {
291          scanForFhirVersion(versions, src, v);
292        }
293      }
294    }
295  }
296
297  public void scanForVersions(List<String> sources, VersionSourceInformation versions) throws FHIRException, IOException {
298    List<String> refs = new ArrayList<String>();
299    ValidatorUtils.parseSources(sources, refs, context);
300    for (String ref : refs) {
301      Content cnt = loadContent(ref, "validate", false);      
302      scanForFhirVersion(versions, ref, cnt.focus);
303    }
304  }
305
306  private void scanForFhirVersion(VersionSourceInformation versions, String ref, byte[] cnt) throws IOException {
307    String s = TextFile.bytesToString(cnt.length > SCAN_HEADER_SIZE ? Arrays.copyOfRange(cnt, 0, SCAN_HEADER_SIZE) : cnt).trim();
308    try {
309      int i = s.indexOf("fhirVersion");
310      if (i > 1) {
311        boolean xml = s.charAt(i) == '<';
312        i = find(s, i, '"');
313        if (!xml) {
314          i = find(s, i+1, '"');          
315        }
316        if (i > 0) {
317          int j = find(s, i+1, '"');
318          if (j > 0) {
319            String v = s.substring(i+1, j);
320            if (VersionUtilities.isSemVer(v)) {
321              versions.see(VersionUtilities.getMajMin(v), "fhirVersion in " + ref);
322              return;
323            }
324          }
325        }
326        i = find(s, i, '\'');
327        if (!xml) {
328          i = find(s, i+1, '\'');          
329        }
330        if (i > 0) {
331          int j = find(s, i+1, '\'');
332          if (j > 0) {
333            String v = s.substring(i, j);
334            if (VersionUtilities.isSemVer(v)) {
335              versions.see(VersionUtilities.getMajMin(v), "fhirVersion in " + ref);
336              return;
337            }
338          }
339        }
340      }
341    } catch (Exception e) {
342      // nothing
343    }
344    if (s.contains("http://hl7.org/fhir/3.0")) {
345      versions.see("3.0", "Profile in " + ref);
346      return;
347    }
348    if (s.contains("http://hl7.org/fhir/1.0")) {
349      versions.see("1.0", "Profile in " + ref);
350      return;
351    }
352    if (s.contains("http://hl7.org/fhir/4.0")) {
353      versions.see("4.0", "Profile in " + ref);
354      return;
355    }
356    if (s.contains("http://hl7.org/fhir/1.4")) {
357      versions.see("1.4", "Profile in " + ref);
358      return;
359    }
360  }
361
362  private int find(String s, int i, char c) {
363    while (i < s.length() && s.charAt(i) != c) {
364      i++;
365    }
366    return i == s.length() ? -1 : i;
367  }
368
369  protected Map<String, byte[]> readZip(InputStream stream) throws IOException {
370    Map<String, byte[]> res = new HashMap<>();
371    ZipInputStream zip = new ZipInputStream(stream);
372    ZipEntry ze;
373    while ((ze = zip.getNextEntry()) != null) {
374      String name = ze.getName();
375      ByteArrayOutputStream b = new ByteArrayOutputStream();
376      int n;
377      byte[] buf = new byte[1024];
378      while ((n = ((InputStream) zip).read(buf, 0, 1024)) > -1) {
379        b.write(buf, 0, n);
380      }
381      res.put(name, b.toByteArray());
382      zip.closeEntry();
383    }
384    zip.close();
385    return res;
386  }
387
388  private String loadPackageForVersion(InputStream stream) throws FHIRException, IOException {
389    return NpmPackage.fromPackage(stream).fhirVersion();
390  }
391
392  private InputStream fetchFromUrlSpecific(String source, boolean optional) throws FHIRException, IOException {
393    try {
394      SimpleHTTPClient http = new SimpleHTTPClient();
395      HTTPResult res = http.get(source + "?nocache=" + System.currentTimeMillis());
396      res.checkThrowException();
397      return new ByteArrayInputStream(res.getContent());
398    } catch (IOException e) {
399      if (optional)
400        return null;
401      else
402        throw e;
403    }
404  }
405
406  private Map<String, byte[]> loadIgSourceForVersion(String src,
407                                                     boolean recursive,
408                                                     boolean explore,
409                                                     VersionSourceInformation versions) throws FHIRException, IOException {
410    if (Common.isNetworkPath(src)) {
411      String v = null;
412      if (src.contains("|")) {
413        v = src.substring(src.indexOf("|") + 1);
414        src = src.substring(0, src.indexOf("|"));
415      }
416      String pid = getPackageCacheManager().getPackageId(src);
417      if (!Utilities.noString(pid)) {
418        versions.see(fetchVersionByPackage(pid + (v == null ? "" : "#" + v)), "Package " + src);
419        return null;
420      } else {
421        return fetchVersionFromUrl(src + (v == null ? "" : "|" + v), explore, versions);
422      }
423    }
424
425    File f = new File(Utilities.path(src));
426    if (f.exists()) {
427      if (f.isDirectory() && new File(Utilities.path(src, "package.tgz")).exists()) {
428        versions.see(loadPackageForVersion(new FileInputStream(Utilities.path(src, "package.tgz"))), "Package " + src);
429        return null;
430      }
431      if (f.isDirectory() && new File(Utilities.path(src, "igpack.zip")).exists())
432        return readZip(new FileInputStream(Utilities.path(src, "igpack.zip")));
433      if (f.isDirectory() && new File(Utilities.path(src, "validator.pack")).exists())
434        return readZip(new FileInputStream(Utilities.path(src, "validator.pack")));
435      if (f.isDirectory())
436        return scanDirectory(f, recursive);
437      if (src.endsWith(".tgz")) {
438        versions.see(loadPackageForVersion(new FileInputStream(src)), "Package " + src);
439        return null;
440      }
441      if (src.endsWith(".pack"))
442        return readZip(new FileInputStream(src));
443      if (src.endsWith("igpack.zip"))
444        return readZip(new FileInputStream(src));
445      Manager.FhirFormat fmt = ResourceChecker.checkIsResource(getContext(), isDebug(), TextFile.fileToBytes(f), src, true);
446      if (fmt != null) {
447        Map<String, byte[]> res = new HashMap<String, byte[]>();
448        res.put(Utilities.changeFileExt(src, "." + fmt.getExtension()), TextFile.fileToBytesNCS(src));
449        return res;
450      }
451    } else if ((src.matches(FilesystemPackageCacheManager.PACKAGE_REGEX) || src.matches(FilesystemPackageCacheManager.PACKAGE_VERSION_REGEX)) && !src.endsWith(".zip") && !src.endsWith(".tgz")) {
452      versions.see(fetchVersionByPackage(src), "Package " + src);
453      return null;
454    }
455    throw new FHIRException("Unable to find/resolve/read -ig " + src);
456  }
457
458
459  private Map<String, byte[]> fetchByPackage(String src, boolean loadInContext) throws FHIRException, IOException {
460    String id = src;
461    String version = null;
462    if (src.contains("#")) {
463      id = src.substring(0, src.indexOf("#"));
464      version = src.substring(src.indexOf("#") + 1);
465    }
466    if (version == null) {
467      version = getPackageCacheManager().getLatestVersion(id);
468    }
469    NpmPackage pi;
470    if (version == null) {
471      pi = getPackageCacheManager().loadPackageFromCacheOnly(id);
472      if (pi != null)
473        System.out.println("   ... Using version " + pi.version());
474    } else
475      pi = getPackageCacheManager().loadPackageFromCacheOnly(id, version);
476    if (pi == null) {
477      return resolvePackage(id, version, loadInContext);
478    } else
479      return loadPackage(pi, loadInContext);
480  }
481
482  private Map<String, byte[]> loadPackage(InputStream stream, String name, boolean loadInContext) throws FHIRException, IOException {
483    return loadPackage(NpmPackage.fromPackage(stream), loadInContext);
484  }
485
486  public Map<String, byte[]> loadPackage(NpmPackage pi, boolean loadInContext) throws FHIRException, IOException {
487    Map<String, byte[]> res = new HashMap<String, byte[]>();
488    for (String s : pi.dependencies()) {
489      if (s.endsWith(".x") && s.length() > 2) {
490        String packageMajorMinor = s.substring(0, s.length() - 2);
491        boolean found = false;
492        for (int i = 0; i < getContext().getLoadedPackages().size() && !found; ++i) {
493          String loadedPackage = getContext().getLoadedPackages().get(i);
494          if (loadedPackage.startsWith(packageMajorMinor)) {
495            found = true;
496          }
497        }
498        if (found)
499          continue;
500      }
501      if (!getContext().getLoadedPackages().contains(s)) {
502        if (!VersionUtilities.isCorePackage(s)) {
503          System.out.println("+  .. load IG from " + s);
504          res.putAll(fetchByPackage(s, loadInContext));
505        }
506      }
507    }
508
509    if (loadInContext) {
510//      getContext().getLoadedPackages().add(pi.name() + "#" + pi.version());
511      getContext().loadFromPackage(pi, ValidatorUtils.loaderForVersion(pi.fhirVersion()));
512    }
513    for (String s : pi.listResources("CodeSystem", "ConceptMap", "ImplementationGuide", "CapabilityStatement", "SearchParameter", "Conformance", "StructureMap", "ValueSet", "StructureDefinition")) {
514      res.put(s, TextFile.streamToBytes(pi.load("package", s)));
515    }
516    String ini = "[FHIR]\r\nversion=" + pi.fhirVersion() + "\r\n";
517    res.put("version.info", ini.getBytes());
518    return res;
519  }
520
521  private Map<String, byte[]> resolvePackage(String id, String v, boolean loadInContext) throws FHIRException, IOException {
522    NpmPackage pi = getPackageCacheManager().loadPackage(id, v);
523    if (pi != null && v == null)
524      System.out.println("   ... Using version " + pi.version());
525    return loadPackage(pi,  loadInContext);
526  }
527
528  private String readInfoVersion(byte[] bs) throws IOException {
529    String is = TextFile.bytesToString(bs);
530    is = is.trim();
531    IniFile ini = new IniFile(new ByteArrayInputStream(TextFile.stringToBytes(is, false)));
532    return ini.getStringProperty("FHIR", "version");
533  }
534
535  private byte[] fetchFromUrlSpecific(String source, String contentType, boolean optional, List<String> errors) throws FHIRException, IOException {
536    try {
537      SimpleHTTPClient http = new SimpleHTTPClient();
538      try {
539        // try with cache-busting option and then try withhout in case the server doesn't support that
540        HTTPResult res = http.get(source + "?nocache=" + System.currentTimeMillis(), contentType);
541        res.checkThrowException();
542        return res.getContent();
543      } catch (Exception e) {
544        HTTPResult res = http.get(source, contentType);
545        res.checkThrowException();
546        return res.getContent();
547      }
548    } catch (IOException e) {
549      if (errors != null) {
550        errors.add("Error accessing " + source + ": " + e.getMessage());
551      }
552      if (optional)
553        return null;
554      else
555        throw e;
556    }
557  }
558
559  private Map<String, byte[]> fetchVersionFromUrl(String src,
560                                                  boolean explore,
561                                                  VersionSourceInformation versions) throws FHIRException, IOException {
562    if (src.endsWith(".tgz")) {
563      versions.see(loadPackageForVersion(fetchFromUrlSpecific(src, false)), "From Package " + src);
564      return null;
565    }
566    if (src.endsWith(".pack"))
567      return readZip(fetchFromUrlSpecific(src, false));
568    if (src.endsWith("igpack.zip"))
569      return readZip(fetchFromUrlSpecific(src, false));
570
571    InputStream stream = null;
572    if (explore) {
573      stream = fetchFromUrlSpecific(Utilities.pathURL(src, "package.tgz"), true);
574      if (stream != null) {
575        versions.see(loadPackageForVersion(stream), "From Package at " + src);
576        return null;
577      }
578      // todo: these options are deprecated - remove once all IGs have been rebuilt post R4 technical correction
579      stream = fetchFromUrlSpecific(Utilities.pathURL(src, "igpack.zip"), true);
580      if (stream != null)
581        return readZip(stream);
582      stream = fetchFromUrlSpecific(Utilities.pathURL(src, "validator.pack"), true);
583      if (stream != null)
584        return readZip(stream);
585      stream = fetchFromUrlSpecific(Utilities.pathURL(src, "validator.pack"), true);
586      //// -----
587    }
588
589    // ok, having tried all that... now we'll just try to access it directly
590    byte[] cnt;
591    if (stream == null)
592      cnt = fetchFromUrlSpecific(src, "application/json", true, null);
593    else
594      cnt = TextFile.streamToBytes(stream);
595
596    Manager.FhirFormat fmt = ResourceChecker.checkIsResource(getContext(), isDebug(), cnt, src, true);
597    if (fmt != null) {
598      Map<String, byte[]> res = new HashMap<String, byte[]>();
599      res.put(Utilities.changeFileExt(src, "." + fmt.getExtension()), cnt);
600      return res;
601    }
602    String fn = Utilities.path("[tmp]", "fetch-resource-error-content.bin");
603    TextFile.bytesToFile(cnt, fn);
604    System.out.println("Error Fetching " + src);
605    System.out.println("Some content was found, saved to " + fn);
606    System.out.println("1st 100 bytes = " + presentForDebugging(cnt));
607    throw new FHIRException("Unable to find/resolve/read " + (explore ? "-ig " : "") + src);
608  }
609
610  private String fetchVersionByPackage(String src) throws FHIRException, IOException {
611    String id = src;
612    String version = null;
613    if (src.contains("#")) {
614      id = src.substring(0, src.indexOf("#"));
615      version = src.substring(src.indexOf("#") + 1);
616    }
617    if (version == null) {
618      version = getPackageCacheManager().getLatestVersion(id);
619    }
620    NpmPackage pi = null;
621    if (version == null) {
622      pi = getPackageCacheManager().loadPackageFromCacheOnly(id);
623      if (pi != null)
624        System.out.println("   ... Using version " + pi.version());
625    } else
626      pi = getPackageCacheManager().loadPackageFromCacheOnly(id, version);
627    if (pi == null) {
628      return resolvePackageForVersion(id, version);
629    } else {
630      return pi.fhirVersion();
631    }
632  }
633
634  private Map<String, byte[]> fetchFromUrl(String src, boolean explore) throws FHIRException, IOException {
635    if (src.endsWith(".tgz"))
636      return loadPackage(fetchFromUrlSpecific(src, false), src, false);
637    if (src.endsWith(".pack"))
638      return readZip(fetchFromUrlSpecific(src, false));
639    if (src.endsWith("igpack.zip"))
640      return readZip(fetchFromUrlSpecific(src, false));
641
642    InputStream stream = null;
643    if (explore) {
644      stream = fetchFromUrlSpecific(Utilities.pathURL(src, "package.tgz"), true);
645      if (stream != null)
646        return loadPackage(stream, Utilities.pathURL(src, "package.tgz"), false);
647      // todo: these options are deprecated - remove once all IGs have been rebuilt post R4 technical correction
648      stream = fetchFromUrlSpecific(Utilities.pathURL(src, "igpack.zip"), true);
649      if (stream != null)
650        return readZip(stream);
651      stream = fetchFromUrlSpecific(Utilities.pathURL(src, "validator.pack"), true);
652      if (stream != null)
653        return readZip(stream);
654      stream = fetchFromUrlSpecific(Utilities.pathURL(src, "validator.pack"), true);
655      //// -----
656    }
657
658    // ok, having tried all that... now we'll just try to access it directly
659    byte[] cnt;
660    List<String> errors = new ArrayList<>();
661    if (stream != null) {
662      cnt = TextFile.streamToBytes(stream);
663    } else {
664      cnt = fetchFromUrlSpecific(src, "application/json", true, errors);
665      if (cnt == null) {
666        cnt = fetchFromUrlSpecific(src, "application/xml", true, errors);
667      }
668    }
669    if (cnt == null) {
670      throw new FHIRException("Unable to fetch content from " + src + " (" + errors.toString() + ")");
671
672    }
673    Manager.FhirFormat fmt = checkFormat(cnt, src);
674    if (fmt != null) {
675      Map<String, byte[]> res = new HashMap<>();
676      res.put(Utilities.changeFileExt(src, "." + fmt.getExtension()), cnt);
677      return res;
678    }
679    throw new FHIRException("Unable to read content from " + src + ": cannot determine format");
680  }
681
682  private boolean isIgnoreFile(File ff) {
683    if (ff.getName().startsWith(".") || ff.getAbsolutePath().contains(".git")) {
684      return true;
685    }
686    return Utilities.existsInList(Utilities.getFileExtension(ff.getName()).toLowerCase(), IGNORED_EXTENSIONS);
687  }
688
689  private Map<String, byte[]> scanDirectory(File f, boolean recursive) throws IOException {
690    Map<String, byte[]> res = new HashMap<>();
691    for (File ff : f.listFiles()) {
692      if (ff.isDirectory() && recursive) {
693        res.putAll(scanDirectory(ff, true));
694      } else if (!ff.isDirectory() && !isIgnoreFile(ff)) {
695        Manager.FhirFormat fmt = ResourceChecker.checkIsResource(getContext(), isDebug(), TextFile.fileToBytes(ff), ff.getAbsolutePath(), true);
696        if (fmt != null) {
697          res.put(Utilities.changeFileExt(ff.getName(), "." + fmt.getExtension()), TextFile.fileToBytes(ff.getAbsolutePath()));
698        }
699      }
700    }
701    return res;
702  }
703
704  private String resolvePackageForVersion(String id, String v) throws FHIRException, IOException {
705    NpmPackage pi = getPackageCacheManager().loadPackage(id, v);
706    return pi.fhirVersion();
707  }
708
709  private String presentForDebugging(byte[] cnt) {
710    StringBuilder b = new StringBuilder();
711    for (int i = 0; i < Integer.min(cnt.length, 50); i++) {
712      b.append(Integer.toHexString(cnt[i]));
713    }
714    return b.toString();
715  }
716
717  private Manager.FhirFormat checkFormat(byte[] cnt, String filename) {
718    System.out.println("   ..Detect format for " + filename);
719    try {
720      org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(cnt);
721      return Manager.FhirFormat.JSON;
722    } catch (Exception e) {
723      log("Not JSON: " + e.getMessage());
724    }
725    try {
726      ValidatorUtils.parseXml(cnt);
727      return Manager.FhirFormat.XML;
728    } catch (Exception e) {
729      log("Not XML: " + e.getMessage());
730    }
731    try {
732      new Turtle().parse(TextFile.bytesToString(cnt));
733      return Manager.FhirFormat.TURTLE;
734    } catch (Exception e) {
735      log("Not Turtle: " + e.getMessage());
736    }
737    try {
738      new StructureMapUtilities(getContext(), null, null).parse(TextFile.bytesToString(cnt), null);
739      return Manager.FhirFormat.TEXT;
740    } catch (Exception e) {
741      log("Not Text: " + e.getMessage());
742    }
743    log("     .. not a resource: " + filename);
744    return null;
745  }
746
747  private boolean exemptFile(String fn) {
748    return Utilities.existsInList(fn, EXEMPT_FILES);
749  }
750
751  protected Resource loadFileWithErrorChecking(String version, Map.Entry<String, byte[]> t, String fn) {
752    log("* load file: " + fn);
753    Resource r = null;
754    try {
755      r = loadResourceByVersion(version, t.getValue(), fn);
756      log(" .. success");
757    } catch (Exception e) {
758      if (!isDebug()) {
759        System.out.print("* load file: " + fn);
760      }
761      System.out.println(" - ignored due to error: " + (e.getMessage() == null ? " (null - NPE)" : e.getMessage()));
762      if (isDebug() || ((e.getMessage() != null && e.getMessage().contains("cannot be cast")))) {
763        e.printStackTrace();
764      }
765    }
766    return r;
767  }
768
769  public Resource loadResourceByVersion(String fhirVersion, byte[] content, String fn) throws IOException, FHIRException {
770    Resource r;
771    if (fhirVersion.startsWith("3.0")) {
772      org.hl7.fhir.dstu3.model.Resource res;
773      if (fn.endsWith(".xml") && !fn.endsWith("template.xml"))
774        res = new org.hl7.fhir.dstu3.formats.XmlParser().parse(new ByteArrayInputStream(content));
775      else if (fn.endsWith(".json") && !fn.endsWith("template.json"))
776        res = new org.hl7.fhir.dstu3.formats.JsonParser().parse(new ByteArrayInputStream(content));
777      else if (fn.endsWith(".txt") || fn.endsWith(".map"))
778        res = new org.hl7.fhir.dstu3.utils.StructureMapUtilities(null).parse(new String(content));
779      else
780        throw new FHIRException("Unsupported format for " + fn);
781      r = VersionConvertorFactory_30_50.convertResource(res);
782    } else if (fhirVersion.startsWith("4.0")) {
783      org.hl7.fhir.r4.model.Resource res;
784      if (fn.endsWith(".xml") && !fn.endsWith("template.xml"))
785        res = new org.hl7.fhir.r4.formats.XmlParser().parse(new ByteArrayInputStream(content));
786      else if (fn.endsWith(".json") && !fn.endsWith("template.json"))
787        res = new org.hl7.fhir.r4.formats.JsonParser().parse(new ByteArrayInputStream(content));
788      else if (fn.endsWith(".txt") || fn.endsWith(".map"))
789        res = new org.hl7.fhir.r4.utils.StructureMapUtilities(null).parse(new String(content), fn);
790      else
791        throw new FHIRException("Unsupported format for " + fn);
792      r = VersionConvertorFactory_40_50.convertResource(res);
793    } else if (fhirVersion.startsWith("4.3")) {
794      org.hl7.fhir.r4b.model.Resource res;
795      if (fn.endsWith(".xml") && !fn.endsWith("template.xml"))
796        res = new org.hl7.fhir.r4b.formats.XmlParser().parse(new ByteArrayInputStream(content));
797      else if (fn.endsWith(".json") && !fn.endsWith("template.json"))
798        res = new org.hl7.fhir.r4b.formats.JsonParser().parse(new ByteArrayInputStream(content));
799      else if (fn.endsWith(".txt") || fn.endsWith(".map"))
800        res = new org.hl7.fhir.r4b.utils.structuremap.StructureMapUtilities(null).parse(new String(content), fn);
801      else
802        throw new FHIRException("Unsupported format for " + fn);
803      r = VersionConvertorFactory_43_50.convertResource(res);
804    } else if (fhirVersion.startsWith("1.4")) {
805      org.hl7.fhir.dstu2016may.model.Resource res;
806      if (fn.endsWith(".xml") && !fn.endsWith("template.xml"))
807        res = new org.hl7.fhir.dstu2016may.formats.XmlParser().parse(new ByteArrayInputStream(content));
808      else if (fn.endsWith(".json") && !fn.endsWith("template.json"))
809        res = new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(new ByteArrayInputStream(content));
810      else
811        throw new FHIRException("Unsupported format for " + fn);
812      r = VersionConvertorFactory_14_50.convertResource(res);
813    } else if (fhirVersion.startsWith("1.0")) {
814      org.hl7.fhir.dstu2.model.Resource res;
815      if (fn.endsWith(".xml") && !fn.endsWith("template.xml"))
816        res = new org.hl7.fhir.dstu2.formats.JsonParser().parse(new ByteArrayInputStream(content));
817      else if (fn.endsWith(".json") && !fn.endsWith("template.json"))
818        res = new org.hl7.fhir.dstu2.formats.JsonParser().parse(new ByteArrayInputStream(content));
819      else
820        throw new FHIRException("Unsupported format for " + fn);
821      r = VersionConvertorFactory_10_50.convertResource(res, new org.hl7.fhir.convertors.misc.IGR2ConvertorAdvisor5());
822    } else if (fhirVersion.startsWith("5.0")) {
823      if (fn.endsWith(".xml") && !fn.endsWith("template.xml"))
824        r = new XmlParser().parse(new ByteArrayInputStream(content));
825      else if (fn.endsWith(".json") && !fn.endsWith("template.json"))
826        r = new JsonParser().parse(new ByteArrayInputStream(content));
827      else if (fn.endsWith(".txt"))
828        r = new StructureMapUtilities(getContext(), null, null).parse(TextFile.bytesToString(content), fn);
829      else if (fn.endsWith(".map"))
830        r = new StructureMapUtilities(null).parse(new String(content), fn);
831      else
832        throw new FHIRException("Unsupported format for " + fn);
833    } else
834      throw new FHIRException("Unsupported version " + fhirVersion);
835    return r;
836  }
837
838  private void log(String s) {
839    if (isDebug()) System.out.println(s);
840  }
841}