Friday, April 8, 2011

UML/OCL or XML/XPath ... which is best

The HL7 / IHE / HealthStory CDA Consolidation project will be submitting the "Meaningful Use" sections for ballot today.  The ballot opens today and can be found on the ballot site under the title: HL7 Implementation Guides for CDA® Release 2: IHE Health Story Consolidation, Release 1 (US Realm), and is available for signup until May 2nd.  If you've signed up, you have until May 9th to submit your comments.

Over time, HL7 has developed a new pattern for conformance statements for CDA Implementation guides based on the following principles:

We use XPath expressions because they are easy for developers to read and understand.  XPath expression have their context (are rooted) at the XML element containing the templateId element.  The keywords SHALL, SHOULD, MAY, NEED NOT, SHOULD NOT and SHALL NOT are highlighted and indicate whether content must, is recommended or simply allowed to be present (or negated in these same forms).

The number of times that things are required or recommended to be present are given in both english text (only one, at least one, one or more, zero or one, zero to many) and in bracketed form [0..*] for commonly used cardinalities.

One of the challenges in developing this guide has been bridging the gap in MDHT between UML and OCL,  where the constraints are represented; and XML and XPath, the way we want to express them to developers.

Here is an example constraint with the associated OCL:

24. SHALL satisfy: At least one [1..*] patientRole element is present in recordTarget (CONF-CONSOL-21)
• [OCL]: self.recordTarget->size() > 0 and self.recordTarget->exists(target : cda::RecordTarget | not target.patientRole.oclIsUndefined())

The same constraint in XPath (assuming the ClincialDocument element as context) is:
count(cda:recordTarget/cda:patientRole) > 0.
That XPath expression will be true if there is at least  1 patientRole element.

