HL7 Validation
Concept
The HAPI library already offers support for validating HL7 messages by definition of rules that check against constraints on type level, message level, and encoded message level. However, definition of these rules is rather cumbersome.
The IPF HL7 module adds support for specifying validation rules in a way that is easy to write and easy to understand. It facilitates the definition of custom validation rules by exploiting features of the Groovy language that is already used in other parts of IPF.
Validation basics
Creating a ValidationContext
Validation rules are defined by instantiating an implementation of the ValidationContext interface of HAPI. IPF comes with the org.openehealth.ipf.modules.hl7.validation.DefaultValidationContext class, which primarily offers two benefits:
- It provides access to a Validation Builder that allows for definition of validation rules using a rather simple Domain Specific Language (DSL)
- It supports Validation rules that constrain their target object by evaluating Groovy closures for maximum flexibility.
import org.openehealth.ipf.modules.hl7.validation.DefaultValidationContext ... DefaultValidationContext context = new DefaultValidationContext() ...
Add rules to the ValidationContext
Although it is possible to manually instantiate the Validation rule classes that come with IPF, you should use the Validation builder.
...
context.configure()
.forVersion('2.5') // following rule applies to HL7 v2.5
...
.forVersion('2.2 2.3 2.4') // following rule applies to HL7 v2.2, 2.3, and 2.4
...
.forVersion().asOf('2.3') // following rule applies to HL7 versions starting with 2.3
...
.forVersion().before('2.3') // following rule applies to HL7 versions older than 2.3
...
.forVersion().except('2.4') // following rule applies to HL7 versions but 2.4
...
.forAllVersions() // following rule applies to all HL7 versions
...
Before actually specifying the validation rule, its application is restricted to one or more HL7 versions. The possibilities are shown in the code example above and are self-describing.
Now the rules can be specified by specifying constraints on one or more of
- primitive type level
- message level
- encoding level
...
context.configure()
.forVersion('2.5') // limit to HL7 v2.5 messages
.type('DT') // constraints for the DT type
...
.message('ADT', 'A01') // constraints for ADT_A01 messages
...
.encoding() // constraints for encoded messages
...
Details on how the validation rules are syntactically defined are shown below in detail.
Reuse a ValidationContext
If there is already an instance of DefaultValidationContext, you can simply add further rules to it:
DefaultValidationContext context = new DefaultValidationContext() // Add a number of rules context.configure() .forVersion() ... // Add some more rules context.configure() .forVersion() ...
However, it is also possible to reuse any ValidationContext instance with a DefaultValidationContext.
DefaultValidationContext context = new DefaultValidationContext() // Use the builder to add Groovy-based validation rules context.configure() .forVersion() ... // Add existing ValidationContext instances context .addContext(additionalValidationContext1) .addContext(additionalValidationContext2) ...
Validate messages
There are two possibilities to execute validation rules
- during parsing
- after parsing
To validate during parsing, simply configure your Parser instance with the ValidationContext.
...
def parser = new PipeParser(context)
Message message = parser.parse(msgText)
...
During parsing, all variants of validation rules (i.e. primitive types, message and encoding; see below) are checked.
To validate after parsing, use the HAPI MessageValidator class
import ca.uhn.hl7v2.validation.MessageValidator ... new MessageValidator(context, true).validate(message)
You can achieve the same by using a bridge implementing the org.openehealth.ipf.commons.core.modules.api.Validator interface:
new HL7Validator().validate(message, context)
This is the variant that IPF's HL7 DSL extension also uses internally. Currently, only message rules are checked when using the HL7Validator.
Validation variants
Validating against primitive type constraints
HL7 Primitive types have no substructure, i.e. they directly contain values. The values are usually restricted by one or more of the following contraints:
- length
- value type (e.g. whether it evaluates to a decimal or number)
- regular expression pattern
Regular expressions can also be used to restrict length and type of the value, but regular expressions are often hard to read and understand.
Each HL7v2 version defines a set of primitive types; over the time the number of types have increased and/or the constraints have been modified.
A type constraint for a range of HL7 versions is defined as follows:
ValidationContext context = new DefaultValidationContext() context.configure() .forVersion().asOf('2.3') .type('DT') .matches(/(\d{4}([01]\d(\d{2})?)?)?/) // YYYY[MM[DD]] .withReference('Version 2.5 Section 2.A.21')
This enforces that all instances of type DT type must match a date pattern, where the year as mandatory and the month the day of month is optional. The example uses a regular expression to specify the date pattern. Please also read about regular expressions in Groovy.
The Validation DSL for primitive types supports the following constraints:
| Constraint type | Method | Example | constraints... |
|---|---|---|---|
| maximum length | .type('XXX').maxSize(int) | maxSize(255) | ... the maximum length to 255 characters |
| length range | .type('XXX')[min..max] | [10..20] | ... the length to be between 10 and 20 |
| existence | .type('XXX').notEmpty() | ... that there must be a value of >= 1 character | |
| matches | .type('XXX').matches(regexp) | matches(/(\d{4}([01]\d(\d{2})?)?)?/) | ... the value be formatted as a date |
| number type | .type('XXX').isNumber() | ... the value to be a decimal number | |
| user defined | .type('XXX').checkIf(Closure) | checkIf { it.size() <= 255 } | ... the maximum length to 255 characters |
The Closure syntax is the most flexible way to define constraints on a type. Internally, all other constraint methods are implemented by calling checkIf using a specific closure.
Additionally, primitive type constraints are "misused" in HAPI for trimming space characters from values. The Validation DSL supports this as well:
| Constraint type | Method | Example | function |
|---|---|---|---|
| trim | .type('XXX').omitLeadingWhitespace() | removes leading whitespace characters | |
| trim | .type('XXX').omitTrailingWhitespace() | removes trailing whitespace characters |
You can combine any of constraint methods to define more than one rule for a type:
...
.type('XXX').omitLeadingWhitespace().isNumber().checkIf { it > 50 && it < 100 }
...
HL7v2 defines constraints for each primitive types for each version. IPF comes with predefined validation rules that enforce these constraints. You can access them using
import ca.uhn.hl7v2.validation.ValidationContext import org.openehealth.ipf.modules.hl7.validation.support.DefaultTypeRulesValidationContext ... ValidationContext context = DefaultTypeRulesValidationContext()
| PipeParser The PipeParser class in IPF is preconfigured with the default type rules. |
| When primitive type rules are checked Note that primitive type rules can only be applied by configuring the parser. Validation then is executed whenever a primitive value is set:
|
Validating against message constraints
By default, the HAPI parsers accept about any message as long as follows the HL7 syntax rules. For messages, groups and segments not defined in the respective HL7 standard, generic structures are instantiated internally. This allows parsing custom messages even without prior definition of Z segments or similar custom structures.
However, any piece of software with an HL7 interface needs to validate whether the received messages comply with either the official specification or specific for a certain customer or project.
| When message type rules are checked Message rules can be enforced by either configuring the parser or by manually validating a parsed message by using e.g. a HL7Validator. |
HL7 Conformance profiles
Conformance profiles have been introduced with HL7 2.5 as a standardized means of defining the static and dynamic properties of a HL7 message. Conformance profiles are encoded in XML and can be seen as a formal specification language. For more details on HL7 conformance profiles, please refer to chapters 2.12 and 2.19 of the HL7v2.5 specification document.
There are tools that facilitate the definition of conformance profiles, most notably the Messaging Workbench, which can be downloaded here.
HAPI supports checks against conformance profiles. In the IPF Validation framework it can be used as follows:
DefaultValidationContext context = new DefaultValidationContext() ProfileStoreFactory.setStore(new ClassPathProfileStore()) context.configure() .forVersion('2.5') .message('QBP', 'Q22').conformsToProfile('IHE-PDQ-QBP-Q22')
In this case, a file with the name IHE-PDQ-QBP-Q22.xml is looked up in the root of the Java classpath. Without the call to ProfileStoreFactory.setStore, the default HAPI ProfileStore is used, which looks into the <hapi.home>/profiles directory.
HL7 Abstract Syntax specification
HL7v2 defines an abstract message syntax (see HL7v2.5 specification, Chapter 2.13), that specifies which groups and segments are expected for a specific message type. Cardinality is indicated by using
- brackets ([...]) for optional groups or segments [0..1]
- braces ({...}) for repeatable groups or segments [1..*]
- a combination of both ({[...]} or [{...}]) for optional and repeatable groups or segments [0..*]
IPF provides support for checking a message instance against such an abstract message syntax definition. The corresponding rule is almost a copy of the Message Syntax, with a few differences:
- segment names are specified in quotes ('')
- group names are specified like function calls inside the cardinality indicators described above
- a choice of one segment from a group of segments is currently not supported.
The following comparison gives an example:
| HL7 Abstract Message Syntax definition | IPF Validation rule |
|---|---|
MSH
[ { SFT } ]
PATIENT_RESULT
PATIENT
{ [ PID
[ PD1 ]
[ { NTE } ]
[ { NK1 } ]
VISIT
[ PV1
[ PV2 ]
]
VISIT
]
PATIENT
ORDER_OBSERVATION
{ [ ORC ]
OBR
[ { NTE } ]
TIMING_QTY
[{ TQ1
[ { TQ2 } ]
}]
TIMING_QTY
[ CTD ]
OBSERVATION
[{ OBX
[ { NTE } ]
}]
OBSERVATION
[ { FT1 } ]
[ { CTI } ]
SPECIMEN
[{ SPM
[ { OBX } ]
}]
SPECIMEN
}
ORDER_OBSERVATION
}
PATIENT_RESULT
[ DSC ]
|
DefaultValidationContext context = new DefaultValidationContext()
context.configure()
.forVersion('2.4')
.message('ORU', 'R01').abstractSyntax(
'MSH',
[ { 'SFT' } ],
{PATIENT_RESULT(
[PATIENT(
'PID',
[ 'PD1' ],
[ { 'NTE' } ],
[ { 'NK1' } ],
[VISIT(
'PV1',
[ 'PV2' ]
)]
)],
{ORDER_OBSERVATION(
[ 'ORC' ],
'OBR',
[{ 'NTE' }],
[{TIMING_QTY(
'TQ1',
[{ 'TQ2' }]
)}],
[ 'CTD' ],
[{OBSERVATION(
'OBX',
[ { 'NTE' } ]
)}],
[{ 'FT1' }],
[{ 'CTI' }],
[{SPECIMEN(
'SPM',
[{ 'OBX' }]
)}]
)}
)},
[ 'DSC' ]
)
|
Note that the fields inside segments can neither be specified nor validated with the abstract message syntax.
Custom validation
You can use message rules also to program you own custom constraints on one or more trigger events. All there is to do is to write a checkIf closure that returns an array of HAPI {{ValidationException}}s. The array is empty, if validation passes.
...
context.configure().forAllVersions()
.message('ADT', 'A01')
.checkIf { msg ->
def validationExceptions = []
// validate and return an (empty) ValidationException array
return validationExceptions
}
.message('ADT', 'A01 A04 A08')
.checkIf { msg ->
// define constraints for all three trigger events
}
.message('ADT', ['A01', 'A04', 'A08'])
.checkIf { msg ->
// same as above but specified as list
}
.message('ADT', '*')
.checkIf { msg ->
// for all trigger events of the ADT message type
}
Validating against encoded messages
These kind of rules can be used to validate the encoded representation of a HL7 message, i.e. their String representation in either ER7 (Pipe) or XML encoding. Besides providing a ClosureEncodingRule class, only a link to HAPI's XMLSchemaRule is defined:
...
.forVersion()
...
.encoding("XML").isValidXML()
...
Settings up validation rules using a Spring application context
To initialize a DefaultValidationContext from a Spring Framework application context, you need a ValidationContextFactoryBean and one or more ValidationContextBuilder beans.
<beans> ... <bean id="parser" class="org.openehealth.ipf.modules.hl7.parser.PipeParser"> <property name="validationContext" ref="context"/> </bean> <bean id="context" class="org.openehealth.ipf.modules.hl7.validation.ValidationContextFactoryBean"/> <bean id="defaultTypeRules" class="org.openehealth.ipf.modules.hl7.validation.builder.DefaultTypeRulesBuilder"/> <bean id="myCustomRules" class="com.my.company.MyValidationContextBuilder"/> ... </beans>
Each ValidationContextBuilder will contribute to the overall set of rules being applied. In the example above, two rule sets are added
- the default set of primitive type rules
- A custom set of rules of either variant (type, message or encoding).
A skeleton implementation for such a custom builder is given in the following example:
import ca.uhn.hl7v2.validation.ValidationContext import ca.uhn.hl7v2.validation.ValidationException import org.openehealth.ipf.modules.hl7.validation.builder.RuleBuilder import org.openehealth.ipf.modules.hl7.validation.builder.ValidationContextBuilder public class MyValidationContextBuilder extends ValidationContextBuilder { public RuleBuilder forContext(ValidationContext context) { context.configure() .forVersion('...') .message(...) ... .type(...) ... .encoding(...) ... } }