001package org.hl7.fhir.validation;
002
003import lombok.Getter;
004import lombok.Setter;
005import lombok.experimental.Accessors;
006
007import org.fhir.ucum.UcumEssenceService;
008import org.fhir.ucum.UcumException;
009import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_50;
010import org.hl7.fhir.convertors.factory.VersionConvertorFactory_14_50;
011import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_50;
012import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50;
013import org.hl7.fhir.convertors.txClient.TerminologyClientFactory;
014import org.hl7.fhir.exceptions.DefinitionException;
015import org.hl7.fhir.exceptions.FHIRException;
016import org.hl7.fhir.r5.conformance.ProfileUtilities;
017import org.hl7.fhir.r5.context.IWorkerContext.ICanonicalResourceLocator;
018import org.hl7.fhir.r5.context.IWorkerContext.PackageVersion;
019import org.hl7.fhir.r5.context.SimpleWorkerContext;
020import org.hl7.fhir.r5.elementmodel.Element;
021import org.hl7.fhir.r5.elementmodel.Manager;
022import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
023import org.hl7.fhir.r5.elementmodel.ObjectConverter;
024import org.hl7.fhir.r5.elementmodel.SHCParser;
025import org.hl7.fhir.r5.formats.FormatUtilities;
026import org.hl7.fhir.r5.formats.IParser.OutputStyle;
027import org.hl7.fhir.r5.formats.JsonParser;
028import org.hl7.fhir.r5.formats.XmlParser;
029import org.hl7.fhir.r5.model.*;
030import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent;
031import org.hl7.fhir.r5.model.NamingSystem.NamingSystemIdentifierType;
032import org.hl7.fhir.r5.model.NamingSystem.NamingSystemUniqueIdComponent;
033import org.hl7.fhir.r5.renderers.RendererFactory;
034import org.hl7.fhir.r5.renderers.utils.RenderingContext;
035import org.hl7.fhir.r5.renderers.utils.RenderingContext.ResourceRendererMode;
036import org.hl7.fhir.r5.utils.EOperationOutcome;
037import org.hl7.fhir.r5.utils.FHIRPathEngine;
038import org.hl7.fhir.r5.utils.validation.BundleValidationRule;
039import org.hl7.fhir.r5.utils.validation.IResourceValidator;
040import org.hl7.fhir.r5.utils.ToolingExtensions;
041import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities;
042import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor;
043import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher;
044import org.hl7.fhir.r5.utils.validation.constants.*;
045import org.hl7.fhir.utilities.TimeTracker;
046import org.hl7.fhir.utilities.*;
047import org.hl7.fhir.utilities.SimpleHTTPClient.HTTPResult;
048import org.hl7.fhir.utilities.npm.CommonPackages;
049import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
050import org.hl7.fhir.utilities.npm.NpmPackage;
051import org.hl7.fhir.utilities.npm.ToolsVersion;
052import org.hl7.fhir.utilities.validation.ValidationMessage;
053import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
054import org.hl7.fhir.validation.BaseValidator.ValidationControl;
055import org.hl7.fhir.validation.cli.services.IPackageInstaller;
056import org.hl7.fhir.validation.cli.utils.*;
057import org.hl7.fhir.validation.instance.InstanceValidator;
058import org.hl7.fhir.validation.instance.utils.ValidatorHostContext;
059import org.xml.sax.SAXException;
060
061import java.io.*;
062import java.net.URISyntaxException;
063import java.util.*;
064
065/*
066Copyright (c) 2011+, HL7, Inc
067All rights reserved.
068
069Redistribution and use in source and binary forms, with or without modification,
070are permitted provided that the following conditions are met:
071
072 * Redistributions of source code must retain the above copyright notice, this
073   list of conditions and the following disclaimer.
074 * Redistributions in binary form must reproduce the above copyright notice,
075   this list of conditions and the following disclaimer in the documentation
076   and/or other materials provided with the distribution.
077 * Neither the name of HL7 nor the names of its contributors may be used to
078   endorse or promote products derived from this software without specific
079   prior written permission.
080
081THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
082ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
083WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
084IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
085INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
086NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
087PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
088WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
089ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
090POSSIBILITY OF SUCH DAMAGE.
091
092*/
093
094/**
095 * This is just a wrapper around the InstanceValidator class for convenient use
096 * <p>
097 * The following resource formats are supported: XML, JSON, Turtle
098 * The following versions are supported: 1.0.2, 1.4.0, 3.0.2, 4.0.1, and current
099 * <p>
100 * Note: the validation engine is intended to be threadsafe
101 * To Use:
102 * <p>
103 * 1/ Initialize
104 * ValidationEngine validator = new ValidationEngine(src);
105 * - this must be the packageId of the relevant core specification
106 * for the version you want to validate against (e.g. hl7.fhir.r4.core)
107 * <p>
108 * validator.connectToTSServer(txServer);
109 * - this is optional; in the absence of a terminology service, snomed, loinc etc will not be validated
110 * <p>
111 * validator.loadIg(src);
112 * - call this any number of times for the Implementation Guide(s) of interest.
113 * - See https://confluence.hl7.org/display/FHIR/Using+the+FHIR+Validator for documentation about the src parameter (-ig parameter)
114 * <p>
115 * validator.loadQuestionnaire(src)
116 * - url or filename of a questionnaire to load. Any loaded questionnaires will be used while validating
117 * <p>
118 * validator.setNative(doNative);
119 * - whether to do xml/json/rdf schema validation as well
120 * <p>
121 * You only need to do this initialization once. You can validate as many times as you like
122 * <p>
123 * 2. validate
124 * validator.validate(src, profiles);
125 * - source (as stream, byte[]), or url or filename of a resource to validate.
126 * Also validate against any profiles (as canonical URLS, equivalent to listing them in Resource.meta.profile)
127 * <p>
128 * if the source is provided as byte[] or stream, you need to provide a format too, though you can
129 * leave that as null, and the validator will guess
130 * <p>
131 * 3. Or, instead of validating, transform (see documentation and use in Validator.java)
132 *
133 * @author Grahame Grieve
134 */
135@Accessors(chain = true)
136public class ValidationEngine implements IValidatorResourceFetcher, IValidationPolicyAdvisor, IPackageInstaller {
137
138  @Getter @Setter private SimpleWorkerContext context;
139  @Getter @Setter private Map<String, byte[]> binaries = new HashMap<>();
140  @Getter @Setter private boolean doNative;
141  @Getter @Setter private boolean noInvariantChecks;
142  @Getter @Setter private boolean wantInvariantInMessage;
143  @Getter @Setter private boolean hintAboutNonMustSupport;
144  @Getter @Setter private boolean anyExtensionsAllowed = false;
145  @Getter @Setter private String version;
146  @Getter @Setter private String language;
147  @Setter private FilesystemPackageCacheManager pcm;
148  @Getter @Setter private PrintWriter mapLog;
149  @Getter @Setter private boolean debug = false;
150  @Getter @Setter private IValidatorResourceFetcher fetcher;
151  @Getter @Setter private IValidationPolicyAdvisor policyAdvisor;
152  @Getter @Setter private ICanonicalResourceLocator locator;
153  @Getter @Setter private boolean assumeValidRestReferences;
154  @Getter @Setter private boolean noExtensibleBindingMessages;
155  @Getter @Setter private boolean noUnicodeBiDiControlChars;
156  @Getter @Setter private boolean securityChecks;
157  @Getter @Setter private boolean crumbTrails;
158  @Getter @Setter private boolean allowExampleUrls;
159  @Getter @Setter private boolean showMessagesFromReferences;
160  @Getter @Setter private Locale locale;
161  @Getter @Setter private List<ImplementationGuide> igs = new ArrayList<>();
162  @Getter @Setter private boolean showTimes;
163  @Getter @Setter private List<BundleValidationRule> bundleValidationRules = new ArrayList<>();
164  @Getter @Setter private QuestionnaireMode questionnaireMode;
165  @Getter @Setter private ValidationLevel level = ValidationLevel.HINTS;
166  @Getter @Setter private FHIRPathEngine fhirPathEngine;
167  @Getter @Setter private IgLoader igLoader;
168
169  /**
170   * Systems that host the ValidationEngine can use this to control what validation the validator performs.
171   * <p>
172   * Using this, you can turn particular kinds of validation on and off. In addition, you can override
173   * the error | warning | hint level and make it a different level.
174   * <p>
175   * Each entry has
176   * * 'allowed': a boolean flag. if this is false, the Validator will not report the error.
177   * * 'level' : set to error, warning, information
178   * <p>
179   * Entries are registered by ID, using the IDs in /org.hl7.fhir.utilities/src/main/resources/Messages.properties
180   * <p>
181   * This feature is not supported by the validator CLI - and won't be. It's for systems hosting
182   * the validation framework in their own implementation context
183   */
184  @Getter @Setter private Map<String, ValidationControl> validationControl = new HashMap<>();
185
186  public ValidationEngine() throws IOException {
187    setContext(SimpleWorkerContext.fromNothing());
188    initContext(null);
189    igLoader = new IgLoader(getPcm(), getContext(), getVersion(), isDebug());
190  }
191
192  public ValidationEngine(String src) throws FHIRException, IOException {
193    loadCoreDefinitions(src, false, null);
194    igLoader = new IgLoader(getPcm(), getContext(), getVersion(), isDebug());
195  }
196
197  public ValidationEngine(String src, String txsrvr, String txLog, FhirPublication version, boolean canRunWithoutTerminologyServer, String vString, String userAgent) throws FHIRException, IOException, URISyntaxException {
198    loadCoreDefinitions(src, false, null);
199    getContext().setUserAgent(userAgent);
200    getContext().setCanRunWithoutTerminology(canRunWithoutTerminologyServer);
201    setTerminologyServer(txsrvr, txLog, version);
202    setVersion(vString);
203    igLoader = new IgLoader(getPcm(), getContext(), getVersion(), isDebug());
204  }
205
206  public ValidationEngine(String src, String txsrvr, String txLog, FhirPublication version, String vString, String userAgent) throws FHIRException, IOException, URISyntaxException {
207    loadCoreDefinitions(src, false, null);
208    getContext().setUserAgent(userAgent);
209    setTerminologyServer(txsrvr, txLog, version);
210    setVersion(vString);
211    igLoader = new IgLoader(getPcm(), getContext(), getVersion(), isDebug());
212  }
213
214  public ValidationEngine(String src, String vString, TimeTracker tt, String userAgent) throws FHIRException, IOException, URISyntaxException {
215    loadCoreDefinitions(src, false, tt);
216    getContext().setUserAgent(userAgent);
217    setVersion(vString);
218    igLoader = new IgLoader(getPcm(), getContext(), getVersion(), isDebug());
219  }
220
221  private void loadCoreDefinitions(String src, boolean recursive, TimeTracker tt) throws FHIRException, IOException {
222    NpmPackage npm = getPcm().loadPackage(src, null);
223    if (npm != null) {
224      version = npm.fhirVersion();
225      context = SimpleWorkerContext.fromPackage(npm, ValidatorUtils.loaderForVersion(version));
226    } else {
227      Map<String, byte[]> source = igLoader.loadIgSource(src, recursive, true);
228      if (version == null) {
229        version = getVersionFromPack(source);
230      }
231      context = SimpleWorkerContext.fromDefinitions(source, ValidatorUtils.loaderForVersion(version), new PackageVersion(src));
232      ValidatorUtils.grabNatives(getBinaries(), source, "http://hl7.org/fhir");
233    }
234    // ucum-essence.xml should be in the class path. if it's not, ask about how to sort this out 
235    // on https://chat.fhir.org/#narrow/stream/179167-hapi
236    try {
237      ClassLoader classLoader = ValidationEngine.class.getClassLoader();
238      InputStream ue = classLoader.getResourceAsStream("ucum-essence.xml");
239      context.setUcumService(new UcumEssenceService(ue));
240    } catch (Exception e) {
241      throw new FHIRException("Error loading UCUM from embedded ucum-essence.xml: "+e.getMessage(), e);
242    }
243    initContext(tt);
244  }
245
246  public void initContext(TimeTracker tt) throws IOException {
247    context.setCanNoTS(true);
248    context.setCacheId(UUID.randomUUID().toString());
249    context.setAllowLoadingDuplicates(true); // because of Forge
250    context.setExpansionProfile(makeExpProfile());
251    if (tt != null) {
252      context.setClock(tt);
253    }
254    NpmPackage npmX = getPcm().loadPackage(CommonPackages.ID_XVER, CommonPackages.VER_XVER);
255    context.loadFromPackage(npmX, null);
256
257    this.fhirPathEngine = new FHIRPathEngine(context);
258  }
259
260  private String getVersionFromPack(Map<String, byte[]> source) {
261    if (source.containsKey("version.info")) {
262      IniFile vi = new IniFile(new ByteArrayInputStream(removeBom(source.get("version.info"))));
263      return vi.getStringProperty("FHIR", "version");
264    } else {
265      throw new Error("Missing version.info?");
266    }
267  }
268
269  private byte[] removeBom(byte[] bs) {
270    if (bs.length > 3 && bs[0] == -17 && bs[1] == -69 && bs[2] == -65)
271      return Arrays.copyOfRange(bs, 3, bs.length);
272    else
273      return bs;
274  }
275
276  private Parameters makeExpProfile() {
277    Parameters ep = new Parameters();
278    ep.addParameter("profile-url", "http://hl7.org/fhir/ExpansionProfile/dc8fd4bc-091a-424a-8a3b-6198ef146891"); // change this to blow the cache
279    // all defaults....
280    return ep;
281  }
282
283  public String connectToTSServer(String url, String log, FhirPublication version) throws URISyntaxException, FHIRException {
284    context.setTlogging(false);
285    if (url == null) {
286      context.setCanRunWithoutTerminology(true);
287      context.setNoTerminologyServer(true);
288      return "n/a: No Terminology Server";
289    } else {
290      try {
291        return context.connectToTSServer(TerminologyClientFactory.makeClient(url, context.getUserAgent(), version), log);
292      } catch (Exception e) {
293        if (context.isCanRunWithoutTerminology()) {
294          return "n/a: Running without Terminology Server (error: " + e.getMessage() + ")";
295        } else
296          throw e;
297      }
298    }
299  }
300
301  public void loadProfile(String src) throws FHIRException, IOException {
302    if (context.hasResource(StructureDefinition.class, src))
303      return;
304    if (context.hasResource(ImplementationGuide.class, src))
305      return;
306
307    byte[] source = ProfileLoader.loadProfileSource(src);
308    FhirFormat fmt = FormatUtilities.determineFormat(source);
309    Resource r = FormatUtilities.makeParser(fmt).parse(source);
310    context.cacheResource(r);
311  }
312
313  // testing entry point
314  public OperationOutcome validate(FhirFormat format, InputStream stream, List<String> profiles) throws FHIRException, IOException, EOperationOutcome {
315    List<ValidationMessage> messages = new ArrayList<ValidationMessage>();
316    InstanceValidator validator = getValidator(format);
317    validator.validate(null, messages, stream, format, asSdList(profiles));
318    return ValidatorUtils.messagesToOutcome(messages, context, fhirPathEngine);
319  }
320
321  public List<StructureDefinition> asSdList(List<String> profiles) throws Error {
322    List<StructureDefinition> list = new ArrayList<>();
323    if (profiles != null) {
324      for (String p : profiles) {
325        StructureDefinition sd = context.fetchResource(StructureDefinition.class, p);
326        if (sd == null) {
327          throw new Error("Unable to resolve profile " + p);
328        }
329        list.add(sd);
330      }
331    }
332    return list;
333  }
334
335  public OperationOutcome validate(String source, List<String> profiles) throws FHIRException, IOException {
336    List<String> l = new ArrayList<String>();
337    l.add(source);
338    return (OperationOutcome) validate(l, profiles, null);
339  }
340
341  public Resource validate(List<String> sources, List<String> profiles, List<ValidationRecord> record) throws FHIRException, IOException {
342    if (profiles.size() > 0) {
343      System.out.println("  Profiles: " + profiles);
344    }
345    List<String> refs = new ArrayList<String>();
346    boolean asBundle = ValidatorUtils.parseSources(sources, refs, context);
347    Bundle results = new Bundle();
348    results.setType(Bundle.BundleType.COLLECTION);
349    for (String ref : refs) {
350      TimeTracker.Session tts = context.clock().start("validation");
351      context.clock().milestone();
352      System.out.print("  Validate " + ref);
353      Content cnt = igLoader.loadContent(ref, "validate", false);
354      try {
355        OperationOutcome outcome = validate(ref, cnt.focus, cnt.cntType, profiles, record);
356        ToolingExtensions.addStringExtension(outcome, ToolingExtensions.EXT_OO_FILE, ref);
357        System.out.println(" " + context.clock().milestone());
358        results.addEntry().setResource(outcome);
359        tts.end();
360      } catch (Exception e) {
361        System.out.println("Validation Infrastructure fail validating " + ref + ": " + e.getMessage());
362        tts.end();
363        throw new FHIRException(e);
364      }
365    }
366    if (asBundle)
367      return results;
368    else
369      return results.getEntryFirstRep().getResource();
370  }
371
372  public OperationOutcome validate(byte[] source, FhirFormat cntType, List<String> profiles, List<ValidationMessage> messages) throws FHIRException, IOException, EOperationOutcome {
373    InstanceValidator validator = getValidator(cntType);
374
375    validator.validate(null, messages, new ByteArrayInputStream(source), cntType, asSdList(profiles));
376    return ValidatorUtils.messagesToOutcome(messages, context, fhirPathEngine);
377  }
378
379  public OperationOutcome validate(String location, byte[] source, FhirFormat cntType, List<String> profiles, List<ValidationRecord> record) throws FHIRException, IOException, EOperationOutcome, SAXException {
380    List<ValidationMessage> messages = new ArrayList<ValidationMessage>();
381    if (doNative) {
382      SchemaValidator.validateSchema(location, cntType, messages);
383    }
384    InstanceValidator validator = getValidator(cntType);
385    validator.validate(null, messages, new ByteArrayInputStream(source), cntType, asSdList(profiles));
386    if (showTimes) {
387      System.out.println(location + ": " + validator.reportTimes());
388    }
389    if (record != null) {
390      record.add(new ValidationRecord(location, messages));
391    }
392    return ValidatorUtils.messagesToOutcome(messages, context, fhirPathEngine);
393  }
394
395  public OperationOutcome validate(String location, byte[] source, FhirFormat cntType, List<String> profiles, IdStatus resourceIdRule, boolean anyExtensionsAllowed, BestPracticeWarningLevel bpWarnings, CheckDisplayOption displayOption) throws FHIRException, IOException, EOperationOutcome, SAXException {
396    List<ValidationMessage> messages = new ArrayList<ValidationMessage>();
397    if (doNative) {
398      SchemaValidator.validateSchema(location, cntType, messages);
399    }
400    InstanceValidator validator = getValidator(cntType);
401    validator.setResourceIdRule(resourceIdRule);
402    validator.setBestPracticeWarningLevel(bpWarnings);
403    validator.setCheckDisplay(displayOption);
404    validator.validate(null, messages, new ByteArrayInputStream(source), cntType, asSdList(profiles));
405    return ValidatorUtils.messagesToOutcome(messages, context, fhirPathEngine);
406  }
407
408  public org.hl7.fhir.r5.elementmodel.Element transform(String source, String map) throws FHIRException, IOException {
409    Content cnt = igLoader.loadContent(source, "validate", false);
410    return transform(cnt.focus, cnt.cntType, map);
411  }
412
413  public org.hl7.fhir.r5.elementmodel.Element transform(byte[] source, FhirFormat cntType, String mapUri) throws FHIRException, IOException {
414    List<Base> outputs = new ArrayList<>();
415    StructureMapUtilities scu = new StructureMapUtilities(context, new TransformSupportServices(outputs, mapLog, context));
416    org.hl7.fhir.r5.elementmodel.Element src = Manager.parseSingle(context, new ByteArrayInputStream(source), cntType);
417    StructureMap map = context.getTransform(mapUri);
418    if (map == null) throw new Error("Unable to find map " + mapUri + " (Known Maps = " + context.listMapUrls() + ")");
419    org.hl7.fhir.r5.elementmodel.Element resource = getTargetResourceFromStructureMap(map);
420    scu.transform(null, src, map, resource);
421    return resource;
422  }
423
424  private org.hl7.fhir.r5.elementmodel.Element getTargetResourceFromStructureMap(StructureMap map) {
425    String targetTypeUrl = null;
426    for (StructureMap.StructureMapStructureComponent component : map.getStructure()) {
427      if (component.getMode() == StructureMap.StructureMapModelMode.TARGET) {
428        targetTypeUrl = component.getUrl();
429        break;
430      }
431    }
432
433    if (targetTypeUrl == null) throw new FHIRException("Unable to determine resource URL for target type");
434
435    StructureDefinition structureDefinition = null;
436    for (StructureDefinition sd : this.context.getStructures()) {
437      if (sd.getUrl().equalsIgnoreCase(targetTypeUrl)) {
438        structureDefinition = sd;
439        break;
440      }
441    }
442
443    if (structureDefinition == null) throw new FHIRException("Unable to find StructureDefinition for target type ('" + targetTypeUrl + "')");
444
445    return Manager.build(getContext(), structureDefinition);
446  }
447
448  public Resource generate(String source, String version) throws FHIRException, IOException, EOperationOutcome {
449    Content cnt = igLoader.loadContent(source, "validate", false);
450    Resource res = igLoader.loadResourceByVersion(version, cnt.focus, source);
451    RenderingContext rc = new RenderingContext(context, null, null, "http://hl7.org/fhir", "", null, ResourceRendererMode.END_USER);
452    genResource(res, rc);
453    return (Resource) res;
454  }
455
456  public void genResource(Resource res, RenderingContext rc) throws IOException, EOperationOutcome {
457    if (res instanceof Bundle) {
458      Bundle bnd = (Bundle) res;
459      for (BundleEntryComponent be : bnd.getEntry()) {
460        if (be.hasResource()) {
461          genResource(be.getResource(), rc);
462        }
463      }
464    } else {
465      RendererFactory.factory(res, rc).render((DomainResource) res);
466    }
467  }
468
469  public void convert(String source, String output) throws FHIRException, IOException {
470    Content cnt = igLoader.loadContent(source, "validate", false);
471    Element e = Manager.parseSingle(context, new ByteArrayInputStream(cnt.focus), cnt.cntType);
472    Manager.compose(context, e, new FileOutputStream(output), (output.endsWith(".json") ? FhirFormat.JSON : FhirFormat.XML), OutputStyle.PRETTY, null);
473  }
474
475  public String evaluateFhirPath(String source, String expression) throws FHIRException, IOException {
476    Content cnt = igLoader.loadContent(source, "validate", false);
477    FHIRPathEngine fpe = this.getValidator(null).getFHIRPathEngine();
478    Element e = Manager.parseSingle(context, new ByteArrayInputStream(cnt.focus), cnt.cntType);
479    ExpressionNode exp = fpe.parse(expression);
480    return fpe.evaluateToString(new ValidatorHostContext(context, e), e, e, e, exp);
481  }
482
483  public StructureDefinition snapshot(String source, String version) throws FHIRException, IOException {
484    Content cnt = igLoader.loadContent(source, "validate", false);
485    Resource res = igLoader.loadResourceByVersion(version, cnt.focus, Utilities.getFileNameForName(source));
486
487    if (!(res instanceof StructureDefinition))
488      throw new FHIRException("Require a StructureDefinition for generating a snapshot");
489    StructureDefinition sd = (StructureDefinition) res;
490    StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
491
492    new ProfileUtilities(context, null, null).setAutoFixSliceNames(true).generateSnapshot(base, sd, sd.getUrl(), null, sd.getName());
493    return sd;
494  }
495
496  public CanonicalResource loadCanonicalResource(String source, String version) throws FHIRException, IOException {
497    Content cnt = igLoader.loadContent(source, "validate", false);
498    Resource res = igLoader.loadResourceByVersion(version, cnt.focus, Utilities.getFileNameForName(source));
499
500    if (!(res instanceof CanonicalResource))
501      throw new FHIRException("Require a CanonicalResource");
502    return (CanonicalResource) res;
503  }
504
505  public void seeResource(Resource r) throws FHIRException {
506    context.cacheResource(r);
507  }
508
509  public void dropResource(String type, String id) {
510    context.dropResource(type, id);
511  }
512
513  public InstanceValidator getValidator(FhirFormat format) throws FHIRException, IOException {
514    InstanceValidator validator = new InstanceValidator(context, null, null);
515    validator.setHintAboutNonMustSupport(hintAboutNonMustSupport);
516    validator.setAnyExtensionsAllowed(anyExtensionsAllowed);
517    validator.setNoInvariantChecks(isNoInvariantChecks());
518    validator.setWantInvariantInMessage(isWantInvariantInMessage());
519    validator.setValidationLanguage(language);
520    if (language != null) {
521      validator.getContext().setValidationMessageLanguage(Locale.forLanguageTag(language));
522    }
523    validator.setAssumeValidRestReferences(assumeValidRestReferences);
524    validator.setNoExtensibleWarnings(noExtensibleBindingMessages);
525    validator.setSecurityChecks(securityChecks);
526    validator.setCrumbTrails(crumbTrails);
527    validator.setAllowExamples(allowExampleUrls);
528    validator.setShowMessagesFromReferences(showMessagesFromReferences);
529    validator.getContext().setLocale(locale);
530    validator.setFetcher(this);
531    validator.getImplementationGuides().addAll(igs);
532    validator.getBundleValidationRules().addAll(bundleValidationRules);
533    validator.getValidationControl().putAll(validationControl);
534    validator.setQuestionnaireMode(questionnaireMode);
535    validator.setLevel(level);
536    validator.setNoUnicodeBiDiControlChars(noUnicodeBiDiControlChars);
537    if (format == FhirFormat.SHC) {
538      igLoader.loadIg(getIgs(), getBinaries(), SHCParser.CURRENT_PACKAGE, true);      
539    }
540
541    return validator;
542  }
543
544  public void prepare() {
545    for (StructureDefinition sd : context.allStructures()) {
546      try {
547        makeSnapshot(sd);
548      } catch (Exception e) {
549        System.out.println("Process Note: Unable to generate snapshot for " + sd.present() + ": " + e.getMessage());
550//        if (debug) {
551          e.printStackTrace();
552//        }
553      }
554    }
555  }
556
557  private void makeSnapshot(StructureDefinition sd) throws DefinitionException, FHIRException {
558    if (sd.hasSnapshot())
559      return;
560    StructureDefinition sdb = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
561    if (sdb != null) {
562      makeSnapshot(sdb);
563      new ProfileUtilities(context, null, null).setAutoFixSliceNames(true).generateSnapshot(sdb, sd, sd.getUrl(), null, sd.getName());
564    }
565  }
566
567  public void handleOutput(Resource r, String output, String version) throws FHIRException, IOException {
568    if (output.startsWith("http://")) {
569      ByteArrayOutputStream bs = new ByteArrayOutputStream();
570      handleOutputToStream(r, output, bs, version);
571      SimpleHTTPClient http = new SimpleHTTPClient();
572      HTTPResult res = http.post(output, "application/fhir+xml", bs.toByteArray(), "application/fhir+xml");
573      res.checkThrowException();
574    } else {
575      FileOutputStream s = new FileOutputStream(output);
576      handleOutputToStream(r, output, s, version);
577    }
578  }
579
580  private void handleOutputToStream(Resource r, String fn, OutputStream s, String version) throws FHIRException, IOException {
581    if (fn.endsWith(".html") || fn.endsWith(".htm") && r instanceof DomainResource)
582      new XhtmlComposer(XhtmlComposer.HTML, true).compose(s, ((DomainResource) r).getText().getDiv());
583    else if (VersionUtilities.isR3Ver(version)) {
584      org.hl7.fhir.dstu3.model.Resource res = VersionConvertorFactory_30_50.convertResource(r);
585      if (fn.endsWith(".xml") && !fn.endsWith("template.xml"))
586        new org.hl7.fhir.dstu3.formats.XmlParser().setOutputStyle(org.hl7.fhir.dstu3.formats.IParser.OutputStyle.PRETTY).compose(s, res);
587      else if (fn.endsWith(".json") && !fn.endsWith("template.json"))
588        new org.hl7.fhir.dstu3.formats.JsonParser().setOutputStyle(org.hl7.fhir.dstu3.formats.IParser.OutputStyle.PRETTY).compose(s, res);
589      else if (fn.endsWith(".txt") || fn.endsWith(".map"))
590        TextFile.stringToStream(org.hl7.fhir.dstu3.utils.StructureMapUtilities.render((org.hl7.fhir.dstu3.model.StructureMap) res), s, false);
591      else
592        throw new FHIRException("Unsupported format for " + fn);
593    } else if (VersionUtilities.isR4Ver(version)) {
594      org.hl7.fhir.r4.model.Resource res = VersionConvertorFactory_40_50.convertResource(r);
595      if (fn.endsWith(".xml") && !fn.endsWith("template.xml"))
596        new org.hl7.fhir.r4.formats.XmlParser().setOutputStyle(org.hl7.fhir.r4.formats.IParser.OutputStyle.PRETTY).compose(s, res);
597      else if (fn.endsWith(".json") && !fn.endsWith("template.json"))
598        new org.hl7.fhir.r4.formats.JsonParser().setOutputStyle(org.hl7.fhir.r4.formats.IParser.OutputStyle.PRETTY).compose(s, res);
599      else if (fn.endsWith(".txt") || fn.endsWith(".map"))
600        TextFile.stringToStream(org.hl7.fhir.r4.utils.StructureMapUtilities.render((org.hl7.fhir.r4.model.StructureMap) res), s, false);
601      else
602        throw new FHIRException("Unsupported format for " + fn);
603    } else if (VersionUtilities.isR2BVer(version)) {
604      org.hl7.fhir.dstu2016may.model.Resource res = VersionConvertorFactory_14_50.convertResource(r);
605      if (fn.endsWith(".xml") && !fn.endsWith("template.xml"))
606        new org.hl7.fhir.dstu2016may.formats.XmlParser().setOutputStyle(org.hl7.fhir.dstu2016may.formats.IParser.OutputStyle.PRETTY).compose(s, res);
607      else if (fn.endsWith(".json") && !fn.endsWith("template.json"))
608        new org.hl7.fhir.dstu2016may.formats.JsonParser().setOutputStyle(org.hl7.fhir.dstu2016may.formats.IParser.OutputStyle.PRETTY).compose(s, res);
609      else
610        throw new FHIRException("Unsupported format for " + fn);
611    } else if (VersionUtilities.isR2Ver(version)) {
612      org.hl7.fhir.dstu2.model.Resource res = VersionConvertorFactory_10_50.convertResource(r, new org.hl7.fhir.convertors.misc.IGR2ConvertorAdvisor5());
613      if (fn.endsWith(".xml") && !fn.endsWith("template.xml"))
614        new org.hl7.fhir.dstu2.formats.JsonParser().setOutputStyle(org.hl7.fhir.dstu2.formats.IParser.OutputStyle.PRETTY).compose(s, res);
615      else if (fn.endsWith(".json") && !fn.endsWith("template.json"))
616        new org.hl7.fhir.dstu2.formats.JsonParser().setOutputStyle(org.hl7.fhir.dstu2.formats.IParser.OutputStyle.PRETTY).compose(s, res);
617      else
618        throw new FHIRException("Unsupported format for " + fn);
619    } else if (VersionUtilities.isR5Ver(version)) {
620      if (fn.endsWith(".xml") && !fn.endsWith("template.xml"))
621        new XmlParser().setOutputStyle(org.hl7.fhir.r5.formats.IParser.OutputStyle.PRETTY).compose(s, r);
622      else if (fn.endsWith(".json") && !fn.endsWith("template.json"))
623        new JsonParser().setOutputStyle(org.hl7.fhir.r5.formats.IParser.OutputStyle.PRETTY).compose(s, r);
624      else if (fn.endsWith(".txt") || fn.endsWith(".map"))
625        TextFile.stringToStream(StructureMapUtilities.render((org.hl7.fhir.r5.model.StructureMap) r), s, false);
626      else
627        throw new FHIRException("Unsupported format for " + fn);
628    } else
629      throw new FHIRException("Encounted unsupported configured version " + version + " loading " + fn);
630
631    s.close();
632  }
633
634  public byte[] transformVersion(String source, String targetVer, FhirFormat format, Boolean canDoNative) throws FHIRException, IOException, Exception {
635    Content cnt = igLoader.loadContent(source, "validate", false);
636    org.hl7.fhir.r5.elementmodel.Element src = Manager.parseSingle(context, new ByteArrayInputStream(cnt.focus), cnt.cntType);
637
638    // if the src has a url, we try to use the java code 
639    if ((canDoNative == null && src.hasChild("url")) || (canDoNative != null && canDoNative)) {
640      try {
641        if (VersionUtilities.isR2Ver(version)) {
642          return VersionConvertor.convertVersionNativeR2(targetVer, cnt, format);
643        } else if (VersionUtilities.isR2BVer(version)) {
644          return VersionConvertor.convertVersionNativeR2b(targetVer, cnt, format);
645        } else if (VersionUtilities.isR3Ver(version)) {
646          return VersionConvertor.convertVersionNativeR3(targetVer, cnt, format);
647        } else if (VersionUtilities.isR4Ver(version)) {
648          return VersionConvertor.convertVersionNativeR4(targetVer, cnt, format);
649        } else {
650          throw new FHIRException("Source version not supported yet: " + version);
651        }
652      } catch (Exception e) {
653        System.out.println("Conversion failed using Java convertor: " + e.getMessage());
654      }
655    }
656    // ok, we try converting using the structure maps
657    System.out.println("Loading hl7.fhir.xver.r4");
658    igLoader.loadIg(getIgs(), getBinaries(), "hl7.fhir.xver.r4", false);
659    String type = src.fhirType();
660    String url = getMapId(type, targetVer);
661    List<Base> outputs = new ArrayList<Base>();
662    StructureMapUtilities scu = new StructureMapUtilities(context, new TransformSupportServices(outputs, mapLog, context));
663    StructureMap map = context.getTransform(url);
664    if (map == null)
665      throw new Error("Unable to find map " + url + " (Known Maps = " + context.listMapUrls() + ")");
666    org.hl7.fhir.r5.elementmodel.Element resource = getTargetResourceFromStructureMap(map);
667    scu.transform(null, src, map, resource);
668    ByteArrayOutputStream bs = new ByteArrayOutputStream();
669    Manager.compose(context, resource, bs, format, OutputStyle.PRETTY, null);
670    return bs.toByteArray();
671  }
672
673  private String getMapId(String type, String targetVer) {
674    if (VersionUtilities.isR2Ver(version)) {
675      if (VersionUtilities.isR3Ver(targetVer)) {
676        return "http://hl7.org/fhir/StructureMap/" + type + "2to3";
677      }
678    } else if (VersionUtilities.isR3Ver(version)) {
679      if (VersionUtilities.isR2Ver(targetVer)) {
680        return "http://hl7.org/fhir/StructureMap/" + type + "3to2";
681      } else if (VersionUtilities.isR4Ver(targetVer)) {
682        return "http://hl7.org/fhir/StructureMap/" + type + "3to4";
683      }
684    } else if (VersionUtilities.isR4Ver(version)) {
685      if (VersionUtilities.isR3Ver(targetVer)) {
686        return "http://hl7.org/fhir/StructureMap/" + type + "4to3";
687      }
688    }
689    throw new FHIRException("Source/Target version not supported: " + version + " -> " + targetVer);
690  }
691
692  public String setTerminologyServer(String src, String log, FhirPublication version) throws FHIRException, URISyntaxException {
693    return connectToTSServer(src, log, version);
694  }
695
696  public ValidationEngine setMapLog(String mapLog) throws FileNotFoundException {
697    if (mapLog != null) {
698      this.mapLog = new PrintWriter(mapLog);
699    }
700    return this;
701  }
702
703  public ValidationEngine setSnomedExtension(String sct) {
704    getContext().getExpansionParameters().addParameter("system-version", "http://snomed.info/sct|http://snomed.info/sct/" + sct);
705    return this;
706  }
707
708  public FilesystemPackageCacheManager getPcm() throws IOException {
709    if (pcm == null) {
710      //System.out.println("Creating Package manager?");
711      pcm = new FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION);
712    }
713    return pcm;
714  }
715
716  @Override
717  public byte[] fetchRaw(IResourceValidator validator, String source) throws IOException {
718    SimpleHTTPClient http = new SimpleHTTPClient();
719    HTTPResult res = http.get(source);
720    res.checkThrowException();
721    return res.getContent();
722  }
723
724  @Override
725  public boolean packageExists(String id, String ver) throws IOException, FHIRException {
726    return getPcm().packageExists(id, ver);
727  }
728
729  @Override
730  public void loadPackage(String id, String ver) throws IOException, FHIRException {
731    igLoader.loadIg(getIgs(), getBinaries(),id + (ver == null ? "" : "#" + ver), true);
732  }
733
734  @Override
735  public Element fetch(IResourceValidator validator, Object appContext, String url) throws FHIRException, IOException {
736    Resource resource = context.fetchResource(Resource.class, url);
737    if (resource != null) {
738      return new ObjectConverter(context).convert(resource);
739    }
740    if (fetcher != null) {
741      return fetcher.fetch(validator, appContext, url);
742    }
743    return null;
744  }
745
746  @Override
747  public ReferenceValidationPolicy policyForReference(IResourceValidator validator, Object appContext, String path, String url) {
748    Resource resource = context.fetchResource(StructureDefinition.class, url);
749    if (resource != null) {
750      return ReferenceValidationPolicy.CHECK_VALID;
751    }
752    if (!(url.contains("hl7.org") || url.contains("fhir.org"))) {
753      return ReferenceValidationPolicy.IGNORE;
754    } else if (policyAdvisor != null) {
755      return policyAdvisor.policyForReference(validator, appContext, path, url);
756    } else {
757      return ReferenceValidationPolicy.CHECK_EXISTS_AND_TYPE;
758    }
759  }
760
761  @Override
762  public ContainedReferenceValidationPolicy policyForContained(IResourceValidator validator,
763                                                      Object appContext,
764                                                      String containerType,
765                                                      String containerId,
766                                                      Element.SpecialElement containingResourceType,
767                                                      String path,
768                                                      String url) {
769    return ContainedReferenceValidationPolicy.CHECK_VALID;
770  }
771
772  @Override
773  public CodedContentValidationPolicy policyForCodedContent(IResourceValidator validator, Object appContext, String stackPath, ElementDefinition definition, StructureDefinition structure, BindingKind kind, ValueSet valueSet, List<String> systems) {
774    return CodedContentValidationPolicy.VALUESET;
775  }
776
777  @Override
778  public boolean resolveURL(IResourceValidator validator, Object appContext, String path, String url, String type) throws FHIRException {
779    if (!url.startsWith("http://") && !url.startsWith("https://")) { // ignore these
780      return true;
781    }
782    if (context.fetchResource(Resource.class, url) != null)
783      return true;
784    if (SIDUtilities.isKnownSID(url) || 
785        Utilities.existsInList(url, "http://hl7.org/fhir/w5", "http://hl7.org/fhir/fivews", "http://hl7.org/fhir/workflow", "http://hl7.org/fhir/ConsentPolicy/opt-out", "http://hl7.org/fhir/ConsentPolicy/opt-in")) {
786      return true;
787    }
788    if (Utilities.existsInList(url, "http://loinc.org", "http://unitsofmeasure.org", "http://snomed.info/sct")) {
789      return true;
790    }
791    for (CanonicalResource cr : context.allConformanceResources()) {
792      if (cr instanceof NamingSystem) {
793        if (hasURL((NamingSystem) cr, url)) {
794          return true;
795        }
796      }
797    }
798    if (url.contains("example.org") || url.contains("acme.com")) {
799      return false; // todo... how to access settings from here?
800    }
801    if (fetcher != null) {
802      try {
803        return fetcher.resolveURL(validator, appContext, path, url, type);
804      } catch (Exception e) {
805        return false;
806      }
807    }
808    return false;
809  }
810
811  private boolean hasURL(NamingSystem ns, String url) {
812    for (NamingSystemUniqueIdComponent uid : ns.getUniqueId()) {
813      if (uid.getType() == NamingSystemIdentifierType.URI && uid.hasValue() && uid.getValue().equals(url)) {
814        return true;
815      }
816    }
817    return false;
818  }
819
820  @Override
821  public CanonicalResource fetchCanonicalResource(IResourceValidator validator, String url) throws URISyntaxException {
822    Resource res = context.fetchResource(Resource.class, url);
823    if (res != null) {
824      if (res instanceof CanonicalResource) {
825        return (CanonicalResource) res;
826      } else {
827        return null;
828      }
829    }
830    return fetcher != null ? fetcher.fetchCanonicalResource(validator, url) : null;
831  }
832
833  @Override
834  public boolean fetchesCanonicalResource(IResourceValidator validator, String url) {
835    return fetcher != null && fetcher.fetchesCanonicalResource(validator, url);
836  }
837
838}