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}