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