I spent 4 hours yesterday writing (manually) conformance statements for the IHE Reconciliation profile following a similar pattern as the CDA Consolidation project.  The manual exercise allowed me to find common patterns which I'd like to share:

  1. A template applies only to certain classes represented in the CDA R-MIM (e.g., a section, act, substanceAdministration, et cetera).
    Template X applies to cda:section elements.
    XPath: self::cda:section
  2. A structural attribute (e.g., moodCode, classCode or typeCode) of the class is fixed to a single value.
    The @classCode attribute of the act element shall be set to the value ACT.
    XPath: @classCode="ACT"
  3. A specific identifier must be used for one of the class attributes.
    The @root attribute of the id element shall be X and
    The @extension attribute of the id element shall be Y
    XPath: @root="X" and @extension="Y"
  4. An identifier from a specific namespace must be used for one of the class attributes.
    The @root attribute of the id element shall be X.
    XPath: @root="X"
  5. An element may be null
    The effectiveTime/@value shall be precise to the day.
    The @nullFlavor attribute may be used on the effectiveTime if the information is unknown.
    XPath: string-length(cda:effectiveTime/@value)>8 or cda:effectiveTime/@nullFlavor
  6. An element shall/should not be null.
    The @nullFlavor attribute shall/should not be present be used on the effectiveTime.
    XPath: not(cda:effectiveTime/@nullFlavor)
  7. A coded class attribute shall/should/may be coded using a specific terminology.
    The @codeSystem attribute shall/should/may of the code element shall be set to the value X.
    XPath: cda:code/@codeSystem="X"
  8. A coded class attribute shall/should/may be used from a specific value set.
    The code element shall contain a value drawn from the X value set.
    XPath: document(ValueSetServiceURL + "?valueSetID=X&code=" + cda:code/@code + "&codeSystem=" + cda:code/@codeSystem)//isValid
    (Note: This is an example using the document() function to call on a RESTful URL that returns XML contain the element isValid if the code and codeSystem given are part of the value set.  The IHE Sharing Value Sets profile is one example of such a service that could be used, but this example doesn't use that syntax.  There's about 10 ways to skin this cat).
  9. A specific data type must be used for a class attribute.
    The value element shall be reported using the PQ datatype.
    XPath: contains(concat(":",cda:value/@xsi:type,":"),":PQ:")
    (Note: This is not quite a perfect.  If matches cases that are wrong because it assumes that if you use cda:PQ that cda:  is the correct namespace declaration.  Better tests can be written but this one would catch most failures to conform). 
  10. A time stamp must/should have a certain precision.
    The effectiveTime/@value attribute of the ClinicalDocument shall be precise to at least the day.
    XPath: substr(@value, 1, string-length(@value) - string-length(substring-after(concat(translate(@value,'+-','|'),'|') ) > 7
    (Note: This one removes any time-zone suffix found by translating the + or - delimiter to a |, adds a | to be sure that a | is ALWAYS present, computes the length of the time-zone suffix, removes it from the string and then checks the lenght of the date.
  11. A name or address component shall/should be present.
    At least one [1..*] family element must be present in the name element.
    XPath: count(cda:name/cda:family)>0
  12. The class shall/should/may contain some number of classes conforming to another template.
    The act shall contain at least one [1..*] observation conforming to Template Name (templateId: 2.16.840...)
    XPath: .//cda:observation[cda:templateId/@root = X]
  13. The class shall/should/may directly contain some number of classes conforming to another template.
    The act shall directly contain only one [1..1] observation conforming to Template Name (templateId: 2.16.840...)
    XPath: ./cda:observation[cda:templateId/@root = X] 
A single constraint addresses a class and attribute in the UML/OCL representation of the CDA content and the templated element  and one or more subordinate attributes or elements and their attributes in the XML/XPath representation.

There's also a couple of different ways to combine constraints:
  1. Disjunction of two or more constraints:  Constraint A OR constraint B or both must be followed.
    The act shall contain at least one of the following:
      only one observation conforming to Template Name (templateId: 2.16.840...)
      one or more observations conforming to Template Name (templateId: 2.16.840...)
  2. Exclusive disjunction of two or more constraints:  Constraint A OR constraint B but not both
    The act shall contain only one of the following:
      only one observation conforming to Template Name (templateId: 2.16.840...)
      one or more observations conforming to Template Name (templateId: 2.16.840...)
  3. A conditional constraint, where if one condition is met, another shall/should be (or not be) met.
    If the birthTime/@value is less than a day in the past, then birthTime/@value shall be precise to the hour.
As presently designed, MDHT represents all constraints as OCL.  I would change this in certain ways.  Rather than use OCL directly, I would rather have a mechanism to store the data needed for the few handfuls of constraints shown above, and a method to translate those into either an OCL or an XPath (or other expression).  If a constraint couldn't be represented in one of these common ways, then I would represent it as either an UML/OCL or XML/XPath constraint.

So, my answer to this question is NEITHER.  What is best is representation of constraints based on common patterns, and only afterwards, translation into the appropriate language.  After all, just because you have a hammer doesn't mean every problem is a nail.

1 comment:

  1. hi Keith

    There's a series of problems around the first constraint.

    > self.recordTarget->size() > 0 and self.recordTarget->exists(target : cda::RecordTarget | not target.patientRole.oclIsUndefined())

    > count(cda:recordTarget/cda:patientRole) > 0

    Firstly, the first part of the OCL is redundant - exists(critera) can only be true if size() > 0.

    Secondly, patientRole is 1..1 required. So it's wrong to have a recordTarget without a patientRole. I suppose it doesn't hurt to check - but it is redundant in an expressed constraint. That applies to both constraints.

    Finally, patientRole is not mandatory, so it could be present and null. In fact, the IG seems to assume that patientRole is not null, but doesn't say so.

    Finally, on general subject, ISO 21090 also maintains constraints as both OCL and XPath. Unlike how you propose, I actually wrote code to convert OCL to XPath - but only for the general patterns you describe above. About 5% of the constraints didn't conform to these patterns, and I had to translate them by hand - indeed, for some of them the way you go about thinking of them has to be different.

    While the % might be lower in the IG's, thery would still exist. How would you propose to handle these exceptions

    ReplyDelete