package ca.uhn.fhir.cli; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.leftPad; import static org.fusesource.jansi.Ansi.ansi; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.text.WordUtils; import org.fusesource.jansi.Ansi.Color; import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport; import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain; import org.hl7.fhir.dstu3.model.StructureDefinition; import org.hl7.fhir.instance.model.api.IBaseResource; import com.phloc.commons.io.file.FileUtils; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.method.MethodUtil; import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; public class ValidateCommand extends BaseCommand { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ValidateCommand.class); @Override public String getCommandDescription() { return "Validate a resource using the FHIR validation tools"; } @Override public String getCommandName() { return "validate"; } @Override public Options getOptions() { Options retVal = new Options(); addFhirVersionOption(retVal); OptionGroup source = new OptionGroup(); source.addOption(new Option("n", "file", true, "The name of the file to validate")); source.addOption(new Option("d", "data", true, "The text to validate")); retVal.addOptionGroup(source); retVal.addOption("x", "xsd", false, "Validate using Schemas"); retVal.addOption("s", "sch", false, "Validate using Schematrons"); retVal.addOption("p", "profile", false, "Validate using Profiles (StructureDefinition / ValueSet)"); retVal.addOption("r", "fetch-remote", false, "Allow fetching remote resources (in other words, if a resource being validated refers to an external StructureDefinition, Questionnaire, etc. this flag allows the validator to access the internet to try and fetch this resource)"); retVal.addOption(new Option("l", "fetch-local", true, "Fetch a profile locally and use it if referenced")); retVal.addOption("e", "encoding", false, "File encoding (default is UTF-8)"); return retVal; } @Override public void run(CommandLine theCommandLine) throws ParseException, Exception { String fileName = theCommandLine.getOptionValue("n"); String contents = theCommandLine.getOptionValue("c"); if (isNotBlank(fileName) && isNotBlank(contents)) { throw new ParseException("Can not supply both a file (-n) and data (-d)"); } if (isBlank(fileName) && isBlank(contents)) { throw new ParseException("Must supply either a file (-n) or data (-d)"); } if (isNotBlank(fileName)) { String encoding = theCommandLine.getOptionValue("e", "UTF-8"); ourLog.info("Reading file '{}' using encoding {}", fileName, encoding); contents = IOUtils.toString(new InputStreamReader(new FileInputStream(fileName), encoding)); ourLog.info("Fully read - Size is {}", FileUtils.getFileSizeDisplay(contents.length())); } EncodingEnum enc = MethodUtil.detectEncodingNoDefault(defaultString(contents)); if (enc == null) { throw new ParseException("Could not detect encoding (json/xml) of contents"); } FhirContext ctx = getSpecVersionContext(theCommandLine); FhirValidator val = ctx.newValidator(); IBaseResource localProfileResource = null; if (theCommandLine.hasOption("l")) { String localProfile = theCommandLine.getOptionValue("l"); ourLog.info("Loading profile: {}", localProfile); String input; try { input = IOUtils.toString(new FileReader(new File(localProfile))); } catch (IOException e) { throw new ParseException("Failed to load file '" + localProfile + "' - Error: " + e.toString()); } localProfileResource = MethodUtil.detectEncodingNoDefault(input).newParser(ctx).parseResource(input); } if (theCommandLine.hasOption("p")) { switch (ctx.getVersion().getVersion()) { case DSTU2: { org.hl7.fhir.instance.hapi.validation.FhirInstanceValidator instanceValidator = new org.hl7.fhir.instance.hapi.validation.FhirInstanceValidator(); val.registerValidatorModule(instanceValidator); org.hl7.fhir.instance.hapi.validation.ValidationSupportChain validationSupport = new org.hl7.fhir.instance.hapi.validation.ValidationSupportChain( new org.hl7.fhir.instance.hapi.validation.DefaultProfileValidationSupport()); if (localProfileResource != null) { org.hl7.fhir.instance.model.StructureDefinition convertedSd = FhirContext.forDstu2Hl7Org().newXmlParser().parseResource(org.hl7.fhir.instance.model.StructureDefinition.class, ctx.newXmlParser().encodeResourceToString(localProfileResource)); instanceValidator.setStructureDefintion(convertedSd); } if (theCommandLine.hasOption("r")) { validationSupport.addValidationSupport(new LoadingValidationSupportDstu2()); } instanceValidator.setValidationSupport(validationSupport); break; } case DSTU3: { FhirInstanceValidator instanceValidator = new FhirInstanceValidator(); val.registerValidatorModule(instanceValidator); ValidationSupportChain validationSupport = new ValidationSupportChain(new DefaultProfileValidationSupport()); if (localProfileResource != null) { instanceValidator.setStructureDefintion((StructureDefinition) localProfileResource); } if (theCommandLine.hasOption("r")) { validationSupport.addValidationSupport(new LoadingValidationSupportDstu3()); } instanceValidator.setValidationSupport(validationSupport); break; } default: throw new ParseException("Profile validation (-p) is not supported for this FHIR version"); } } val.setValidateAgainstStandardSchema(theCommandLine.hasOption("x")); val.setValidateAgainstStandardSchematron(theCommandLine.hasOption("s")); ValidationResult results = val.validateWithResult(contents); StringBuilder b = new StringBuilder("Validation results:" + ansi().boldOff()); int count = 0; for (SingleValidationMessage next : results.getMessages()) { count++; b.append(App.LINESEP); String leftString = "Issue " + count + ": "; int leftWidth = leftString.length(); b.append(ansi().fg(Color.GREEN)).append(leftString); if (next.getSeverity() != null) { b.append(next.getSeverity()).append(ansi().fg(Color.WHITE)).append(" - "); } if (isNotBlank(next.getLocationString())) { b.append(ansi().fg(Color.WHITE)).append(next.getLocationString()); } String[] message = WordUtils.wrap(next.getMessage(), 80 - leftWidth, "\n", true).split("\\n"); for (String line : message) { b.append(App.LINESEP); b.append(ansi().fg(Color.WHITE)); b.append(leftPad("", leftWidth)).append(line); } } b.append(App.LINESEP); if (count > 0) { ourLog.info(b.toString()); } if (results.isSuccessful()) { ourLog.info("Validation successful!"); } else { ourLog.warn("Validation FAILED"); } } }