- 1 Introduction
- 1.1 Getting started
- 1.1.1 Initial reading
- 1.1.2 Infrastructure setup
- 1.1.3 First project
- 1.2 IPF overview
- 1.3 IPF subprojects
- 1.4 IPF architecture
- 1.4.1 Component architecture
- 1.4.1.1 IHE components
- 1.4.1.2 IPF and OSGi
- 1.4.1.3 Namespace and component descriptions
- 2 Scripting layer
- 2.1 DSL extension mechanism
- 2.1.1 Limitations
- 2.2 Predefined DSL extensions
- 3 Core features
- 3.1 Module adapters
- 3.2 DSL extensions
- 3.2.1 DSL extension for existing Camel features
- 3.2.1.1 Closure support
- 3.2.1.2 Bean lookup
- 3.2.1.3 Error handler
- 3.2.1.4 Interceptor
- 3.2.2 DSL extensions for ExpressionClause
- 3.2.2.1 Exception objects and messages
- 3.2.3 DSL extensions for custom IPF processors
- 3.2.3.1 Content enrichment
- 3.2.3.2 Validation process
- 3.2.3.3 Splitter
- 3.2.4 DSL extensions for IPF module adapters
- 3.2.4.1 Transmogrifier
- 3.2.4.1.1 Inclusion options
- 3.2.4.1.2 Transmogrifier input
- 3.2.4.1.3 Transmogrifier output
- 3.2.4.1.4 Transmogrifier implementations
- 3.2.4.2 Validator
- 3.2.4.3 Parser
- 3.2.4.3.1 Unmarshalling via Parser
- 3.2.4.4 Renderer
- 3.2.4.4.1 Marshalling via Renderer
- 3.2.4.5 Predicate
- 3.2.4.6 Aggregator
- 3.2.4.7 Adapter extension summary
- 3.2.4.7.1 Relevant types
- 3.2.4.7.2 Parameters and input
- 3.2.4.7.3 Closure profiles
- 3.2.5 DSL extensions for Groovy XML processing
- 3.2.5.1 XML Unmarshalling with Groovy XmlParser
- 3.2.5.2 XML Unmarshalling with Groovy XmlSlurper
- 3.2.5.3 XML Marshalling with Groovy XmlNodePrinter
- 3.2.5.4 XML transmogrifiers
- 4 Mapping Service
- 5 HL7 v2 Messaging
- 5.1 HL7 v2 Messaging Overview
- 5.2 Features
- 5.3 Configuring HL7 v2 Messaging
- 5.4 HL7 v2 DSL
- 5.4.1 Construction
- 5.4.2 Navigation
- 5.4.2.1 Navigation to groups and segments
- 5.4.2.2 Navigation to fields
- 5.4.2.3 Field values
- 5.4.2.4 HL7 Null Values
- 5.4.2.5 Repetitions
- 5.4.2.6 Smart navigation
- 5.4.2.7 Access target objects
- 5.4.3 Manipulation
- 5.4.3.1 Manipulating segments
- 5.4.3.2 Manipulating fields
- 5.4.3.3 Adding repetitions
- 5.4.4 Rendering
- 5.5 Functional Extensions to HAPI
- 5.5.1 HL7 PipeParser and custom ModelClassFactory
- 5.5.1.1 CustomModelClassFactory
- 5.5.1.2 Custom PipeParser
- 5.5.2 Methods added to the HAPI Message interface
- 5.5.2.1 New Messages
- 5.5.2.2 Acknowledgements and Responses
- 5.5.2.3 Message checks
- 5.5.2.4 Message dump
- 5.5.3 Methods added to the HAPI Structure interface
- 5.5.3.1 New segments
- 5.5.3.2 Printing structures
- 5.5.4 Methods added to the HAPI Type interface
- 5.5.4.1 New fields
- 5.5.4.2 Printing types
- 5.5.4.3 Mapping Service
- 5.6 Examples for HL7 Messaging
- 5.6.1 Create a message from scratch
- 5.6.2 Create a ORU_R01 v2.5 message
- 5.6.3 Conclusion
- 5.7 Language reference
- 5.7.1 Message elements
- 5.7.2 Read access operations
- 5.7.2.1 Read access operations on non-repeating message elements
- 5.7.2.2 Read access operations on repeating message elements
- 5.7.3 Write access operations
- 5.7.4 Access to special objects
- 6 HL7 v2 Message Validation
- 6.1 Overview
- 6.2 Validation Basics
- 6.2.1 Validation
- 6.2.2 IPF HL7 Validation Rules
- 6.2.3 Reusing a DefaultValidationContext
- 6.3 Configuring HL7 v2 Validation
- 6.4 Primitive Type Constraints
- 6.5 Message Constraints
- 6.5.1 HL7 Abstract Message Syntax
- 6.5.2 HL7 Conformance Profiles
- 6.5.3 Custom Message Validation
- 6.6 Encoded Message Constraints
- 7 Camel DSL Extensions for HL7 v2
- 7.1 Configuring Camel DSL Extensions for HL7 v2 Messaging
- 7.2 DSL extensions for HL7 v2 Messaging
- 7.2.1 HL7 v2 DSL MessageAdapter (un)marshalling
- 7.2.2 (Un)marshaling options
- 7.2.3 HL7 v2 Message Validation
- 7.3 Camel DSL Extensions for HL7 v2 Messaging Example
- 8 IHE support
- 8.1 Concepts
- 8.2 Quick reference
- 8.3 XDS.b
- 8.3.1 XDS standard conformance
- 8.3.2 XDS.b configuration
- 8.3.3 Exposing an XDS.b service
- 8.3.4 Making calls to an XDS.b service
- 8.3.5 Message types
- 8.3.5.1 Raw ebXML 3.0 support
- 8.3.5.2 Simplified model classes
- 8.3.6 Large document content
- 8.3.7 Validation
- 8.4 XDS.a
- 8.4.1 XDS standard conformance
- 8.4.2 ITI-17 configuration
- 8.4.3 Exposing the ITI-17 transaction
- 8.4.4 Making calls to the ITI-17 transaction
- 8.4.5 Message types
- 8.4.5.1 Raw ebXML 2.1/3.0 support
- 8.4.5.2 Simplified model classes
- 8.4.6 Large document content
- 8.4.7 Validation
- 8.5 PIX + PDQ
- 8.5.1 Making and accepting calls via PIX/PDQ endpoints
- 8.5.2 URL Parameters
- 8.5.2.1 Differences between the PIX/PDQ components and camel-mina / camel-hl7
- 8.5.2.2 Additional URL parameters
- 8.5.3 Handling of data types and exceptions
- 8.5.4 Validation
- 8.6 ATNA
- 8.6.1 Configuring auditors
- 8.6.1.1 Individual configuration
- 8.6.1.2 Group configuration
- 8.6.2 Disabling auditing
- 8.6.3 Writing down incomplete audit records
- 8.6.4 Configure auditing transport: Reliable and special auditing
- 8.6.5 Routing audit messages to Camel endpoints
- 8.7 URL Parameters' Summary
- 9 CDA support
- 9.1 Clinical Document Architecture - a brief overview
- 9.2 Generic CDA support
- 9.2.1 Configuration
- 9.2.2 Creating generic CDA documents
- 9.2.2.1 Groovy builders
- 9.2.2.2 CDA Header
- 9.2.2.3 CDA body
- 9.2.2.3.1 Narrative Block
- 9.2.2.3.2 Structured part
- 9.2.2.4 CDA builder tips
- 9.2.2.4.1 Including complete parts
- 9.2.2.4.2 Variable-typed values
- 9.2.2.4.3 Using regular Groovy code inside CDA builder
- 9.2.3 Parsing and Rendering of CDA documents
- 9.2.3.1 Parsing
- 9.2.3.2 Extracting information from CDA documents
- 9.2.3.3 Rendering
- 9.2.4 Validating CDA documents
- 9.3 DSL extensions
- 9.3.1 Configuration
- 9.3.2 CDA (un)marshalling
- 9.3.3 (Un)marshaling options
- 9.3.4 CDA document validation
- 9.4 CDA Builder Syntax Reference
- 9.4.1 General Builder Syntax
- 9.4.2 CDA Schema Names
- 9.5 CDA profile support
- 9.5.1 CCD
- 9.5.1.1 Configuration
- 9.5.1.2 Usage
- 9.5.1.3 Purpose section
- 9.5.1.3.1 Builder Elements
- 9.5.1.3.2 Example
- 9.5.1.4 Payers section
- 9.5.1.4.1 Builder Elements
- 9.5.1.4.2 Example
- 9.5.1.5 Advance Directives section
- 9.5.1.5.1 Builder Elements
- 9.5.1.5.2 Example
- 9.5.1.6 Support section
- 9.5.1.6.1 Builder Elements
- 9.5.1.6.2 Example
- 9.5.1.7 Functional Status section
- 9.5.1.7.1 Functional Status
- 9.5.1.7.2 Example
- 9.5.1.8 Problems section
- 9.5.1.8.1 Builder Elements
- 9.5.1.8.2 Example
- 9.5.1.9 Familiy History section
- 9.5.1.9.1 Builder Elements
- 9.5.1.9.2 Example
- 9.5.1.10 Social History section
- 9.5.1.10.1 Builder Elements
- 9.5.1.10.2 Example
- 9.5.1.11 Alerts section
- 9.5.1.11.1 Builder Elements
- 9.5.1.11.2 Example
- 9.5.1.12 Medications section
- 9.5.1.12.1 Builder Elements
- 9.5.1.12.2 Example
- 9.5.1.13 Medical Equipment section
- 9.5.1.13.1 Builder Elements
- 9.5.1.13.2 Example
- 9.5.1.14 Immunizations section
- 9.5.1.14.1 Builder Elements
- 9.5.1.14.2 Example
- 9.5.1.15 Vital Signs section
- 9.5.1.15.1 Builder Elements
- 9.5.1.15.2 Example
- 9.5.1.16 Results section
- 9.5.1.16.1 Builder Elements
- 9.5.1.16.2 Example
- 9.5.1.17 Procedures section
- 9.5.1.17.1 Builder Elements
- 9.5.1.17.2 Example
- 9.5.1.18 Encounters section
- 9.5.1.18.1 Builder Elements
- 9.5.1.18.2 Example
- 9.5.1.19 Plan of Care section
- 9.5.1.19.1 Builder Elements
- 9.5.1.19.2 Example
- 10 Flow management
- 10.1 Concept
- 10.2 JMX interface
- 10.2.1 Message content
- 10.2.2 JConsole extension
- 10.3 Configuration
- 10.3.1 Derby database
- 10.3.2 Oracle Database
- 10.4 DSL extensions
- 10.4.1 The initFlow DSL extension
- 10.4.1.1 Parameterization of initFlow
- 10.4.2 The ackFlow DSL extension
- 10.4.2.1 Parameterization of ackFlow
- 10.4.3 The nakFlow DSL extension
- 10.4.3.1 Parameterization of nakFlow
- 10.4.4 The dedupe DSL extension
- 10.5 Splits and multicasts
- 10.5.1 IPF version <= 1.6.0
- 10.5.2 IPF version > 1.6.0
- 11 Flow removal
- 11.1 JMX interface
- 11.2 Configuration
- 12 Event infrastructure
- 12.1 Overview
- 12.1.1 Architecture
- 12.1.2 Terminology
- 12.1.3 Usage summary
- 12.2 Details
- 12.2.1 Event Engine
- 12.2.2 Events
- 12.2.3 Event sources
- 12.2.3.1 Event publishing via the DSL
- 12.2.3.2 Event publishing via API
- 12.2.4 Event handlers and filters
- 12.2.5 Event channels and adapters
- 12.2.5.1 Using Camel routes as channels
- 12.2.6 Modularization
- 13 Large binary support
- 13.1 Concept
- 13.2 DSL extensions
- 13.3 Using the LBS
- 13.3.1 Configure a project to use the LBS
- 13.3.2 Set up a disk store
- 13.3.3 Adding support for the HTTP endpoint
- 13.3.3.1 Storing singlepart uploads
- 13.3.3.2 Storing multipart uploads
- 13.3.3.3 Storing downloads
- 13.3.3.4 Uploading stored binaries
- 13.3.4 Adding support for the CXF endpoint
- 13.3.4.1 Storing binaries from a SOAP request
- 13.3.4.2 Storing binaries from a SOAP response
- 13.3.4.3 Preparing a SOAP request with stored binaries
- 13.3.5 Adding support for the MINA endpoint
- 14 Performance measurement
- 14.1 Performance questions to be answered
- 14.2 Usage of the component
- 14.2.1 Statistical reports generated by the component
- 14.2.2 Configuration
- 14.3 Single node deployment with a performance measurement server
- 14.3.1 Statistical reports generated by the performance measurement server
- 14.3.2 Configuration of the application to use a performance measurement server
- 14.3.3 Deployment and configuration of the performance measurement server
- 14.4 Cluster deployment with a performance measurement server
- 14.5 DSL extensions for performance measurement
- 14.6 HTTP interface of the performance measurement component
- 15 Quality of service
- 15.1 Recoverability
- 15.1.1 Transactional messaging
- 15.1.1.1 Recover from processor failures
- 15.1.1.2 Recover from JVM crashes
- 15.1.1.3 Recover from failed deliveries
- 15.1.1.4 Distributed transaction processing
- 15.1.1.5 Configuration
- 15.1.1.6 Transactional routes
- 15.1.2 Flow management
- 15.1.3 Process management
- 15.2 Availability
- 16 Appendix A - IPF development
- 16.1 Environment
- 16.2 Sources
- 16.2.1 Checkout via command line
- 16.2.2 Checkout via TortoiseSVN
- 16.2.3 Checkout via Subclipse
- 16.2.4 Structure
- 16.3 Binaries
- 16.4 Projects
- 16.4.1 Eclipse import
- 16.5 Archetypes
- 16.5.1 Overview
- 16.5.2 Example
- 16.5.2.1 Create project
- 16.5.2.2 Create assembly
- 16.5.2.3 Eclipse import
- 17 Appendix B - IPF tutorials
- 17.1 First steps tutorial
- 17.1.1 Source code
- 17.1.2 Project creation
- 17.1.3 Route definition
- 17.1.4 Route extension
- 17.1.5 Project testing
- 17.1.5.1 Unit test
- 17.1.5.2 Server test
- 17.1.6 Assembly and installation
- 17.1.7 Start server
- 17.1.8 Summary
- 17.2 First details tutorial
- 17.2.1 Source code
- 17.2.2 Project creation
- 17.2.2.1 Route definition
- 17.2.2.2 Extension definition
- 17.2.2.3 Application configuration
- 17.2.2.4 Project descriptor
- 17.2.2.5 Assembly descriptor
- 17.2.3 Project customization
- 17.2.4 Project testing
- 17.2.4.1 Unit test
- 17.2.4.2 Server test
- 17.2.5 Assembly and installation
- 17.2.6 Start server
- 17.3 HL7 processing tutorial
- 17.3.1 Validation
- 17.3.2 Transformation
- 17.3.3 Route design
- 17.3.4 Source code
- 17.3.5 Project creation
- 17.3.6 Extend project descriptor
- 17.3.7 Extend application context
- 17.3.8 Route definition
- 17.3.9 Code mapping
- 17.3.10 Route testing
- 17.3.10.1 Automated test
- 17.3.10.2 Manual test
- 17.3.11 Assembly and installation
- 17.3.12 Start server
- 17.4 Tutorial for routing to a webservice via HTTP
- 17.4.1 Source code
- 17.4.2 Create a basic project using the IPF and LBS
- 17.4.3 Create the webservice
- 17.4.4 Add the routing
- 17.5 Reference application
- 17.6 XDS demo repository
- 17.6.1 Overview
- 17.6.2 Running XDSToolKit tests against the demo repository
- 17.6.3 IPF XDS related code snippets
- 17.6.3.1 Basic configuration
- 17.6.3.2 Exposing the endpoints
- 17.6.3.3 Validating incoming messages
- 17.6.3.4 Using the meta data classes
- 17.6.3.4.1 Evaluating the query type
- 17.6.3.4.2 Splitting for individual entry processing
- 17.6.3.5 Secure transport
- 17.6.3.6 Auditing
- 18 Appendix C - IPF Guidelines
- 18.1 DSL extensions guide
- 18.1.1 Processor with custom name
- 18.1.2 DSL extensions using a model class
- 18.1.3 Parameterized DSL extensions
- 19 Appendix D - IPF context
- 19.1 IHE
- 19.1.1 Deployment options
- 20 Appendix E - Known camel issues
- 21 Appendix F - DSL extensions index
- 21.1 DSL Extensions provided by platform-camel-core
- 21.2 DSL Extensions provided by platform-camel-flow
- 21.3 DSL Extensions provided by platform-camel-lbs
- 21.4 DSL Extensions provided by platform-camel-event
- 21.5 DSL Extensions provided by platform-camel-hl7
- 21.6 DSL Extensions provided by platform-camel-cda
- 21.7 DSL Extensions provided by platform-camel-test
- 21.8 DSL Extensions provided by platform-camel-ihe-xds-core
- 21.9 DSL Extensions provided by platform-camel-ihe-mllp-core
Introduction
Getting started
Prerequisites
|
Initial reading
After you've learned some basics about Apache Camel the best way to start with IPF is to read through the IPF overview section. It will give you a high-level view of the features provided by IPF including some links to more detailed documentation. The IPF architecture section starts with diagrams of the physical components that make up IPF. A short description of the component namspaces and the individual components is given as well.
Infrastructure setup
Before you start working with IPF make sure that you've read the IPF development pages. These will explain how to
- Setup the development environment.
- Checkout and compile the sources (optional).
- Import the IPF sources into Eclipse (optional).
- Create new projects using archetypes (see also IPF Tutorials).
First project
The first steps tutorial is a good starting point for creating your first IPF project. It demonstrates how to use IPF project archetypes for creating a project and walks through the key project artifacts. A simple message processing example will introduce some core features of IPF. You can find further tutorials on the IPF tutorials page.
IPF overview
The Open eHealth Integration Platform (IPF) is an extension of the Apache Camel routing and mediation engine. It has an application programming layer based on the Groovy programming language and comes with comprehensive support for message processing and connecting systems in the eHealth domain. IPF provides domain-specific languages (DSLs) for implementing Enterprise Integration Patterns in general-purpose as well as healthcare-specific integration solutions. These DSLs are extensible via Groovy meta-programming. An example of an healthcare-related use case of IPF is the implementation of interfaces for transactions specified in IHE profiles, but you may also use it for developing integration solutions in other domains. IPF can be easily embedded into any Java application and additionally supports deployments inside OSGi environments. Failure recovery and high-availability features support application developers implementing non-functional requirements. The following table summarizes the IPF features.
| Feature | Description |
|---|---|
| Apache Camel | IPF is based on Apache Camel. For an overview of Camel's rich feature set (which can be fully used in IPF applications) refer to the project's integration patterns and integration components pages. |
| Groovy scripting layer | With IPF you define integration routes with the Groovy programming language. It is more than a mere usage of Camel's domain-specific language (internal DSL or fluent API) inside Groovy: Camel's native DSL has been extended to support e.g. the usage of closures (for inline definitions of message processors, routing rules etc.) and also provides a DSL extension mechanism to define custom extensions to the Camel DSL. |
| DSL extension mechanism | The DSL extension mechanism is a Groovy meta-programming-based mechanism for defining new DSL elements to be used in integration routes. This is especially useful if you want to provide custom language elements for re-occurring message processing patterns or if you want to design a project-specific message processing DSL (e.g. one that is related to the HL7 domain). |
| DSL extension index | An index of all predefined DSL extensions provided by IPF. |
| Core features | These are domain-neutral message processors and DSL extensions usable for general-purpose message processing. The core features also enhance existing Camel DSL elements for usage with Groovy-specific language elements such as closures. For XML message processing there is special Groovy XML support. |
| HL7 message processing | Basis for HL7 message processing is the HL7 DSL, the HAPI extensions and the HL7 validation DSL. These provides the basis for implementing HL7 message processing routes. |
| IHE support | A set of components for creating actor interfaces as specified in IHE profiles. IPF currently supports creation of actor interfaces for the IHE profiles XDS.a, XDS.b, PIX and PDQ. |
| CDA support | A domain-specific language for building and navigating CDA documents. This DSL supports the creation of structurally correct CDA documents by enforcing CDA-relevant schema definitions but without dealing with low-level XML details. |
| Flow management | A platform service to monitor, query, audit, replay and cleanup message flows. The management interfaces are based on JMX. |
| OSGi support | Enables the deployment of IPF components (bundles) to OSGi platforms. IPF service bundles register platform services at the OSGi service registry for consumption by IPF applications. Extender bundles control the activation of DSL extensions inside an OSGi environment. A reference implementation of IPF on top of Eclipse Equinox is available as IPF runtime in the IPF Tools project. This project also maintains the complete IPF OSGi documentation. |
| Event infrastructure | An infrastructure for unified publishing of system-events and application-events. Subscriber components can be configured to translate application events to e.g. Atom/RSS feeds or log files to mention a few. |
| Performance measurement | DSL and tools to determine the performance characteristics of IPF applications. These allow for measuring the processing time of messages for routes or route parts as well as the message throughput. Performance measurement results can be viewed with a web browser. |
| Large message support | Provides memory efficient processing of messages with large content sizes. |
| Quality of service | IPF provides extensions, guidance and solution blueprints (code examples) for implementing non-functional requirements. Covered topics are transactional messaging, flow management, load-balaning and high-availability. |
| Module adapters | An infrastructure for including platform-independent message processing libraries into platform-specific message processing routes. An alternative is Camel's bean integration mechanism. |
| Tutorials | A bunch of tutorials that help you get started with IPF. |
| Guidelines | Guidelines for IPF application development. For example, the DSL extensions guide describes how to write you own DSL extensions. |
| Project templates | Maven archetypes for most commonly used IPF project types, ranging from simple embedded integration solutions to cluster configurations supporting high-availability scenarios. Usage examples of IPF features are provided as well. |
IPF subprojects
Since version 2.0 of IPF several existing components as well as newly developed components have been moved to IPF subprojects. There are currently two IPF subprojects which are described in the following subsections.
IPF Tools
This project provides Eclipse-based development, administration and monitoring tools for the Open eHealth Integration Platform.
- The development tools support developers in creating, testing and packaging IPF applications within the Eclipse plugin development environment (PDE). IPF applications created with these tools can be deployed and operated on OSGi R4.1 platforms. Part of the development tools is the IPF Runtime - the OSGi edition of IPF.
- The administration and monitoring tools are Eclipse plugins for managing IPF platform and application resources. Examples are JMX-based system management clients or the IPF platform manager.
- The IPF runtime is an Eclipse plugin that make IPF features and services available for application development inside Eclipse. The runtime is used to develop OSGi-based IPF applications.
For guidelines how to contribute to this project refer to the Contributing page of the IPF documentation.
IPF Lab
This project is the incubation area for the projects Open eHealth Integration Platform and Open eHealth Integration Platform Tools. Contributions of new features from the IPF community will be mainly done in this project. New features may come from existing IPF applications or are developed from scratch directly in the IPF Lab. This project also hosts components from current research activities such as integrating IPF into cloud-computing environments or into different web application frameworks. For guidelines how to contribute to this project refer to the Contributing page of the IPF documentation.
IPF architecture
| IPF architecture in OSGi environments described elsewhere IPF's architecture in OSGi environments is described in the architecture section of the OSGi support page. It complements the information presented here. |
Component architecture
The following figure gives an overview of the IPF components, their dependencies and their namespaces (abbreviated package names). The components of the ihe subpackages (white color) are shown in the next section. The last subsection describes the namespaces and components in more detail. The component names match the jar file names in the Maven repository. The IPF component architecture is also closely related to the project structure in the code repository.
IHE components
The next figure shows the IPF IHE components. The Camel-independent components are contained in the ipf.commons.ihe package, Camel-specific IHE components are contained in the ipf.platform-camel.ihe package. IPF IHE components can be used to implement actor interfaces of IHE transactions. The transaction numbers (ITI) contained in the component name correspond to the transaction numbers in IHE specifications.
IPF and OSGi
All IPF components have been designed to run on an OSGi R4 version 4.1 platform. The osgi package additionally containes specialized OSGi bundles such as extender bundles and configuration fragments (not shown). For a detailed description about IPF and OSGi refer to the OSGi support section.
Namespace and component descriptions
| Namespace | Description |
|---|---|
| commons | Namespace for commonly used libraries. Components in this namespace are independent of Apache Camel and have no dependencies to platform-camel. |
| modules | Namespace for domain-specific (e.g. HL7) libraries and message processing components. Components in this namespace are independent of Apache Camel and have no dependencies to platform-camel. |
| osgi | Namespace for IPF OSGi infrastructure and configuration bundles. |
| platform-camel | Namespace for extensions to the Apache Camel routing and mediation engine. These extensions together with Apache Camel are referred to as the Open eHealth Integration Platform (IPF) from a technical viewpoint. |
| tutorials | Namespace for tutorials demonstrating the features of the Open eHealth IPF (not shown in any of the figures). |
| archetypes | Namespace for IPF project archetypes (not shown in any of the figures). |
| Component | Description |
|---|---|
| commons-core | A library that defines the common message processing API to be implemented by components contained in the modules folder. platform-camel provides special integration points for components implementing this common API. The API was defined for implementing message processing components independent of a certain integration infrastructure like Apache Camel. This increases their re-usability and allows applications to implement light-weight message processing functionality without implementing an integration platform or enterprise service bus. |
| commons-flow | A library implementing the flow management services. It is used to monitor, query, audit and replay message flows. The management interfaces are based on JMX. An integration into Apache Camel is provided by platform-camel-flow. |
| commons-ihe-atna | A library used by other IPF IHE components to implement IHE ATNA. |
| commons-ihe-xds-* | Libraries used to implements IHE XDS.a and XDS.b actor interfaces. The libraries are Camel-independent |
| commons-map | A code-mapping library. |
| commons-lbs | A library providing storage for large binaries and javax.activation.DataSource implementations to represent stored binaries. |
| commons-event | A library containing the Camel independent functionality of the Event infrastructure. |
| commons-test | A library providing support for HTTP-based integration and performance tests. |
| commons-xml | An XML library with focus on Schematron validation. |
| modules-cda | A library for creating, parsing, navigating and rendering CDA documents. Also includes support for several CDA profiles. |
| modules-hl7 | A library that implements extensions to the HAPI library. |
| modules-hl7dsl | A library that implements a DSL for manipulating HAPI messages. |
| osgi-commons | See IPF bundle overview. |
| osgi-extender-basic | See IPF bundle overview. |
| osgi-extender-spring | See IPF bundle overview. |
| osgi-config-log | See IPF bundle overview. |
| osgi-config-jms | See IPF bundle overview. |
| osgi-config-flow-repository | See IPF bundle overview. |
| osgi-config-flow-manager | See IPF bundle overview. |
| platform-camel-core | A component that is required by most other platform-camel component. It provides:
|
| platform-camel-flow | A component that implements a flow management DSL for integrating commons-flow into Camel routes. The DSL was built with the DSL extension mechanism. |
| platform-camel-event | A component offering the DSL extensions of the event infrastructure |
| platform-camel-hl7 | A component that implements an HL7 message processing DSL for integrating modules-hl7 and modules-hl7dsl into Camel routes. The DSL was built with the DSL extension mechanism. |
| platform-camel-lbs-core | A component offering the DSL extensions of the large binary support mechanism as well as the interface to adapt various endpoint technologies to the LBS. |
| platform-camel-lbs-cxf | A component with adaptor implementations for using CXF endpoints with the large binary support. |
| platform-camel-lbs-http | A component with adaptor implementations for using HTTP/Jetty endpoints with the large binary support. |
| platform-camel-lbs-mina | A component with adaptor implementations for using MINA/HL7 endpoints with the large binary support. |
| platform-camel-ihe-mllp-* | Provides MLLP-related functionality used by IPF PIX and PDQ components. |
| platform-camel-ihe-pix-* | Components to implement actor interfaces of IHE PIX transactions. See IHE support. |
| platform-camel-ihe-pdq-* | Components to implement actor interfaces of IHE PDQ transactions. See IHE support. |
| platform-camel-ihe-xds-* | Components to implement actor interfaces of IHE XDS.a and XDS.b transactions. See IHE support. |
| platform-camel-test | A component for measuring the performance characteristics of IPF applications. It provides DSL extensions used to measure processing time of messages (for route parts and whole routes) and the message throughput. Performance measurement reports can be created as well. |
| tutorials-basic | A Hello World-level tutorial for getting started with platform-camel |
| tutorials-flow | A tutorial demonstrating IPF flow management features. |
| tutorials-hl7 | A tutorial demonstrating HL7 message processing features. |
| tutorials-lbs | A tutorial demonstrating the use of the LBS with CXF and HTTP endpoints. |
| tutorials-osgi-* | A tutorial demonstrating how to write IPF OSGi applications. |
| tutorials-ref | A complete reference application that may be used as blueprint for production-ready IPF applications. The reference application is a comprehensive demonstration of IPF features. |
| tutorials-xds | A tutorial that demonstrates how to use IPF XDS components. |
| ipf-archetype-* | Archetypes for creating IPF integration projects with a ready-to-use Maven 2 and Eclipse configuration plus an example route and unit tests. |
Scripting layer
IPF message processing routes are written with a domain-specific language (DSL) which is based on the Apache Camel DSL. Any DSL feature from Apache Camel is also available within IPF. One important design goal of IPF was to provide healthcare-specific message processing capabilities as extensions to the Camel DSL. For example, implementing an HL7 message validator should be as easy as implementing a XSD validator. To make DSL extensions possible IPF provides a DSL extension mechanism which is based on Groovy meta-programming. A meta-programming based approach was chosen because Camel currently doesn't provide an interface for extending its DSL. Consequently, for using the IPF DSL (i.e. the Camel DSL plus IPF DSL extensions) message processing routes must be written in the Groovy programming language. This doesn't mean that all parts of your program must be written in Groovy e.g. custom message transformers can still be implemented in Java (or any other JVM language). It is only important that route definitions (i.e the route builder) are written in Groovy.
All predefined DSL extensions provided by IPF have been implemented using this extensions mechansim. One important aspect of the extension mechanism is that it can also be used by IPF application to implement application-specific DSL extensions. There is a separate DSL extensions guide that explains how to. This makes application-specific message processing routes more readable and allows other projects to reuse these extensions. DSL extensions may also be based on other DSL extensions i.e. composition of DSL extensions is supported as well.
The DSL extension mechanism including all DSL extensions contributed by individual IPF components are referred to as the IPF scripting layer. The feature set of the IPF DSL therefore depends on which IPF components have been deployed. In other words, the DSL extension mechanism supports modularization of DSL extensions. An overview of all predefined IPF DSL extensions is given in the IPF extensions index. The following subsection exaplains the DSL extension mechanism in more detail.
DSL extension mechanism
| DSL extensions guide and tutorials The example presented here is explained in much more detail in the DSL extensions guide. Furthermore, all tutorials make use of the DSL extension mechanism and the predefined DSL extensions. |
The easiest way to learn the DSL extension mechanism is to start with a simple example. Let's say we want to introduce a new translate DSL element that translates a message from one language into another. To keep the examle simple we assume that the translation magic is done inside a single Translator processor.
package example; import org.apache.camel.Exchange; import org.apache.camel.Processor; public class Translator implements Processor { public void process(Exchange exchange) throws Exception { // do language translation here ... } }
Without using DSL extensions such a processor can be included into route definitions like in the following example.
package example import org.apache.camel.spring.SpringRouteBuilder class MyRouteBuilder extends SpringRouteBuilder { void configure() { from('direct:input') .process(new Translator()) .to('direct:output') } }
The goal however is to introduce a translate DSL extension that makes use of this processor. The result should look like.
package example import org.apache.camel.spring.SpringRouteBuilder class MyRouteBuilder extends SpringRouteBuilder { void configure() { from('direct:input') .translate() .to('direct:output') } }
To be able to use the translate() method in our route definitions we need to add that method into the corresponding DSL model class which is org.apache.camel.model.ProcessorDefinition in our example. Adding the method is done via Groovy meta-programming inside an extensions block.
package example import org.apache.camel.model.ProcessorDefinition class MyExtension { static extensions = { ProcessorDefinition.metaClass.translate = {-> delegate.process(new Translator()) } } }
You can add an extensions block to any class you like. To make the example work, the DSL extension, the route builder and the infrastructure components of the DSL extension mechanism must be wired together within a Spring application context.
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:camel="http://camel.apache.org/schema/spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd"> <camel:camelContext id="camelContext"> <camel:routeBuilder ref="routeBuilder"/> </camel:camelContext> <bean id="routeBuilder" depends-on="routeModelExtender" class="example.MyRouteBuilder"> </bean> <bean id="exampleModelExtension" class="example.MyExtension"> </bean> <bean id="coreModelExtension" class="org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension"> </bean> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> <ref bean="coreModelExtension" /> <ref bean="exampleModelExtension" /> </list> </property> </bean> </beans>
The above example also shows how to include the predefined DSL extensions provided by platform-camel-core. Please note that this is not required to make our example work. It's only an example how to combine predefined DSL extensions with custom DSL extensions.
| DSL extensions and OSGi In an OSGi environment DSL extensions are automatically detected by IPF-specific extender bundles, so there is no need to add them to a Spring application context. |
Limitations
There are some limitations you should be aware of when using the IPF extension mechanism. DSL extensions defined in Groovy are not available for Camel's Java DSL or for the Spring-based XML configuration. Also, DSL extensions currently do not show up for code completion in IDEs such as the Groovy plugins for Eclipse or IntelliJ IDEA. However, we are currently working on a solution for the code-completion problem.
Predefined DSL extensions
Predefined DSL extensions are provided by the following IPF components.
| Component | Extension class | Documentation |
|---|---|---|
| platform-camel-core | org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension | Core features DSL |
| platform-camel-flow | org.openehealth.ipf.platform.camel.flow.extend.FlowModelExtension | Flow management DSL |
| platform-camel-hl7 | org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension | HL7 processing DSL |
| platform-camel-lbs | org.openehealth.ipf.platform.camel.lbs.core.extend.LbsModelExtension | LBS processing DSL |
| platform-camel-event | org.openehealth.ipf.platform.camel.event.extend.EventModelExtension | Event processing DSL |
| platform-camel-ihe-xds | org.openehealth.ipf.platform.camel.ihe.xds.core.extend.XDSModelExtension | IHE support |
| platform-camel-test | org.openehealth.ipf.platform.camel.test.performance.extend.PerformanceModelExtension | Performance measurement |
A description of these components is given on the IPF architecture page. For a detailed description of the extensions follow the links in the Documentation column. The extension classes can be combined as shown in the configuration example above.
| DSL extensions index A summary of all predefined DSL extensions is given in the DSL extensions index. |
Core features
The content of this page describes the features provided by the platform-camel-core component. These are the the module adapters and the core DSL extensions. For an overview how platform-camel-core fits into its architectural context refer to the IPF architecture page.
Module adapters
Module adapters integrate components of the modules (and partly the commons) namespace into Camel routes. Components defined in these namespaces implement interfaces defined in the org.openehealth.ipf.commons.core.modules.api package which is part of the commons-core component. These interfaces represent message processors like Validator, Aggregator or Transmogrifer (a transformer), to mention a few. The intention of these interfaces was to define message processors that are independent of integration infrastructures like Apache Camel in order to increase their reusability in other contexts. The integration of these message processors into IPF routes is done via generic module adapters. They translate from Camel-specific interfaces to org.openehealth.ipf.commons.core.modules.api.* interfaces and are provided by the platform-camel-core component. The following UML diagram gives an overview (some of the relevant interfaces and adapters have been omitted). For details how to use module adapters in IPF routes refer to the DSL extensions for IPF module adapters section.

| Module adapter alternatives An alternative to module adapters is Camel's bean integration mechanism. |
DSL extensions
DSL extension for existing Camel features
Closure support
Several Camel DSL elements have been extended to accept closures as arguments. For example, the following processor closure reverses the string contained in the input message body.
from('direct:input')
.process {exchange ->
exchange.in.body = exchange.in.body.reverse()
}
.to('mock:output')
The closure parameter is the org.apache.camel.Exchange to be processed.
A filter closure is a closure that evaluates an org.apache.camel.Exchange and returns either true or false i.e. a filter closure is a predicate:
from('direct:input')
.filter {exchange ->
exchange.in.body == 'blah'
}
.to('mock:output')
Here, the filter closure returns true if the in-message body equals 'blah'. If true is returned the filter forwards the message to the next processor, otherwise, the message is filtered out.
Closures can also be used for setting message headers. In the following example the foo-header is assigned the in-message's body.
from('direct:input')
.setHeader('foo') {exchange ->
exchange.in.body
}
.to('mock:output')
The header name is given as first setHeader argument. The header value is the return value of the closure. You can also use closures with setOutHeader and setFaultHeader.
To set an exchange property you can use a setProperty-closure.
from('direct:input')
.setProperty('foo') {exchange ->
exchange.in.body
}
.to('mock:output')
You can also set the in-message body with a closure. In the following example the in-message body is set to the value of the in-message foo-header.
from('direct:input')
.setBody {exchange ->
exchange.in.headers.foo
}
.to('mock:output')
The transform DSL element also supports expression closures. For example, to reverse a string contained in the in-message body and write the result to the out-message body use:
from('direct:input')
.transform {exchange ->
exchange.in.body.reverse()
}
Closure support was also added for content-based routing:
from('direct:input1')
.choice()
.when { it.in.body == 'a'}
.to('mock:output1')
.when { it.in.body == 'b'}
.to('mock:output2')
when-closures implement routing decisions and return either true or false. In our example: If the in-message body equals a the message is forwarded to endpoint mock:endpoint1. If the in-message body equals b the message is forwarded to endpoint mock:endpoint2.
The onWhen DSL element allows for finer-grained exception handling. With IPF you can implement onWhen predicates with closures. The following example tries to match the in-message body with a regular expression.
onException(Exception.class)
.onWhen {it.in.body ==~ /u.w/}
.to('direct:error')
Bean lookup
The process DSL element can also be used in combination with a bean name. The following route definition references a bean named sampleProcessor from the application context. The bean must implement the org.apache.camel.Processor interface. This is equivalent to Camel's processRef(String).
from('direct:input1')
.process('sampleProcessor')
.to('mock:output')
Error handler
Another convenience DSL extension provided by IPF is the unhandled DSL element. It replaces the verbose errorHandler(noErrorHandler()) statement in route definitions i.e. it drops the error handler from a route.
from('direct:input')
.unhandled()
// further processing here ...
.to('mock:output')
The unhandled extension currently cannot be used globally for all routes i.e. a builder.unhandled() call is not supported yet.
Interceptor
Camel's 1.x support for the intercept DSL element has been removed in Camel 2.0. For backwards compatibility-reasons, IPF re-introduces intercept as DSL extension. Valid argument types are org.apache.camel.DelegateProcessor and groovy.lang.Closure.
from('direct:input1')
.intercept(new MyDelegateProcessor())
.to('mock:output')
from('direct:input1')
.intercept {exchange, next ->
// do some pre-processing work
// ...
// proceed with the next processor
next.proceed(exchange)
// do some post-processing work ...
// ...
}
.to('mock:output')
The intercept closure is passed a second argument (called next in our example) on which the proceed() method must be called. This method continues processing with the next processor and returns after down-stream processing has been done.
from('direct:input1')
.intercept('InterceptorBean')
.to('mock:output')
The bean must implement the interface org.openehealth.ipf.platform.camel.core.process.Interceptor, e.g.:
import org.apache.camel.Exchange; import org.apache.camel.Processor; import org.openehealth.ipf.platform.camel.core.process.Interceptor; public class MyInterceptor implements Interceptor { @Override public void process(Exchange exchange, Processor next) throws Exception { exchange.getIn().setBody("before"); // Do something before next.process(exchange); // Call the intercepted processor exchange.getOut().setBody("after"); // Do something afterwards } }
DSL extensions for ExpressionClause
Exception objects and messages
The model class org.apache.camel.builder.ExpressionClause has been extended with expressions to access the exception object or the exception message of an exchange. The corresponding DSL extensions are exceptionMessage and exceptionObject. For example,
from('direct:input')
.onException(ValidationException.class).setBody().exceptionMessage().to('mock:error').end()
// ... ValidationException thrown here ...
.to('mock:output')
sets the detail message of the ValidationException (obtained via ValidationException.getMessage) to the in-message body of the exchange. This extensions is particularly useful in exception routes because there you don't have access to the exception object via Exchange.getException(). In the next example we set the exception object on a custom header.
from('direct:input')
.onException(ValidationException.class).setHeader('foo').exceptionObject().to('mock:error').end()
// ... ValidationException thrown here ...
.to('mock:output')
Here, the in-message that arrives at mock:error has a foo header with the ValidationException object.
DSL extensions for custom IPF processors
IPF provides message processors that have been developed on top of Apache Camel. Initially, they were included into route definitions via the default extension points provided by Camel (e.g. the org.apache.camel.Processor or org.apache.camel.Predicate interfaces). With the IPF scripting layer, they are now available as first-class DSL elements. This section presents those DSL extensions that are independent of the IPF module adapters. The next section is about module-adapter-related DSL extensions.
Content enrichment
| Content enricher contributed to Camel The IPF content enricher has been contributed to Camel and is available there since version 2.0-m1. We will drop the content enricher from the IPF code base as soon as we upgraded to Camel 2.0. However, we will continue to support merge closures (see below). The contributed content enricher is also documented in the Camel Wiki. |
The content enricher creates an additional message exchange from the original exchange for communicating with a so-called resource endpoint. This endpoint is used to obtain additional data to enrich the original exchange. The response from the resource endpoint is merged into the original message exchange using a configurable merge logic. In the following example the merge logic is defined by a closure:
from('direct:input')
.enrich('direct:resource') {originalExchange, resourceExchange ->
originalExchange.in.body += ':' + resourceExchange.out.body
// return value is optional if originalExchange
// shall be returned as merge result, otherwise
// it must be returned explicitly.
originalExchange
}
.to('mock:output')
In this example, the enricher obtains additional data from the direct:resource endpoint and passes the originalExchange and resourceExchange to a closure that merges data from the resourceExchange into originalExchange. The message exchange containing the merge result is returned by the closure. Returning an exchange is optional if the originalExchange object contains the merge result. Instead of providing merge logic via a closure you may also provide an org.apache.camel.processor.aggregate.AggregationStrategy instance.
AggregationStrategy strategy = new MyCleverAggregationStrategy()
from('direct:input')
.enrich('direct:resource', strategy)
.to('mock:output')
Validation process
The validation DSL extension implements a simple validation process that delegates the actual message validation to a validator which can be an endpoint or an object that implements validation logic. If validation succeeds the message is forwarded to the next processor defined in the route, otherwise, the message is dropped. In both cases the response generated by the validator is returned to the sender of the original message exchange.
In the following example the validation process delegates message validation to the direct:validator endpoint. If validation succeeds the message is forwarded as in-only exchange to mock:output otherwise it is dropped. The response returned from direct:validator is returned to the direct:input endpoint.
from('direct:input')
.validation('direct:validator')
.to('mock:output')
The validation process interprets a message exchange as failed if any of the following conditions is true:
- an exception was thrown
- the message exchange contains an exception
- the message exchange contains a fault message (i.e. an out message marked as fault)
Instead of providing an endpoint you may also provide validator logic using a closure.
from('direct:input1')
// generate a 'success' validation response
.validation {exchange -> exchange.out.body = 'success'}
.to('mock:output')
from('direct:input2')
// generate a 'failure' validation fault
.validation {exchange -> exchange.out.body = 'failed'; exchange.out.fault = true}
.to('mock:output')
from('direct:input3')
// throw a validation exception
.validation {throw new ValidationException('input sucks in any case')}
.to('mock:output')
You may also provide an org.apache.camel.Processor instance for validation
Processor validator = new MyFamousValidator()
from('direct:input')
.validation(validator)
.to('mock:output')
Splitter
The Splitter is used to split a message into multiple messages. You specify a split rule that defines the way the splitting is performed. Each message generated by the Splitter is send to the next processor defined in the route. The results of this processing are aggregated into the original message using a configurable aggregation strategy.
| IPF and Camel splitters The splitter implemented by IPF differs from Camel's splitter. To choose between these two implementation use .split() for Camel's splitter and ipf().split() for IPF's splitter. |
The following shows a String-based example of the Splitter that splits a comma-separated string coming from direct:input and passes the parts on to the mock:output endpoint. If you send the body "hello,world,!" to direct:input, the mock:output will receive three messages with "hello", "world" and "!" in the body.
from('direct:input')
.ipf().split { Exchange exchange -> exchange.in.body.split(',') }
.to('mock:output')
The split rule can also be defined via org.apache.camel.Expression:
Expression splitExpression = new MyCommaSplittingExpression()
from('direct:input')
.split(splitExpression)
.to('mock:output')
No matter how the split rule is defined, the aggregation of all results is by default performed using the UseLatestAggregationStrategy. This strategy simply uses the result of the last message generated by the split. In the above example, "!" is returned to the sender, because it is the last message that was split off.
You can specify a different strategy using aggregationStrategy after the split processor. The following example extends the previous one generating an output containing the parts reassembled and separated by a ':'. Therefore, the sender will get the result "hello:world:!".
from('direct:input')
.ipf().split { Exchange exchange -> exchange.in.body.split(',') }
.aggregationStrategy { oldExchange, newExchange ->
String oldContent = oldExchange.in.body
String newContent = newExchange.in.body
Exchange aggregate = oldExchange.copy()
aggregate.in.body = oldContent + ":" + newContent
aggregate
}
.to('mock:output')
The aggregation strategy is called only if the split resulted in multiple messages. If no messages were generated, the result is the original message. If only one message was split off, this message is used as the result. In the above example, the aggregation strategy is called twice with the following messages as parameters:
First call: oldExchange = "hello", newExchange = "world" -> returns "hello:world"
Second call: oldExchange = "hello:world", newExchange = "!" -> returns "hello:world:!"
DSL extensions for IPF module adapters
| How this section is organized This section is organized in the following way.
|
Transmogrifier
The easiest way to describe the DSL extensions for IPF module adapters is to start with an example. Let's use the org.openehealth.ipf.commons.core.modules.api.Transmogrifier interface for that purpose. Inspired by Calvin and Hobbes, a transmogrifier converts anything into whatever you like. Transmogrification is accompanied by a loud zap:
public interface Transmogrifier<S, T> { T zap(S object, Object... params); }
Implementations of Transmogrifier are often used for message transformation. Transformation input is given by the object parameter and optionally some additional params. The transformation result is the return value of the zap method. To include a Transmogrifier instance into a Camel route we use the transmogrify DSL extension:
org.openehealth.ipf.commons.core.modules.api.Transmogrifier transmogrifier = new MyTransmogrifier()
from('direct:input')
.transmogrify(transmogrifier)
.to('mock:output')
Behind the scenes the transmogrify element creates an org.openehealth.ipf.platform.camel.core.adapter.TransmogrifierAdapter as described in the module adapters section. This adapter adapts the org.openehealth.ipf.commons.core.modules.api.Transmogrifier interface to an org.apache.camel.Processor interface. The adapter (processor) is included into the Camel route at the position where the transmogrify extension is used. During message processing the adapter accepts an org.apache.camel.Exchange, extracts the input from that exchange, delegates message processing to the adapted transmogrifier instance and populates the exchange with the transformation result.
Inclusion options
There are three different ways of including a transmogrifier into a Camel route.
- Pass a transmogrifier object as argument to the transmogrify() method. This has already been shown in the example route above.
- Pass the name of a transmogrifier bean as argument to the transmogrify method (see below). A bean with that name must exist in the Spring application context.
- Define a transmogrifier logic inline using a closure (see below). This is comparable to implement an anonymous Transmogrifier class.
from('direct:input')
.transmogrify('myTransmogrifierBean')
.to('mock:output')
from('direct:input')
.transmogrify { body, headers ->
def result = ... // create result from input body and headers
return result // return the transformation result
}
.to('mock:output')
| Inclusion pattern This is a pattern that also applies to all other DSL extensions for IPF module adapters: validate, parse, render, predicate and aggregationStrategy. The adapted object can either be included into a Camel route directly as object, indirectly via a Spring bean name or defined (inline) with a closure. Closures are not supported for parse and render. |
Transmogrifier input
Default arguments to the Transmogrifier.zap(S object, Object... params) method are:
- The in-message body for the object parameter.
- The in-message headers for the params parameter.
A transmogrify closure may define a one or two parameters.
- The first parameters corresponds to the object parameter.
- The second parameter corresponds to the params parameter.
of the zap method.
- The default argument to the first closure parameter is the in-message body.
- The default argument to the second closure parameter are the in-message headers (a java.util.Map).
Input to the zap method as well as the transmogrify closure can be customized via the
- input
- params and
- staticParams
DSL extensions. input and params either accept an org.apache.camel.Expression as argument or an expression closure. In both cases an org.apache.camel.Exchange is evaluated. The evaluation result will be used as transmogrifier input. The following snippet causes the transmogrifier's zap method to be called with the in-message's foo-header as first argument and the in-messages bar-header as the second argument
from('direct:input')
.transmogrify('myTransmogrifierBean')
.input { exchange -> exchange.in.headers.foo }
.params { exchange -> exchange.in.headers.bar }
.to('mock:output')
The same rules apply for the transmogrify closure parameters.
from('direct:input')
.transmogrify { fooHeader, barHeader ->
// ...
}
.input { it.in.headers.foo }
.params { it.in.headers.bar }
.to('mock:output')
The params DSL extension also supports predefined expressions. These are accessible by calling params with no arguments. The following predefined expressions are currently supported as part of the DSL.
...
.params().headers() // in-message headers (default for transmogrifiers)
...
.params().header('foo') // in-message foo-header
...
.params().builder() // a Groovy XML builder
...
.params().headersAndBuilder() // in-message headers and a Groovy XML builder (params array of length 2)
...
These predefined expressions are implemented by the org.openehealth.ipf.platform.camel.core.model.ParamsDefinition model class. The builder and headersAndBuilder extensions are described in section DSL extensions for Groovy XML processing.
The staticParams extension can be used to pass constant values to the transmogrifier or transmogrifier closure. This extension method defines a variable argument parameter. For example to pass a String array with elements 'a', 'b' and 'c' via the params parameter or via the second closure parameter you could use
from('direct:input')
.transmogrify('myTransmogrifierBean')
.staticParams('a', 'b', 'c')
.to('mock:output')
from('direct:input')
.transmogrify { body, stringArray ->
// ...
}
.staticParams('a', 'b', 'c')
.to('mock:output')
Input customization for other adapter extensions
|
Transmogrifier output
The return value of the Transmogrifier.zap(S object, Object... params) method or the return value of the transmogrify closure is written to the org.apache.camel.Exchange object from which the input was taken. It depends on the exchange pattern to which exchange message the result is written. If the exchange is out-capable (i.e. exchange.getPattern().isOutCapable() returns true) then the result is written to the exchange's out-message body, otherwise, it is written to the in-message body. Furthermore, if the exchange is out-capable, the in-message is copied onto the out-message before the result is written (this is useful e.g. for preserving message headers along a precessing chain).
Transmogrifier implementations
IPF provides two Transmogrifier implementations out of the box:
- org.openehealth.ipf.commons.xml.XsltTransmogrifier for transforming XML documents
- org.openehealth.ipf.commons.xml.SchematronTransmogrifier for creating Schematron validation reports from XML documents
These implementations are "by-products" for Schematron validation, but you can use them independently as well. Compared to Camel's xslt endpoint (http://camel.apache.org/xslt.html), the IPF counterpart
- can use variable stylesheets
- caches XSLT templates for better performance
- accepts explicit XSLT parameters (not just as Camel message header)
The input is automatically converted into a StreamSource. By default, all Camel headers are added as parameters which are available in the stylesheet unless you define parameters by either using params(...) or staticParams(...).
Example:
from('direct:input1')
.transmogrify().xslt().staticParams('path/to/stylesheet') // static stylesheet
.to('mock:output')
from('direct:input1')
.transmogrify().xslt().staticParams('path/to/stylesheet', parameterMap) // static stylesheet with parameters
.to('mock:output')
from('direct:input3')
.setHeader('stylesheet', constant('path/to/stylesheet'))
.transmogrify().xslt().params().header('stylesheet') // dynamic stylesheet
.to('mock:output')
// In most cases you will need the SchematronValidator, which scans the Schematron report
// for failed assertions. Use only if you require custom processing of the report in the
// route.
from('direct:input3')
.transmogrify().schematron().staticParams('path/to/rules', options ) // static rules
.to('mock:output')
from('direct:input4')
.setHeader('rules', constant('path/to/rules'))
.transmogrify('schematron').params().header('rules') // dynamic rules
.to('mock:output')
Please refer to Schematron validation for a description of the available Schematron options.
By default, XSLT transformations return a javax.xml.transform.Result object, which is, however, not very useful for further processing. IPF's XSLT-related transmogrifiers therefore return a String by default. You can influence the returned type using a Class parameter to the xslt()/schematron() extensions or with a subsequent call to convertBodyTo(Class):
from('direct:input1')
.transmogrify().xslt(InputStream.class).staticParams('path/to/stylesheet')
.to('mock:output')
from('direct:input2')
.transmogrify().xslt().staticParams('path/to/stylesheet')
.convertBodyTo(InputStream.class)
.to('mock:output')
Validator
The modules API defines an org.openehealth.ipf.commons.core.modules.api.Validator interface for message validation.
public interface Validator<S, P> { void validate(final S message, final P profile); }
It defines a single validate method that validates a message against a profile. If validation fails an org.openehealth.ipf.commons.core.modules.api.ValidationException is thrown. The validator is included into Camel routes via the validate DSL extension. The validate extension accepts either a validator object, a validator bean name or a validator closure as argument. If a closure is used, a failed validation is either indicated by throwing an org.openehealth.ipf.commons.core.modules.api.ValidationException or by returning false. If false is returned IPF internally generates a ValidationException. Here are some examples.
// route 1 from('direct:input1') .validate {body -> body == 'blah'} .to('mock:output') // route 2 from('direct:input2') .validate {throw new ValidationException('always fail')} .to('mock:output') // route 3 from('direct:input3') .validate {body, profile -> body == profile } .staticProfile('blah') .to('mock:output') // route 4 from('direct:input4') .validate {fooHeader, profile -> fooHeader == profile } .input {it.in.headers.foo} .staticProfile('abcd') .to('mock:output') // route 5 from('direct:input5') .validate(...) .input(...) .profile {exchange -> exchange.in.headers.customProfile } .to('mock:output') // you may also use validator objects ... .validate(new MyCustomValidator()) ... // you may also use validator beans ... .validate('myValidatorBean') ...
- In route 1 validation will fail if the in-message body doesn't equal 'blah'. In this case false is returned which causes IPF to throw a ValidationException.
- In route 2 validation will fail because a ValidationException is thrown directly (regardless of the message content).
- In route 3 we define a closure with a second parameter for passing a validation profile. By default it is null but it can be customized via the staticProfile DSL extension *). As in route 1 the validation will fail if the in-message body doesn't equal 'blah'.
- In route 4 we see how input is used to pass the in-message's foo-header as first argument to the validation closure. If the foo-header doesn't equal 'abcd' validation will fail.
- In route 5 a validation profile is obtained from the in-messages's customProfile header using the profile() DSL extension and a closure. Instead of the closure one can also use an org.apache.camel.Expression instance.
*) In this example we could have hard-coded this profile directly inside the closure as well. Using the profile extension makes more sense when using validator objects or beans like in validate(myValidator) or validate('myValidatorBean').
As of version 1.7, IPF provides a Validator implementation that validates an XML Source against an W3C XML Schema.
from('direct:input1')
.validate().xsd().staticProfile('schema location')
.to('mock:output')
The schema location value can be either a URL or a non-URL string, in the latter case the classpath is searched for the schema resource.
As of version 1.7, IPF also provides a Validator implementation that validates an XML Source against a set of Schematron rules.
import org.openehealth.ipf.commons.xml.SchematronProfile; ... from('direct:input1') .validate().schematron().staticProfile(new SchematronProfile('rules location', options)) .to('mock:output')
Note that you have to provide an instance of SchematronProfile, not just the plain Schematron rules location. The rules location value can be either a URL or a non-URL string, in the latter case the classpath is searched for the schema resource.
The options parameter is optional. If present, it must be of type Map<String, Object> Its purpose is to configure Schematron's validation process. Please refer to the Schematron website http://www.schematron.com for more details.
| key | description | values | default |
|---|---|---|---|
| phase | Select the phase for validation. Schematron allows for staged validation by assigning phases to validation rules. | NMTOKEN | #ALL | #ALL |
| allow-foreign | Pass non-Schematron elements and rich markup to the generated stylesheet | 'true' | 'false' | 'false' |
| diagnose | Add the diagnostics to the assertion test in reports | 'true' | 'false' | 'true' |
| property | Experimental: Add properties to the assertion test in reports | 'true' | 'false' | 'true' |
| generate-paths | Generate the @location attribute with XPaths | 'true' | 'false' | 'true' |
| sch.exslt.imports | semi-colon delimited string of filenames for some EXSLT implementations | string | '' |
| optimize | Use only when the schema has no attributes as the context nodes | 'visit-no-attributes' | '' |
| generate-fired-rule | Generate fired-rule elements. Significantly increases report size | 'true' | 'false' | 'true' |
Parser
The modules API org.openehealth.ipf.commons.core.modules.api.Parser interface declares methods for parsing an external representation of information into an internal model.
public interface Parser<S> { S parse(String message, Object... params); S parse(InputStream message, Object... params) throws IOException; S parse(Source source, Object... params) throws IOException; S parse(Reader reader, Object... params) throws IOException; }
The external representation can be obtained from a java.io.InputStream, a java.io.Reader, a javax.xml.transform.Source or directly from a java.lang.String. These options are represented by the four parse methods. A parser is included into Camel routes via the parse DSL extension. IPF selects the appropriate method depending on the type of input data. The return value is the parse result and is written to the org.apache.camel.Exchange object from which the input was taken. It depends on the exchange pattern to which exchange message the result is written. If the exchange is out-capable (i.e. exchange.getPattern().isOutCapable() returns true) then the result is written to the exchange's out-message body, otherwise, it is written to the in-message body. Furthermore, if the exchange is out-capable, the in-message is copied onto the out-message before the result is written (this is useful e.g. for preserving message headers along a precessing chain). Here are some examples.
// route 1 from('direct:input1') .parse(new MyParser()) .to('mock:output') // route 2 from('direct:input2') .parse('myParserBean') .input { it.in.headers.foo } .params { it.in.headers.bar } .to('mock:output')
- In route 1 we directly include a MyParser object into the Camel route. Here, the parser input data are taken from the in-message body, the parser params are null.
- In route 2 we include a Spring bean with name 'myParserBean' into the Camel route. Here, the parser's input data are taken from the in-message's foo-header, the parser params from the bar-header.
Closures for parse are currently not suppported.
Unmarshalling via Parser
IPF also provides an org.apache.camel.spi.DataFormat implementation that delegates unmarshal work to a parser.
from("direct:input1") .unmarshal().parse(new MyParser()) ... from("direct:input2") .unmarshal().parse('myParserBean') ...
However, using parse for unmarshalling currently doesn't allow input customization via input, params or staticParams.
Renderer
The modules API org.openehealth.ipf.commons.core.modules.api.Renderer interface declares methods for creating an external representation of an internal model.
public interface Renderer<T> { Result render(final T model, Result result, final Object... params) throws IOException; OutputStream render(final T model, OutputStream result, final Object... params) throws IOException; Writer render(final T model, Writer result, final Object... params) throws IOException; String render(final T model, final Object... params); }
Currently, only the last method i.e. the one that returns a java.lang.String is used by IPF. A renderer is included into Camel routes via the render DSL extension. IPF selects the appropriate method depending on the type of input data. The return value is the rendering result and is written to the org.apache.camel.Exchange object from which the input was taken. It depends on the exchange pattern to which exchange message the result is written. If the exchange is out-capable (i.e. exchange.getPattern().isOutCapable() returns true) then the result is written to the exchange's out-message body, otherwise, it is written to the in-message body. Furthermore, if the exchange is out-capable, the in-message is copied onto the out-message before the result is written (this is useful e.g. for preserving message headers along a precessing chain). Here are some examples.
// route 1 from('direct:input1') .render(new MyRenderer()) .to('mock:output') // route 2 from('direct:input2') .render('myRendererBean') .input { it.in.body[0] } .params { it.in.headers.bar } .to('mock:output')
- In route 1 we directly include a MyRenderer object into the Camel route. Here, the renderer input data are taken from the in-message body, the renderer params are null.
- In route 2 we include a Spring bean with name 'myRendererBean' into the Camel route. Here, the renderer's input data are taken from the first element of a list that is contained in the in-message's body. The renderer params are taken from the in-messages bar-header.
Closures for render are currently not suppported.
Marshalling via Renderer
IPF also provides an org.apache.camel.spi.DataFormat implementation that delegates marshal work to a renderer.
from("direct:input1") .marshal().render(new MyRenderer()) ... from("direct:input2") .unmarshal().render('myRendererBean') ...
However, using render for marshalling currently doesn't allow input customization via input, params or staticParams.
Predicate
The org.openehealth.ipf.commons.core.modules.api.Predicate interface declares a matches method for evaluating a binary predicate on a source object.
public interface Predicate<T> { boolean matches(T source, Object... params); }
The predicate DSL extension can be used to include these predicates into Camel routes. It is implemented with the org.openehealth.ipf.platform.camel.core.adapter.PredicateAdapter that translates between org.openehealth.ipf.commons.core.modules.api.Predicate and org.apache.camel.Predicate. The predicate extension supports a predicate object, a bean name or a closure as argument. The created Camel predicate can then be used with e.g. filter or other DSL elements that expect an org.apache.camel.Predicate. The predicate extension is provided by an IPF extension to org.apache.camel.spring.SpringRouteBuilder. Here are some examples:
import org.apache.camel.spring.SpringRouteBuilder import org.openehealth.ipf.commons.core.modules.api.Predicate ... class MyRouteBuilder extends SpringRouteBuilder { void configure() { Predicate myPredicate = new MyPredicate() def predicate1 = predicate(myPredicate) def predicate2 = predicate('myPredicateBean') def predicate3 = predicate { body -> body == 'test'} from('direct:input1').filter(predicate1).to('mock:output') from('direct:input2').filter(predicate2).to('mock:output') from('direct:input3').filter(predicate3).to('mock:output') ...
Input to the org.openehealth.ipf.commons.core.modules.api.Predicate.matches() method can be customized via the input, params or staticParams DSL extensions.
def predicate4 =
predicate { fooHeader, barHeader -> ... }
.input { it.in.headers.foo }
.params { it.in.headers.bar }
Aggregator
The org.openehealth.ipf.commons.core.modules.api.Aggregator interface is a transmogrifier that combines/aggregates a collection of input object into a result object. The result object is the return value of the zap method.
public interface Transmogrifier<S, T> { T zap(S object, Object... params); } public interface Aggregator<S, T> extends Transmogrifier<Collection<S>, T>
{anchor:aggregationStrategy}}
The aggregationStrategy DSL extension can be used to include an Aggregator into Camel routes. It is implemented with the org.openehealth.ipf.platform.camel.core.adapter.AggregatorAdapter that translates between org.openehealth.ipf.commons.core.modules.api.Aggregator and org.apache.camel.processor.aggregate.AggregationStrategy. The aggregationStrategy extension supports an aggregator object, a bean name or a closure as argument. The created Camel AggregationStrategy can then be used with e.g. enrich or other DSL elements that expect an org.apache.camel.processor.aggregate.AggregationStrategy. The aggregationStrategy extension is provided by an IPF extension of the org.apache.camel.spring.SpringRouteBuilder. Here are some examples:
def aggregationStrategy1 = aggregationStrategy(new TestAggregator()) def aggregationStrategy2 = aggregationStrategy('sampleAggregator') def aggregationStrategy3 = aggregationStrategy {originalInBody, resourceOutBody -> originalInBody + ':' + resourceOutBody } def aggregationStrategy4 = aggregationStrategy {originalInBody, resourceOutBody, fooHeader -> originalInBody + ':' + resourceOutBody + ':' + fooHeader } .input {originalExchange -> originalExchange.in.body} // relates to 1st parameter .aggregationInput {resourceExchange -> resourceExchange.out.body} // relates to 2nd parameter .params {originalExchange -> originalExchange.in.headers.foo} // relates to 3rd parameter from('direct:input1').enrich('direct:resource', aggregationStrategy1).to('mock:output') from('direct:input2').enrich('direct:resource', aggregationStrategy2).to('mock:output') from('direct:input3').enrich('direct:resource', aggregationStrategy3).to('mock:output') from('direct:input4').enrich('direct:resource', aggregationStrategy4).to('mock:output')
The collection passed to the org.openehealth.ipf.commons.core.modules.api.Aggregator.zap method is a list of two objects - objects that have been derived from the arguments to the org.apache.camel.processor.aggregate.AggregationStrategy.aggregate method. If you use a closure you must define at least two parameters that correspond to the parameters of org.apache.camel.processor.aggregate.AggregationStrategy.aggregate. An optional third parameters corresponds to the params parameter that is common to all modules interfaces. Input can be customized via the input, params or staticParams DSL extensions. Input for the second object in the input list or the second closure parameter can be customized via the aggregationInput DSL extension (see aggregationStrategy4 in the example above).
Adapter extension summary
Relevant types
| DSL extension | Modules Interface *) | Adapter class **) | Extended model class |
|---|---|---|---|
| transmogrify | Transmogrifier | TransmogrifierAdapter | org.apache.camel.model.ProcessorDefinition |
| validate | Validator | ValidatorAdapter | org.apache.camel.model.ProcessorDefinition |
| parse | Parser | ParserAdapter | org.apache.camel.model.ProcessorDefinition org.apache.camel.builder.DataFormatClause |
| render | Renderer | RendererAdapter | org.apache.camel.model.ProcessorDefinition org.apache.camel.builder.DataFormatClause |
| predicate | Predicate | PredicateAdapter | org.apache.camel.spring.SpringRouteBuilder |
| aggregationStrategy | Aggregator | AggregatorAdapter | org.apache.camel.spring.SpringRouteBuilder |
*) in package org.openehealth.ipf.commons.core.modules.api
**) in package org.openehealth.ipf.platform.camel.core.adapter
Parameters and input
| DSL extension | Parameter types | Input customization |
|---|---|---|
| transmogrify |
|
|
| validate |
|
|
| parse |
|
|
| render |
|
|
| predicate |
|
|
| aggregationStrategy |
|
|
Closure profiles
| DSL extension | Parameter 1 | Parameter 2 | Parameter 3 | Return value |
|---|---|---|---|---|
| transmogrify |
|
|
- | Transformation result (any type) |
| validate |
|
|
- | boolean or throws ValidationException |
| predicate |
|
|
- | boolean |
| aggregationStrategy |
|
|
|
aggregation result (any type) |
DSL extensions for Groovy XML processing
IPF provides support for Groovy XML processing within Camel routes. Here's a summary of features
- Unmarshal an XML stream or string into a groovy.util.Node using groovy.util.XmlParser
- Unmarshal an XML stream or string into a groovy.util.slurpersupport.GPathResult using groovy.util.XmlSlurper
- Marshal a groovy.util.Node to an output stream.
- Marshal a groovy.util.slurpersupport.GPathResult to an output stream (currently not supported).
- Groovy XML builders are injected into transmogrifer objects or closures for creating XML results.
XML Unmarshalling with Groovy XmlParser
To unmarshal an XML stream or string using a groovy.util.XmlParser use Camel's unmarshal method with IPF's gnode extension.
from('direct:input1')
.unmarshal().gnode()
.transmogrify { node ->
// process XML ...
}
This puts the parser result into the message body which can then be used in subsequent processors. By default, gnode is namespace-aware. To disable namespace-awareness use gnode(false).
from('direct:input1')
.unmarshal().gnode(false)
.transmogrify { node ->
// process XML ...
}
As of IPF 1.7, in order to add XML schema validation, simply add a schema location parameter. The value can be either a URL or a non-URL string, in the latter case the classpath is searched for the schema resource:
from('direct:input1')
// will throw an Exception if XML does not validate against the schema
.unmarshal().gnode('path/to/schema.xsd', true)
.transmogrify { node ->
// process XML ...
}
XML Unmarshalling with Groovy XmlSlurper
To unmarshal an XML stream or string using a groovy.util.XmlSlurper use Camel's unmarshal method with IPF's gpath extension.
from('direct:input1')
.unmarshal().gpath()
.transmogrify { gpathResult ->
// process XML ...
}
This puts the slurper result into the message body which can then be used in subsequent processors. By default, gpath is namespace-aware. To disable namespace-awareness use gpath(false).
from('direct:input1')
.unmarshal().gpath(false)
.transmogrify { gpathResult ->
// process XML ...
}
As of IPF 1.7, in order to add XML schema validation, simply add a schema location parameter. The value can be either a URL or a non-URL string, in the latter case the classpath is searched for the schema resource:
from('direct:input1')
// will throw an Exception if XML does not validate against the schema
.unmarshal().gpath('path/to/schema.xsd', true)
.transmogrify { node ->
// process XML ...
}
XML Marshalling with Groovy XmlNodePrinter
The reverse operations to unmarshal().gnode() and unmarshal().gpath() are marshal().gnode() and marshal().gpath(), respectively. Currently only marshal().gnode() is supported. This writes a groovy.util.Node to an output stream using Groovy's groovy.util.XmlNodePrinter
...
.marshal().gnode()
.to('mock:mock')
This makes the printer result available as byte array in the message body. By default gnode is namespace-aware. To disable namespace-awareness use gnode(false).
...
.marshal().gnode(false)
.to('mock:mock')
XML transmogrifiers
IPF provides an easy way to make Groovy XML markup builders available inside transmogrifiers or transmogrifier closures. Here's how it works for transmogrifier closures.
from('direct:input1')
.transmogrify { body, xmlBuilder ->
// use xmlBuilder to create XML
...
// return xmlBuilder result
xmlBuilder.result
}
.params().builder()
The params().builder() call makes the builder available via the second closure parameter. A new instance of the builder is passed with every call, so using that builder is thread-safe. If you want to have both, the in-message headers and the builder, for the second closure parameter then use the predefined headersAndBuilder() extension. This extension creates an array of length 2 where the first array element is the in-message headers and the second element is the XML builder.
from('direct:input1')
.transmogrify { body, params ->
def msgHeaders = params[0]
def xmlBuilder = params[1]
// use builder to create XML
...
// return builder result
xmlBuilder.result
}
.params().headersAndBuilder()
The xmlBuilder.result property was added by a class that subclasses groovy.xml.MarkupBuilder. It contains the result XML document as String.
| XML message transmformations By combining params().builder() with unmarshal().gnode() or unmarshal().gpath() transmogrifiers can implement XML message transformations based on Groovy's XML support. Transformation logic usually extracts content from an XML source document and includes that content into a result document. Navigation and content extraction from source documents is done via GPath expressions. Creation of result documents is done with Groovy's XML markup builder. The following example shows how an XML transformation route can be set up. from(...)
.unmarshal().gpath()
.transmogrify { gpathResult, xmlBuilder ->
// use gpathResult to read from source XML
// use xmlBuilder to create result XML
...
// return builder result
xmlBuilder.result
}
.params().builder()
.to(...)
{code}
|
In most cases creating a result document using an XML builder goes over a large number of lines of code and you probably want to factor that code out to a Transmogrifier that is implemented in Groovy. Here's an example.
public class MyTransmogrifier implements Transmogrifier { Object zap(Object input, Object... params) { // obtain builder from params def builder = params[0] // create XML document using builder ... // return result builder.result } }
Such a Transmogrifier implementation can then be included into Camel routes either directly as transmogrifier object or via a Spring bean name.
from('direct:input1')
.transmogrify(new MyTransmogrifier())
.params().builder()
from('direct:input1')
.transmogrify('myTransmogrifierBean')
.params().builder()
Mapping Service
Overview
The org.openehealth.ipf.commons.map.MappingService interface deals with the requirement that message processing often involves mapping between code systems, i.e. from one set of codes into a corresponding set of codes. For example, HL7 version 2 to HL7 version 3 use different code systems for most coded values like message type, gender, clinical encounter type, marital status codes, address and telecommunication use codes, just to mention a few. MappingService implementations provide the mapping logic, which can be a simple java.util.Map, but can also be a facade for a remote terminology service.
The commons-map component extends the java.lang.String and java.util.Collection classes with methods targeted at mapping.
The commons-map library provides one MappingService implementation (org.openehealth.ipf.commons.map.BidiMappingService), which implements
- bidirectional mapping
- mapping of arbitrary objects
- definitions of mappings using external Groovy Scripts
Configuring the Mapping Service
This section explains how to configure IPF's BidiMappingService.
Using the BidiMappingService bean
- Add the necessary dependencies to your project's Maven 2 descriptor.
pom.xml
... <!-- Dependency for accessing and manipulating HL7 v2 structures --> <dependency> <groupId>org.openehealth.ipf.commons</groupId> <artifactId>commons-map</artifactId> <version>${ipf-version}</version> </dependency> ...
- To use BidiMappingService, define a Spring bean and initialize it with the external Groovy resource:
context.xml
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:lang="http://www.springframework.org/schema/lang" xmlns:camel="http://camel.apache.org/schema/spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-2.5.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd"> ... <!-- Groovy class that provides the operations on the mappings --> <bean id="myMappingService" class="org.openehealth.ipf.commons.map.BidiMappingService"> <property name="mappingScript" value="classpath:example.groovy"/> </bean> ...
- Register the Mapping Service extensions in the Spring Application Context of your application
context.xml
... <!-- Mapping extensions --> <bean id="mappingExtension" class="org.openehealth.ipf.commons.map.extend.MappingExtension"> <property name="mappingService" ref="myMappingService" /> </bean> <!-- Register the extensions --> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> ... <ref bean="mappingExtension" /> </list> </property> </bean> ... </beans>
Using the BidiMappingServiceConfigurer bean
The BidiMappingServiceConfigurer supports distributed configurations of a BidiMappingService instance. This is useful when several components want to contribute mappings to a shared mapping service. Each component defines a BidiMappingServiceConfigurer bean in its application context that references the shared mapping service.
<bean id="sharedMappingService" class="org.openehealth.ipf.commons.map.BidiMappingService"> </bean>
<bean id="bidiMappingServiceConfigurer1" class="org.openehealth.ipf.commons.map.BidiMappingServiceConfigurer"> <property name="mappingService" ref="sharedMappingService" /> <property name="mappingScript" value="configurer1.map" /> </bean>
<bean id="bidiMappingServiceConfigurer2" class="org.openehealth.ipf.commons.map.BidiMappingServiceConfigurer"> <property name="mappingService" ref="sharedMappingService" /> <property name="mappingScripts"> <list> <value>configurer2.map</value> <value>configurer3.map</value> </list> </property> </bean>
Definition of Mappings
A mapping example is displayed below. The example maps a couple of codes from HL7-related code systems (see HL7 Messaging for more details on IPF's HL7 support).
mappings = {
encounterType(['2.16.840.1.113883.12.4','2.16.840.1.113883.5.4'],
E : 'EMER',
I : 'IMP',
O : 'AMB'
)
vip(['2.16.840.1.113883.12.99','2.16.840.1.113883.5.1075'],
Y : 'VIP',
(ELSE) : { it }
)
messageType(
'ADT^A01' : 'PRPA_IN402001'
(ELSE) : { throw new HL7Exception("Invalid message type", 207) }
)
}
This defines three mappings (encounterType, vip, and messageType), having an optional definition for ISO Object Identifiers (OIDs) to identify key and value code systems. The encounterType mapping has three entries, while the vip and messageType mappings have only one.
You can use the mappingService directly, or you can take advantage of the MappingExtension:
def x = mappingService.get('encounterType', 'E') // using the service bean reference
def y = 'E'.map('encounterType') // more concise: using the dynamic map method
def z = 'E'.mapEncounterType() // even more concise
// x == y == z == 'EMER'
The ELSE entry is called on MappingService.get() request with unknown keys. ELSE can be
- a Closure, which takes the key as parameter and is then executed
- any other Object o, which will return o.toString().
In the example above,
- for the vip mapping the key is returned, so that mappingService.get('vip', 'X') == 'X'
- for the messageType mapping, an Exception is thrown.
The services also allow mapping in the backward direction:
def x = mappingService.getKey('vip', 'VIP') // Y
def y = 'VIP'.mapReverse('vip') // Y
def z = 'VIP'.mapReverseVip() // Y
| Ambiguous mappings In case that a mapping definition maps more than one key to the same value (e.g. A->C and B->C), the backward mapping only contains the last entry, i.e. C->B. |
BidiMappingService also can be initialized using a list of mapping files:
... <!-- Groovy class that provides the operations on the mappings --> <bean id="myMappingService" class="org.openehealth.ipf.commons.map.BidiMappingService"> <property name="mappingScripts"> <list> <value>classpath:example1.groovy</value> <value>classpath:example2.groovy</value> </list> </property> </bean> ...
Conflicting mappings are overridden by later list entries, i.e. mappings defined in example2.groovy override existing mappings defined in example1.groovy.
Furthermore, BidiMappingService supports default reverse mappings, i.e. you can specify an ELSE mapping also from the reverse direction:
mappings = {
reverseMapping(
key : 'value',
(ELSE) : 'unknownKey',
'unknownValue' : (ELSE)
)
reverseMappingWithClosures(
key : 'value',
(ELSE) : 'unknownKey',
{ 'key' } : (ELSE)
)
}
The reverseMappingWithClosures mapping also demonstrates how to use a closure in order to return a default key that is already defined as key for a regular mapping.
HL7 v2 Messaging
This chapter describes how IPF facilitates HL7 v2 messaging.
HL7 v2 Messaging Overview
HL7's Version 2.x messaging standard is the workhorse of electronic data exchange in the clinical domain and arguably the most widely implemented standard for healthcare in the world. There have been seven releases of the Version 2.x Standard to date.
The HL7 Standard covers messages that exchange information in the general areas of Patient Demographics, Patient Charges and Accounting, Clinical Observations, Medical Records Document Management, and many more.
HL7 Version 2.6 represents HL7's latest development efforts to the line of Version 2 Standards that date back to 1989.
Features
IPF's HL7 v2 support does not reinvent the wheel. It leverages HAPI (http://hl7api.sourceforge.net), one of the most proven HL7 v2 Java libraries. It provides, however, features on top of HAPI that adds a lot of convenience compared to the original API, and retrofits some missing items.
| Feature | Functionality | See |
|---|---|---|
| HL7v2 DSL | A domain specific language based on the Groovy programming language for manipulating HL7 messages. HL7 message processing in IPF applications becomes almost trivial. | HL7 DSL |
| Extended HL7 Parser classes and Factories | For more flexibility in defining valid sets of HL7 structures | Extensions to HAPI |
| Convenient creation of HL7 messages | API for creating new messages and responses, in particular HL7 acknowledgements. | Extensions to HAPI |
| HL7 v2 Validation API | A specialized DSL dedicated to defining validation rules for HL7 messages | HL7 Message Validation |
| Camel adapters | Camel data types for using the HL7 v2 DSL and Validation inside Camel integration routes. | Camel DSL Extensions for HL7 |
Configuring HL7 v2 Messaging
This section explains how to configure IPF's HL7 messaging support.
Make sure you already have correctly set up
- Maven 2.0.9 or better
- a possibly empty IPF project
After configuration you will be able to use the HL7 v2 related DSLs in a standalone scenario.
- Add the necessary dependencies to your project's Maven 2 descriptor.
pom.xml for standalone scenario
... <!-- Dependency for accessing and manipulating HL7 v2 structures --> <dependency> <groupId>org.openehealth.ipf.modules</groupId> <artifactId>modules-hl7dsl</artifactId> <version>${ipf-version}</version> </dependency> <!-- Dependency for extending the API of the HAPI HL7 library --> <dependency> <groupId>org.openehealth.ipf.modules</groupId> <artifactId>modules-hl7</artifactId> <version>${ipf-version}</version> </dependency> ...
- Depending on the HL7 v2 versions, add the corresponding dependencies to the HAPI library. HAPI supports versions 2.2 through 2.6.
pom.xml
... <!-- Dependency for HL7 v2.5 --> <dependency> <groupId>ca.uhn.hapi</groupId> <artifactId>hapi-structures-v25</artifactId> <version>0.6</version> </dependency> <!-- Dependency for HL7 v2.5.1 --> <dependency> <groupId>ca.uhn.hapi</groupId> <artifactId>hapi-structures-v251</artifactId> <version>0.6</version> </dependency> ...
- Register the HL7 extensions in the Spring Application Context of your application
context.xml
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:lang="http://www.springframework.org/schema/lang" xmlns:camel="http://camel.apache.org/schema/spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-2.5.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd"> <!-- Setting up the Camel context --> <camel:camelContext id="camelContext"> <camel:routeBuilder ref="routeBuilder" /> </camel:camelContext> <bean id="routeBuilder" depends-on="routeModelExtender" class="..." /> ... <!-- HAPI extensions --> <bean id="hapiModelExtension" class="org.openehealth.ipf.modules.hl7.extend.HapiModelExtension"> <property name="mappingService" ref="..." /> </bean> <!-- Register the extensions --> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> ... <ref bean="hapiModelExtension" /> </list> </property> </bean> ... </beans>
HL7 v2 DSL
This section gives a detailed introduction to the Groovy-based HL7 v2 domain specific language.
The HL7 v2 DSL provides a unique programming interface for handling HL7 messages. Its API aligns very closely with natural language and the syntax of HL7 v2 as often seen in specifications and requirements. You don't need to translate anymore from the language of the "HL7 world" into the language of the "developer's world".
The DSL can be subdivided into the following groups of functionality:
- Construction: copying or loading messages from file or a plain string
- Navigation: accessing HL7 v2 substructures like groups, segments, or fields
- Manipulation: assigning new values to HL7 structures
- Rendering: writing a message or parts thereof to their external representation
For the purpose of demonstrating the DSL, a ORU_R01 message of HL7 v2.5 is taken as example.
Construction
Use load to construct a message from an HL7 file on the classpath or from an InputStream.
import static org.openehealth.ipf.modules.hl7dsl.MessageAdapters.* def message = load('oru-r01-25.hl7')
Alternatively, the message can be created from a string representation of a message directly:
import static org.openehealth.ipf.modules.hl7dsl.MessageAdapters.* def messageString = ... // an HL7 message string def message = make(messageString)
You can easily create a message as a copy of an existing message:
def messageCopy = message.copy()
| Anonymous types Groovy doesn't require to specify the exact type of a variable, instead you can use the def keyword. For HL7 v2 processing, this is a very convenient feature that saves you many explicit type checks and type casts. |
The message object that is constructed is a org.openehealth.ipf.modules.hl7dsl.MessageAdapter object that wraps the original HAPI message object. The complete HL7 v2 DSL only works with MessageAdapter objects. If you have a native HAPI message object, you can wrap it manually:
ca.uhn.hl7v2.model.Message hapiMessage = ....
MessageAdapter message = new MessageAdapter(hapiMessage)
Navigation
The DSL offers a position-based navigation of HL7 structures and fields. It's all valid Groovy Syntax, accomplished by operator overloading and metaclass programming, so you don't need a intermediate step that parses the expressions (cf. the Terser class in HAPI.)
Navigation to groups and segments
Groups and Segments can be accessed by name like an object property.
def msh = message.MSH // Obtain the Message Header Segment def group = message.PATIENT_RESULT(0) // Obtain the first PATIENT_RESULT group def pid = group.PATIENT.PID // Obtain the PID segment contained inside the PATIENT group.
Note that although the HL7 DSL hides much of the technical details and APIs, you still require profound knowledge of the HL7 specifications when working with HL7 messages. In the example above, e.g. you need to know that for ORU_R01 structures the PID segment is nested inside two groups.
Navigation to fields
Obtaining fields is similar to obtaining structures except that fields are often referred to by number instead of by name. Fields are accessed like an array field; components in a composite field are accessed like a two-dimension array:
def composite = message.MSH[3] // MSH-3 = sending application composite field def primitive = message.MSH[3][2] // MSH-3-2 = universal ID primitive field
It's also possible to navigate by specifying the field names instead of the number.
def primitive = message.MSH.sendingApplication.universalIDType
Take care, however, that along with the change of internal message structures, individual field names change between HL7 versions although they refer to the same position of the field in a segment. If you don't know the version of the HL7 message in advance, better use the more concise index notation. Example:
def messageType = message.MSH.messageType.messageType // only works for HL7 v2.2 and 2.3 messages messageType = message.MSH.messageType.messageCode // only works for HL7 v2.4\+ messages messageType = message.MSH[9][1] // works for all HL7 versions
Field values
Field variables render to their string encoding e.g. when printed, by implementing an appropriate toString() method. However, for literal comparison or variable assignment you
use the value property to obtain the value of a primitive field.
String primitiveValue = message.MSH[3][2].value primitiveValue = message.MSH[3][2].toString() if ("xyz".equals(message.MSH[3][2].value)) { ... }
HL7 Null Values
The HL7 DSL treats explicit HL7 null values (two double quotes "", cf. HL7 2.5, Final, Section 2.5.3) in a special way.
- value will convert "" into an empty string
- originalValue returns the double quotes
- isNullValue() returns true, if the original value of the field was "".
Therefore, if PID[11](0)[1][1] (first Street or Mailing Address) was "", the following assertions are true:
assertEquals '' , pid[11](0)[1][1].value assertEquals '""', pid[11](0)[1][1].originalValue assertTrue pid[11](0)[1][1].isNullValue()
Repetitions
Groups, Segments and Fields may repeat. Use parentheses like with regular method calls in order to obtain a certain element of a repeating structure. The next example shows how to navigate in a nested repetitive structure.
def group = message.PATIENT_RESULT(0).PATIENT // access first PATIENT_RESULT group def nk1 = group.NK1(0) // access first NK1 segment def phone = nk1[5](0) // access first NK1-5 field (phone)
To get a list of elements of a repeating structure, simply omit the index so that it looks like a method call without parameters.
def phones = nk1[5]() // returns a list of phone elements
Furthermore, repetitions can be counted:
def count = nk1.count(5) // returns size of the phone number list
Smart navigation
Navigating HL7 messages as described above usually requires knowledge about the specified message structure, which is often not visible by looking at the printed message:
def nk1 = message.PATIENT_RESULT(0).PATIENT.NK1(0) // Repetitions of groups or segments: def phoneNumber = nk1[5](0)[1] // Repetition of fields def familyName = nk1[2][1][1] // Name is first component of FN composite type
To make things worse, the internal structure changes between HL7 versions. In higher versions, primitive fields are sometimes replaced with composite fields, having the so far used primitive as first component. This appears to be backwards compatible on printed messages, but requires different DSL expressions when obtaining field values.
Smart navigation resolves these problems by assuming reasonable defaults when repetitions or component operators are omitted:
- If a repetition operator () is omitted, the first repetition of a group, segment or field is assumed
assert message.PATIENT_RESULT(0).PATIENT == message.PATIENT_RESULT.PATIENT // group assert group.NK1(0)[5](0)[1].value == group.NK1[5](0)[1].value // segment assert group.NK1(0)[5](0)[1].value == group.NK1[5][1].value // field
- If a component is omitted, the first component or subcomponent of a composite is assumed
assert group.NK1(0)[5](0)[1].value == group.NK1[5].value assert group.NK1(0)[2][1][1].value == message.NK1[2].value
- Consequently, Smart Navigation also works with HL7 Null values:
assertEquals '' , pid[11][1][1].value // 'full' expression assertEquals '""', pid[11][1][1].originalValue assertTrue pid[11][1][1].isNullValue() assertEquals '' , pid[11].value // 'smart' expression assertEquals '""', pid[11].originalValue assertTrue pid[11].isNullValue()
Using smart navigation, the navigation expressions are usually shorter and less error-prone. Furthermore, in many cases the same expressions can be used for different HL7 versions that define new structures in a backward-compatible way.
Access target objects
Objects of the HAPI DSL layer internally reference objects defined in the HAPI ca.uhn.hl7v2.model package. These can be accessed via the target property.
ca.uhn.hl7v2.model.Segment hapiSegment = nk1.target
However, this is usually not needed because any property access or method call not applicable to HAPI DSL model objects is forwarded to target objects.
int cardinality1 = message.NK1(0).target.getMaxCardinality(3) int cardinality2 = message.NK1(0).getMaxCardinality(3) // equivalent String segmentName1 = message.NK1(0).target.name String segmentName2 = message.NK1(0).name // equivalent
Typically you need to reference the target object e.g. when passing control to code that is unaware of the HL7 DSL or explicitly requires HAPI classes.
Group and Segment emptyness (as of IPF 2.1)
Emptyness for segments and groups is defined as follows:
- a segment is empty if all fields are empty
- a group is empty if all contained groups and segments are empty
For brevity, the GroupAdapter and SegmentAdapter classes both implement an isEmpty() method.
assert msg1.PATIENT_RESULT.PATIENT.PV1.isEmpty() == false // not empty because some fields are filled assert msg1.PATIENT_RESULT.PATIENT.PV2.isEmpty() == true // empty because all fields are empty assert msg1.PATIENT_RESULT.isEmpty() == false // non empty because it contains a non-empty group
Iterative functions (as of IPF 2.1)
As HL7 messages are compound structures, you can imagine to iterate over them. Thus, the HL7 DSL implements iterators for HL7 messages and groups. Due to their nested structures, iteration is implemented as a depth first traversal over all non-empty substructures, i.e. non-empty groups and segments (see previous section).
An iterator() function is defined for the GroupAdapter and MessageAdapter classes. You seldomly will use iterator() directly, however, a lot of Groovy's iterative functions only rely on the existence of an iterator function. As a consequence, you can e.g. use the following Groovy functions on HL7 messages and groups:
- each
- eachWithIndex
- every
- any
- collect
- find
- findAll
- split
- for statement
- the spread operator
Some examples:
// Count the number of substructures int numberOfStructures = 0 msg1.each { numberOfStructures++ } println "The message has $numberOfStructures substructures" // Check if there are any groups boolean hasGroups = msg1.any { it instanceof GroupAdapter } // A list of the names of all substructures def names = msg1*.name // For loop for (def structure in msg1) { // do something with structure } // Find the first nested OBX segment def obx = msg1.find { it.name == 'OBX' } obx = msg1.findOBX() // shortcut notation // Find all nested OBX segments def obxList = msg1.findAll { it.name == 'OBX' } obxList = msg1.findAllOBX() // shortcut notation
The find/findAll methods are handy in the following use cases:
- accessing data in a deeply nested message structure that is not visible in the pipe-encoded representation.
- uniformly accessing corresponding fields in messages with different structure
- messages that have a group structure in a newer HL7 version while having a flat structure in previous versions.
def patientName = msg1.PATIENT_RESULT(0).PATIENT.PID[5][1].value
patientName = msg1.findPID()[5][1].value // equivalent, shorter, and group-structure-agnostic
Manipulation
Message manipulation is as straightforward as navigation. You navigate to a segment or field and assign it a new object.
Manipulating segments
Currently you can change segments only, assignment to groups isn't supported yet.
msg1.EVN = msg2.EVN // copy over EVN segment from msg2 to msg1 msg1.EVN.from(msg2.EVN) // equivalent
There's a dedicated method nrp(index) available for adding a repetitions to a repeating field
def newField = message.PATIENT_RESULT(0).PATIENT.NK1(0)[5].nrp(5) // Adds a repetition to NK1[5]
There are two caveats:
First, segments are copied with the assignment (i.e. =) operator only if the assignment operator follows a property read-access operation (via .property or ['property']). If you make an assignment directly to a segment variable, you assign object references.
def mySegment = ... message.EVN = mySegment // mySegment copied into message.EVN def targetSegment = message.EVN targetSegment = mySegment // message.EVN remains unchanged, // targetSegment and mySegment reference the // same object
Second, when you obtain a segment from a repetition using using the () operator (method call) then you cannot assign directly because this will break Groovy/Java syntax. In this case, you must use the from method instead.
def mySegment = ... // assignment to another NK1 segment instance def group = message.PATIENT_RESULT(0).PATIENT group.NK1(0) = 'abc' // syntax error! msg1.NK1(0) = mySegment // syntax error! msg1.NK1(0).from(mySegment) // works!
Manipulating fields
To change a field value, navigate to the field (either by name or index, as shown above) and either assign it a string value or another field. Fields may also be changed by using the from() method.
def nk1 = message.PATIENT_RESULT(0).PATIENT.NK1(0) def otherNk1 = message.PATIENT_RESULT(0).PATIENT.NK1(0) nk1[4] = otherNk1[4] // copy address nk1[4][4] = otherNk1[4][4] // copy state or province only nk1[4][4].from(otherNk1[4][4]) // equivalent nk1[4][4] = 'NY' // set state or province directly
There are the same caveats with manipulating fields as with manipulating segments:
First, Composites are copied with the assignment (i.e. =) operator only if the assignment operator follows a subscript (i.e. []) operation. If you make an assignment to a composite variable directly you assign the respective object references
def otherXad = ... // a contact address = HL7 composite type XAD def nk1 = group.NK1(0) nk1[4] = otherXad // otherXad copied into nk1[4] def xad = nk1[4] xad = otherXad // nk1[4] remains unchanged, xad and otherXad reference // the same object
Second, when you obtain a field from a repetition using using the () operator (method call) then you cannot assign directly because this will break Groovy/Java syntax. In this case, you must use the from method instead.
def field = ... // a primitive or composite field def other = ... // a primitive or composite field to be assigned field(0) = 'abc' // syntax error! field(0) = other // syntax error! field(0).from(other) // works for primitives and composites field(0).value = 'abc' // works for primitives only
Adding repetitions
Repetitions occur in HL7 groups, segments and fields. When creating a new message or manipulating an parsed message, it may become necessary to add a repeating element. A good example is the ORU_R01 message in HL7 v2.5, which includes nested repeatable groups, which in return contain repeatable segments that have repeatable fields.
There are two ways to add a repeating element: explicitly and implicitly.
Explicitly calling nrp() (for "new repetition") adds an element and returns it to the caller. The argument is of type String for repeating structures or int for repeating fields:
def message = new MessageAdapter(new ORU_R01()) def patientResult = message.nrp('PATIENT_RESULT') // add a PATIENT_RESULT group def order = patientResult.nrp('ORDER_OBSERVATION') // add a ORDER_OBSERVATION group def observation = order.nrp('OBSERVATION') // add a OBSERVATION group def obx5 = observation.OBX.nrp(5) // add a OBX-5 field
For consistency with HAPI, an element is also added if you access a repetition that does not exist yet.
def message = new MessageAdapter(new ORU_R01()) def patientResult = message.PATIENT_RESULT(0) // add a PATIENT_RESULT group def order = patientResult.ORDER_OBSERVATION(0) // add a ORDER_OBSERVATION group def observation = order.OBSERVATION(0) // add a OBSERVATION group def obx5 = observation.OBX[5](0) // add a OBX-5 field
| Index out of bounds! The DSL does not distinguish whether the new repetition would be the next one to be created or not. If there's no PATIENT_RESULT group in the message, then msg.PATIENT_RESULT(8) does not silently add seven empty groups and returns the eighth! Instead only one group is added and returned, i.e. you actually obtain msg.PATIENT_RESULT(0). |
Together with the Smart Navigation feature, it is particularly convenient that accessing a repeated element without index does a default to its first repetition. Hence, the code above can be condensed to:
def message = new MessageAdapter(new ORU_R01()) def obx5 = message.PATIENT_RESULT.ORDER_OBSERVATION.OBSERVATION.OBX[5]
Rendering
Rendering writes the internal representation of a HL7 v2 message to its external representation, which is usually the ER7-encoded form with pipe field seperators.
To write a message to stdout, messages can be written to stream using the left-shift (<<) operator.
System.out << message
Otherwise, using the message variable in a string context or explicitly calling toString() does the same job:
assert message.toString() == "${message}"
Functional Extensions to HAPI
While the HL7 v2 DSL has its focus on providing a domain-specific syntax to navigate in HL7 messages and changing fields within messages, the functional extensions retrofit a couple of convenient functions on top of HAPI. By means of Groovy metaprogramming, however, it looks like these extensions are part of the HAPI API, i.e. you can call the methods on both the raw HAPI objects and the wrapper objects invisibly added by the HL7 v2 DSL.
It's important to note that the HL7 v2 DSL and the functional extensions do not depend on each other - you can employ any one or both feature sets as you like.
HL7 PipeParser and custom ModelClassFactory
In order to instantiate concrete implementations of Message, Group, Segment etc, the HAPI Parsers use a ModelClassFactory member object that looks up classes for these model components. The default implementation provides access to model components as specified in the HL7 specs.
In real world HL7 projects you frequently need to deal with non-standard HL7 "dialects" which are not covered by the specification and causes the parser to fail or generate "generic" model classes when used out-of-the-box. Although it's possible to implement a custom ModelClassFactory, there remains a lack of flexibility, e.g. it's not possible to use two distinct sets of "dialects" within one Java process. The HAPI extension library offers a solution for this limitation.
CustomModelClassFactory
The factory implementation org.openehealth.ipf.modules.hl7.parser.CustomModelClassFactory can be configured to map a HL7 version to a list of package names in which the HAPI model classes are looked up. If it fails to find the requested class, the call is delegated to HAPI's default implementation. Example:
def customModelClasses = ['2.5' :
['com.mycompany.profile1.hl7def.v25',
'com.mycompany.profile2.hl7def.v25']]
def customFactory = new CustomModelClassFactory(customModelClasses)
The following subpackages are looked up for the respective model classes:
| model interface | package |
|---|---|
| Message | X.message |
| Group | X.group |
| Segment | X.segment |
| Type | X.datatype |
Note that the value side of the map is always a List. In the example above, the Message classes for version 2.5 are looked up in the following order:
- com.mycompany.profile1.hl7def.v25.message
- com.mycompany.profile2.hl7def.v25.message
- ca.uhn.hl7v2.model.v25.message (the default)
If you use a custom model class factory, it's strongly recommended to provide the factory instance to the HapiModelExtension extension class. This ensures that the various extensions that create HL7 message or structures (see below for details) use this factory to create the HL7 objects.
...
<!-- A custom model for HL7 v2.5 message -->
<bean id="myModelClassFactory" class="org.openehealth.ipf.modules.hl7.parser.CustomModelClassFactory">
<property name="customModelClasses">
<map>
<entry key="2.5">
<list>
<value>com.mycompany.profile1.hl7def.v25</value>
<value>com.mycompany.profile2.hl7def.v25</value>
</list>
</entry>
....
</map>
</property>
</bean>
...
<!-- HAPI extensions -->
<bean id="hapiModelExtension"
class="org.openehealth.ipf.modules.hl7.extend.HapiModelExtension">
<property name="mappingService" ref="..." />
<property name="factory" ref="myModelClassFactory"/>
</bean>
...
Custom PipeParser
The PipeParser implementation provided by this module (org.openehealth.ipf.modules.hl7.parser.PipeParser) by default uses the CustomModelClassFactory and a default set of Primitive validation rules (see HL7 v2 Message Validation chapter for details). Apart from that it does not add any features to the HAPI PipeParser class.
import org.openehealth.ipf.modules.hl7.parser.PipeParser; ... def customParser = new PipeParser(customFactory)
Methods added to the HAPI Message interface
New Messages
You can create a new message from scratch by specifying event type, trigger event and version. Its message header fields are populated with the event type, trigger event, version, the current time as message date, and the common separators.
import ca.uhn.hl7v2.model.Message // Static method extension to the HAPI Message class def msg = Message.ADT_A01('2.5') // creates a ca.uhn.hl7v2.model.v25.message.ADT_A01 object
| HL7 Message Structures The message structure is a data structure that expresses an association of a message type with an event for a class of HL7 messages. Each message structure also contains a unique ID, e.g. ADT_A01. When you create new messages using IPF's HAPI extensions, the message structure (and therefore the HAPI class to be used) is automatically derived from event type, trigger event and message version, and MSH-9 is populated accordingly. Example: import ca.uhn.hl7v2.model.Message def msg = Message.ADT_A04('2.5') assert msg instanceof ca.uhn.hl7v2.model.v25.message.ADT_A01 |
Acknowledgements and Responses
You can create positive or negative acknowledgments to HL7 messages with a single method call. The acknowledgment message
- is in the same HL7 version as the original message
- refers to the message metadata of the original message (e.g. swapped sender and receiver fields)
- contains the current timestamp as message date
- is populated with MSA and/or ERR segments as specified in the parameters.
// Positive Acknowledgement def ack = msg.ack() // Negative Acknowledgements def nak1 = msg.nak('Reason for failure', AckTypeCode.AE) def nak2 = msg.nak(new HL7Exception('reason for failure', 204), AckTypeCode.AE)
In case of parsing errors there's no message object available to derive the negative acknowledgment from. In this case you can reuse the Exception thrown by the parser to create a generic negative acknowledgement of a specific version.
def nak = ca.uhn.hl7v2.model.Message.defaultNak(e, AckTypeCode.AE, '2.5') // NAK of version 2.5
Generating acknowledgments is only a special case of generating a response to an original message. If a response is defined as dedicated HL7 message as with responses to Query messages, you have to use the respond(eventType, triggerEvent) extension method. The response message
- is in the same HL7 version as the original message
- refers to the message metadata of the original message (e.g. swapped sender and receiver fields)
- contains the current timestamp as message date
- has a populated MSA segment
def rsp = msg.respond('RSP','K21') // generates a RSP_K21 message
Message checks
Use the matches extension method to check for specific message types
if (msg.matches('ADT','A01','2.5')) { // true if msg is ADT_A01 version 2.5 } else if (msg.matches('ADT','*','*')) { // true if msg from ADT domain of any version }
You can check the three parameters of matches individually, too:
def version = msg.version // 2.5 def eventType = msg.eventType // ADT def triggerEvent = msg.triggerEvent // A01
Message dump
For debugging purposes, it's often useful to know the internal (hierarchical) data structure of a HAPI Message. For complex messages, the returned structure can be pretty extensive, so you should avoid using this in production environments:
println msg.dump()
Methods added to the HAPI Structure interface
New segments
Just as creating a message, you can also create a segment by calling its respective name as static method on the ca.uhn.hl7v2.model.Structure interface. You need to pass the enclosing Message object as argument, which determines the HL7 version to be used.
import ca.uhn.hl7v2.model.Segment ... // Static method extension to the HAPI Message class def obx = Segment.OBX(msg) // creates a ca.uhn.hl7v2.mode.v25.segment.OBX object // obx = Segment.OBX(msg.target) if msg is a MessageAdapter
Printing structures
All HAPI Structures (i.e. not only Messages, but also arbitrary Groups and Segments) can be converted into their pipe-encoded representation by calling the encode() extension method. Note that a Message is a subclass of Group.
assert message.MSH.encode() == 'MSH|^~\\&|SAP-ISH|HZL|||20040805152637||ADT^A01|123456|T|2.2|||ER' println message.encode() // prints the complete message
Methods added to the HAPI Type interface
New fields
Just as creating a message or segment, you can also create a field by calling its respective name as static method on the ca.uhn.hl7v2.model.Composite or ca.uhn.hl7v2.model.Primitive}}interface. You need to pass the enclosing {{Message object as argument, which determines the HL7 version to be used.
Composites may be initialized with a map containing the component values. Primitives may be initialized with a literal string value.
import ca.uhn.hl7v2.model.Composite import ca.uhn.hl7v2.model.Primitive ... // Static method extension to the HAPI Composite class def ce = Composite.CE(msg, [identifier:'T57000', text:'GALLBLADDER', nameOfCodingSystem:'SNM']) // ce = Composite.CE(msg.target, ...) if msg is a MessageAdapter // Static method extension to the HAPI Primitive class def st = Primitive.ST(msg, 'value') // st = Primitive.ST(msg.target, 'value') if msg is a MessageAdapter
Printing types
All HAPI Types (i.e. Primitives, Composites, and Varies) can be converted into their pipe-encoded representation by calling the encode() extension.
assert message.MSH.messageType.encode() == 'ADT^A01' // Together with the HL7 DSL, you can also write assert message.MSH[9].encode() == 'ADT^A01'
Mapping Service
The Mapping Service has been moved to the IPF Core features. After all, although often used in HL7 processing, code system mapping is not a feature that is inherently exclusive for HL7. Please refer to the Mapping Service chapter.
What remains specific to IPF's HL7 v2 support, however, is that the mapping extensions can be applied directly on all HAPI types. The encode() extension is called before the mapping is executed.
Given the following mapping example:
mappings = {
encounterType(['2.16.840.1.113883.12.4','2.16.840.1.113883.5.4'],
E : 'EMER',
I : 'IMP',
O : 'AMB'
)
vip(['2.16.840.1.113883.12.99','2.16.840.1.113883.5.1075'],
Y : 'VIP',
(ELSE) : { it }
)
messageType(
'ADT^A01' : 'PRPA_IN402001'
(ELSE) : { throw new HL7Exception("Invalid message type", 207) }
)
}
You can use the mapping functions directly on composite or primitive field objects:
// Mapping primitives assert msg.PV1.patientClass.value == 'I' assert msg.PV1.patientClass.map('encounterType') == 'IMP' assert msg.PV1.patientClass.mapEncounterType() == 'IMP' // Together with the HL7 v2 DSL, you can also write assert msg.PV1[2].mapEncounterType() == 'IMP' // To map a Composite field, you can write assert msg.MSH.messageType.mapMessageType() == 'PRPA_IN402001' assert msg.MSH[9].mapMessageType() == 'PRPA_IN402001'
Examples for HL7 Messaging
This section shows the HL7 v2 DSL and Functional Extensions to HAPI in action. As the primary purpose of DSL and extensions is to augment the original HAPI library, it seems appropriate to take HAPI examples code and reimplement them.
Create a message from scratch
The first example is to create a simple message from scratch, taken from http://hl7api.sourceforge.net/xref/ca/uhn/hl7v2/examples/CreateAMessage.html.
The example shows
- how to use Groovy properties instead of getter methods for named access to fields
- how to use IPF's functional extension to create messages with a prefilled MSH segment
- how to employ Groovy's with clause.
import org.openehealth.ipf.modules.hl7.extend.HapiModelExtension import org.openehealth.ipf.modules.hl7.parser.PipeParser import ca.uhn.hl7v2.model.* public class CreateAMessage{ static def makeMessage() { def msg = Message.ADT_A01('2.4') // 'with' is a Groovy feature that delegate all unknown method properties // in this case to the msg.MSH object. msg.MSH.with { sendingApplication.namespaceID.value = 'TestSendingSystem' sequenceNumber.value = '123' } msg.PID.with { getPatientName(0).familyName.surname.value = 'Doe' getPatientName(0).givenName.value = 'John' getPatientIdentifierList(0).ID.value = '123456' } println "Printing ER7 Encoded Message:" println new PipeParser().encode(msg) } /** * @param args */ public static void main(def args) { // Initialize the MetaClass extension ExpandoMetaClass.enableGlobally() new HapiModelExtension().extensions.call() // makeMessage() } }
The console output is:
Printing ER7 Encoded Message: MSH|^~\&|TestSendingSystem||||20090923152839||ADT^A01^ADT_A01|1956|P^T|2.4|123 PID|||123456||Doe^John
The message timestamp will vary as it reflects the point of time when the message is being created.
Create a ORU_R01 v2.5 message
The second example is to create a not-so-simple message, taken from http://hl7api.sourceforge.net/xref/ca/uhn/hl7v2/examples/PopulateOBXSegment.html. The problem about this message is its nested structure of repeatable elements.
This time, the implementation also takes advantage of the HL7 v2 DSL, replacing all named field accessors by their position-based syntax.
In addition to the first example, this example shows
- how to wrap a HAPI message into a HL7 v2 DSL MessageAdapter object
- how to apply the HL7 v2 DSL
- how to transparently add repetition to repeatable structures and fields
- how to omit default indices due to the DSL's Smart Navigation feature
- how to create composite and primitive fields from scratch
- how to work with "Varies" types like in OBX-5
import org.openehealth.ipf.modules.hl7.extend.HapiModelExtension import org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension import org.openehealth.ipf.modules.hl7dsl.MessageAdapter import ca.uhn.hl7v2.model.* public class PopulateOBXSegment{ static def makeOBX() { // Create message and wrap def msg = new MessageAdapter(Message.ORU_R01('2.5')) // Populate OBR. Group repetitions are created while navigating def obr = msg.PATIENT_RESULT.ORDER_OBSERVATION.OBR obr[1] = '1' obr[3][1] = '1234' obr[3][2] = 'LAB' obr[4] = '88304' // Smart Navigation expands this to obr[4][1] // Populate the first OBX // Note that we don't specify the repetition with PATIENT_RESULT and // ORDER_OBSERVATION because Smart Navigation def obx = msg.PATIENT_RESULT.ORDER_OBSERVATION.OBSERVATION(0).OBX obx[1] = '1' obx[2] = 'CE' // the type of OBX-5 obx[3] = '88304' obx[4] = '1' def ce = Composite.CE(msg.target, [identifier:'T57000', text:'GALLBLADDER', nameOfCodingSystem:'SNM']) // OBX-5 is a repeatable Varies field. // Don't care about it, the DSL gets it right for you. obx[5] = ce // equivalent with obx[5](0).data = ce // Populate the second OBX obx = msg.PATIENT_RESULT.ORDER_OBSERVATION.OBSERVATION(1).OBX obx[1] = '2' obx[2] = 'TX' // the type of OBX-5 obx[3] = '88304' // The second OBX in the sample message has an extra subcomponent at // OBX-3-1. This component is actually an ST, but the HL7 specification allows // extra subcomponents to be tacked on to the end of a component. This is // uncommon, but HAPI nontheless allows it. obx[3][1].extraComponents[0].data = Primitive.ST(msg.target, 'MDT') obx[4] = '2' def tx = Primitive.TX(msg.target, 'MICROSCOPIC EXAM SHOWS HISTOLOGICALLY NORMAL GALLBLADDER TISSUE') obx[5] = tx // Return the message msg } public static void main(def args){ // This example requires both extension packages ExpandoMetaClass.enableGlobally() new HapiModelExtension().extensions.call() new Hl7ModelExtension().extensions.call() println makeOBX() } }
The console output is:
MSH|^~\&|||||20090923151109||ORU^R01^ORU_R01|98|P^T|2.5 OBR|1||1234^LAB|88304 OBX|1|CE|88304|1|T57000^GALLBLADDER^SNM OBX|2|TX|88304&MDT|2|MICROSCOPIC EXAM SHOWS HISTOLOGICALLY NORMAL GALLBLADDER TISSUE
Conclusion
The example showed that with IPF's HL7 support you can work with HL7 messages without ever seeing and touching much of the programming interface of the underlying HAPI library. The code is readable for HL7 v2 domain experts without much knowledge in programming.
This impressively underlines the purpose of domain specific languages to narrow the gap between domain experts and software development experts, here applied to the domain of HL7 v2 messaging.
Language reference
Message elements
| Element | Implementation |
|---|---|
| message | org.openehealth.ipf.modules.hl7dsl.MessageAdapter |
| group | org.openehealth.ipf.modules.hl7dsl.GroupAdapter |
| segment | org.openehealth.ipf.modules.hl7dsl.SegmentAdapter |
| composite field | org.openehealth.ipf.modules.hl7dsl.CompositeAdapter |
| primitive field | org.openehealth.ipf.modules.hl7dsl.PrimitiveAdapter |
| undefined field | org.openehealth.ipf.modules.hl7dsl.VariesAdapter |
| repeating group | groovy.lang.Closure containing groups and/or messages |
| repeating segment | groovy.lang.Closure containing segments |
| repeating field | groovy.lang.Closure containing composites and or primitives |
Also refer to understanding repetitions for an introduction to repeating groups, segments and fields.
Read access operations
Read access operations on non-repeating message elements
The following table specifies the effect of operators for read-access operations on non-repeating message elements.
| Type | . operator (dot) | [] operator (subscript) |
|---|---|---|
| message or group | Access to group or segment by name:
message.<groupName> // contained group with name <groupName> message.<segmentName> // contained segment with name <segmentName> |
Access to group or segment by name:
message['<groupName>'] // contained group with name <groupName> message['<segmentName>'] // contained segment with name <segmentName> |
| segment | Access to field by symbolic field name: segment.<symbolicFieldName> // field of segment with name <symbolicFieldName> message.MSH.sendingApplication // 3rd field of MSH segment |
Access to field by symbolic field name or index: segment['<symabolicFieldName>'] // field of segment with name <symbolicFieldName> segment[i] // i-th field of segment (i=1..n) message.MSH['sendingApplication'] // 3rd field of MSH segment message.MSH[3] // 3rd field of MSH segment |
| composite field | N/A | Access to component composite[i] // i-th component of composite (i=1..n) message.NK1(0)[4][4] // 4-th component of composite field message.NK1(0)[4] |
| primitive field | Access to primitive field's string value
field.value // the field's string value ('""' removed)
field.originalValue // the field's original value
|
N/A |
Read access operations on repeating message elements
The following table specifies the effect of the () operator for read-access operations on repeating message elements.
| Type | () (closure call) |
|---|---|
| repeating group | Access to group repetitions and its members groups() // list of groups in repetition groups(i) // i-th group in repetition (i=0..n) message.PATIENT_RESULT() // all groups of the repeating PATIENT_RESULT group message.PATIENT_RESULT(0) // first group of the repeating PATIENT_RESULT group |
| repeating segment | Access to segment repetitions and its members segments() // list of segments in repetition segments(i) // i-th segment in repetition (i=0..n) message.NK1() // all segments of the repeating NK1 segment message.NK1(0) // first segment of the repeating NK1 segment |
| repeating field | Access to field repetitions and its members fields() // list of fields in repetition fields(i) // i-th field in repetition (i=0..n) segment.count(j) // returns the number of repetitions of the j-th field segment.nrp(j) // adds a repetition to the j-th field, returning the new object message.NK1(0)[5]() // all fields of the repeating message.NK1(0)[5] field message.NK1(0)[5](1) // second field of the repeating message.NK1(0)[5] field |
Write access operations
The following table specifies the effect of operators for write-access operations on non-repeating message elements.
| Type | .\ operator (dot) | []\ operator (subscript) |
|---|---|---|
| message or group | Copy group or segment by name:
message.<groupName> = group // where group.name == groupName message.<segmentName> = segment // where segment.name == segmentName |
Copy group or segment by name:
message['<groupName>'] = group // where group.name == groupName message['<segmentName>'] = segment // where segment.name == segmentName |
| segment | Set primitive field by symbolic field name: segment.<symbolicFieldName> = value // value is a string Copy composite or primitive by symbolic field name: segment.<symbolicFieldName> = composite segment.<symbolicFieldName> = primitive Example: messsage1.MSH.sendingApplication = 'XYZ' |
Set primitive by symbolic field name or index: segment['<symbolicFieldName>'] = value // value is a string segment[i] = value // value is a string segment['<symbolicFieldName>'] = composite segment['<symbolicFieldName>'] = primitive segment[i] = composite segment[i] = primitive message1.MSH['sendingApplication'] = 'XYZ' message1.MSH[3] = 'XYZ' message1.EVN[7] = message2.EVN[7] |
| composite field | N/A | Set primitive by index: composite[i] = value // value is a string composite[i] = component // non-primitive composite[i] = primitive message1.NK1(0)[4][4] = 'abc' message1.NK1(0)[4][4] = message2.NK1(0)[4][4] |
| primitive field | Set primitive's value
field.value = value // value is a string |
N/A |
Write access operations on repeating message elements
Direct write access to repeating elements is not possible. Write access to message elements obtained via the () operator must be done via the from() method. If the obtained element is a primitive field then you may also use the .value property to assign strings.
Write access operations on message elements defined as separate variable
When using the = operator for write access on message elements defined as separate variable then the usual Java/Groovy semantics apply. In this case the variable will just reference another object. Only if the = operator follows a [] (subscript) or . (property access) operation the assignment operator will cause a copy operation.
Method and property dispatch
- Any property access not processed by an adapter is dispatched to the target object.
- Any method call not processed by an adapter is dispatched to the target object.
- Returned objects from the target are adapted if a corresponding adapter exists.
Access to special objects
- The adapted target objects can be obtained from any adapter via adapter.target.
- The corresponding message adapter to a structure or field adapter can be obtained via adapter.message
HL7 v2 Message Validation
This chapter describes how IPF supports the validation of HL7 v2 messages.
Overview
HL7 v2.x is a complex flat-file structure that, despite being considered a data standard, is also highly flexible. It is often expected of an HL7 integration engine that non-standard compliant data be accepted and processed without notification to the receiving system of non-compliance.
However, to achieve real interoperability, the HL7 standard should be constrained to reduce the degree of freedom e.g. how to use certain fields or whether to populate optional fields or not. This happens either based on a written specification or in addition as machine-readble conformance profile. In order to check whether HL7 messages actually conform to the defined constraints, message validation is essential.
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
Validation rules are defined by instantiating an implementation of the ca.uhn.hl7v2.validation.ValidationContext interface of HAPI. Then you use this context to validate messages against the contained constraints.
Validation
There are two ways to validate:
- while parsing a message
- as dedicated operation on an existing message
To validate together with parsing, simply configure your Parser instance with the ValidationContext. If validation fails an exception is thrown.
... Parser parser = new PipeParser(context) Message message = parser.parse(msgText) // Does not reach code below on parsing or validation error ...
To validate after parsing, use the HAPI MessageValidator class. If validation fails an exception is thrown.
import ca.uhn.hl7v2.validation.MessageValidator ... new MessageValidator(context, true).validate(message) // Does not reach code below on parsing or validation error ...
IPF HL7 Validation Rules
IPF provides a special ValidationContext: the org.openehealth.ipf.modules.hl7.validation.DefaultValidationContext class. It offers two benefits:
- It provides access to a Validation Builder that allows for definition of validation rules using a simple Domain Specific Language (DSL)
- It supports Validation rules that constrain their target object by evaluating Groovy closures for better flexibility.
Although it is possible to manually instantiate the Validation rule classes that come with IPF, you normally should not care and instead use the Validation builder.
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.
import org.openehealth.ipf.modules.hl7.validation.DefaultValidationContext ... DefaultValidationContext context = new DefaultValidationContext() 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 ...
Now the rules can be defined by specifying constraints on
- primitive type level (see Primitive Type Constraints)
- message level (see Message Constraints)
- encoding level (see Encoded Message Constraints)
...
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
...
Reusing a DefaultValidationContext
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() ...
It is also possible to reuse any ValidationContext implementation (i.e. also not IPF-based contexts) 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) ...
Configuring HL7 v2 Validation
This section explains how to configure Validation Context and Validation Rules in an application based on the Spring Framework.
- To initialize a DefaultValidationContext from a Spring Framework ApplicationContext, you need a ValidationContextFactoryBean and one or more ValidationContextBuilder beans. Each ValidationContextBuilder will contribute to the overall set of rules being applied.
context.xml
<beans> ... <!-- The FactoryBean creates an instance of DefaultValidationContext --> <bean id="context" class="org.openehealth.ipf.modules.hl7.validation.ValidationContextFactoryBean"/> <!-- Rule Set 1: Default rules for primitive types that come with IPF --> <bean id="defaultTypeRules" class="org.openehealth.ipf.modules.hl7.validation.builder.DefaultTypeRulesBuilder"/> <!-- Rule Set 2: Custom rules of either variant (type, message or encoding) --> <bean id="myCustomRules" class="com.my.company.MyValidationContextBuilder"/> <!-- Optional: initialize a Parser instance with the ValidationContext --> <bean id="parser" class="org.openehealth.ipf.modules.hl7.parser.PipeParser"> <property name="validationContext" ref="context"/> </bean> ... </beans>
- Provide the custom ValidationContextBuilder implementation. A skeleton for such a custom builder is given in the following example:
MyValidationContextBuilder.groovy
package com.my.company 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(...) ... } }
Primitive Type Constraints
This section shows how to define constraints on primitive HL7 data types.
| Standard validation rules provided IPF already provides a set of constraints for primitive types as defined in the HL7 specification for each 2.x version. If this is fits your needs, use org.openehealth.ipf.modules.hl7.validation.support.DefaultTypeRulesValidationContext as your ValidationContext implementation and optionally add more validation rules to it. |
Primitive types have no substructure, i.e. they directly contain string values. The values are usually constrained by one or more of the following restrictions:
- 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, however, regular expressions are sometimes hard to read and understand.
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 is mandatory and the month and the day is optional. The example uses a regular expression to specify the date pattern. Please also read about regular expressions in Groovy (http://groovy.codehaus.org/Regular+Expressions).
The Validation DSL for primitive types supports the following constraints:
| Constraint type | Method .type('X') | Example | constraints... |
|---|---|---|---|
| maximum length | .maxSize(int) | maxSize(255) | ... the maximum length to 255 characters |
| length range | [min..max] | [10..20] | ... the length to be between 10 and 20 |
| existence | .notEmpty() | ... that there must be a value of >= 1 character | |
| matches | .matches(regexp) | matches(/(\d{4}) | ... the value has four digits |
| number type | .isNumber() | ... the value to be a decimal number | |
| user defined | .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, however, it's not recommended because it unnecessarily couples validation with preprocessing.
| Constraint type | Method .type('X') | Example | function |
|---|---|---|---|
| trim | .omitLeadingWhitespace() | removes leading whitespace characters | |
| trim | .omitTrailingWhitespace() | removes trailing whitespace characters |
You can combine any of constraint methods to define more than one rule for a type:
...
.type('X').omitLeadingWhitespace().isNumber().checkIf { it > 50 && it < 100 }
...
Each HL7 v2.x version defines a set of primitive types; over the time the number of types have increased and/or the constraints have been modified. IPF comes with predefined validation rules that enforce these constraints. Also note that the IPF PipeParser class is preconfigured with the default type rules.
import ca.uhn.hl7v2.validation.ValidationContext import org.openehealth.ipf.modules.hl7.validation.support.DefaultTypeRulesValidationContext ... ValidationContext context = DefaultTypeRulesValidationContext()
| 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:
|
Message Constraints
This section explains how complete HL7 messages can be validated.
By default, the HAPI parsers accept about any message as long as it follows the HL7 syntactic rules. For messages, groups and segments not defined in the respective HL7 2.x standard, generic structures are internally instantiated. This allows parsing custom messages even without prior definition of Z segments or similar custom structures.
Because the parser itself can not guarantee specification-compliant messages, this has to be added either by configuring the parser or executing validation seperately.
| When message validation rules are checked Message rules can be enforced by either configuring the parser or by manually validating a parsed message. |
There are three subtypes of message validation rules provided by IPF.
| Validation Rule | ValidationBuilder clause | Description |
|---|---|---|
| HL7 Abstract Message Syntax | abstractSyntax | validates the existance, order, and cardinality of HL7 groups and segments |
| Conformance Profiles | conformsToProfile | validates the message against the static part of a HL7 Conformance Profile |
| Custom | checkIf | passes the Message object into as parameter into a Groovy closure, where custom validation can be executed |
HL7 Abstract Message Syntax
HL7 v2 defines an Abstract Message Syntax (see HL7v2.5 specification, Chapter 2.13), that determines how 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 with the abstractSyntax builder expression. The corresponding rule is almost a copy of the definition, with only a few differences:
- segment names are enclosed in quotes ('')
- group names are specified like function calls inside the cardinality indicators as 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 ]
|
context.configure()
.forVersion('2.5')
.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 fields inside segments can neither be specified nor validated with the Abstract Message Syntax.
HL7 Conformance Profiles
Conformance Profiles have been introduced with HL7 v2.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 HL7 v2.5 specification document.
There are tools that facilitate the definition of Conformance Profiles, most notably the Messaging Workbench, which can be downloaded at http://sl.infoway-inforoute.ca/downloads/MWB%20Release%206-7p1.zip.
HAPI supports checks against conformance profiles. In the IPF Validation framework it can be used with the conformsToProfile builder expression.
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.
Custom Message 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
}
Encoded Message Constraints
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()
...
Camel DSL Extensions for HL7 v2
This chapter shows how HL7 Messaging support can be used in IPF integration routes.
Configuring Camel DSL Extensions for HL7 v2 Messaging
This section explains how to configure the DSL extensions for HL7 messaging support.
Make sure you already have correctly set up
- Maven 2.0.9 or better
- a possibly empty IPF project
After configuration you will be able to use the HL7 v2 related DSLs together with Apache Camel's routing engine.
- Add the necessary dependencies to your project's Maven 2 descriptor.
pom.xml for integration scenario... <!-- Dependency for DSL extensions for HL7 v2 --> <dependency> <groupId>org.openehealth.ipf.platform-camel</groupId> <artifactId>platform-camel-hl7</artifactId> <version>${ipf-version}</version> </dependency> ...
- Depending on the HL7 v2 versions, add the corresponding dependencies to the HAPI library. HAPI supports versions 2.2 through 2.6.
pom.xml
... <!-- Dependency for HL7 v2.5 --> <dependency> <groupId>ca.uhn.hapi</groupId> <artifactId>hapi-structures-v25</artifactId> <version>0.6</version> </dependency> <!-- Dependency for HL7 v2.5.1 --> <dependency> <groupId>ca.uhn.hapi</groupId> <artifactId>hapi-structures-v251</artifactId> <version>0.6</version> </dependency> ...
- Register the Camel extensions in the Spring Application Context of your application. If you use the functional extensions to HAPI, register the HapiModelExtensions as well.
context.xml
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:lang="http://www.springframework.org/schema/lang" xmlns:camel="http://camel.apache.org/schema/spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-2.5.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd <!-- Setting up the Camel context --> <camel:camelContext id="camelContext"> <camel:routeBuilder ref="routeBuilder" /> </camel:camelContext> <bean id="routeBuilder" depends-on="routeModelExtender" class="..." /> ... <!-- HAPI extensions --> <bean id="hapiModelExtension" class="org.openehealth.ipf.modules.hl7.extend.HapiModelExtension"> <property name="mappingService" ref="..." /> </bean> <!-- General Mapping extensions --> <bean id="mappingExtension" class="org.openehealth.ipf.commons.map.extend.MappingExtension"> <property name="mappingService" ref="..." /> </bean> <!-- Camel DSL extensions --> <bean id="hl7ModelExtension" class="org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension"> </bean> <!-- Register the extensions --> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> ... <ref bean="hl7ModelExtension" /> <ref bean="hapiModelExtension" /> <ref bean="mappingExtension" /> </list> </property> </bean> ... </beans>
DSL extensions for HL7 v2 Messaging
This section describes the DSL extensions for using HL7 v2 together with Apache Camel, provided by the platform-camel-hl7 component. For a description of Camel-independent HL7 message processing features visit HL7 Messaging and HL7 Message Validation.
The main purpose of this set of DSL extensions is to make the HL7 v2 DSL, HAPI extensions and HL7 v2 Validation features available in Camel routes. Extensions provided may well be combined with other extensions that comply with the DSL extension mechanism.
HL7 v2 DSL MessageAdapter (un)marshalling
The ghl7() DSL extension allows you to convert between HL7 message strings (or streams) and org.openehealth.ipf.modules.hl7dsl.MessageAdapter objects. For example, to unmarshal a message adapter from a string (or stream) use
// ...
from('...')
.unmarshal().ghl7()
.to('...')
// ...
To marshal a message adapter to an output stream use
// ...
from('...')
.marshal().ghl7()
.to('...')
// ...
(Un)marshaling options
HL7 v2 DSL MessageAdapter unmarshalling and marshalling can be customized in various ways. You can define
- a custom character set via the ghl7(java.lang.String charset) parameter,
- a custom HAPI parser via the ghl7(ca.uhn.hl7v2.parser.Parser parser) parameter
- or both via ghl7(ca.uhn.hl7v2.parser.Parser parser, java.lang.String charset)
The charset parameter is used to define the character set used for reading from and writing to a byte stream.
The parser parameter allows you to define a custom HAPI parser when you unmarshal a message adapter from a stream. The message adapter adapts a HAPI message which in turn is created by a HAPI parser.
Here's an example:
def ca.uhn.hl7v2.parser.Parser parser = new MyCustomParser()
// ...
.unmarshal().ghl7(parser, 'ISO-8859-1')
// ...
.marshal().ghl7('ISO-8859-1')
//
HL7 v2 Message Validation
HL7 v2 messages can be validated in routes with the validate().ghl7() extension. If you don't want to use a default validation context you can provide one via the staticProfile() extension. Custom validation contexts can be created as described in HL7 v2 Message Validation. Here's an example:
// Create or configure a custom HL7 validation context.
DefaultValidationContext context = ...
// route 1
// ...
.unmarshal().ghl7()
.validate().ghl7() // HL7 message validation with default validation context
// ...
// route 2
// ...
.unmarshal().ghl7()
.validate().ghl7().staticProfile(context) // HL7 message validation with custom validation context
// ...
The DSL Extensions for HL7 v2 Message Validation relies on org.openehealth.ipf.modules.hl7dsl.MessageAdapter message bodies. These are created via unmarshal().ghl7 from input streams or strings. If you want to create a message ValidationContext from an org.apache.camel.Exchange you can use the profile() DSL extension which defines an org.apache.camel.Expression or a Groovy closure as parameter.
org.apache.camel.Expression contextExpression = ...
// route 1
// ...
.unmarshal().ghl7()
.validate().ghl7().profile(contextExpression) // Validation context created by an expression object
// ...
// route 2
// ...
.unmarshal().ghl7()
.validate().ghl7().profile {exchange -> // Validation context created by a closure
// obtain or create validation
// context from message exchange
// and return it
}
// ...
| Backwards compatibility Earlier IPF versions allowed to set static profiles with the profile() extension. This is still possible but it is recommended to use staticProfile() instead. However for profile expressions you should use profile(). |
Camel DSL Extensions for HL7 v2 Messaging Example
Often, for processing HL7 messages you might first want to unmarshal a MessageAdapter from an InputStream, validate the message using either a default or custom validation context and then use the HL7 DSL in processors, transmogrifiers or content based routers. Then the processing result is marshalled again for being transported to another endpoint.
from("direct:input1") // create a message adapter from an HL7 string .unmarshal().ghl7() // validate the message using a default validation context .validate().ghl7() // transmogrifiers are passed in-message bodies // and message headers by default. .transmogrify { msg, headers -> // set the MSH[5] field to whatever is contained in the foo message header // (using the HAPI DSL) msg.MSH[5] = headers.foo msg } .choice() // when-closures are passed messages exchanges by default. Here the // routing decisions are made based in the MSH[5] field value of the HL7 message // (using the HL7 v2 DSL) .when { it.in.body.MSH[5].value == 'blah' } .marshal().ghl7() // adapter -> string .to('mock:output1') .when { it.in.body.MSH[5].value == 'blub' } .marshal().ghl7() // adapter -> string .to('mock:output2')
IHE support
| Early access Please note that the following documentation and the underlying code for IHE support is early access and might be subject to change. We also didn't test IHE support in OSGi environments yet. |
Concepts
The IPF provides support for several IHE profiles. The basic idea is to offer a Camel component for each IHE transaction. These components ensure that the technical requirements of the profile are met by applications built on top of the IPF IHE support.
Quick reference
All IPF IHE components are named according to the profile and transaction that they implement. Each profile corresponds to a Maven artifact in the group org.openehealth.ipf.platform-camel, whereby each artifact contains a set of components, as listed below:
Artifact platform-camel-xds
| IHE Transaction ID | Description | IPF IHE component |
|---|---|---|
| ITI-14 | XDS.a Register Document Set | xds-iti14 |
| ITI-15 | XDS.a Provide & Register Document Set | xds-iti15 |
| ITI-16 | XDS.a Query Registry | xds-iti16 |
| ITI-17 | XDS.a Retrieve Document | xds-iti17 |
| ITI-18 | XDS.a+b Registry Stored Query | xds-iti18 |
| ITI-41 | XDS.b Provide & Register Document Set | xds-iti41 |
| ITI-42 | XDS.b Register Document Set | xds-iti42 |
| ITI-43 | XDS.b Retrieve Document Set | xds-iti43 |
Artifact platform-camel-pixpdq
| IHE Transaction ID | Description | IPF IHE component |
|---|---|---|
| ITI-8 | Patient Identity Feed (PIX Feed) | pix-iti8 xds-iti8 |
| ITI-9 | PIX Query | pix-iti9 |
| ITI-10 | PIX Update Notification | pix-iti10 |
| ITI-21 | Patient Demographics Query (PDQ) | pdq-iti21 |
| ITI-22 | Patient Demographics and Visit Query (PDQ) | pdq-iti22 |
Artifact platform-camel-pixpdqv3 (IPF 2.1 only)
| IHE Transaction ID | Description | IPF IHE component |
|---|---|---|
| ITI-44 | Patient Identity Feed (PIX Feed) v3 | pixv3-iti44 xds-iti44 |
| ITI-45 | PIX Query v3 | pixv3-iti45 |
| ITI-46 | PIX Update Notification v3 | pixv3-iti46 |
| ITI-47 | Patient Demographics Query (PDQ) v3 | pdqv3-iti47 |
Moreover, there are packages which serve for translation of request and response messages of selected ITI transactions between HL7 v3 and HL7 v2:
XDS.b
The IHE XDS.b profile mandates the use of the web service technology. The IPF provides these web services in conformance with the specification, together with their WSDL documents. The XDS.b actors (Registry, Repository, Source and Consumer) are not directly implemented by the IPF. Instead, the transactions required by the actors are offered via components that create endpoints for both the client and server sides. An application assembles these components to build the actors:

Note that registries are also required to support the PIX feed transaction, which is covered in the corresponding section.
XDS standard conformance
The IPF implementation of the XDS profiles conforms to Version 6.0 (August 2009) of the IHE infrastructure technical framework with the addition of the final texts of the change proposals up to CP 426.
Because the XDS components do not represent a full implementation of the XDS actors, they cannot fulfill all parts of the specification. E.g. they cannot check whether a patient ID is known to the registry. Such checks need to be performed within the route implementation. The following list should give a brief overview of the parts of the requirements that have been met by the IPF implementation:
- protocol specific requirements: most notably those listed in Appendix V of the ITI Technical Framework Volume 2x (Web Services for IHE Transactions). This includes WS-Addressing, SOAP 1.2/1.1 and mustUnderstand attributes in SOAP headers.
- meta class model requirements: as listed in chapter 4 of the ITI Technical Framework Volume 3 (Cross-Transaction Specifications). This also includes transformations between ebXML classes and meta model classes where applicable.
- stored query classes: as an extension to the meta model, simplified classes have been defined for each stored query type specified in section 3.18 of the ITI Technical Framework Volume 2a (Registry Stored Query).
XDS.b configuration
To use of the IPF XDS.b components in an application, you have to add the dependencies of the profile component to your pom.xml:
<dependency> <groupId>org.openehealth.ipf.platform-camel</groupId> <artifactId>platform-camel-ihe-xds</artifactId> <version>${ipf-version}</version> </dependency>
The web service implementation within the IPF component is based on Apache CXF. The necessary dependencies are transitively included via the above dependency. CXF itself can be used in various ways. For the purpose of this documentation we will use Spring configuration via imports and deploy the components into Apache Tomcat. The following snippet is an example for an application context that imports all relevant CXF and IPF beans:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:camel="http://camel.apache.org/schema/spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd "> <!-- Importing the CXF configuration --> <import resource="classpath:META-INF/cxf/cxf.xml"/> <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml"/> <import resource="classpath:META-INF/cxf/cxf-servlet.xml"/> <import resource="classpath:META-INF/cxf/cxf-extension-addr.xml"/> <camel:camelContext id="camelContext"> <camel:jmxAgent id="agent" disabled="true"/> <camel:routeBuilder ref="routeBuilder"/> </camel:camelContext> <bean id="routeBuilder" depends-on="routeModelExtender" class="org.openehealth.ipf.platform.camel.ihe.xds.iti18.GroovyRouteBuilder"> </bean> <bean id="coreModelExtension" class="org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension"> </bean> <bean id="xdsModelExtension" class="org.openehealth.ipf.platform.camel.ihe.xds.core.extend.XDSModelExtension"> </bean> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> <ref bean="coreModelExtension" /> <ref bean="xdsModelExtension" /> </list> </property> </bean> </beans>
Deployment within Tomcat is done by defining a web.xml that references the above application context as well as instantiating a CXF-related servlet. A simple setup could look like this:
<?xml version="1.0"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <display-name>Test IPF IHE Web-App</display-name> <context-param> <!-- configures the classpath of the Spring application context --> <param-name>contextConfigLocation</param-name> <param-value>classpath:path/to/your/context.xml</param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <servlet> <servlet-name>CXFServlet</servlet-name> <servlet-class> org.apache.cxf.transport.servlet.CXFServlet </servlet-class> </servlet> <servlet-mapping> <!-- configures the address of the servlet path under which our web services are published --> <servlet-name>CXFServlet</servlet-name> <url-pattern>/services/*</url-pattern> </servlet-mapping> </web-app>
Exposing an XDS.b service
Similar to other Camel components, the XDS.b components are used within your routes:
from('xds-iti18:myIti18Service')
...
This statement creates a web service that implements the ITI-18 transaction and publishes it under the service name myIti18Service. The full path of the service URL is made up by the web container and the servlet configuration. With the above configuration, the service will be published under http://HOSTNAME:PORT/WEBAPP/services/myIti18Service, where HOSTNAME is the name of the machine that the service is running on, PORT represents the TPC/IP port configured within the web container (e.g. in Tomcat you can configure this in server.xml, default value is 8080), and WEBAPP is a name that depends on your web application deployment (in Tomcat this is simply the name of the directory of the web application). More information about how to setup a simple web application with Apache Tomcat can be found here.
Secure transport can be used with the XDS.b services by configuring your container. There is no additional configuration required for IPF XDS components. Details on how to configure Apache Tomcat with SSL support can be found here.
Making calls to an XDS.b service
You access an XDS.b service within a route similar to other Camel endpoints:
...
.to('xds-iti18://localhost:8080/myWebApp/services/myIti18Service')
The whole endpoint URI resembles the service URL as it was described in the previous section. The only difference is that the protocol name is changed from http to xds-iti18.
Per default, all requests will be sent using SOAP 1.2, as prescribed by the specification. To switch to SOAP 1.1, URL parameter soap11=true can be used. Note that because of a bug in CXF, XDS.b consumer components will produce SOAP 1.2 responses independently from the actual SOAP version used in requests. This will be corrected as soon as the new CXF version will be available for the IPF. (Note 2010-02-10: parameter "soap11" is not supported any more)
Secure transport via HTTPS can be configured by adding the URL parameter secure=true. The following sample shows a call to a registry over HTTPS:
...
.to('xds-iti18://localhost:8080/myWebApp/services/myIti18Service?secure=true')
In addition, the HTTP client used by CXF must be configured as well. This is done within the application context according to the CXF documentation:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:camel="http://camel.apache.org/schema/spring" xmlns:http="http://cxf.apache.org/transports/http/configuration" xmlns:sec="http://cxf.apache.org/configuration/security" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd http://cxf.apache.org/transports/http/configuration http://cxf.apache.org/schemas/configuration/http-conf.xsd http://cxf.apache.org/configuration/security http://cxf.apache.org/schemas/configuration/security.xsd "> <import resource="classpath:META-INF/cxf/cxf.xml" /> <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" /> <import resource="classpath:META-INF/cxf/cxf-servlet.xml" /> <http:conduit name="*.http-conduit"> <http:tlsClientParameters disableCNCheck="true"> <sec:keyManagers keyPassword="changeit"> <sec:keyStore type="JKS" password="changeit" file="keystore" /> </sec:keyManagers> <sec:trustManagers> <sec:keyStore type="JKS" password="changeit" file="keystore" /> </sec:trustManagers> <sec:cipherSuitesFilter> <!-- these filters ensure that a ciphersuite with export-suitable or null encryption is used, but exclude anonymous Diffie-Hellman key change as this is vulnerable to man-in-the-middle attacks --> <sec:include>.*_EXPORT_.*</sec:include> <sec:include>.*_EXPORT1024_.*</sec:include> <sec:include>.*_WITH_DES_.*</sec:include> <sec:include>.*_WITH_NULL_.*</sec:include> <sec:exclude>.*_DH_anon_.*</sec:exclude> </sec:cipherSuitesFilter> </http:tlsClientParameters> </http:conduit> ...
Message types
Messages sent between XDS.b actors use a payload defined by ebXML 3.0. The profile specifies how messages are created to represent requests and responses as well as document entries, submission sets and folders. Besides the support of raw ebXML 3.0 messages, the IPF offers a model to abstract from the underlying details of transforming and validating ebXML 3.0 messages.
Raw ebXML 3.0 support
The body of input and output messages depends on the transaction:
| Transaction | Input message body | Output message body |
|---|---|---|
| ITI-18 | org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.query.AdhocQueryRequest | org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.query.AdhocQueryResponse |
| ITI-41 | org.openehealth.ipf.commons.ihe.xds.core.ebxml.ebxml30.ProvideAndRegisterDocumentSetRequestType | org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.rs.RegistryResponseType |
| ITI-42 | org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.lcm.SubmitObjectsRequest | org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.rs.RegistryResponseType |
| ITI-43 | org.openehealth.ipf.commons.ihe.xds.core.ebxml.ebxml30.RetrieveDocumentSetRequestType | org.openehealth.ipf.commons.ihe.xds.core.ebxml.ebxml30.RetrieveDocumentSetResponseType |
The IPF also provides the necessary ebRS 3.0 classes that are referenced within these types. All ebRS 3.0 related classes can be found in the sub-packages of org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.
Simplified model classes
ebXML 3.0 message bodies can be converted to a simplified model. The following table shows the types that can be used for each transaction:
| Transaction | Input message body | Output message body |
|---|---|---|
| ITI-18 | org.openehealth.ipf.commons.ihe.xds.core.requests.QueryRegistry | org.openehealth.ipf.commons.ihe.xds.core.responses.QueryResponse |
| ITI-41 | org.openehealth.ipf.commons.ihe.xds.core.requests.ProvideAndRegisterDocumentSet | org.openehealth.ipf.commons.ihe.xds.core.responses.Response |
| ITI-42 | org.openehealth.ipf.commons.ihe.xds.core.requests.RegisterDocumentSet | org.openehealth.ipf.commons.ihe.xds.core.responses.Response |
| ITI-43 | org.openehealth.ipf.commons.ihe.xds.core.requests.RetrieveDocumentSet | org.openehealth.ipf.commons.ihe.xds.core.responses.RetrievedDocumentSet |
These model classes can be used via type converters, as shown in the following Groovy route:
import static org.openehealth.ipf.commons.ihe.xds.core.responses.Status.* import org.apache.camel.spring.SpringRouteBuilder import org.openehealth.ipf.commons.ihe.xds.core.requests.QueryRegistry import org.openehealth.ipf.commons.ihe.xds.core.requests.query.FindDocumentsQuery import org.openehealth.ipf.commons.ihe.xds.core.responses.QueryResponse import org.openehealth.ipf.commons.ihe.xds.core.metadata.ObjectReference import org.openehealth.ipf.platform.camel.core.util.Exchanges public class RouteBuilder extends SpringRouteBuilder { @Override public void configure() throws Exception { from('xds-iti18:myIti18Service') .convertBodyTo(QueryRegistry.class) .choice() // Return an object reference for a find documents query .when { it.in.body.query instanceof FindDocumentsQuery } .transform { def response = new QueryResponse(SUCCESS) response.references.add(new ObjectReference('document01')) response } // Any other query else is a failure .otherwise() .transform { new QueryResponse(FAILURE) } } }
This route only accepts the stored query FindDocuments. If it receives such a query, it returns a single reference to a document (document01). Any other query returns a failure response. Of course this route does not fulfill the functional requirements of the ITI-18 transaction (e.g. all stored query types must be supported by a registry). However, it shows how a registry can be implemented without directly using the ebXML 3.0 classes.
| convertBodyTo Note that convertBodyTo(QueryRegistry.class) is not strictly required. If it is not used, the conversion must be done when accessing the body. In this case the when statement needs to use it.in.getBody(QueryRegistry.class).query. This will leave the body unchanged. However, keep in mind that converting the ebXML object every time you need to access the body can be time consuming. |
Large document content
The transactions ITI-41 and ITI-43 have to send document content as part of their request or response messages. In practice such messages can become quite large. To allow for memory-efficient streaming of the document content, the aforementioned components rely on CXF's support for binary data. CXF streams the content on disk and then provides a DataHandler to access the file. Therefore, it is not necessary to use IPF's large binary support (LBS).
Validation
The XDS.b components provide functionality for transaction-specific validation of ebXML 3.0 messages. This validation uses the validate DSL extension of the IPF. The following code snippet shows an example of a validation in
from('xds-iti18:myIti18Service')
.validate().iti18Request()
... // Process the message and create a response.
.validate().iti18Response()
The corresponding DSL extensions for validation need to be included into the model extender. Make sure that the following beans are defined within your application context:
... <bean id="coreModelExtension" class="org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension" /> <bean id="xdsModelExtension" class="org.openehealth.ipf.platform.camel.ihe.xds.core.extend.XDSModelExtension" /> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> <ref bean="coreModelExtension" /> <ref bean="xdsModelExtension" /> <!-- ... other model extension bean if necessary ... --> </list> </property> </bean> ...
XDS.a
Support for XDS.a is similar to that of XDS.b. Most transactions presume web service-based endpoints; only ITI-17 directly uses HTTP for the download of documents. With the exception of ITI-18, which is the same in both XDS.a and XDS.b, XDS.a transaction numbers differ from those in XDS.b and use SOAP 1.1 and SwA (SOAP with Attachments) instead of SOAP 1.2 and MTOM respectively. The following image gives a quick overview of the actors and transactions involved in the XDS.a profile:

Note that the Registry actor has to support the PIX feed transaction, which is covered in the corresponding section.
Configuring, exposing and accessing of the XDS.a components are to be performed in the same way as in the case of XDS.b. As already mentioned above, the only significant difference is that the ITI-17 transaction is not implemented as a SOAP-based web service. Therefore, it requires a few additional steps that are mentioned in the next sections.
XDS standard conformance
The IPF implementation of the XDS profiles conforms to Version 6.0 (August 2009) of the IHE infrastructure technical framework with the addition of the final texts of the change proposals up to CP 426.
ITI-17 configuration
To offer the ITI-17 transaction for a repository implementation you have to add an additional servlet to your web.xml:
<?xml version="1.0"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <display-name>Test IPF IHE Web-App</display-name> <context-param> <!-- configures the classpath of the Spring application context --> <param-name>contextConfigLocation</param-name> <param-value>classpath:org/openehealth/ipf/platform/camel/ihe/xdsb/example/context.xml</param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <servlet> <!-- Servlet used for all CXF web services --> <servlet-name>CXFServlet</servlet-name> <servlet-class> org.apache.cxf.transport.servlet.CXFServlet </servlet-class> </servlet> <servlet> <!-- Servlet used only for ITI-17 --> <servlet-name>Iti17Servlet</servlet-name> <servlet-class> org.openehealth.ipf.platform.camel.ihe.xds.iti17.servlet.Iti17Servlet </servlet-class> </servlet> <servlet-mapping> <!-- configures the address of the servlet path under which our web services are published --> <servlet-name>CXFServlet</servlet-name> <url-pattern>/services/*</url-pattern> </servlet-mapping> <servlet-mapping> <!-- configures the address of the servlet path under which the ITI-17 transaction is published --> <servlet-name>Iti17Servlet</servlet-name> <url-pattern>/iti17/*</url-pattern> </servlet-mapping> </web-app>
Exposing the ITI-17 transaction
The ITI-17 transaction is exposed via
from('xds-iti17:myIti17Service')
...
The URL that this transaction is accessible under is determined by the ITI-17 servlet configuration in your web.xml. For the above sample web.xml, the URL results in http://HOSTNAME:PORT/WEBAPP/iti17/myIti17Service. Note that the servlet path (iti17) is different to that of other transactions (services) that use the CXFServlet.
Making calls to the ITI-17 transaction
Calling the ITI-17 transaction from a document consumer can be achieved via:
...
.to('xds-iti17://localhost:8080/myWebApp/iti17/myIti17Service')
Again, note the difference in the URL. The ITI-17 servlet is accessed through a different path than the CXFServlet.
Message types
Messages sent between XDS.a actors use ebXML 2.1 (ebXML 3.0 for ITI-18) as payload format. The profile specifies how messages are created to represent requests and responses as well as document entries, submission sets and folders. Besides the support for raw ebXML messages, the IPF offers a model to abstract from the underlying details of transforming and validating ebXML messages.
Raw ebXML 2.1/3.0 support
The body of input and output messages depend on the transaction:
| Transaction | Input message body | Output message body |
|---|---|---|
| ITI-14 | org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs21.rs.SubmitObjectsRequest | org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs21.rs.RegistryResponse |
| ITI-15 | org.openehealth.ipf.commons.ihe.xds.core.ebxml.ebxml21.ProvideAndRegisterDocumentSetRequestType | org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs21.rs.RegistryResponse |
| ITI-16 | org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs21.query.AdhocQueryRequest | org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs21.rs.RegistryResponse |
| ITI-17 | String (document URL part) | InputStream |
| ITI-18 | org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.query.AdhocQueryRequest | org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30.query.AdhocQueryResponse |
The IPF also provides the necessary ebRS 2.1 and 3.0 classes that are referenced within these types. All ebRS-related classes can be found in the sub-packages of org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs30 and org.openehealth.ipf.commons.ihe.xds.core.stub.ebrs21.
The input of the ITI-17 transaction is the part of the document URL that is appended to the URL of the service. The exact structure depends on the implementation of the Repository actor. It can be a path, a query, or a combination of both:
| Document URL | ITI-17 input message body |
|---|---|
http://HOSTNAME:PORT/WEBAPP/iti17/myIti17Service/my/path/to/document |
/my/path/to/document |
http://HOSTNAME:PORT/WEBAPP/iti17/myIti17Service?docId=123 |
?docId=123 |
http://HOSTNAME:PORT/WEBAPP/iti17/myIti17Service/my/path?docId=321 |
/my/path?docId=321 |
Simplified model classes
ebXML message bodies can be converted to a simplified model. The following table shows the types that can be used for each transaction:
| Transaction | Message input body type | Message output body type |
|---|---|---|
| ITI-14 | org.openehealth.ipf.commons.ihe.xds.core.requests.RegisterDocumentSet | org.openehealth.ipf.commons.ihe.xds.core.responses.Response |
| ITI-15 | org.openehealth.ipf.commons.ihe.xds.core.requests.ProvideAndRegisterDocumentSet | org.openehealth.ipf.commons.ihe.xds.core.responses.Response |
| ITI-16 | org.openehealth.ipf.commons.ihe.xds.core.requests.QueryRegistry | org.openehealth.ipf.commons.ihe.xds.core.responses.QueryResponse |
| ITI-18 | org.openehealth.ipf.commons.ihe.xds.core.requests.QueryRegistry | org.openehealth.ipf.commons.ihe.xds.core.responses.QueryResponse |
For an example of how to use these classes, refer to the corresponding XDS.b section.
Large document content
XDS.a uses SOAP with Attachments (SwA) instead of MTOM, which is used in XDS.b. The implementation of SwA within CXF has to be checked for efficient support for large documents. This is currently an open issue. However, it only applies to the ITI-15 transaction that performs the "upload" of documents. The ITI-17 transaction is a simple HTTP GET download that results in an InputStream contained in the exchange. This allows efficient streaming of the document.
Validation
Apart from ITI-17, XDS.a messages are validated similar to XDS.b messages. For ITI-17 there is no validation support.
PIX + PDQ
The IPF provides components for PIX and PDQ transactions that use HL7 v.2 as data format and MLLP as transport protocol. The following picture shows the relevant actors and transactions:

In fact, these components represent extensions of the standard camel-mina component. Exactly as for XDS.a+b, the application's pom.xml must state the corresponding dependency:
<dependency> <groupId>org.openehealth.ipf.platform-camel</groupId> <artifactId>platform-camel-ihe-pixpdq</artifactId> <version>${ipf-version}</version> </dependency>
This will automatically (transitively) involve, in particular, camel-mina, camel-hl7, HAPI and IPF HL7 processing support.
Starting from version 2.1-m2, the IPF provides client-side and server-side support for HL7 interactive message continuation, unsolicited message fragmentation as well as segment fragmentation.
Making and accepting calls via PIX/PDQ endpoints
The URL format is the same for consumers and producers:
from('pix-iti8://0.0.0.0:8888?param1=value1¶m2=value2')
...
...
.to('pix-iti8://hostname.org:9090?param3=value3¶m4=value4')
Only parameters are optional, all other URL parts are obligatory.
URL Parameters
There are two groups of URL parameters in the PIX/PDQ components: the ones inherited from the basis Camel components and the additional ones.
Parameters inherited from camel-mina and camel-hl7
Some parameters defined in camel-mina have obtained constant values in the PIX/PDQ components. It means that these parameters are actually not configurable by the user any more; values provided via endpoint URLs are silently ignored. These parameters are the following:
| Parameter name | Type | Constant value in PIX/PDQ components |
|---|---|---|
| sync | boolean | true |
| lazySessionCreation | boolean | true |
| transferExchange | boolean | false |
| encoding | String | corresponds to the charset name configured for the HL7 codec factory, as described below |
Moreover, camel-hl7 defines a parameter named codec, which contains the name of a Spring bean that corresponds to an HL7 codec factory. PIX/PDQ components set "#hl7codec" as a default value for this parameter. The user still has to define the corresponding bean, though:
<bean id="hl7codec" class="org.apache.camel.component.hl7.HL7MLLPCodec"> <property name="charset" value="iso-8859-1"/> </bean>
The character set name set up for the HL7 codec factory will be automatically
- propagated to the Camel component (see parameter encoding in the table above),
- stored in the Exchange.CHARSET_NAME property of each Camel exchange, and
- used in all data transformation activities.
SSL-related URL parameters
Security aspects of MLLP-based transactions are controlled by the following URL parameters:
| Parameter name | Type | Default value | Short description |
|---|---|---|---|
| secure | boolean | false | whether SSL should be supported by the given endpoint |
| mutualTLS | boolean | false | whether client authentication for mutual TLS is required on the given endpoint |
| sslContext | String | - | Spring bean name of a user-defined SSL context, if any, optionally with leading '#'. If not set, a default SSL contents will be used |
| sslProtocols | String | system defaults | comma-separated list of SSL protocols which should be supported by the given endpoint |
| sslCiphers | String | system defaults | comma-separated list of SSL cipher suites which should be supported by the given endpoint |
URL parameters for HL7v2 continuation and fragmentation
HL7v2 continuation and fragmentation features can be configured by means of the following URL parameters:
| Parameter name | Type | Default value | Short description |
|---|---|---|---|
| supportInteractiveContinuation | boolean | false | whether interactive message continuation should be supported by the given endpoint |
| interactiveContinuationDefaultThreshold | int | -1 | default threshold (maximal count of data records per message) for interactive message continuation. Relevant on consumer side only and will be used only if RCP-2-2 of the request does not equal to 'I' or RCP-2-1 does not contain a parseable value. Values smaller than 1 lead to no continuation |
| interactiveContinuationStorage | String | - | Spring bean name of a storage for interactive continuation chains. If not set, a default in-memory storage will be used |
| supportUnsolicitedFragmentation | boolean | false | whether unsolicited message fragmentation should be supported by the given endpoint |
| unsolicitedFragmentationThreshold | int | -1 | threshold (maximal count of segments per message) for unsolicited message fragmentation. Relevant on producer side only. Values smaller than 3 lead to no fragmentation. |
| supportSegmentFragmentation | boolean | false | whether segment fragmentation should be supported by the given endpoint |
| segmentFragmentationThreshold | int | -1 | threshold (maximal count of characters per segment) for segment fragmentation. Values smaller than 5 lead to no segment fragmentation. |
Other URL parameters
| Parameter name | Type | Default value | Short description |
|---|---|---|---|
| audit | boolean | true | whether ATNA audit should be performed on the corresponding endpoint |
| allowIncompleteAudit | boolean | false | whether incomplete ATNA audit resords are allowed to be sent to the audot repository as well |
| interceptorBeans | String | - | user-defined MLLP interceptors — a comma-separated list of Spring bean names representing instances of org.openehealth.ipf.platform.camel.ihe.mllp.core.intercept.MllpCustomInterceptor |
Handling of data types and exceptions
Incoming messages (i.e. requests on the consumer side and responses on the producer side) are automatically unmarshalled into IPF MessageAdapter's.
Outgoing messages on the producer side (i.e. requests) are expected to belong to one of the following types in order to be able to be successfully marshalled and sent:
- IPF MessageAdapter
- "raw" HAPI message
- String
- byte[]
- NIO ByteBuffer
- InputStream
- File
- org.apache.camel.component.file.GenericFile<File>
For outgoing messages on the consumer side (i.e. responses), the same set of data types as for the producer side requests is supported. An addition, the message body can contain an Exception instance, which will be transformed into a NAK response. Any exceptions thrown in the route will lead to NAK responses as well.
When neither the data type of the response message is supported nor an exception has been thrown in the route, the message header org.openehealth.ipf.platform.camel.ihe.mllp.core.MllpComponent.ACK_TYPE_CODE_HEADER will be taken into consideration. When the value of this header belongs to the enumeration type org.openehealth.ipf.modules.hl7.AckTypeCode, an acknowledgement will be automatically generated and sent back to the requestor — a positive one for AckTypeCode.AA, a negative one (NAK) for AckTypeCode.AE and AckTypeCode.AR.
When even this header is not set or when its value is not of desired type, the route fails.
Validation
Same as for XDS.a+b messages, the IPF provides a validation mechanism for PIX/PDQ requests and responses. To use it, corresponding DSL extensions should be activated in the application's Spring descriptor as shown below:
... <bean id="mllpModelExtension" class="org.openehealth.ipf.platform.camel.ihe.mllp.core.extend.MllpModelExtension"/> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> <ref bean="coreModelExtension" /> <ref bean="mllpModelExtension" /> </list> </property> </bean> ...
After that, the validation can be performed using the validate DSL extension, e.g. for PIX Feed (ITI-8):
from('xds-iti8://0.0.0.0:9999?audit=false')
.onException(ValidationException.class)
.maximumRedeliveries(0)
.end()
.validate().iti8Request()
.process {
// generate a response
Exchanges.resultMessage(it).body = ...
}
.validate().iti8Response()
Continuation and fragmentation support
Implemented features correspond to Sections 2.10.2.1 (Segment fragmentation/continuation using the ADD segment), 2.10.2.2 (Segment fragmentation/continuation using the DSC segment), and 5.6.3 (Interactive continuation of response messages) of the HL7 v2.5 specification, and are being referred to as "segment fragmentation", "unsolicited (message) fragmentation" and "interactive (message) continuation", respectively.
Segment fragmentation
Segment fragmentation is supported on both producer and consumer sides, for both incoming and outgoing messages. This support can be activated by setting the URL parameter supportSegmentFragmentation of the corresponding endpoint to true. For outgoing messages, the additional parameter segmentFragmentationThreshold should be set to an integer value greater on equal to 5 — it denotes the maximal allowed length of segments in outgoing messages, without consideration of segment separators '\r'. For example, when this threshold equals to 10, the message
MSH|^~\\&|MESA_PD_CONSUMER|MESA_DEPARTMENT|MESA_PD_SUPPLIER|PIM|20081031112704||QBP^Q22|324406609|P|2.5|||ER QPD|IHE PDQ Query|1402274727|@PID.3.1^12345678~@PID.3.2.1^BLABLA~'@PID.3.4.2^1.2.3.4~@PID.3.4.3^KRYSO||||| RCP|I|10^RD|||||
will be sent as
MSH|^~\&|M ADD|ESA_PD ADD|_CONSU ADD|MER|ME ADD|SA_DEP ADD|ARTMEN ADD|T|MESA ADD|_PD_SU ADD|PPLIER ADD||PIM|2 ADD|008103 ADD|111270 ADD|4||QBP ADD|^Q22|3 ADD|244066 ADD|09|P|2 ADD|.5|||E ADD|R QPD|IHE PD ADD|Q Quer ADD|y|1402 .....
Note that segment fragmentation across messages (described in Section 2.10.2.3 of the HL7 v2.5 specification) is not supported yet.
Unsolicited message fragmentation
This feature can be activated by setting the URL parameter of the corresponding endpoint supportUnsolicitedFragmentation to true.
On producer side, this will lead to automatical fragmentation of outgoing request messages based on the value of the URL parameter unsolicitedFragmentationThreshold, which denotes maximal count of segments allowed in an outgoing message. This value should be greater than 2 in order to "let in" at least one segment in addition to MSH and DSC. The producer will perform all necessary actions of the corresponding message exchange pattern to transport these fragments to the receiver, as depicted on the following interaction diagram (click to enlarge):
An IPF PIX/PDQ consumer with enabled segment fragmentation support is able to automatically collect pieces of fragmented requests and put them into the route as a single cumulative request message (click to enlarge):
Segment MSH of the resulting request message is the one of the very first fragment, therefore the corresponding response will contain its control ID (MSH-10) in its MSA-2.
On both producer and consumer sides, all unsolicited fragments or fragment requests (messages with filled MSH-14 and DSC-1, respectively), which cannot be attributed to any active conversation, will be unchanged passed through to the route. Unsolicited fragmentation is not possible, when the request message to be fragmented does already contain non-empty values in the fields MSH-14 and/or DSC-1.
Interactive continuation
Interactive continuation is like unsolicited fragmentation, but relates to response messages instead of request ones and uses data records count instead of segments count as the message splitting criterion (HL7 specification declares some other counts — e.g. those of lines, characters, or pages — to be usable as splitting criteria as well, but they are not supported by the IPF). What a "data record" does actually mean thereby, is transaction-dependent — for example, in PDQ (ITI-21), each data record corresponds to a QUERY_RESPONSE group which consists of segments PID, PD1, NK1, and QRI (data record definitions for ITI-21 and ITI-22 are available in the IPF out-of-the-box).
Support for interactive continuation can be enabled for an endpoint by setting its URL parameter supportInteractiveContinuation to true.
In this case, a consumer will apply transaction-specific rules to split the messages into fragments, using threshold value from the field RCP-2-1, provided that RCP-2-2 is equal to "RD". If the mentioned threshold field is not filled in the expected way, the value of the URL parameter interactiveContinuationDefaultThreshold will be used. When this parameter is not configured as well or its value is less than 1, no message splitting will be performed.
Interaction steps performed by the consumer are shown on the diagram below (click to enlarge):
![]()
Each fragment can be requested more than once, in arbitrary order. Fragments are stored internally, whereby the user has the possibility to provide a custom storage bean via the interactiveContinuationStorage URL parameter of the consumer endpoint. This bean must implement the interface org.openehealth.ipf.platform.camel.ihe.mllp.core.ContinuationStorage, refer to javadocs for details. Per default, an in-memory storage will be used.
Obsolete fragment chains can be removed from the storage either by means of a corresponding cancel message QCN^J01 sent by the client (as usual, such messages are automatically served by the IPF), by calling the storage's method purge() (for example, from a timer-based Camel endpoint), or by some proprietary mechanisms of the storage, if available.
When an IPF PIX/PDQ producer with enabled interactive continuation support recognizes, that the response message it just received is actually the first fragment of an interactive chain, it automatically adds its segment DSC to the initial request message and sends the latter again, as prescribed by the HL7 specification, requesting the next fragment in that way. This step will be repeated for all subsequent fragments until the last fragment of the chain has arrived. After that, the producer joins all collected data records together (using the same transaction-specific rules as the consumer used) and delivers the cumulative response to the caller (i.e. to the Camel route containing the corresponding .to(...) statement). The diagram below shows these interaction steps (click to enlarge):
Same as in case of unsolicited message fragmentation, all unexpected fragments will be passed through to the route without changes. This rule applies to cancel messages which relate to non-existent interactive chains (represented by their query tags) as well.
Combination of fragmentation/continuation features
Segment fragmentation can be harmlessly combined with both unsolicited message fragmentation and interactive message continuation. Note that in case of outgoing request messages, unsolicited message fragmentation is performed before segment fragmentation, therefore the resulting count of segments can be actually greater than the value of the unsolicitedFragmentationThreshold parameter.
On the other hand, simultaneous activation of unsolicited message fragmentation and interactive message continuation can be problematic, because each of them makes use of the same fields in the segment DSC. For example, when a request message was sent using unsolicited fragmentation, and the response represents an interactive continuation fragment, it will be impossible to send the request for the second response fragment using unsolicited fragmentation, because DSC-1 will contain a non-empty value.
PIXv3 + PDQv3
Starting from version 2.1, IPF provides basic support for IHE transactions with numbers 44-47. They are in fact counterparts for PIX/PDQ transactions discussed in the previous section, but use HL7 v3 instead of v2 as data format and Web Services instead of MLLP as transport protocol.

Standard conformance
Normative description for PIXv3/PDQv3 transaction can be found in the corresponding IHE Supplement for Trial Implementation.
Note that ATNA logging is not defined there yet, therefore the IPF does not log any interaction events related to PIXv3/PDQv3.
Configuration
pom.xml must contain the following dependency:
<dependency> <groupId>org.openehealth.ipf.platform-camel</groupId> <artifactId>platform-camel-ihe-pixpdqv3</artifactId> <version>${ipf-version}</version> </dependency>
PIXv3/PDQv3 transactions use the same Web Service machinery as XDS.b transactions, therefore they must be configured in the same way. Sections "Exposing a service" and "Call a service" apply respectively, too.
There are two variants of ITI-44 endpoints — one for the PIX Identifier Cross-Reference Manager (Section 3.44.4.1.3.1 of the aforementioned IHE Supplement) and another one for the XDS.b Document Registry (Section 3.44.4.1.4.1). This leads to the existence of two pretty similar components in the IPF, namely pixv3-iti44 and xds-iti44, which differ in naming conventions and namespaces in WSDL documents.
Because of unsupported ATNA auditing, the only available URL parameter is secure.
Message types
Currently, PIXv3/PDQv3 components do not use any special data model and expect both request and response to be String representations of XML payload. Web Service metadata (WSDL documens) reflects this peculiarity by declaring all message parts to be xsd:anyType.
IPF's DSL extensions for XML processing can be efficiently used to handle and prepare request and response messages.
Validation
Validation of PIXv3/PDQv3 request and response messages is currently XSD-based. Same as for other transactions, there are corresponding DSL extensions pre-defined, which can be activated for any given IPF application using the following Spring configuration:
... <bean id="pixpdqv3ModelExtension" class="org.openehealth.ipf.platform.camel.ihe.pixpdqv3.extend.PixPdqV3ModelExtension"/> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> <ref bean="pixpdqv3ModelExtension" /> <!-- references to other extension beans --> </list> </property> </bean> ...
After that, requests and responses can be validated as outlined in the example route below:
from("pixv3-iti44:iti44service") .onException(Exception.class) .maximumRedeliveries(0) .end() .validate().iti44Request() .unmarshal().gnode() .process { // prepare response as String // <MCCI_IN000002UV01 xmlns="urn:hl7-org:v3" ...> // <id root="PID-3-4-2" extension="PID-3-1" /> // ... // </MCCI_IN000002UV01> } .validate().iti44Response()
Translation between HL7 v2 and HL7 v3 message models
| Preliminary contents Informations provided in this chapter relate to an early-access functionality, which may be changed out of all recognition in the near future. |
IPF 2.1 contains utilities for translation between HL7 v2 and HL7 v3, thus giving the possibility to implement HL7 v3 transactions on top ot their HL7 v2 counterparts and to avoid redundancy in that way. Currently supported transaction pairs are PIX Feed (ITI-8/ITI-44), PIX Query (ITI-9/ITI-45), PIX Update Notification (ITI-10/ITI-46), and PDQ (ITI-21/ITI-47).
Configuration and Use
This section describes configuration steps the user has to perform in order to use the HL7 translators, and provides instructions how to perform the translation. The next section contains an example of a bridge route.
Step 1
First of all, the corresponding Maven artefact must be referenced as a dependency in the application's pom.xml, as described above. (Translation utilities per se belong to the artefact commons-ihe-pixpdqv3, which will be loaded transitively from platform-camel-ihe-pixpdqv3.)
Step 2
For translation of PIX Feed and PDQ messages, the IPF Mapping Service must be activated and configured to use the mappings provided by the IPF (these mappings can be accessed as classpath resources). Here is a snippet of Spring-based configuration:
<bean id="mappingService" class="org.openehealth.ipf.commons.map.BidiMappingService"> <property name="mappingScripts"> <list> <!-- for PIX Feed message translation --> <value>classpath:META-INF/map/pixfeed-translation.map</value> <!-- for PDQ message translation --> <value>classpath:META-INF/map/pdq-translation.map</value> <!-- other mappings --> <value>...</value> </list> </property> </bean> <bean id="hapiModelExtension" class="org.openehealth.ipf.modules.hl7.extend.HapiModelExtension"> <property name="mappingService" ref="mappingService" /> </bean> <bean id="mappingModelExtension" class="org.openehealth.ipf.commons.map.extend.MappingExtension"> <property name="mappingService" ref="mappingService" /> </bean> <bean id="pixpdqv3ModelExtension" class="org.openehealth.ipf.platform.camel.ihe.pixpdqv3.extend.PixPdqV3ModelExtension" /> <bean id="coreModelExtension" class="org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension" /> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> <ref bean="coreModelExtension" /> <ref bean="hapiModelExtension" /> <ref bean="mappingModelExtension" /> <ref bean="pixpdqv3ModelExtension" /> <ref bean="..." /> </list> </property> </bean>
Step 3
The third configuration step is to instantiate translator beans.
Package org.openehealth.ipf.commons.ihe.pixpdqv3.translation contains the following set of classes:
- PixFeedRequest3to2Translator — translates PIX Feed requests from v3 to v2.
- PixFeedAck2to3Translator — translates PIX Feed acknowledgements (responses) from v2 to v3.
- PixQueryRequest3to2Translator — translates PIX Query requests from v3 to v2.
- PixQueryResponse2to3Translator — translates PIX Query responses from v2 to v3.
- PixUpdateNotification2to3Translator — translates PIX Update Notifications from v2 to v3.
- PixUpdateNotificationAck3to2Translator — translates PIX Update Notification acknowledgements from v3 to v2.
- PdqRequest3to2Translator — translates Patient Demographic Queries from v3 to v2.
- PdqResponse2to3Translator — translates Patient Demographic Responses from v2 to v3.
Each translator has a set of configurable properties. Their descriptions can be currently taken from javadoc of the corresponding classes. Note also that there are reasonable default values, therefore the explicite configuration (see <property> items in the example below) can be harmlessly omitted in many cases.
An example for PIX Feed:
<bean name="pixFeedRequestTranslator" class="org.openehealth.ipf.commons.ihe.pixpdqv3.translation.PixFeedRequest3to2Translator"> <property name="useSenderDeviceName" value="true" /> <property name="useReceiverDeviceName" value="true" /> <property name="copyEmailAs" value="PID-13-1" /> <property name="copyAccountNumberAs" value="PID-18" /> <property name="accountNumberRoot" value="1.2.3" /> <property name="copyNationalIdentifierAs" value="PID-19" /> <property name="nationalIdentifierRoot" value="2.16.840.1.113883.4.1" /> <property name="birthNameCopyTo" value="PID-5" /> <property name="useOtherIds" value="true" /> </bean> <bean name="pixFeedAckTranslator" class="org.openehealth.ipf.commons.ihe.pixpdqv3.translation.PixFeedAck2to3Translator"> <property name="ackCodeFirstCharacter" value="C" /> </bean>
Step 4
Package platform-camel-ihe-pixpdqv3, being the basis for the PIXv3 and PDQv3 transactions' impementation, provides DSL extensions which can be used to embed HL7 translation functionality into a Camel route. Spring configuration performed on Step 2 already contains all necessary statements to activate these extensions — consider the bean pixpdqv3ModelExtension.
Using DSL Extensions
The DSL extension .translateHL7v3toHL7v2(translator) is responsible for the translation of messages from HL7 v3 to HL7 v2. Its parameter translator is the corresponding translator bean, e.g. it could be the bean pixFeedRequestTranslator defined on Step 3. Moreover, this DSL extensions saves the original HL7 v3 request internally, because some parts of it will have to be yielded unmodified into the HL7 v3 response message.
Translation of response messages from HL7 v2 to HL7 v3 is the task of the DSL extension .translateHL7v2toHL7v3(translator). The parameter translator denotes a response translator bean, e.g. pixFeedAckTranslator defined on Step 3.
When the translation of a response message was not preceded by the translation of the corresponding request message and therefore the request could not been saved automagically by means of IPF's internal machinery, the user has to provide the request manually (as a String containing XML document or a MessageAdapter instance depending on the transaction under consideration) in the property org.openehealth.ipf.platform.camel.ihe.pixpdqv3.extend.HL7V3_ORIGINAL_REQUEST_PROPERTY of the Camel exchange.
Example
Here is a sample Camel route that impements bridges PIX Feed v3 requests (ITI-44) to an HL7 v2-based Patient Identifier Cross-Reference Manager (ITI-8), and does the same in reverse direction for acknowledgements.
class BridgeRouteBuilder extends SpringRouteBuilder { // injected by Spring def pixFeedRequestTranslator def pixFeedAckTranslator def pixManagerUri void configure() throws Exception { from('pixv3-iti44:iti44service') .onException(Exception.class) .maximumRedeliveries(0) // some reasonable exception handling .end() .translateHL7v3toHL7v2(pixFeedRequestTranslator) .to("pix-iti8://${pixManagerUri}") .translateHL7v2toHL7v3(pixFeedAckTranslator) } }
This is, probably, the shortest possible HL7v3 PIX Manager implementation
.
ATNA
ATNA auditing functionality is fully integrated into the corresponding IPF IHE components (excluding PIXv3/PDQv3 ones). The only thing the user has to configure is the URI of the target syslog server, as described in the next section.
Configuring auditors
Each of the currently supported IHE actor types has a corresponding singleton auditor, i.e. the following set is available:
- XDSRegistryAuditor
- XDSRepositoryAuditor
- XDSSourceAuditor
- XDSConsumerAuditor
- PIXManagerAuditor (serves the Patient Demographic Supplier actor as well)
- PIXSourceAuditor
- PIXConsumerAuditor
- PDQConsumerAuditor
Auditors can be configured both individually and all together. Besides the syslog server URI mentioned above (actually the only mandatory parameter), the following optional parameters can be set up (see Section 5.4 of RFC 3881 for details):
- audit source ID
- audit enterprise site ID
| Warning Configuration of auditors is stored in instances of the class org.openhealthtools.ihe.atna.auditor.context.AuditorModuleConfig. These instances contain much more configurable fields than the three described here, but the user is actually not ought to change them. |
Individual configuration
To configure a particular auditor (for example, the XDS Registry-related one), the user can write
import org.openhealthtools.ihe.atna.auditor.context.AuditorModuleConfig; import org.openhealthtools.ihe.atna.auditor.XDSRegistryAuditor; ... AuditorModuleConfig config = new AuditorModuleConfig(); config.setAuditRepositoryHost("my.syslog.server"); config.setAuditRepositoryPort(514); config.setAuditSourceId(...); config.setAuditEnterpriseSiteId(...); XDSRegistryAuditor.getAuditor().setConfig(config);
To perform the same operation using Spring, the following obvious bean definitions can be used:
<bean id="config" class="org.openhealthtools.ihe.atna.auditor.context.AuditorModuleConfig"> <property name="auditRepositoryHost" value="my.syslog.server" /> <property name="auditRepositoryPort" value="514" /> <property name="auditSourceId" value="..." /> <property name="audituditEnterpriseSiteId" value="..." /> </bean> <bean id="registryAuditor" class="org.openhealthtools.ihe.atna.auditor.XDSRegistryAuditor" factory-method="getAuditor"> <property name="config" ref="config" /> </bean>
Group configuration
From a Java application:
import org.openhealthtools.ihe.atna.auditor.context.AuditorModuleContext; ... AuditorModuleContext.getContext().getConfig().setAuditRepositoryHost("my.syslog.server"); AuditorModuleContext.getContext().getConfig().setAuditRepositoryPort(514);
Using Spring descriptor:
<bean id="iheAuditorContext" class="org.openhealthtools.ihe.atna.auditor.context.AuditorModuleContext" factory-method="getContext"> </bean> <bean id="iheAuditorConfig" factory-bean="iheAuditorContext" factory-method="getConfig"> <property name="auditRepositoryHost" value="my.syslog.server" /> <property name="auditRepositoryPort" value="514" /> </bean>
Disabling auditing
In order to disable auditing for a particular service endpoint (i.e. a Camel consumer) or for a particular client invocation (i.e. a Camel producer), the corresponding URL should be extended with parameter audit=false, e.g.
to('xds-iti18://localhost:9091/xds-iti18-service?audit=false')
or
from('xds-iti18:xds-iti18-service?audit=false')
Note that this feature is (currently) not supported for ITI-17.
Writing down incomplete audit records
Under some circumstances (for example, when the request does not contain all required elements) the system is not able to collect all necessary data to construct a well-formed audit record. Per default, no audit is performed in this case, because it would violate the specification.
But — as an exception, e.g. for debug purposes — the user can change this behavior by setting the parameter allowIncompleteAudit in the URL of the corresponding XDS to true:
to('xds-iti18://localhost:9091/xds-iti18-service?allowIncompleteAudit=true')
Of course, this setting will not have any effect when the auditing functionality is generally switched off, i.e. when the parameter audit in the mentioned endpoint URL was set to false, as described in the previous section.
Note that this feature is (currently) not supported for ITI-17.
Configure auditing transport: Reliable and special auditing
Default implementation of ATNA auditing is based on unreliable UDP communication (Syslog protocol), as prescribed by the IHE IT TF, Vol. 2, Section 3.20.6.1. This choice is also explained in the ATNA FAQ.
In order to change this setting, a custom implementation of org.openhealthtools.ihe.atna.auditor.sender.AuditMessageSender must be provided and registered via AuditorModuleContext.getContext().setSender(mySender).
The delivery queue can be customized in a similar way, i.e. by implementing the interface org.openhealthtools.ihe.atna.auditor.queue.AuditMessageQueue and installing the corresponding class instance via AuditorModuleContext.getContext().setQueue(myQueue).
These settings will affect all auditors, because the auditor module context is a singleton.
Routing audit messages to Camel endpoints
This section is an application of the previous one. Instead of sending audit messages to a syslog server they can also be sent to Camel endpoints. For that purpose an CamelEndpointSender must be configured in the application context.
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:camel="http://camel.apache.org/schema/spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd"> <camel:camelContext id="camelContext"> <camel:routeBuilder ref="..."/> </camel:camelContext> <bean id="auditModuleConfig" factory-method="getConfig" factory-bean="auditModuleContext"> <property name="auditRepositoryHost" value="0.0.0.0" /> <property name="auditRepositoryPort" value="0" /> </bean> <bean id="auditModuleContext" factory-method="getContext" class="org.openhealthtools.ihe.atna.auditor.context.AuditorModuleContext"> <property name="sender" ref="camelEndpointSender" /> </bean> <bean id="camelEndpointSender" class="org.openehealth.ipf.platform.camel.ihe.atna.util.CamelEndpointSender"> <!-- autowired if not explicitly configured --> <property name="camelContext" ref="camelContext" /> <!-- endpoint registered in the Camel context --> <property name="endpointUri" value="direct:input" /> </bean> ... </beans>
In this example a CamelEndpointSender is configured to send audit messages to the direct:input endpoint that is registered at the Camel context. The AuditorModuleContext singleton is configured to use the custom audit message sender. Although ignored by the sender, the Open Health Tools require us to set the audit repository host and port. Hence, they can be set to arbitrary values, 0.0.0.0 and 0, respectively, in our example. These values are overwritten by the message sender. The sender derives them from the Camel endpoint URI. For exmple, direct:input yields 0.0.0.0 and -1, http://www.openehealth.org:7000 yields 174.143.206.54 and 7000.
The audit message is sent as in-only exchange to the endpoint where the in-message body contains a string-representation of the audit XML message. Additionally the following headers are defined.
| Header | Description |
|---|---|
| org.openehealth.ipf.platform.camel.ihe.atna.datetime | The date and time when this message was generated. |
| org.openehealth.ipf.platform.camel.ihe.atna.destination.address | The audit repository IP address derived from the configured endpoint URI. |
| org.openehealth.ipf.platform.camel.ihe.atna.destination.port | The audit repository port derived from the configured endpoint URI. |
The CamelEndpointSender is contained in the platform-camel-ihe-atna-util component which needs to be added as dependency in the Maven pom.xml. Adjust the version accordingly depending on the latest IPF release.
<dependencies>
<dependency>
<groupId>org.openehealth.ipf.platform-camel</groupId>
<artifactId>platform-camel-ihe-atna-util</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
</dependencies>
URL Parameters' Summary for XDS and PIXv3/PDQv3 transactions
The following table gives an overview of all URL parameters defined for IPF XDS and PIXv3/PDQv3 endpoints (ITI-14..18, 41..47):
| Name | Type | Default value | Transaction numbers | Side | Short description |
|---|---|---|---|---|---|
| secure | boolean | false | all except 17 | client only | whether Transport Layer Security mechanisms should be applied when sending messages |
| audit | boolean | true | all except 17 and 44-47 | both service and client | whether ATNA auditing should be performed by the corresponding party |
| allowIncompleteAudit | boolean | false | all except 17 and 44-47 | both service and client | whether incomplete audit records should be sent to the ATNA repository as well |
| soap11 | boolean | false | 18 and 41-43 | client only | whether requests should be sent using SOAP 1.1 instead of SOAP 1.2 (not supported any more since 2010-02-10) |
CDA support
| Early access Please note that the following documentation and the underlying code for CDA support is early access and might be subject to change. |
Support for CDA processing in IPF is provided by several IPF components. This is summarized in the following table.
| Component | Description | Documentation |
|---|---|---|
| modules-cda | Provides functionality for creating, parsing, rendering, and validation CDA documents | Generic CDA Support |
| platform-camel-cda | Provides HL7 extensions to the Camel DSL. | DSL Extensions |
Clinical Document Architecture - a brief overview
The HL7 Clinical Document Architecture (CDA) is an XML-based markup standard intended to specify the encoding, structure and semantics of clinical documents for exchange. CDA is part of the HL7 version 3 standard.
By the use of XML, the HL7 v3 standard and coded vocabularies, CDA allows for the exchange of documents that are both machine and human-readable enabling electronic processing for decision support etc whilst being easily retrieved and used by the people who need them.
Support for 'vanilla' CDA
IPF provides support for
- creating CDA document objects from scratch
- rendering a CDA document object into its XML representation
- parsing of existing CDA documents
- extracting individual pieces of information from CDA documents
As in IPF's HL7 module, support really means more than just providing some sort of CDA API for its model and services. CDA documents can be created and analyzed by means of a domain-specific language (DSL) that hides away most of the technical details you usually encounter when dealing with complex XML documents.
| CDA specification Even though some technical details are hidden, the domain-specific details are not (at least for the generic CDA support). Be sure to read the CDA specification and have a printed copy of the CDA R-MIM at hands while working with CDA documents. |
Support for CDA content profiles
In a technical sense, a CDA content profile defines a set of constraints on CDA that define how to use the CDA to communicate clinical documents bound to a certain use case, e.g. clinical summaries. CDA content profiles are also often referred to as CDA Implementation Guides.
The first content profile to be supported is the Continuity of Care Document (CCD) profile, as it serves as a baseline for many other profiles published by standard bodies like HL7, IHE, or HITSP.
Generic CDA support
CDA support is assembled from a variety of sources.
- The underlying CDA object model, parser, and renderer is provided by Open Health Tools (OHT)'s IHE Profiles project. The OHT libraries are redistributed as part of IPF's CDA support.
- Tooling to create and validate CDA documents are provided natively by IPF.
- CDA Parser, Renderer and Validator are adapted to implement the Module Adapters of IPF. This ensures, that they can be used as processors in Camel-based integration routes.
| Use of IPF CDA without Apache Camel IPF's generic CDA support has no dependencies on Apache Camel and can therefore as well be used independently of integration solutions based on Apache Camel. |
The CDA builder used to create CDA documents is strongly based on Groovy's Builders and defines its own kind of domain specific language. Compared to "traditional" APIs, the natural hierarchical syntax makes it very easy to assemble parts of a CDA document or a whole CDA document, while at the same time enforces a significant amount of restrictions defined by the specification.
| Background info: MetaBuilder CDA builder uses and is derived from MetaBuilder, a more sophisticated builder implementation, which allows for a declarative builder implementation. In fact, the CDA Builder itself is defined using MetaBuilder and Groovy meta class programming. |
Configuration
For setting up Maven follow the instructions on the IPF development page. If you want to use the CDA Features standalone in your Groovy projects then you only need to include
<dependency> <groupId>org.openehealth.ipf.modules</groupId> <artifactId>modules-cda</artifactId> <version>${ipf-version}</version> </dependency>
For using the CDA support inside Camel routes you need to include the following dependency:
<dependency> <groupId>org.openehealth.ipf.platform-camel</groupId> <artifactId>platform-camel-cda</artifactId> <version>${ipf-version}</version> </dependency>
where ${ipf-version} must be replaced with the IPF version you want to use.
Like the HL7 DSL, IPF adds a couple of Groovy metaclass extensions on top of the underlying CDA Object model to facilitate accessing CDA documents. You can register these extensions manually:
import org.openehealth.ipf.modules.cda.builder.CDAR2ModelExtension ... ExpandoMetaClass.enableGlobally() new CDAR2ModelExtension().extensions.call()
Usually you would use a Spring ApplicationContext to register the extensions, especially in conjunction with Camel routes:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:camel="http://camel.apache.org/schema/spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd"> <camel:camelContext id="camelContext"> <camel:routeBuilder ref="routeBuilder"/> </<camel:camelContext> ... <bean id="cdaModelExtension" class="org.openehealth.ipf.modules.cda.builder.CDAR2ModelExtension"> </bean> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> ... <ref bean="cdaModelExtension" /> </list> </property> </bean>
Creating generic CDA documents
The basic pattern of how to create a CDA document is
import org.openehealth.modules.cda.builder.CDAR2Builder .... def builder = new CDAR2Builder() def document = builder.build { clinicalDocument { // add Header information // add Body information }
Header and Body information is defined by a sequence of nested elements, which are described in more detail below.
Note that the CDA builder does not assemble the XML document directly. Instead it builds up an internal model of the CDA document; rendering to XML is a seperate step (see section on Parsing and Rendering). This comes with a number of advantages:
- the order of the elements built within an hierarchy level does not matter
- you don't have to care about the type of an element - the builder knows that e.g. a code attribute inside a ClinicalDocument is of HL7 RIM type CE.
- unknown elements or attributes are detected as well as wrong cardinalities etc.
- the builder itself comes with further rules that are covered by neither the CDA XML schema nor the underlying object model, e.g. that codes of type CE must either have a nullFlavor set or at least a code attribute.
- there are shortcut notations for a less verbose syntax, in particular with respect to simple types and enumerated values, e.g. a HL7 RIM PQ type can be instantiated by string consisting of value and unit. Thus, pq(value:86.0, unit:'kg') and pq('86 kg') are equivalent.
Groovy builders
Builders are based on the builder pattern from the GOF design pattern book. It provides a way to build your own DSL and represents a powerful concept in Groovy.
To put it short, Groovy builders allow to build up a hierarchical structure of nodes, while each node may have attributes (i.e. a set of key/value pairs) and content. Obviously, this matches very well with XML documents, thus also for CDA.
IPF's CDA support uses MetaBuilder, that adds an extra layer on top of plain Groovy builders.
| Instantiate CDAR2Builder once! Instantiating org.openehealth.modules.cda.builder.CDAR2Builder instances is very expensive. It can take several seconds of ramp-up time to initialize all builder rules defined for generating CDA documents. |
The general syntax for creating a CDA element is defined recursively. The [brackets] contain optional syntax.
top-element = schema-name([ value ], [ attributes ]) [ nested-elements ]
elements = element [ elements ]
element = name([value], [ attributes ]) [ nested-elements ]
nested-elements = '{' elements '}'
attributes = attribute [, attributes ]
attribute = key-name : value
The syntax elements (printed in italics) is listed below;
| syntax element | description | examples |
|---|---|---|
| schema-name | The schema (class) of the outermost CDA element that is created in the builder statement. schema-name often corresponds closely with the HL7v3 RIM type of the corresponding XML element name. | clinicalDocument, section, ii |
| name | Name of a CDA element. name often corresponds closely with the the corresponding XML element name. | author, id, code |
| key-name | Name of a nested CDA element or attribute. Used when the element or attribute is provided in the value instead of being recursively built | root, code |
| value | A valid Groovy expression that returns a object reference. Can also be a primitive value | 57, '1976', object |
Let's start with how the header information of a CDA document is assembled.
CDA Header
Building the CDA header also explains the different kinds of elements and attributes and how they a created using the CDA builder.
def document = builder.build {
clinicalDocument {
// templateId(root:'2.16.840.1.113883.3.27.1776') // 1
id(root:'2.16.840.1.113883.19.4', extension:'c266') // 2
code( // 3
code:'11488-4',
codeSystem:'2.16.840.1.113883.6.1',
codeSystemName:'LOINC',
displayName:'Consultation note'
)
effectiveTime('20000407') // 4
title('Good Health Clinic Consultation Note') // 5
versionNumber(2) // 6
...
}
}
clinicalDocument is the schema-name, containing a closure to nest other elements. id is the name for the first element - the CDA Builder knows the internal structure of a CDA document and creates an object of the HL7v3 RIM type II. root and extension are the key-name for the corresponding attributes of a II element, and they are assigned the value '2.16.840.1.113883.19.4' and 'c266', respectively.
Let's examine this piece of code line by line
- In general, infrastructure attributes (templateId, classCode, moodCode, typeCode) with static defaults are not required to be set. Generic CDA documents always carry the same templateId, so you can omit this element.
- This adds an id element with a root and extension attribute. Under the hood, the builder checks that the root attribute is present, otherwise an Exception is thrown. Also note that id is mandatory, so omitting would throw an Exception, too.
- Adding a code just works like adding the id. Remember, you never have to care about the class of the corresponding CDA model object.
- Looking at the CDA specification, effectiveTime and title appear in inverted order. As we are not building the XML document directly, we don't have to care, as the rendering process restores the correct ordering of elements. effectiveTime requires a date value (HL7 TS datatype), but the builder allows to use its simple string representation (YYYYMMDD).
- The title is of HL7 ST datatype. Again, you can just pass a string and conversion is done for you.
- The versionNumber is of HL7 INT datatype. Here you can pass an integer value.
The next part of the CDA header definition introduces a couple of other 'shortcuts' available.
...
confidentialityCode('N') // 1
// code:'N',
// codeSystem:'2.16.840.1.113883.5.25')
recordTarget { // 2
patientRole {
id('12345@2.16.840.1.113883.19.5') // 3
// extension='12345'
// root='2.16.840.1.113883.19.5'
patient {
name {
given('Henry')
family('Levin')
suffix('the 7th')
}
administrativeGenderCode('M') // 4
// code='M'
// codeSystem='2.16.840.1.113883.5.1'
birthTime('19320924')
}
providerOrganization {
id('2.16.840.1.113883.19.5') // 5
}
}
}
...
- Codes with more or less fixed code systems have their OID and, if applicable, code system name predefined. In this case, you can specify the code as content and skip the codeSystem and codeSystemName attributes. The rendered CDA XML will nevertheless contain all attributes.
- In this block, we traverse the RIM model from the document act over a Participation (recordTarget) and a Role (patientRole) to the Entity (patient).
- id elements can be created by specifying a 'extension@root' content rather than the attribute map as shown above. Both notations are equivalent.
- This is another example for a predefined code system.
- id elements may contain only a root attribute, but no extension. In this case, you can provide the root attribute as content.
For reference purposes, here's the remainder of a rather complete CDA header definition. Note that most of the elements are optional - check the CDA specification or RMIM diagram. If any mandatory elements are skipped, the CDA Builder will complain with an Exception before you get the clinical document as result.
...
author {
time('2000040714')
assignedAuthor {
id(extension:'KP00017',root:'2.16.840.1.113883.19.5')
assignedPerson {
name {
given('Robert')
family('Dolin')
suffix('MD')
}
}
representedOrganization {
id(root:'2.16.840.1.113883.19.5')
}
}
}
custodian {
assignedCustodian {
representedCustodianOrganization {
id('2.16.840.1.113883.19.5')
name('Good Health Clinic')
}
}
}
legalAuthenticator {
time('20000408')
signatureCode('S')
assignedEntity {
id(extension:'KP00017', root:'2.16.840.1.113883.19.5')
assignedPerson {
name {
given('Robert')
family('Dolin')
suffix('MD')
}
}
representedOrganization {
id('2.16.840.1.113883.19.5')
}
}
}
relatedDocument(typeCode:'APND') { // 1
parentDocument {
id(extension:'a123', root:'2.16.840.1.113883.19.4')
setId(extension:'BB35', root:'2.16.840.1.113883.19.7')
versionNumber(1)
}
}
componentOf {
encompassingEncounter {
id(extension:'KPENC1332', root:'2.16.840.1.113883.19.6')
effectiveTime {
low('20000407')
}
encounterParticipant(typeCode:'CON') {
time('20000407')
assignedEntity {
id(extension:'KP00017',root:'2.16.840.1.113883.19.5')
assignedPerson {
name {
given('Robert')
family('Dolin')
suffix('MD')
}
}
representedOrganization {
id(root:'2.16.840.1.113883.19.5')
}
}
}
location {
healthCareFacility {
code(
code:'GIM',
codeSystem:'2.16.840.1.113883.5.10588',
displayName:'General internal medicine clinic'
)
}
}
}
}
}
}
CDA body
The body of a CDA document is created correspondingly like the header part.
The CDA body can be either an unstructured blob, or can be comprised of structured markup. Every CDA document has one body at most - associated with the ClinicalDocument class through the component relationship -, which can be either non-structured or structured.
nonXMLBody() represents a document body that is in some format other than XML, e.g. a image of PDF document. nonXMLBody.text is used to reference data that is stored externally to the CDA document or to encode the data directly inline.
...
component {
nonXMLBody {
text(
mediaType: 'application/pdf',
representation: 'B64',
'JVBERi0xLjMKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdG...'
)
}
}
...
structuredBody() represents an XML document body that is comprised of one or more document sections. Document sections can nest, can override context propagated from the header, and can contain narrative and CDA entries.
...
component {
structuredBody {
component { // Section 1
section {
...
}
}
component { // Section 2
section {
...
}
}
... // ...
}
}
...
Narrative Block
section.text is used to store a narrative text and is therefore referred to as the CDA Narrative Block.
For structured bodies, it is the document originator's responsibility to properly populate the Narrative Block, regardless of whether information is also conveyed in CDA entries. Vice versa, it is the recipient's responsibility to properly render the narrative block in human readable manner, e.g. using a XSL transformation.
The CDA DSL for the Narrative Block follows exactly chapter 4.3.5 of the CDA specification. See the examples below.
...
section {
title('Some title'}
text('A simple narrative content')
...
}
...
...
section {
title('Some title'}
text {
list {
item('Theodur 200mg BID')
item('Proventil inhaler 2puffs QID PRN')
item('Prednisone 20mg qd')
item('HCTZ 25mg qd')
}
}
...
}
...
...
section {
title('Some title'}
text {
paragraph('Payer information')
table(border: '1', width: '100%') {
thead {
tr {
th('Payer name')
th('Policy type')
th('Covered Party ID')
th('Authorizations')
}
}
tbody {
tr {
td('Good Health Insurance')
td('Extended healthcare / Self')
td('14d4a520-7aae-11db-9fe1-0800200c9a66')
td {
linkHtml(href: 'Colonoscopy.pdf', 'Colonoscopy')
}
}
}
}
}
...
}
...
Structured part
Sections can define participants like author, informant, and subject (i.e. the primary target of the recorded entries). Sections can also have relationships to entries, which contain structured computer-processable components. Each section can contain zero to many entries. There is a number of entry classes:
- act
- encounter
- observation
- observationMedia
- organizer
- procedure
- regionOfInterest
- substanceAdministration
- supply
Entries in return can have participants and relationships to other entries. For details on these entry classes, please refer to the CDA R2 specification.
With CDA Builder you create the structured part following the same patterns as with the CDA Header of Narrative Block. The following example creates a substanceAdministration entry.
...
structuredBody {
component {
section {
title('My title')
text('My narrative text')
code(....)
entry {
substanceAdministration(classCode:'PROC', moodCode:'EVN') {
consumable{
manufacturedProduct {
manufacturedLabeledDrug {
code(
code:'10312003',
codeSystem:'2.16.840.1.113883.6.96' ,
codeSystemName:'SNOMED CT',
displayName:'Prednisone preparation')
}
}
}
}
}
...
}
}
...
}
Sections and their contained participants and entries can get arbitrarily complex. Furthermore, the structure is very generic, i.e. it's possible to express a certain clinical concept in a variety of ways. Unconstrained CDA is therefore not very helpful when it comes to real semantic interoperability. To be useful in the real world, CDA has to be constrained, which can happen on at least two levels:
- using section-level templates
- using entry level templates
The RIM's InfrastructureRoot class contains an attribute, templateId, which is available for use in CDA. Thus, CDA provides a mechanism to reference a template or implementation guide that has been assigned a unique template identifier.
CDA templates are usually collected in CDA content profiles, one of which is the CCD (Continuity of Care Document).
CDA builder tips
Including complete parts
With CDA Builder it's possible to construct a complete document in a single builder statement. As CDA documents are built from several very well separated parts, it might as well make sense to create such a part for itself and include it into the final document in a separate step. This way, a document part can also be reused for several CDA documents.
CDA builder supports this strategy out of the box. The cardinality of such a part within its containing element plays an important role.
Single cardinality
In this example, we create a element of type CE and include it into a code element inside a Supply clinical statement. Alternatively, we can also build the code inline:
...
def myCode = builder.build {
ce(code:'30549001',
codeSystem:'2.16.840.1.113883.6.96',
displayName:'Suture removal')
}
// Assign code as attribute
def myEntry = builder.build {
entry {
supply(classCode:'SPLY', moodCode:'EVN', code:myCode) {
statusCode(code:'completed')
effectiveTime(value:'200004071430')
}
}
}
// Alternative: build myCode inline
myEntry = builder.build {
entry {
supply(classCode:'SPLY', moodCode:'EVN') {
code(code:'30549001',
codeSystem:'2.16.840.1.113883.6.96',
displayName:'Suture removal')
statusCode(code:'completed')
effectiveTime(value:'200004071430')
}
}
}
...
The difference is that myCode is assigned as an attribute like classCode or moodCode is, while when being built inline it is nested under the supply element. Also note that for pre-constructing myCode you need to know the schema name of its HL7v3 type (ce in this case), while when being built inline the CDA Builder knows to instantiate a CE object for a Supply code.
Multiple cardinality
In this case, the complete part must be added to its container collection instead of just being assigned to it. Unfortunately, this is currently not possible within a CDA builder statement.
...
def mySection = builder.build {
section {
title('My section')
}
}
mySection.entry.add(myEntry)
...
Variable-typed values
In some cases, the CDA specification defines attributes to be of either any type or any subtype of a certain type. Most importantly, the value of an observation clinical statement is variable-typed. As another example, the effectiveTime of a substanceAdministration is of type GTS, which can be a range of different timing sub-types.
In these cases, obviously, the CDA builder can not infer the exact type from a attribute name, just because it's variable. Therefore, you have to give a hint, as shown in the following fictive example:
def myObservationEntry = builder.build {
entry {
observation {
id('9d3d416d-45ab-4da1-912f-4583e0632000')
....
value(
make {
snomedCode(code:'40275004', displayName:'Contact dermatitis') {
translation(
code: '692.9',
codeSystem: '2.16.840.1.113883.6.2',
codeSystemName: 'ICD9CM',
displayName: 'Contact Dermatitis, NOS')
}
}
)
value(
make { _int(10) } // int is a reserved Groovy keyword,
// so we have to use _int
)
...
}
}
}
This observation has two values: a (SNOMED) code and an integer. You simply wrap the type into a make element. In fact, make is more or less an abbreviation of builder.build, i.e. you create an object of the desired type and assign it to the variable-typed attribute.
Using regular Groovy code inside CDA builder
As with the HL7 DSL of IPF, the Groovy Builder is an internal DSL, i.e. it is expressed by means of the Groovy programming language and can be executed without an additional parser. Therefore, it is also possible to mix and match CDA builder code with regular Groovy code.
The following example shows how to derive a Medication section from a tabular data structure, using loops, conditiional statements, and debugging output.
...
// Some medication data, stored in a list of maps
def data = [
[medication:'Albuterol inhalant',
instructions:'2 puffs QID',
startDate:null,
period:'6 h',
routeCode:'IPINHL',
dose:'2',
administrationUnitCode:'415215001',
medicationCode:'307782',
id:'cdbd33f0-6cde-11db-9fe1-0800200c9a66'],
[medication:'Prednisone',
instructions:'20mg PO daily',
startDate:'20000328',
period:'24 h',
routeCode:'PO',
dose:'1',
medicationCode:'312615',
id:'cdbd5b03-6cde-11db-9fe1-0800200c9a66']
]
...
POCDMT000040Section section = builder.build {
section {
templateId('2.16.840.1.113883.10.20.1.8')
code('10160-0@2.16.840.1.113883.6.1')
title('Medications')
text {
table(border:'1',width:'100%') {
thead {
tr {
th('Medication')
th('Instructions')
th('Start date')
}
}
tbody {
// Iterate over all medications. Must assign a iteration variable!
data.each { m ->
tr {
td(m.medication)
td(m.instructions)
td(m.startDate ?: '') // Avoid 'null' output
}
}
}
}
}
// Iterate over all medications. Must assign a iteration variable!
data.each { m ->
// Insert diagnostic output...
println "Creating medication " + m.medication
entry {
substanceAdministration(classCode:'SBADM', moodCode:'EVN'){
id(m.id)
// Conditional element. Skip if not available
if (m.startDate) {
effectiveTime(make {
pivlts { period(m.period) }
})
}
routeCode(code:m.routeCode, codeSystem:'2.16.840.1.113883.5.112')
doseQuantity(m.dose)
consumable {
manufacturedProduct {
manufacturedLabeledDrug {
code(code:m.medicationCode,
codeSystem:'2.16.840.1.113883.6.96') {
originalText(m.medication)
}
}
}
}
}
}
}
}
}
Assert.assertNotNull(section)
// We have two sections ...
Assert.assertEquals 2, section.entry.size()
// with individual IDs ...
Assert.assertEquals 'cdbd33f0-6cde-11db-9fe1-0800200c9a66',
section.entry[0].substanceAdministration.id[0].root
Assert.assertEquals 'cdbd5b03-6cde-11db-9fe1-0800200c9a66',
section.entry[1].substanceAdministration.id[0].root
// ... the first medication has no start date
Assert.assertEquals 0, section.entry[0].substanceAdministration.effectiveTime.size()
Assert.assertEquals 1, section.entry[1].substanceAdministration.effectiveTime.size()
Parsing and Rendering of CDA documents
Parsing
You have two options for parsing CDA documents
- Use an instance of org.openehealth.ipf.modules.cda.CDAR2Parser to parse a document into its internal CDA object model
- As CDA documents are plain XML, use a native Groovy XML parser (e.g. XMLSlurper) to parse it into a hierarchy of generic Node objects.
| Aspect | CDAR2Parser | XMLSlurper |
|---|---|---|
| processing speed | slow | fast |
| direct access using Groovy GPath syntax | yes | yes |
| usage of CDA model 'shortcuts' | yes | no |
| depthfirst/breadtfirst traversal in element tree | no | yes |
This is an example on how to parse a CDA document from the file system using the CDAR2Parser
import org.openehealth.ipf.modules.cda.CDAR2Parser InputStream is = getClass().getResourceAsStream("/SampleCDADocument.xml") def clinicalDocument = new CDAR2Parser().parse(is)
Parsing using Groovy's XMLSlurper is equivalent:
InputStream is = getClass().getResourceAsStream("/SampleCDADocument.xml") def clinicalDocument = new XMLSlurper().parse(is)
The next section explains for both cases, how you can access and extract data from the parsed document.
Extracting information from CDA documents
Extracting data from the parsed document differs a bit depending on whether CDAR2Parser or Groovy's XMLSlurper has been used for parsing. In the latter case, please also take a look at the corresponding Groovy documentation.
Whether to use CDAR2Parser or XMLSlurper
|
Depending on the parsing strategy, there are subtle differences on how to access the data:
| Aspect | CDAR2Parser | XMLSlurper |
|---|---|---|
| usage of CDA model 'shortcuts' | yes | no |
| depthfirst/breadtfirst traversal in element tree | no | yes |
| accessing attributes | element.attribute | element.@attribute |
| accessing attribute/element content | depends on data type | xxx.text() |
The following code snippets shows how to select and extract data from the sample CDA document contained in the CDA specification.
Parsed with CDAR2Parser
InputStream is = getClass().getResourceAsStream(
"/SampleCDADocument.xml");
def clinicalDocument = new CDAR2Parser().parse(is);
assertNotNull(clinicalDocument);
def components = clinicalDocument.component.structuredBody.component
// Simple navigation
assertEquals('en-US', clinicalDocument.languageCode.code)
assertEquals('KP00017', clinicalDocument.author[0].assignedAuthor.id[0].extension)
// Avoid NullPointerException by with safe dereferencing using the ?. operator
assertEquals('KP00017', clinicalDocument?.author[0].assignedAuthor.id[0].extension)
def clinicalDocument2 = null
assertNull(clinicalDocument2?.languageCode?.code)
// Use any(Closure) to check if the predicate is true at least once
assertTrue(components.any { it.section.code.code == '10164-2' })
// Use every(Closure) to check if the predicate is always true
assertTrue(components.every { it.section.code.codeSystem == '2.16.840.1.113883.6.1' })
// Use find(Closure) to return the first value matching the closure condition
assertEquals('History of Present Illness',
components.find { it.section.code.code == '10164-2' }.section.title.text)
// Use findAll to return all values matching the closure condition
assertEquals(1, components.findAll { it.section.code.code == '10164-2' }.size())
// Use findIndexOf to return the index of the first item that matches the
// condition specified in the closure.
assertEquals(1, components.findIndexOf { it.section.code.code == '10153-2' })
// Use collect to iterate through an object transforming each value into a
// new value using the closure as a transformer, returning a list of transformed values.
assertEquals([
'History of Present Illness',
'Past Medical History',
'Medications',
'Allergies and Adverse Reactions',
'Family history',
'Social History',
'Physical Examination',
'Labs',
'In-office Procedures',
'Assessment',
'Plan'],
components.collect { it.section.title.text })
// The spread operator parent*.action is equivalent to
// parent.collect{ child -> child?.action }
assertEquals([
'History of Present Illness',
'Past Medical History',
'Medications',
'Allergies and Adverse Reactions',
'Family history',
'Social History',
'Physical Examination',
'Labs',
'In-office Procedures',
'Assessment',
'Plan'],
components.section.title.text)
}
Parsed with XMLSlurper
InputStream is = getClass().getResourceAsStream(
"/SampleCDADocument.xml");
def clinicalDocument = new XmlSlurper().parse(is)
def components = clinicalDocument.component.structuredBody.component
// Simple navigation
assertEquals('en-US', clinicalDocument.languageCode.@code.text())
assertEquals('KP00017', clinicalDocument.author[0].assignedAuthor.id[0].@extension.text())
// Avoid NullPointerException by with safe dereferencing using the ?. operator
assertEquals('KP00017', clinicalDocument?.author[0].assignedAuthor.id[0].@extension.text())
def clinicalDocument2 = null
assertNull(clinicalDocument2?.languageCode?.@code?.text())
// Use any(Closure) to check if the predicate is true at least once
assertTrue(components.any { it.section.code.@code == '10164-2' })
// Use every(Closure) to check if the predicate is always true
assertTrue(components.every { it.section.code.@codeSystem == '2.16.840.1.113883.6.1' })
// Use find(Closure) to return the first value matching the closure condition
assertEquals('History of Present Illness',
components.find { it.section.code.@code == '10164-2' }.section.title.text())
// Use findAll to return all values matching the closure condition
assertEquals(1, components.findAll { it.section.code.@code == '10164-2' }.size())
// Use findIndexOf to return the index of the first item that matches
// the condition specified in the closure.
assertEquals(1, components.findIndexOf { it.section.code.@code == '10153-2' })
// Use collect to iterate through an object transforming each value into a
// new value using the closure as a transformer, returning a list of transformed values.
assertEquals([
'History of Present Illness',
'Past Medical History',
'Medications',
'Allergies and Adverse Reactions',
'Family history',
'Social History',
'Physical Examination',
'Labs',
'In-office Procedures',
'Assessment',
'Plan'],
components.collect { it.section.title.text() })
// The spread operator parent*.action is equivalent to
// parent.collect{ child -> child?.action }
assertEquals([
'History of Present Illness',
'Past Medical History',
'Medications',
'Allergies and Adverse Reactions',
'Family history',
'Social History',
'Physical Examination',
'Labs',
'In-office Procedures',
'Assessment',
'Plan'],
components.section.title*.text())
// Use depthFirst (or '**') to search for elements anywhere in
// the structure
def drugCodes = clinicalDocument.depthFirst().findAll
{ it.name() == "manufacturedLabeledDrug" }.code*.@code
assertEquals([
'66493003',
'91143003',
'10312003',
'376209006',
'10312003',
'331646005' ],
drugCodes*.text())
// Use of helper functions to encapsulate commonly used GPath expressions
def drugCodes2 = findAllElements(clinicalDocument, "manufacturedLabeledDrug").code*.@code
assertEquals(drugCodes, drugCodes2)
}
private Collection findAllElements(GPathResult result, String name) {
return result.depthFirst().findAll { it.name() == name }
}
Rendering
CDA documents created by either parsing or building their internal object representation can be easily rendered by using an instance of org.openehealth.ipf.modules.cda.CDAR2Renderer.
import org.openehealth.ipf.modules.cda.CDAR2Renderer def document = builder.build { clinicalDocument { ... } } def renderer = new CDAR2Renderer() def opts = [XMLResource.OPTION_DECLARE_XML : true, // include XML declaration XMLResource.OPTION_ENCODING : 'utf-8'] // encode as utf-8 println(renderer.render(document, opts))
The output looks like this:
<?xml version="1.0" encoding="utf-8"?> <ClinicalDocument xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:hl7-org:v3" xsi:schemaLocation="urn:hl7-org:v3 CDA.xsd"> <id extension="c266" root="2.16.840.1.113883.19.4"/> <code code="11488-4" codeSystem="2.16.840.1.113883.5.1" codeSystemName="LOINC" displayName="Consultation note"/> <title>Good Health Clinic Consultation Note</title> <effectiveTime value="20000407"/> ... </ClinicalDocument>
Note that the renderer cared about adding proper namespaces to the XML document.
Validating CDA documents
CDA document instances in their XML representation can be validated using the W3C XML Schema and Schematron validators. The class org.openehealth.ipf.modules.cda.CDAR2Constants provides constants for the location of schema and schematron resources, e.g.
import org.openehealth.ipf.modules.cda.CDAR2Constants import org.openehealth.ipf.commons.xml.XsdValidator ... def validator = new XsdValidator() validator.validate(xmldoc, CDAR2Constants.CDAR2_SCHEMA) ...
CDA Validation is nicely included into IPF's DSL extensions mechanism. For details refer to CDA DSL Extensions
Note that currently only XML-encoded CDA documents can be validated.
However, when creating a CDA document from scratch, the CDA Builder restricts this process by applying rules very close alongside the CDA MIF definition and the derived XML schema, even though you are working on an object structure rather than an XML document. So, there's a good chance that CDA documents created with the CDA Builder also pass the validators.
DSL extensions
This section describes DSL extensions provided by the platform-camel-cda component. The extensions allow to seemlessly integrate CDA parser, renderer, and validator into Camel/IPF integration routes.
CDA DSL extensions are defined in the org.openehealth.ipf.platform.camel.cda.extend.CDAModelExtension.groovy class. Their main purpose is to make CDA processing features available in Camel routes. Extensions provided by this class may well be combined with other extensions that comply with the DSL extension mechanism.
Configuration
For using the CDA DSL extensions you need to include the following dependency:
<dependency> <groupId>org.openehealth.ipf.platform-camel</groupId> <artifactId>platform-camel-cda</artifactId> <version>${ipf-version}</version> </dependency>
This transitively includes the platform-camel-core and modules-cda libraries as well.
CDA (un)marshalling
The cdar2() DSL extension allows you to convert between CDA document strings (or streams) and org.openhealthtools.ihe.common.cdar2.POCDMT000040ClinicalDocument objects. For example, to unmarshal a CDA document from a string (or stream) use
// ...
from('...')
.unmarshal().cdar2()
.to('...')
// ...
in your Groovy route definitions. As mentioned in the Parsing sections, CDA documents are plain XML, and if you do not require a semantic CDA model, you can also use Groovy's XMLSlurper, which reads the document into a hierarchy of Node objects. Please refer to DSL extensions for Groovy XML processing for more detail.



