Thursday, 12 February 2009

Camel Routes and HL7

I've been spending some time with Apache Camel and ActiveMQ, with a view to using Camel in a healthcare integration project.  Camel has support for HL7 both at the message structure level (wrapping access to the HAPI libraries) and at the protocol level (via the Apache Mina extensions for HL7 MLLP).

As anyone who's looked at Camel (or IONA's FUSE Mediation Router) will know, Camel implements a rather nice Java DSL for building routes.  This lets you write things like:

from("hl7listener")
            .unmarshal(hl7Format)
            .choice()
              .when( header("hl7.msh.messageType").isEqualTo("ORM") )
                    .beanRef("hl7handler", "handleORM")
              .otherwise()
                    .beanRef("hl7handler", "badMessage")
              // end choice block- marshal the ACK/NAK back to the TCP endpoint
            .end()
            .marshal(hl7Format);

Very readable, but as you might expect, it hides quite a bit of complexity.  This is no bad thing, but you do need to understand what's going on behind the DSL. 

Camel exploits Spring, and in the above fragment "hl7listener" and "hl7handler" are bean names. The unmarshal and marshal methods allow DataFormat objects to convert between byte-streams and more convenient objects, such as HAPI messages.  The choice/when/otherwise operators allow predicates to control routing.

It's taking a while for me to grok Camel; here is a couple of things I'm not yet clear about:
  • Knowing how to get an intermediate stage in a route to return an ACK or NAK to the originator, without subsequent (or parallel) stages in the route from doing so. 
  • Creating compound boolean expressions in predicates. For instance, if I want a conjunction inside a when(), I can't have it. There don't appear to be combinators for expressions.  
In the fragment above for example, how can I express a conjunction/disjunction in the when( ... ) expression?  It only seems to be able to handle a single condition: I haven't found a way to combine predicates e.g. I can't do this:

when ( header("hl7.msh.messageType").isEqualTo("ORM") &&
             header("hl7.msh.triggerEvent").isEqualTo("O01") ).    // etc.

I have tried alternatives, e.g. nesting the when clauses:

from("hl7listener")
            .unmarshal(hl7Format)
            .choice()
              .when( header("hl7.msh.messageType").isEqualTo("ORM") )
                  .choice()
                    .when( header("hl7.msh.triggerEvent").isEqualTo("O01") )
                        .beanRef("hl7handler", "handleORM")
                    .otherwise()
                        .beanRef("hl7handler", "badMessage")
              .otherwise()
                  .beanRef("hl7handler", "badMessage")
            // end choice block- marshal the ACK/NAK back to the TCP endpoint
            .end()
            .marshal(hl7Format);

But this doesn't work either.  I'm just starting out with Camel, so I expect I simply haven't read the right bit of the documentation (which even the creators admit is one of Camel's weak points).

Another problem is controlling which component in the route returns a response to the originating endpoint (the MLLP sender, in the HL7 case).  I may have solved this one, but it's worth setting out the problem and my approach, in case it helps someone else.

My requirements are for the pipeline to log the inbound message (i.e. whatever is actually received on the HL7 socket), unmarshal the message to HL7 (HAPI), then validate the HL7 message type: at this stage in the pipeline I want to return a NAK to the sender (on the HL7 MLLP connection) if the message is not of the expected type, otherwise I want to write the message to a JMS queue for further processing and return an ACK.

The following route appears to work (note that I have omitted the message-logger stage):

        // We need the specific HL7 DataFormat object for unmarshalling HL7 ER7 from the
        // inbound MLLP link.
        DataFormat hl7Format = new HL7DataFormat();

        // Inbound HL7 comes from MLLP endpoint. 
        // Timestamp message, log receipt, unmarshal and perform initial
        // handling, which just determines if its the right kind of HL7
        // message.
        // If so, return ACK to originator and push message to JMS queue.
        // If not, return NAK to originator and end.
        //
        from("hl7listener")
            .unmarshal(hl7Format)
            .choice()
              .when( header("hl7.msh.messageType").isEqualTo("ORM") )
                    .marshal().hl7().to("jms:queue:orderMessage.queue")
              .otherwise()
                    .beanRef("hl7handler", "badMessage")
            .end()
            .marshal(hl7Format);
        
        // The order message processor - pulls message from JMS queue and sends to
        // the processor bean, which creates and returns an ACK to the hl7 channel
        // if the message is of the expected type.
        //
        from("jms:queue:orderMessage.queue")
            .unmarshal().hl7().beanRef("hl7handler", "handleORM"); 

All of this routing can be expressed in XML instead of using the Java DSL. Once I'm comfortable with Camel I may try writing a NetBeans plugin or RCP application to create visuals tools for Camel routes.


6 comments:

  1. Hi Roger

    Very nice post.

    I have created a blog response how to create compound predicates in Camel and to use it in your route:

    The post is here on my blog:
    http://davsclaus.blogspot.com/2009/02/apache-camel-and-using-compound.html

    ReplyDelete
  2. Great post! BTW to answer your very last comment - there is an existing Eclipse based viewer of camel routes if you fancy tinkering with it...

    http://code.google.com/p/camel-route-viewer/

    ReplyDelete
  3. And to answer the question: which component delivers the response to the starting endpoint, eg. the HL7 listener.

    Camel will return the OUT body (if any exists, otherwise it fallbacks to IN body) as the response from the last step in the route path. I believe eg. Mule does the same.

    You can use the transform() DSL to set a explicit OUT body at the end of your route.

    See part 5 of this tutorial where we do something like this, but for a webservice as starting endpoint instead:
    http://camel.apache.org/tutorial-example-reportincident-part5.html

    ReplyDelete
  4. @James: thanks for the link - I will take a look at the Eclipse tool. I'm a NetBeans user, and as there's a very nice Visual Library for NB, I'd love to have a crack at Camel route tooling on that platform. Just got to find the time...!

    ReplyDelete
  5. Hi, can you show at what point and how the ack are sent? Or does camel handle that for you transparently?

    ReplyDelete
  6. Is there any camel route viewers availalbe for Java DSl?

    ReplyDelete