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}