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