001package org.hl7.fhir.validation.cli.services;
002
003import org.hl7.fhir.exceptions.FHIRException;
004import org.hl7.fhir.r5.context.SimpleWorkerContext;
005import org.hl7.fhir.r5.context.TerminologyCache;
006import org.hl7.fhir.r5.elementmodel.Manager;
007import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
008import org.hl7.fhir.r5.formats.IParser;
009import org.hl7.fhir.r5.formats.JsonParser;
010import org.hl7.fhir.r5.formats.XmlParser;
011import org.hl7.fhir.r5.model.*;
012import org.hl7.fhir.r5.model.OperationOutcome.IssueSeverity;
013import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
014import org.hl7.fhir.r5.renderers.spreadsheets.CodeSystemSpreadsheetGenerator;
015import org.hl7.fhir.r5.renderers.spreadsheets.ConceptMapSpreadsheetGenerator;
016import org.hl7.fhir.r5.renderers.spreadsheets.StructureDefinitionSpreadsheetGenerator;
017import org.hl7.fhir.r5.renderers.spreadsheets.ValueSetSpreadsheetGenerator;
018import org.hl7.fhir.r5.utils.ToolingExtensions;
019import org.hl7.fhir.utilities.TextFile;
020import org.hl7.fhir.utilities.TimeTracker;
021import org.hl7.fhir.utilities.Utilities;
022import org.hl7.fhir.utilities.VersionUtilities;
023import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
024import org.hl7.fhir.utilities.npm.ToolsVersion;
025import org.hl7.fhir.utilities.validation.ValidationMessage;
026import org.hl7.fhir.validation.IgLoader;
027import org.hl7.fhir.validation.ValidationEngine;
028import org.hl7.fhir.validation.ValidationRecord;
029import org.hl7.fhir.validation.cli.model.*;
030import org.hl7.fhir.validation.cli.renderers.CSVRenderer;
031import org.hl7.fhir.validation.cli.renderers.DefaultRenderer;
032import org.hl7.fhir.validation.cli.renderers.ESLintCompactRenderer;
033import org.hl7.fhir.validation.cli.renderers.NativeRenderer;
034import org.hl7.fhir.validation.cli.renderers.ValidationOutputRenderer;
035import org.hl7.fhir.validation.cli.utils.EngineMode;
036import org.hl7.fhir.validation.cli.utils.VersionSourceInformation;
037
038import java.io.FileOutputStream;
039import java.io.IOException;
040import java.io.OutputStream;
041import java.io.PrintStream;
042import java.lang.management.ManagementFactory;
043import java.lang.management.MemoryMXBean;
044import java.util.ArrayList;
045import java.util.List;
046
047public class ValidationService {
048
049  private final SessionCache sessionCache;
050
051  public ValidationService() {
052    sessionCache = new SessionCache();
053  }
054
055  protected ValidationService(SessionCache cache) {
056    this.sessionCache = cache;
057  }
058
059  public ValidationResponse validateSources(ValidationRequest request) throws Exception {
060    if (request.getCliContext().getSv() == null) {
061      String sv = determineVersion(request.getCliContext(), request.sessionId);
062      request.getCliContext().setSv(sv);
063    }
064
065    String definitions = VersionUtilities.packageForVersion(request.getCliContext().getSv()) + "#" + VersionUtilities.getCurrentVersion(request.getCliContext().getSv());
066
067    String sessionId = initializeValidator(request.getCliContext(), definitions, new TimeTracker(), request.sessionId);
068    ValidationEngine validator = sessionCache.fetchSessionValidatorEngine(sessionId);
069
070    if (request.getCliContext().getProfiles().size() > 0) {
071      System.out.println("  .. validate " + request.listSourceFiles() + " against " + request.getCliContext().getProfiles().toString());
072    } else {
073      System.out.println("  .. validate " + request.listSourceFiles());
074    }
075
076    ValidationResponse response = new ValidationResponse().setSessionId(sessionId);
077
078    for (FileInfo fp : request.getFilesToValidate()) {
079      List<ValidationMessage> messages = new ArrayList<>();
080      validator.validate(fp.getFileContent().getBytes(), Manager.FhirFormat.getFhirFormat(fp.getFileType()),
081        request.getCliContext().getProfiles(), messages);
082      ValidationOutcome outcome = new ValidationOutcome().setFileInfo(fp);
083      messages.forEach(outcome::addMessage);
084      response.addOutcome(outcome);
085    }
086    System.out.println("  Max Memory: "+Runtime.getRuntime().maxMemory());
087    return response;
088  }
089
090  public VersionSourceInformation scanForVersions(CliContext cliContext) throws Exception {
091    VersionSourceInformation versions = new VersionSourceInformation();
092    IgLoader igLoader = new IgLoader(
093      new FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION),
094      new SimpleWorkerContext.SimpleWorkerContextBuilder().fromNothing(),
095      null);
096    for (String src : cliContext.getIgs()) {
097      igLoader.scanForIgVersion(src, cliContext.isRecursive(), versions);
098    }
099    igLoader.scanForVersions(cliContext.getSources(), versions);
100    return versions;
101  }
102
103  public void validateSources(CliContext cliContext, ValidationEngine validator) throws Exception {
104    long start = System.currentTimeMillis();
105    List<ValidationRecord> records = new ArrayList<>();
106    Resource r = validator.validate(cliContext.getSources(), cliContext.getProfiles(), records);
107    MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();
108    System.out.println("Done. " + validator.getContext().clock().report()+". Memory = "+Utilities.describeSize(mbean.getHeapMemoryUsage().getUsed()+mbean.getNonHeapMemoryUsage().getUsed()));
109    System.out.println();
110
111    PrintStream dst = null;
112    if (cliContext.getOutput() == null) {
113      dst = System.out;
114    } else {
115      dst = new PrintStream(new FileOutputStream(cliContext.getOutput()));
116    }
117
118    ValidationOutputRenderer renderer = makeValidationOutputRenderer(cliContext);
119    renderer.setOutput(dst);
120    renderer.setCrumbTrails(validator.isCrumbTrails());
121    
122    int ec = 0;
123    
124    if (r instanceof Bundle) {
125      if (renderer.handlesBundleDirectly()) {
126        renderer.render((Bundle) r);
127      } else {
128        renderer.start(((Bundle) r).getEntry().size() > 1);
129        for (Bundle.BundleEntryComponent e : ((Bundle) r).getEntry()) {
130          OperationOutcome op = (OperationOutcome) e.getResource();
131          ec = ec + countErrors(op); 
132          renderer.render(op);
133        }
134        renderer.finish();
135      }
136    } else if (r == null) {
137      ec = ec + 1;
138      System.out.println("No output from validation - nothing to validate");
139    } else {
140      renderer.start(false);
141      OperationOutcome op = (OperationOutcome) r;
142      ec = countErrors(op);
143      renderer.render((OperationOutcome) r);
144      renderer.finish();
145    }
146    
147    if (cliContext.getOutput() != null) {
148      dst.close();
149    }
150
151    if (cliContext.getHtmlOutput() != null) {
152      String html = new HTMLOutputGenerator(records).generate(System.currentTimeMillis() - start);
153      TextFile.stringToFile(html, cliContext.getHtmlOutput());
154      System.out.println("HTML Summary in " + cliContext.getHtmlOutput());
155    }
156    System.exit(ec > 0 ? 1 : 0);
157  }
158
159  private int countErrors(OperationOutcome oo) {
160    int error = 0;
161    for (OperationOutcome.OperationOutcomeIssueComponent issue : oo.getIssue()) {
162      if (issue.getSeverity() == OperationOutcome.IssueSeverity.FATAL || issue.getSeverity() == OperationOutcome.IssueSeverity.ERROR)
163        error++;
164    }
165    return error;    
166  }
167
168  private ValidationOutputRenderer makeValidationOutputRenderer(CliContext cliContext) {
169    String style = cliContext.getOutputStyle();
170    // adding to this list? 
171    // Must document the option at https://confluence.hl7.org/display/FHIR/Using+the+FHIR+Validator#UsingtheFHIRValidator-ManagingOutput
172    // if you're going to make a PR, document the link where the outputstyle is documented, along with a sentence that describes it, in the PR notes 
173    if (Utilities.noString(style)) {
174      if (cliContext.getOutput() == null) {
175        return new DefaultRenderer();        
176      } else if (cliContext.getOutput().endsWith(".json")) {
177        return new NativeRenderer(FhirFormat.JSON);
178      } else {
179        return new NativeRenderer(FhirFormat.XML);
180      }
181    } else if (Utilities.existsInList(style, "eslint-compact")) {
182      return new ESLintCompactRenderer();
183    } else if (Utilities.existsInList(style, "csv")) {
184      return new CSVRenderer();
185    } else if (Utilities.existsInList(style, "xml")) {
186      return new NativeRenderer(FhirFormat.XML);
187    } else if (Utilities.existsInList(style, "json")) {
188      return new NativeRenderer(FhirFormat.JSON);
189    } else {
190      System.out.println("Unknown output style '"+style+"'");
191      return new DefaultRenderer();      
192    }
193  }
194
195  public void convertSources(CliContext cliContext, ValidationEngine validator) throws Exception {
196    System.out.println(" ...convert");
197    validator.convert(cliContext.getSources().get(0), cliContext.getOutput());
198  }
199
200  public void evaluateFhirpath(CliContext cliContext, ValidationEngine validator) throws Exception {
201    System.out.println(" ...evaluating " + cliContext.getFhirpath());
202    System.out.println(validator.evaluateFhirPath(cliContext.getSources().get(0), cliContext.getFhirpath()));
203  }
204
205  public void generateSnapshot(CliContext cliContext, ValidationEngine validator) throws Exception {
206    StructureDefinition r = validator.snapshot(cliContext.getSources().get(0), cliContext.getSv());
207    System.out.println(" ...generated snapshot successfully");
208    if (cliContext.getOutput() != null) {
209      validator.handleOutput(r, cliContext.getOutput(), cliContext.getSv());
210    }
211  }
212
213  public void generateNarrative(CliContext cliContext, ValidationEngine validator) throws Exception {
214    Resource r = validator.generate(cliContext.getSources().get(0), cliContext.getSv());
215    System.out.println(" ...generated narrative successfully");
216    if (cliContext.getOutput() != null) {
217      validator.handleOutput(r, cliContext.getOutput(), cliContext.getSv());
218    }
219  }
220
221  public void transform(CliContext cliContext, ValidationEngine validator) throws Exception {
222    if (cliContext.getSources().size() > 1)
223      throw new Exception("Can only have one source when doing a transform (found " + cliContext.getSources() + ")");
224    if (cliContext.getTxServer() == null)
225      throw new Exception("Must provide a terminology server when doing a transform");
226    if (cliContext.getMap() == null)
227      throw new Exception("Must provide a map when doing a transform");
228    try {
229      List<StructureDefinition> structures = validator.getContext().allStructures();
230      for (StructureDefinition sd : structures) {
231        if (!sd.hasSnapshot()) {
232          if (sd.getKind() != null && sd.getKind() == StructureDefinitionKind.LOGICAL) {
233            validator.getContext().generateSnapshot(sd, true);
234          } else {
235            validator.getContext().generateSnapshot(sd, false);
236          }
237        }
238      }
239      validator.setMapLog(cliContext.getMapLog());
240      org.hl7.fhir.r5.elementmodel.Element r = validator.transform(cliContext.getSources().get(0), cliContext.getMap());
241      System.out.println(" ...success");
242      if (cliContext.getOutput() != null) {
243        FileOutputStream s = new FileOutputStream(cliContext.getOutput());
244        if (cliContext.getOutput() != null && cliContext.getOutput().endsWith(".json"))
245          new org.hl7.fhir.r5.elementmodel.JsonParser(validator.getContext()).compose(r, s, IParser.OutputStyle.PRETTY, null);
246        else
247          new org.hl7.fhir.r5.elementmodel.XmlParser(validator.getContext()).compose(r, s, IParser.OutputStyle.PRETTY, null);
248        s.close();
249      }
250    } catch (Exception e) {
251      System.out.println(" ...Failure: " + e.getMessage());
252      e.printStackTrace();
253    }
254  }
255
256  public void compile(CliContext cliContext, ValidationEngine validator) throws Exception {
257    if (cliContext.getSources().size() > 0)
258      throw new Exception("Cannot specify sources when compling transform (found " + cliContext.getSources() + ")");
259    if (cliContext.getMap() == null)
260      throw new Exception("Must provide a map when compiling a transform");
261    if (cliContext.getOutput() == null)
262      throw new Exception("Must provide an output name when compiling a transform");
263    try {
264      List<StructureDefinition> structures = validator.getContext().allStructures();
265      for (StructureDefinition sd : structures) {
266        if (!sd.hasSnapshot()) {
267          if (sd.getKind() != null && sd.getKind() == StructureDefinitionKind.LOGICAL) {
268            validator.getContext().generateSnapshot(sd, true);
269          } else {
270            validator.getContext().generateSnapshot(sd, false);
271          }
272        }
273      }
274      validator.setMapLog(cliContext.getMapLog());
275      StructureMap map = validator.compile(cliContext.getMap());
276      if (map == null)
277        throw new Exception("Unable to locate map " + cliContext.getMap());
278      validator.handleOutput(map, cliContext.getOutput(), validator.getVersion());
279      System.out.println(" ...success");
280    } catch (Exception e) {
281      System.out.println(" ...Failure: " + e.getMessage());
282      e.printStackTrace();
283    }
284  }
285
286  public void transformVersion(CliContext cliContext, ValidationEngine validator) throws Exception {
287    if (cliContext.getSources().size() > 1) {
288      throw new Exception("Can only have one source when converting versions (found " + cliContext.getSources() + ")");
289    }
290    if (cliContext.getTargetVer() == null) {
291      throw new Exception("Must provide a map when converting versions");
292    }
293    if (cliContext.getOutput() == null) {
294      throw new Exception("Must nominate an output when converting versions");
295    }
296    try {
297      if (cliContext.getMapLog() != null) {
298        validator.setMapLog(cliContext.getMapLog());
299      }
300      byte[] r = validator.transformVersion(cliContext.getSources().get(0), cliContext.getTargetVer(), cliContext.getOutput().endsWith(".json") ? Manager.FhirFormat.JSON : Manager.FhirFormat.XML, cliContext.getCanDoNative());
301      System.out.println(" ...success");
302      TextFile.bytesToFile(r, cliContext.getOutput());
303    } catch (Exception e) {
304      System.out.println(" ...Failure: " + e.getMessage());
305      e.printStackTrace();
306    }
307  }
308
309  public ValidationEngine initializeValidator(CliContext cliContext, String definitions, TimeTracker tt) throws Exception {
310    return sessionCache.fetchSessionValidatorEngine(initializeValidator(cliContext, definitions, tt, null));
311  }
312
313  public String initializeValidator(CliContext cliContext, String definitions, TimeTracker tt, String sessionId) throws Exception {
314    tt.milestone();
315    if (!sessionCache.sessionExists(sessionId)) {
316      if (sessionId != null) {
317        System.out.println("No such cached session exists for session id " + sessionId + ", re-instantiating validator.");
318      }
319      System.out.print("  Load FHIR v" + cliContext.getSv() + " from " + definitions);
320      ValidationEngine validator = new ValidationEngine.ValidationEngineBuilder().withVersion(cliContext.getSv()).withTimeTracker(tt).withUserAgent("fhir/validator").fromSource(definitions);
321
322      sessionId = sessionCache.cacheSession(validator);
323
324      FhirPublication ver = FhirPublication.fromCode(cliContext.getSv());
325      IgLoader igLoader = new IgLoader(validator.getPcm(), validator.getContext(), validator.getVersion(), validator.isDebug());
326      System.out.println(" - " + validator.getContext().countAllCaches() + " resources (" + tt.milestone() + ")");
327      igLoader.loadIg(validator.getIgs(), validator.getBinaries(), "hl7.terminology", false);
328      System.out.print("  Terminology server " + cliContext.getTxServer());
329      String txver = validator.setTerminologyServer(cliContext.getTxServer(), cliContext.getTxLog(), ver);
330      System.out.println(" - Version " + txver + " (" + tt.milestone() + ")");
331      validator.setDebug(cliContext.isDoDebug());
332      for (String src : cliContext.getIgs()) {
333        igLoader.loadIg(validator.getIgs(), validator.getBinaries(), src, cliContext.isRecursive());
334      }
335      System.out.print("  Get set... ");
336      validator.setQuestionnaireMode(cliContext.getQuestionnaireMode());
337      validator.setLevel(cliContext.getLevel());
338      validator.setDoNative(cliContext.isDoNative());
339      validator.setHintAboutNonMustSupport(cliContext.isHintAboutNonMustSupport());
340      validator.setAnyExtensionsAllowed(cliContext.isAnyExtensionsAllowed());
341      validator.setLanguage(cliContext.getLang());
342      validator.setLocale(cliContext.getLocale());
343      validator.setSnomedExtension(cliContext.getSnomedCTCode());
344      validator.setAssumeValidRestReferences(cliContext.isAssumeValidRestReferences());
345      validator.setShowMessagesFromReferences(cliContext.isShowMessagesFromReferences());
346      validator.setNoExtensibleBindingMessages(cliContext.isNoExtensibleBindingMessages());
347      validator.setNoUnicodeBiDiControlChars(cliContext.isNoUnicodeBiDiControlChars());
348      validator.setNoInvariantChecks(cliContext.isNoInvariants());
349      validator.setWantInvariantInMessage(cliContext.isWantInvariantsInMessages());
350      validator.setSecurityChecks(cliContext.isSecurityChecks());
351      validator.setCrumbTrails(cliContext.isCrumbTrails());
352      validator.setShowTimes(cliContext.isShowTimes());
353      validator.setAllowExampleUrls(cliContext.isAllowExampleUrls());
354      StandAloneValidatorFetcher fetcher = new StandAloneValidatorFetcher(validator.getPcm(), validator.getContext(), validator);
355      validator.setFetcher(fetcher);
356      validator.getContext().setLocator(fetcher);
357      validator.getBundleValidationRules().addAll(cliContext.getBundleValidationRules());
358      TerminologyCache.setNoCaching(cliContext.isNoInternalCaching());
359      validator.prepare(); // generate any missing snapshots
360      System.out.println(" go (" + tt.milestone() + ")");
361    } else {
362      System.out.println("Cached session exists for session id " + sessionId + ", returning stored validator session id.");
363    }
364    return sessionId;
365  }
366
367  public String determineVersion(CliContext cliContext) throws Exception {
368    return determineVersion(cliContext, null);
369  }
370
371  public String determineVersion(CliContext cliContext, String sessionId) throws Exception {
372    if (cliContext.getMode() != EngineMode.VALIDATION) {
373      return "current";
374    }
375    System.out.println("Scanning for versions (no -version parameter):");
376    VersionSourceInformation versions = scanForVersions(cliContext);
377    for (String s : versions.getReport()) {
378      if (!s.equals("(nothing found)")) {
379        System.out.println("  " + s);
380      }
381    }
382    if (versions.isEmpty()) {
383      System.out.println("  No Version Info found: Using Default version '" + VersionUtilities.CURRENT_VERSION + "'");
384      return "current";
385    }
386    if (versions.size() == 1) {
387      System.out.println("-> use version " + versions.version());
388      return versions.version();
389    }
390    throw new Exception("-> Multiple versions found. Specify a particular version using the -version parameter");
391  }
392
393  public void generateSpreadsheet(CliContext cliContext, ValidationEngine validator) throws Exception {
394    CanonicalResource cr = validator.loadCanonicalResource(cliContext.getSources().get(0), cliContext.getSv());
395    boolean ok = true;
396    if (cr instanceof StructureDefinition) {
397      new StructureDefinitionSpreadsheetGenerator(validator.getContext(), false, false).renderStructureDefinition((StructureDefinition) cr).finish(new FileOutputStream(cliContext.getOutput()));
398    } else if (cr instanceof CodeSystem) {
399      new CodeSystemSpreadsheetGenerator(validator.getContext()).renderCodeSystem((CodeSystem) cr).finish(new FileOutputStream(cliContext.getOutput()));
400    } else if (cr instanceof ValueSet) {
401      new ValueSetSpreadsheetGenerator(validator.getContext()).renderValueSet((ValueSet) cr).finish(new FileOutputStream(cliContext.getOutput()));
402    } else if (cr instanceof ConceptMap) {
403      new ConceptMapSpreadsheetGenerator(validator.getContext()).renderConceptMap((ConceptMap) cr).finish(new FileOutputStream(cliContext.getOutput()));
404    } else {
405      ok = false;
406      System.out.println(" ...Unable to generate spreadsheet for "+cliContext.getSources().get(0)+": no way to generate a spreadsheet for a "+cr.fhirType());
407    }
408    
409    if (ok) {
410      System.out.println(" ...generated spreadsheet successfully");
411    } 
412  }
413}