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      SimpleWorkerContext.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 transformVersion(CliContext cliContext, ValidationEngine validator) throws Exception {
257    if (cliContext.getSources().size() > 1) {
258      throw new Exception("Can only have one source when converting versions (found " + cliContext.getSources() + ")");
259    }
260    if (cliContext.getTargetVer() == null) {
261      throw new Exception("Must provide a map when converting versions");
262    }
263    if (cliContext.getOutput() == null) {
264      throw new Exception("Must nominate an output when converting versions");
265    }
266    try {
267      if (cliContext.getMapLog() != null) {
268        validator.setMapLog(cliContext.getMapLog());
269      }
270      byte[] r = validator.transformVersion(cliContext.getSources().get(0), cliContext.getTargetVer(), cliContext.getOutput().endsWith(".json") ? Manager.FhirFormat.JSON : Manager.FhirFormat.XML, cliContext.getCanDoNative());
271      System.out.println(" ...success");
272      TextFile.bytesToFile(r, cliContext.getOutput());
273    } catch (Exception e) {
274      System.out.println(" ...Failure: " + e.getMessage());
275      e.printStackTrace();
276    }
277  }
278
279  public ValidationEngine initializeValidator(CliContext cliContext, String definitions, TimeTracker tt) throws Exception {
280    return sessionCache.fetchSessionValidatorEngine(initializeValidator(cliContext, definitions, tt, null));
281  }
282
283  public String initializeValidator(CliContext cliContext, String definitions, TimeTracker tt, String sessionId) throws Exception {
284    tt.milestone();
285    if (!sessionCache.sessionExists(sessionId)) {
286      if (sessionId != null) {
287        System.out.println("No such cached session exists for session id " + sessionId + ", re-instantiating validator.");
288      }
289      System.out.print("  Load FHIR v" + cliContext.getSv() + " from " + definitions);
290      ValidationEngine validator = new ValidationEngine(definitions, cliContext.getSv(), tt, "fhir/validator");
291      sessionId = sessionCache.cacheSession(validator);
292
293      FhirPublication ver = FhirPublication.fromCode(cliContext.getSv());
294      IgLoader igLoader = new IgLoader(validator.getPcm(), validator.getContext(), validator.getVersion(), validator.isDebug());
295      System.out.println(" - " + validator.getContext().countAllCaches() + " resources (" + tt.milestone() + ")");
296      igLoader.loadIg(validator.getIgs(), validator.getBinaries(), "hl7.terminology", false);
297      System.out.print("  Terminology server " + cliContext.getTxServer());
298      String txver = validator.setTerminologyServer(cliContext.getTxServer(), cliContext.getTxLog(), ver);
299      System.out.println(" - Version " + txver + " (" + tt.milestone() + ")");
300      validator.setDebug(cliContext.isDoDebug());
301      for (String src : cliContext.getIgs()) {
302        igLoader.loadIg(validator.getIgs(), validator.getBinaries(), src, cliContext.isRecursive());
303      }
304      System.out.print("  Get set... ");
305      validator.setQuestionnaireMode(cliContext.getQuestionnaireMode());
306      validator.setLevel(cliContext.getLevel());
307      validator.setDoNative(cliContext.isDoNative());
308      validator.setHintAboutNonMustSupport(cliContext.isHintAboutNonMustSupport());
309      validator.setAnyExtensionsAllowed(cliContext.isAnyExtensionsAllowed());
310      validator.setLanguage(cliContext.getLang());
311      validator.setLocale(cliContext.getLocale());
312      validator.setSnomedExtension(cliContext.getSnomedCTCode());
313      validator.setAssumeValidRestReferences(cliContext.isAssumeValidRestReferences());
314      validator.setShowMessagesFromReferences(cliContext.isShowMessagesFromReferences());
315      validator.setNoExtensibleBindingMessages(cliContext.isNoExtensibleBindingMessages());
316      validator.setNoUnicodeBiDiControlChars(cliContext.isNoUnicodeBiDiControlChars());
317      validator.setNoInvariantChecks(cliContext.isNoInvariants());
318      validator.setWantInvariantInMessage(cliContext.isWantInvariantsInMessages());
319      validator.setSecurityChecks(cliContext.isSecurityChecks());
320      validator.setCrumbTrails(cliContext.isCrumbTrails());
321      validator.setShowTimes(cliContext.isShowTimes());
322      validator.setAllowExampleUrls(cliContext.isAllowExampleUrls());
323      StandAloneValidatorFetcher fetcher = new StandAloneValidatorFetcher(validator.getPcm(), validator.getContext(), validator);
324      validator.setFetcher(fetcher);
325      validator.getContext().setLocator(fetcher);
326      validator.getBundleValidationRules().addAll(cliContext.getBundleValidationRules());
327      TerminologyCache.setNoCaching(cliContext.isNoInternalCaching());
328      validator.prepare(); // generate any missing snapshots
329      System.out.println(" go (" + tt.milestone() + ")");
330    } else {
331      System.out.println("Cached session exists for session id " + sessionId + ", returning stored validator session id.");
332    }
333    return sessionId;
334  }
335
336  public String determineVersion(CliContext cliContext) throws Exception {
337    return determineVersion(cliContext, null);
338  }
339
340  public String determineVersion(CliContext cliContext, String sessionId) throws Exception {
341    if (cliContext.getMode() != EngineMode.VALIDATION) {
342      return "current";
343    }
344    System.out.println("Scanning for versions (no -version parameter):");
345    VersionSourceInformation versions = scanForVersions(cliContext);
346    for (String s : versions.getReport()) {
347      if (!s.equals("(nothing found)")) {
348        System.out.println("  " + s);
349      }
350    }
351    if (versions.isEmpty()) {
352      System.out.println("  No Version Info found: Using Default version '" + VersionUtilities.CURRENT_VERSION + "'");
353      return "current";
354    }
355    if (versions.size() == 1) {
356      System.out.println("-> use version " + versions.version());
357      return versions.version();
358    }
359    throw new Exception("-> Multiple versions found. Specify a particular version using the -version parameter");
360  }
361
362  public void generateSpreadsheet(CliContext cliContext, ValidationEngine validator) throws Exception {
363    CanonicalResource cr = validator.loadCanonicalResource(cliContext.getSources().get(0), cliContext.getSv());
364    boolean ok = true;
365    if (cr instanceof StructureDefinition) {
366      new StructureDefinitionSpreadsheetGenerator(validator.getContext(), false, false).renderStructureDefinition((StructureDefinition) cr).finish(new FileOutputStream(cliContext.getOutput()));
367    } else if (cr instanceof CodeSystem) {
368      new CodeSystemSpreadsheetGenerator(validator.getContext()).renderCodeSystem((CodeSystem) cr).finish(new FileOutputStream(cliContext.getOutput()));
369    } else if (cr instanceof ValueSet) {
370      new ValueSetSpreadsheetGenerator(validator.getContext()).renderValueSet((ValueSet) cr).finish(new FileOutputStream(cliContext.getOutput()));
371    } else if (cr instanceof ConceptMap) {
372      new ConceptMapSpreadsheetGenerator(validator.getContext()).renderConceptMap((ConceptMap) cr).finish(new FileOutputStream(cliContext.getOutput()));
373    } else {
374      ok = false;
375      System.out.println(" ...Unable to generate spreadsheet for "+cliContext.getSources().get(0)+": no way to generate a spreadsheet for a "+cr.fhirType());
376    }
377    
378    if (ok) {
379      System.out.println(" ...generated spreadsheet successfully");
380    } 
381  }
382}