FHIR Validation
|
I thought I'd leave some bread crumbs on this. Some of the type choices look a little weird; that's be cause this code came from a large work using streams and bytes instead of strings.
public void method() { InputStreamReader isReader = new InputStreamReader( inputStream ); BufferedReader reader = new BufferedReader( isReader ); String format = reader.lines().collect( Collectors.joining( "\n" ) ) ); }
...using format obtained earlier.
public void method() { FhirContext fhirContext = FhirContext.forR4(); IParser fhirXmlParser = fhirContext.newXmlParser().setPrettyPrint( true ); IParser fhirJsonParser = fhirContext.newJsonParser().setPrettyPrint( true ); IParser parser = ( format.equals( "XML" ) ) ? fhirXmlParser : fhirJsonParser; IBaseResource resource = parser.parseResource( inputStream ); byte[] document = parser.encodeResourceToString( resource).getBytes() ) }
...using fhirContext obtained earlier. Note: you will want to consider the information about line- and column numbers in the Appendix below. Here we do it using a Java object for the resource, but there's a better way. In the meantime, ...
public void method() { ValidationSupportChain supportChain = new ValidationSupportChain( new DefaultProfileValidationSupport( fhirContext ), new InMemoryTerminologyServerValidationSupport( fhirContext ), new CommonCodeSystemsTerminologyService( fhirContext ) ); FhirValidator validator = fhirContext.newValidator().registerValidatorModule( fhirModule ); FhirInstanceValidator instanceValidator = new FhirInstanceValidator( supportChain ); instanceValidator.setAnyExtensionsAllowed( true ); ValidationResult results = validator.validateWithResult( resource ); }
...using results and document obtained earlier. (There's simpler code to be found in the Appendix below.)
import java.io.ByteArrayInputStream; import java.io.InputStreamReader; import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; import com.windofkeltia.utilities.StringUtilities; protected void outputPlainText() throws IOException { StringBuilder sb = new StringBuilder(); System.out.print( "Validation Results\n\n" ); sb.append( " Validation " ); if( results.isSuccessful() ) sb.append( "succeeded.\n\n" ); else sb.append( "failed with " ) .append( results.getMessages().size() ) .append( " errors, warnings or other notes.\n\n" ); System.out.print( sb.toString() ); sb.setLength( 0 ); if( !results.isSuccessful() ) { sb.append( " " ) .append( StringUtilities.padStringRight( " ", 4 ) ) .append( StringUtilities.padStringRight( "Severity", 11 ) ) .append( "Location (FHIRPath) " ); .append( "Profile, Element and Problem description\n" ); .append( " " ) .append( StringUtilities.padStringRight( " ", 4 ) ) .append( StringUtilities.padStringRight( "--------", 11 ) ) .append( "------------------- " ); .append( "----------------------------------------\n" ); System.out.print( sb.toString() ); sb.setLength( 0 ); int count = 1; for( SingleValidationMessage message : results.getMessages() ) { sb.setLength( 0 ); sb.append( " " ) .append( StringUtilities.padStringRight( ""+count++, 4 ) ) .append( StringUtilities.padStringRight( message.getSeverity().toString(), 11 ) ) .append( message.getLocationString() ).append( " " ) .append( message.getMessage() ).append( '\n' ); System.out.print( sb ); } } System.out.print( "\n\nContent of Validated Document\n\n" ); outputDocumentWithLineNumbers( document ); } private void outputDocumentWithLineNumbers( final byte[] document ) throws IOException { BufferedReader reader = new BufferedReader( new InputStreamReader( new ByteArrayInputStream( document ) ) ); String line; int lineNumber = 1; while( nonNull( line = reader.readLine() ) ) { System.out.print( StringUtilities.padStringLeft( lineNumber+": ", 5 ) ); System.out.print( line ); System.out.print( '\n' ); lineNumber++; } }
Based on a suggestion James Agnew made, I figured out that the absence of useful FHIR validator line and column numbers is explainable and remediable. If you validate an actual Java resource like
Bundle bundle = new Bundle()...
...and fill it with contents, then pass it to the HAPI FHIR validator, HAPI FHIR will first serialize the resource (which will of course result in a string with no newlines, indentation, etc.). So, when you get the SingleValidationMessage from the results, then ask for the line number, you always get 1 (duh).
When you ask for the column number, if you apply what comes back to your (nice, pretty, mental) representation of the FHIR resource—all neatly indented, etc.—the column number corresponds to nothing logical even if you realize that it's relative to the beginning of the (only) line.
The solution is to eschew the validateWithResult( IBaseResource ) method and, instead, use your neatly indented, pretty-printed FHIR resource, the one a human being would find pleasant to look at, passed to the validateWithResult( String ) method.
Armed with this understanding, you can put line numbers and columns back into the validation results with spectacular effect by doing/** * @param some pretty XML or JSON representation */ public ValidationResult getResultsFromString( final String CONTENT ) { FhirContext context = new FhirContext( FhirVersionEnum.R5 ); ValidationSupportChain supportChain = new ValidationSupportChain( new DefaultProfileValidationSupport( context ), new InMemoryTerminologyServerValidationSupport( context ), new CommonCodeSystemsTerminologyService( context ) ); IValidatorModule module = new FhirInstanceValidator( context ); FhirValidator validator = context.newValidator().registerValidatorModule( module ); FhirInstanceValidator instanceValidator = new FhirInstanceValidator( supportChain ); instanceValidator.setAnyExtensionsAllowed( true ); return validator.validateWithResult( CONTENT ); }
...instead of
/** * @param some Java object representation */ public ValidationResult getResultsFromString( final IBaseResource RESOURCE ) { FhirContext context = new FhirContext( FhirVersionEnum.R5 ); ValidationSupportChain supportChain = new ValidationSupportChain( new DefaultProfileValidationSupport( context ), new InMemoryTerminologyServerValidationSupport( context ), new CommonCodeSystemsTerminologyService( context ) ); IValidatorModule module = new FhirInstanceValidator( context ); FhirValidator validator = context.newValidator().registerValidatorModule( module ); FhirInstanceValidator instanceValidator = new FhirInstanceValidator( supportChain ); instanceValidator.setAnyExtensionsAllowed( true ); return validator.validateWithResult( RESOURCE ); }
The imports for all of the above:
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.IValidatorModule; import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
For the utility of it, let's print these validation results to the console:
public static void formatOutput( ValidationResult results ) { StringBuilder sb = new StringBuilder(); int count = 1; for( SingleValidationMessage message : results.getMessages() ) { sb.append( " " ) .append( count ) .append( message.getSeverity().toString() ) .append( message.getLocationString() ).append( " " ) .append( message.getLocationLine() ).append( ',' ) .append( message.getLocationCol() ).append( " " ) .append( message.getMessage() ).append( '\n' ); System.out.print( sb ); sb.setLength( 0 ); } }
...and, finally, the sample output, something like:
Severity Location Line,Column Message 1 ERROR Bundle 1,37 bdl-3: 'entry.request mandatory for batch/transaction/history, allowed for subscription-notification, otherwise prohibited' failed 2 ERROR Bundle.entry[0].resource.ofType(Patient).id 10,33 As specified by profile http://hl7.org/fhir/StructureDefinition/Patient, Element 'id' is out of order 3 ERROR Bundle.entry[0].resource.ofType(Patient).telecom[0] 25,18 As specified by profile http://hl7.org/fhir/StructureDefinition/Patient, Element 'telecom' is out of order 4 ERROR Bundle.entry[1].resource.ofType(Encounter) 34,15 Encounter.status: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/Encounter) 5 ERROR Bundle.entry[1].resource.ofType(Encounter) 34,15 Encounter.class: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/Encounter) 6 ERROR Bundle.entry[2].resource.ofType(Encounter) 45,15 Encounter.status: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/Encounter) 7 ERROR Bundle.entry[2].resource.ofType(Encounter) 45,15 Encounter.class: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/Encounter)
This was the content to be validated:
<Bundle xmlns="http://hl7.org/fhir"> <type value="transaction"/> <entry> <fullUrl value="urn:uuid:38826a3f-8e5c-4846-95c3-7f12a233447d" /> <resource> <Patient xmlns="http://hl7.org/fhir"> <meta> <lastUpdated value="2021-01-05T00:00:00.000-06:00" /> </meta> <id value="9812850.PI"/> <name> <family value="Munster" /> <given value="Lily" /> </name> <gender value="female" /> <birthDate value="1827-04-01" /> <deceasedBoolean value="false" /> <address> <text value="1313 Mockingbird Lane, Mockingbird Heights, Beverly Hills, CA 90210" /> <line value="1313 Mockingbird Lane" /> <city value="Beverly Hills" /> <state value="CA" /> <postalCode value="90210" /> </address> <telecom> <system value="phone" /> <value value="+3035551212" /> <use value="home" /> </telecom> </Patient> </resource> </entry> <entry> <resource> <Encounter xmlns="http://hl7.org/fhir"> <id value="emerg" /> <period> <start value="2017-02-01T08:45:00+10:00" /> <end value="2017-02-01T09:27:00+10:00" /> </period> </Encounter> </resource> </entry> <entry> <resource> <Encounter xmlns="http://hl7.org/fhir"> <id value="emerg" /> <period> <start value="2019-11-01T08:45:00+10:00" /> <end value="2019-11-01T09:27:00+10:00" /> </period> </Encounter> </resource> </entry> </Bundle>