- 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 architecture
- 1.3.1 Component architecture
- 1.3.1.1 Platform core components
- 1.3.1.2 OSGi infrastructure components
- 1.3.1.3 Platform manager components
- 1.3.1.4 Project archetype components
- 1.3.1.5 Namespace and component descriptions
- 2 Core features
- 2.1 Scripting layer
- 2.1.1 DSL extension mechanism
- 2.1.1.1 Example
- 2.1.1.2 Limitations
- 2.1.2 Predefined DSL extensions
- 2.2 Module adapters
- 2.3 Custom processors
- 2.4 DSL extensions
- 2.4.1 DSL extension for existing Camel features
- 2.4.1.1 Closure support
- 2.4.1.2 Bean lookup
- 2.4.1.3 Error handler
- 2.4.2 DSL extensions for ExpressionClause
- 2.4.2.1 Exception objects and messages
- 2.4.3 DSL extensions for custom IPF processors
- 2.4.3.1 Content enrichment
- 2.4.3.2 Validation process
- 2.4.3.3 Splitter
- 2.4.4 DSL extensions for IPF module adapters
- 2.4.4.1 Transmogrifier
- 2.4.4.1.1 Inclusion options
- 2.4.4.1.2 Transmogrifier input
- 2.4.4.1.3 Transmogrifier output
- 2.4.4.1.4 Transmogrifier implementations
- 2.4.4.2 Validator
- 2.4.4.3 Parser
- 2.4.4.3.1 Unmarshalling via Parser
- 2.4.4.4 Renderer
- 2.4.4.4.1 Marshalling via Renderer
- 2.4.4.5 Predicate
- 2.4.4.6 Aggregator
- 2.4.4.7 Adapter extension summary
- 2.4.4.7.1 Relevant types
- 2.4.4.7.2 Parameters and input
- 2.4.4.7.3 Closure profiles
- 2.4.5 DSL extensions for Groovy XML processing
- 2.4.5.1 XML Unmarshalling with Groovy XmlParser
- 2.4.5.2 XML Unmarshalling with Groovy XmlSlurper
- 2.4.5.3 XML Marshalling with Groovy XmlNodePrinter
- 2.4.5.4 XML transmogrifiers
- 3 HL7 processing
- 3.1 Configuration
- 3.2 HAPI DSL
- 3.2.1 Camel integration
- 3.2.2 Getting started
- 3.2.2.1 Maven setup
- 3.2.2.2 Message construction
- 3.2.2.3 Navigate to structures
- 3.2.2.4 Navigate to fields
- 3.2.2.5 Smart navigation
- 3.2.2.6 Change structures
- 3.2.2.7 Change fields
- 3.2.2.8 Access target objects
- 3.2.2.9 Render messages
- 3.2.3 Understanding repetitions
- 3.2.4 Language reference
- 3.2.4.1 Message elements
- 3.2.4.2 Read access operations
- 3.2.4.2.1 Read access operations on non-repeating message elements
- 3.2.4.2.2 Read access operations on repeating message elements
- 3.2.4.3 Write access operations
- 3.2.4.3.1 Write access operations on repeating message elements
- 3.2.4.3.2 Write access operations on message elements defined as separate variable
- 3.2.4.4 Method and property dispatch
- 3.2.4.5 Access to special objects
- 3.3 HAPI Extensions
- 3.3.1 Integration with HAPI DSL
- 3.3.2 Getting Started
- 3.3.2.1 Maven Setup
- 3.3.3 HL7 Parser and ModelClassFactory
- 3.3.3.1 CustomModelClassFactory
- 3.3.3.2 Custom PipeParser
- 3.3.4 Mapping Service
- 3.3.5 API Extensions
- 3.3.5.1 Strings
- 3.3.5.2 HAPI Message
- 3.3.5.3 HAPI Structures
- 3.3.5.4 HAPI Types
- 3.3.5.5 Collection
- 3.4 HL7 Validation
- 3.4.1 Concept
- 3.4.2 Validation basics
- 3.4.2.1 Creating a ValidationContext
- 3.4.2.2 Add rules to the ValidationContext
- 3.4.2.3 Reuse a ValidationContext
- 3.4.2.4 Validate messages
- 3.4.3 Validation variants
- 3.4.3.1 Validating against primitive type constraints
- 3.4.3.2 Validating against message constraints
- 3.4.3.2.1 HL7 Conformance profiles
- 3.4.3.2.2 HL7 Abstract Syntax specification
- 3.4.3.2.3 Custom validation
- 3.4.3.3 Validating against encoded messages
- 3.4.4 Settings up validation rules using a Spring application context
- 3.5 DSL extensions
- 3.5.1 HL7 adapter (un)marshalling
- 3.5.2 (Un)marshaling options
- 3.5.3 HL7 message validation
- 3.5.4 Example
- 4 IHE support
- 4.1 Concepts
- 4.2 Quick reference
- 4.3 XDS.b
- 4.3.1 XDS standard conformance
- 4.3.2 XDS.b configuration
- 4.3.3 Exposing an XDS.b service
- 4.3.4 Making calls to an XDS.b service
- 4.3.5 Message types
- 4.3.5.1 Raw ebXML 3.0 support
- 4.3.5.2 Simplified model classes
- 4.3.6 Large document content
- 4.3.7 Validation
- 4.4 XDS.a
- 4.4.1 XDS standard conformance
- 4.4.2 ITI-17 configuration
- 4.4.3 Exposing the ITI-17 transaction
- 4.4.4 Making calls to the ITI-17 transaction
- 4.4.5 Message types
- 4.4.5.1 Raw ebXML 2.1/3.0 support
- 4.4.5.2 Simplified model classes
- 4.4.6 Large document content
- 4.4.7 Validation
- 4.5 PIX
- 4.6 PDQ
- 4.7 ATNA
- 4.7.1 Configuring auditors
- 4.7.1.1 Individual configuration
- 4.7.1.2 Group configuration
- 4.7.2 Disabling auditing
- 4.7.3 Writing down incomplete audit records
- 4.7.4 Configure auditing transport: Reliable and special auditing
- 4.8 URL Parameters' Summary
- 5 CDA support
- 5.1 Clinical Document Architecture - a brief overview
- 5.1.1 Generic CDA support
- 5.1.2 Support for CDA content profiles
- 5.1.2.1 Continuity of Care Document (CCD)
- 5.1.2.2 HITSP C32 construct
- 5.2 Generic CDA support
- 5.2.1 Creating generic CDA documents
- 5.2.1.1 Groovy builders
- 5.2.1.2 CDA Header
- 5.2.1.3 CDA body
- 5.2.1.3.1 Narrative Block
- 5.2.1.3.2 Structured part
- 5.2.2 Parsing and Rendering of CDA documents
- 5.2.2.1 Parsing
- 5.2.2.2 Extracting information from CDA documents
- 5.2.2.3 Rendering
- 5.2.3 Validating CDA documents
- 5.3 DSL extensions
- 5.3.1 CDA (un)marshalling
- 5.3.2 (Un)marshaling options
- 5.3.3 CDA document validation
- 5.4 CDA profile support
- 5.5 Complete Examples
- 5.6 CDA builder gotchas
- 5.6.1 Including complete parts
- 5.6.1.1 Single cardinality
- 5.6.1.2 Multiple cardinality
- 5.6.2 Variable-typed values
- 5.7 References
- 6 Flow management
- 6.1 Concept
- 6.2 JMX interface
- 6.2.1 Message content
- 6.2.2 JConsole extension
- 6.3 Configuration
- 6.3.1 Derby database
- 6.3.2 Oracle Database
- 6.4 DSL extensions
- 6.4.1 The initFlow DSL extension
- 6.4.1.1 Parameterization of initFlow
- 6.4.2 The ackFlow DSL extension
- 6.4.2.1 Parameterization of ackFlow
- 6.4.3 The nakFlow DSL extension
- 6.4.3.1 Parameterization of nakFlow
- 6.4.4 The dedupe DSL extension
- 6.5 Splits and multicasts
- 6.5.1 IPF version <= 1.6.0
- 6.5.2 IPF version > 1.6.0
- 7 Platform manager
- 7.1 Connection management
- 7.2 Flow management client
- 7.3 JMX client
- 8 OSGi support
- 8.1 Scope
- 8.2 Technology
- 8.3 Tooling
- 8.4 Downloads
- 8.5 IPF runtime installation and startup
- 8.6 Architecture
- 8.6.1 Bundle categories
- 8.6.2 Bundle overview
- 8.7 Extender bundles
- 8.7.1 Extension definition
- 8.7.2 Extension scope
- 8.7.3 The osgi-extender-basic bundle
- 8.7.4 The osgi-extender-spring fragment
- 8.8 Service bundles
- 8.8.1 The commons-flow bundle
- 8.8.2 The platform-camel-flow bundle
- 8.8.3 The modules-hl7 bundle
- 8.8.4 The osgi-config-jms bundle
- 8.9 Extension bundles
- 8.9.1 The platform-camel-core bundle
- 8.9.2 The platform-camel-flow bundle
- 8.9.3 The platform-camel-hl7 bundle
- 8.9.4 The modules-hl7 bundle
- 8.10 Application configuration
- 8.10.1 Flow management
- 8.10.2 Shared components
- 8.10.3 Route builder
- 9 Event infrastructure
- 9.1 Overview
- 9.1.1 Architecture
- 9.1.2 Terminology
- 9.1.3 Usage summary
- 9.2 Details
- 9.2.1 Event Engine
- 9.2.2 Events
- 9.2.3 Event sources
- 9.2.3.1 Event publishing via the DSL
- 9.2.3.2 Event publishing via API
- 9.2.4 Event handlers and filters
- 9.2.5 Event channels and adapters
- 9.2.5.1 Using Camel routes as channels
- 9.2.6 Modularization
- 10 Large binary support
- 10.1 Concept
- 10.2 DSL extensions
- 10.3 Using the LBS
- 10.3.1 Configure a project to use the LBS
- 10.3.2 Set up a disk store
- 10.3.3 Adding support for the HTTP endpoint
- 10.3.3.1 Storing singlepart uploads
- 10.3.3.2 Storing multipart uploads
- 10.3.3.3 Storing downloads
- 10.3.3.4 Uploading stored binaries
- 10.3.4 Adding support for the CXF endpoint
- 10.3.4.1 Storing binaries from a SOAP request
- 10.3.4.2 Storing binaries from a SOAP response
- 10.3.4.3 Preparing a SOAP request with stored binaries
- 10.3.5 Adding support for the MINA endpoint
- 11 Performance measurement
- 11.1 Performance questions to be answered
- 11.2 Usage of the component
- 11.2.1 Statistical reports generated by the component
- 11.2.2 Configuration
- 11.3 Single node deployment with a performance measurement server
- 11.3.1 Statistical reports generated by the performance measurement server
- 11.3.2 Configuration of the application to use a performance measurement server
- 11.3.3 Deployment and configuration of the performance measurement server
- 11.4 Cluster deployment with a performance measurement server
- 11.5 DSL extensions for performance measurement
- 11.6 HTTP interface of the performance measurement component
- 12 Quality of service
- 12.1 Recoverability
- 12.1.1 Transactional messaging
- 12.1.1.1 Recover from processor failures
- 12.1.1.2 Recover from JVM crashes
- 12.1.1.3 Recover from failed deliveries
- 12.1.1.4 Distributed transaction processing
- 12.1.1.5 Configuration
- 12.1.1.6 Transactional routes
- 12.1.2 Flow management
- 12.1.3 Process management
- 12.2 Availability
- 13 Appendix A - IPF development
- 13.1 Environment
- 13.2 Sources
- 13.2.1 Checkout via command line
- 13.2.2 Checkout via TortoiseSVN
- 13.2.3 Checkout via Subclipse
- 13.2.4 Structure
- 13.3 Binaries
- 13.4 Projects
- 13.4.1 Eclipse import
- 13.4.2 Platform manager
- 13.5 Archetypes
- 13.5.1 Overview
- 13.5.2 Example
- 13.5.2.1 Create project
- 13.5.2.2 Create assembly
- 13.5.2.3 Eclipse import
- 14 Appendix B - IPF tutorials
- 14.1 First steps tutorial
- 14.1.1 Source code
- 14.1.2 Project creation
- 14.1.3 Route definition
- 14.1.4 Route extension
- 14.1.5 Project testing
- 14.1.5.1 Unit test
- 14.1.5.2 Server test
- 14.1.6 Assembly and installation
- 14.1.7 Start server
- 14.1.8 Summary
- 14.2 First details tutorial
- 14.2.1 Source code
- 14.2.2 Project creation
- 14.2.2.1 Route definition
- 14.2.2.2 Extension definition
- 14.2.2.3 Application configuration
- 14.2.2.4 Project descriptor
- 14.2.2.5 Assembly descriptor
- 14.2.3 Project customization
- 14.2.4 Project testing
- 14.2.4.1 Unit test
- 14.2.4.2 Server test
- 14.2.5 Assembly and installation
- 14.2.6 Start server
- 14.3 HL7 processing tutorial
- 14.3.1 Validation
- 14.3.2 Transformation
- 14.3.3 Route design
- 14.3.4 Source code
- 14.3.5 Project creation
- 14.3.6 Extend project descriptor
- 14.3.7 Extend application context
- 14.3.8 Route definition
- 14.3.9 Code mapping
- 14.3.10 Route testing
- 14.3.10.1 Automated test
- 14.3.10.2 Manual test
- 14.3.11 Assembly and installation
- 14.3.12 Start server
- 14.4 OSGi tutorial
- 14.4.1 Application setup
- 14.4.1.1 Install IPF runtime
- 14.4.1.2 Download application bundles
- 14.4.1.3 Checkout sources (optional)
- 14.4.1.4 Install application bundles
- 14.4.1.4.1 Installation via config.ini
- 14.4.1.4.2 Installation via console
- 14.4.1.5 Start application bundles
- 14.4.1.6 Running the sample
- 14.4.1.6.1 Read input message from file
- 14.4.1.6.2 Read input message via HTTP
- 14.4.1.6.3 Flow management
- 14.4.2 Application walkthrough
- 14.4.2.1 The tutorials-osgi-mapping bundle
- 14.4.2.2 The tutorials-osgi-extension bundle
- 14.4.2.3 The tutorials-osgi-service bundle
- 14.4.2.4 The tutorials-osgi-route-web bundle
- 14.4.2.5 The tutorials-osgi-route-file bundle
- 14.5 Tutorial for routing to a webservice via HTTP
- 14.5.1 Source code
- 14.5.2 Create a basic project using the IPF and LBS
- 14.5.3 Create the webservice
- 14.5.4 Add the routing
- 14.6 Reference application
- 15 Appendix C - IPF Guidelines
- 15.1 DSL extensions guide
- 15.1.1 Processor with custom name
- 15.1.2 DSL extensions using a model class
- 15.1.3 Parameterized DSL extensions
- 16 Appendix D - IPF context
- 16.1 IHE
- 16.1.1 Deployment options
- 17 Appendix E - Known Camel Issues
- 17.1 HTTP Component
- 17.2 Stream Caching
- 18 Appendix F - DSL extensions index
- 18.1 DSL Extensions provided by platform-camel-core
- 18.2 DSL Extensions provided by platform-camel-flow
- 18.3 DSL Extensions provided by platform-camel-lbs
- 18.4 DSL Extensions provided by platform-camel-event
- 18.5 DSL Extensions provided by platform-camel-hl7
- 18.6 DSL Extensions provided by platform-camel-cda
- 18.7 DSL Extensions provided by platform-camel-test
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. |
| Platform manager | An Eclipse RCP-based front end to the flow manager. The platform manager also provides a generic JMX client. |
| 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. |
| 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 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 next three subsections give an overview of the IPF components, their dependencies and their namespaces (abbreviated package names). 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.
Platform core components
OSGi infrastructure components
Although all IPF components have been enabled to run on an OSGi platform, this section only shows components from the org.openehealth.ipf.osgi namespace. These are extender bundles and configuration fragments for the IPF service bundles. For details refer to the OSGi support section. |
Platform manager components
Project archetype components
Namespace and component descriptions
| Namespace | Description |
|---|---|
| commons | Namespace for commonly used libraries. There are no dependencies to the platform-camel and platform-servicemix namespaces. |
| modules | Namespace for domain-specific (e.g. HL7) libraries and message processing components. There are no dependencies to the platform-camel and platform-servicemix namespaces. |
| 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. |
| platform-servicemix | Namespace for extensions to the Apache ServiceMix enterprise service bus. This package currently doesn't contain any components. It will focus on extensions to ServiceMix 4 and the OSGi-based ServiceMix 4 Kernel |
| platform-manager | Namespace for the Eclipse RCP-based Integration Platform Manager. It is the graphical front-end to the components commons-flow and platform-camel-flow and is made up of Eclipse plugins (OSGi bundles) where each plugin is a separate Eclipse project (not shown as component in the next table). The server communication is purely JMX-based, therefore, you can alternatively use a generic JMX client for flow management as well. |
| tutorials | Namespace for tutorials demonstrating the features of the Open eHealth IPF. |
| ipf-archetypes | Namespace for IPF project archetypes. |
| 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 and platform-servicemix provide 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, Apache ServiceMix or OpenESB. 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. A flow management front-end is provided by platform-manager. |
| 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 tests. |
| 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-hl7 | 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-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-event | A component offering the DSL extensions of the event infrastructure |
| platform-camel-http | An HTTP component based on the Camel HTTP component. This component is offered due to current issues of the HTTP component in Camel 1.6. |
| 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. |
| ipf-archetype-basic | Archetype for creating integration projects using IPF core features for embedded and single-node deployments. |
| ipf-archetype-default | Archetype for creating integration projects using IPF core, flow management and HL7 processing features for embedded and single-node deployments. Currently work in progress. |
| ipf-archetype-cluster | Archetype for creating integration projects using IPF core, flow management and HL7 processing features for multi-node deployments. Currently work in progress. |
Core features
The content of this page describes the features provided by the platform-camel-core component. These are the scripting layer, the module adapters and the DSL extensions. For an overview how platform-camel-core fits into its architectural context refer to the IPF architecture page.
Scripting layer
The IPF scripting layer not only brings the Groovy programming language to your route definitions it also allows you to define your own DSL extensions. It consists of the DSL extension mechanism and the predefined DSL extensions. All predefined DSL extensions provided by IPF have been built using the DSL extension mechanism. You can get an overview of these extensions in the IPF extensions index. For a guide how to write you own DSL extensions refer to the DSL extensions guide or continue reading this section to see a very simple example. In IPF, all platform-camel-* components contribute their extensions to the scripting layer i.e. the DSL extension mechanism supports modularization (DSL extensions provided by individual components are pluggable).
In addition to predefined DSL extensions applications can easily define their own DSL extensions, for example, to give a project-specific message processor or a message processing pattern a custom name. This is explained in detail in the DSL extensions guide. It makes 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. There is also extensive support for Groovy closures in combination with existing Camel DSL elements and IPF DSL extensions.
DSL extension mechanism
The DSL extension mechanism allows for extending the Apache Camel DSL and the IPF DSL with custom language elements. The definition of extensions is done with Groovy meta-programming. Route definitions that use DSL extensions must therefore be written in Groovy. A Groovy meta-programming-based mechanism was chosen because Camel itself doesn't support DSL extensions on Java-level (at least at the time of writing where Camel 1.6 was out).
Example
| 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 translating 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') } }
Our 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.ProcessorType in our example. Adding the method is done via Groovy meta-programming inside an extensions block.
package example import org.apache.camel.model.ProcessorType class MyExtension { static extensions = { ProcessorType.metaClass.translate = {-> delegate.process(new Translator()) } } }
You can add an extensions block to any class you like. To make the example work we have to wire the DSL extension, the route builder and the infrastructure components of the DSL extension mechanism 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://activemq.apache.org/camel/schema/spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://activemq.apache.org/camel/schema/spring http://activemq.apache.org/camel/schema/spring/camel-spring.xsd"> <camel:camelContext id="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 for demonstration purposes how to combine predefined DSL extensions with custom DSL extensions.
Limitations
DSL extensions defined in Groovy are not available for Camel's Java DSL or for the Spring-based XML configuration. Defining extensions for multiple languages is currently under discussion for Camel 2.x. Also, Java IDE's cannot detect extensions introduced via Groovy meta-programming i.e. code completion for these extensions isn't supported at the moment. We currently investigate whether this is possible with IntelliJ IDEA.
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 |
A description of these components is given on the IPF architecture page. For a detailed description of the extensions follow the links in the extension documentation column. The extension classes can be combined as shown in the application configuration section.
| Summary of all predefined DSL extensions A summary of all predefined DSL extensions provided by IPF is given in the DSL extensions index appendix. |
Module adapters
Module adapters integrate components of the modules namespace into Camel routes. Components defined in this namespace usually 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 or Apache ServiceMix in order to increase their reusability. The integration of these message processors into Camel 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)

Factory methods for module adapters are defined in the org.openehealth.ipf.platform.camel.core.builder.RouteHelper class. The adapters created by these factory methods implement Camel-specific interfaces like org.apache.camel.Processor or org.apache.camel.Predicate, for example. Use the factory methods directly if you want to write your route definitions in Java. If you want to use module adapters in Groovy you might want to have a look at the DSL extensions for IPF module adapters section.
| Module adapter alternatives An alternative to module adapters is Camel's bean integration mechanism. |
Custom processors
Custom processors provided by platform-camel-core are described in the DSL extensions for custom IPF processors section. If you want to use these processors in Java-based route definitions you might want to have a look at the corresponding factory methods in the org.openehealth.ipf.platform.camel.core.builder.RouteHelper class. These factory methods and the processors they create are described in detail in the API docs.
DSL extensions
DSL extension for existing Camel features
Closure support
The following Camel core DSL elements have been extended to accept closures as arguments
- process
- intercept
- filter
- setHeader
- setOutHeader
- setFaultHeader
- setProperty
- setBody
- transform
- when
- onWhen
Closure support for further DSL elements will follow in later IPF releases. These extensions are defined in the org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension.groovy class. For example, to define a processor closure that reverses the input message body you would write:
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. Defining an interceptor closure is similar:
from('direct:input')
.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 interceptor 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.
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.
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.
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
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.fault.body = 'failure'}
.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 strategy.
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')
.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')
Finally, the split rule can be implemented as an org.apache.camel.Expression and referred to as a bean:
from('direct:input')
.split("MySplitBean")
.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 aggregate 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')
.split { Exchange exchange -> exchange.in.body.split(',') }
.aggregate { 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.ParamsType 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 validationfor description of the available Schematron options.
By default, XSLT transformations return a javax.xml.transform.Result object, which is, however, not very usaful 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(String.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.ProcessorType |
| validate | Validator | ValidatorAdapter | org.apache.camel.model.ProcessorType |
| parse | Parser | ParserAdapter | org.apache.camel.model.ProcessorType org.apache.camel.builder.DataFormatClause |
| render | Renderer | RendererAdapter | org.apache.camel.model.ProcessorType 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()
HL7 processing
Support for HL7 message processing in IPF is provided by several IPF components. This is summarized in the following table.
| Component | Description | Documentation |
|---|---|---|
| modules-hl7dsl | Implements a Groovy DSL on top of the HAPI library. | HAPI DSL |
| modules-hl7 | Provides functional extensions to the HAPI library. | HAPI extensions HL7 validation |
| platform-camel-hl7 | Provides HL7 extensions to the Camel DSL. | DSL extensions |
An overview of the components can be found on the IPF architecture page. For a detailed description follow the links in the Documentation column. The components modules-hl7 and modules-hl7dsl can be used independently of Apache Camel in Groovy projects. The platform-camel-hl7 component is Camel-specific and makes the HAPI DSL, HAPI extensions and HL7 validation available in IPF route definitions. The extensions provided by platform-camel-hl7 and modules-hl7 can be activated via the DSL extension mechanism (see next section).
| HL7 features of Camel and IPF Extensions provided by platform-camel-hl7 are complementary to features provided by the camel-hl7 component (available since Camel version 1.5). |
| HL7 tutorial The HL7 message processing tutorial guides you through a simple IPF HL7 application using some features described in this section. |
Configuration
Refer to the IPF Scripting Layer page if you need an introduction how to configure IPF applications that use the DSL extension mechanism. In this section you'll see how to activate the extensions provided by the components platform-camel-hl7 and modules-hl7. The extension classes of these components are listed in the following table.
| Component | Extension class |
|---|---|
| platform-component-camel | org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension |
| modules-hl7 | org.openehealth.ipf.modules.hl7.extend.HapiModelExtension |
Here's an example Spring application context XML file:
<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://activemq.apache.org/camel/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://activemq.apache.org/camel/schema/spring http://activemq.apache.org/camel/schema/spring/camel-spring.xsd"> <camel:camelContext id="camelContext" /> ... <bean id="routeBuilder" depends-on="routeModelExtender" class="..." /> <bean id="hapiModelExtension" class="org.openehealth.ipf.modules.hl7.extend.HapiModelExtension"> <property name="mappingService" ref="..." /> </bean> <bean id="coreModelExtension" class="org.openehealth.ipf.platform.camel.core.extend.CoreModelExtension"> </bean> <bean id="hl7ModelExtension" class="org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension"> </bean> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> <ref bean="hapiModelExtension" /> <ref bean="coreModelExtension" /> <ref bean="hl7ModelExtension" /> </list> </property> </bean> ... </beans>
In this example, we also use the extensions provided by the platform-camel-core component (coreModelExtension bean), otherwise, the following example wouldn't work. For details about the mappingService property of the hapiModelExtension bean refer to the mapping service section on the HAPI extensions page.
HAPI DSL
HAPI DSL (modules-hl7dsl component) is a library that implements a domain-specific language (DSL) for manipulating HL7 version 2 messages. The DSL is based on Groovy and internally makes use of the HAPI library. Using modules-hl7dsl, HL7 message processing in IPF applications becomes almost trivial.
Camel integration
Refer to the DSL extensions section of the HL7 processing page for an example how to use the HAPI DSL within Camel routes. The modules-hl7dsl library may also be used standalone i.e. independent of Apache Camel and other IPF components in Groovy applications.
Getting started
This section explains how to get started with the modules-hl7dsl library. It explains what to include in Maven 2 project descriptors (pom.xml files) and presents some usage examples. For further examples you may also want to look at the modules-hl7dsl unit tests. A more formal description of the HAPI DSL is given in the language reference section.
Maven setup
For setting up Maven follow the instructions on the IPF development page. If you want to use the the HAPI DSL standalone in your Groovy projects then you only need to include
<dependency> <groupId>org.openehealth.ipf.modules</groupId> <artifactId>modules-hl7dsl</artifactId> <version>${ipf-version}</version> </dependency>
in your pom.xml file. For using the HAPI DSL inside Camel routes you additionally need to include the following dependency.
<dependency> <groupId>org.openehealth.ipf.platform-camel</groupId> <artifactId>platform-camel-hl7</artifactId> <version>${ipf-version}</version> </dependency>
where ${ipf-version} must be replaced with the IPF version you want to use.
As of IPF 1.7-m3, the underlying HAPI library has been updated. The HL7 version-dependent classes have to be included separately:
<dependency> <groupId>ca.uhn.hapi</groupId> <artifactId>hapi-structures-v25</artifactId> <version>0.6</version> </dependency> <dependency> <groupId>ca.uhn.hapi</groupId> <artifactId>hapi-structures-v251</artifactId> <version>0.6</version> </dependency>
Despite the extra configuration, it reduces the overall size of your project as the other version's HL7 classes are not included any more.
Message construction
What follows are some examples how to use the modules-hl7dsl component. First we create a message from an HL7 file on the classpath.
import static org.openehealth.ipf.modules.hl7dsl.MessageAdapters.* def message = load('msg-02.hl7')
Alternatively, the adaptor 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 also easily create a message as a copy of an existing message:
def messageCopy = message.copy()
Navigate to structures
In the following example we obtain the PID segment contained in the PATIENT group which is again contained in the first element of the repeatable PATIENT_RESULT group. In the HL7 world it is common to use parentheses i.e. () to refer to an elements in a repetition. The same notation is supported in the HAPI DSL.
def segment = message.PATIENT_RESULT(0).PATIENT.PID
Navigate to fields
Obtaining fields is similar to obtaining structures except that fields are often referred to by number instead of by name. To obtain the MSH-3 field from a message we can write:
def composite = message.MSH[3]
This field is a composite (by specification) and we want to obtain a primitive component e.g. the second one:
def primitive = message.MSH[3][2]
or, equivalently,
def primitive = message.MSH.sendingApplication.universalIDType
As shown above, it's also possible to navigate by specifying the field names instead of the number. 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 we don't know the version of the HL7 message in advance, we 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
Fields may repeat. To obtain a certain element of a repeating field we use parentheses like with structures. In the next example we obtain the first element of the repeating NK1-5 field. Since NK1 is also a repeating segment we have to define which NK1 segment to use.
def field = message.NK1(0)[5](0)
To get a list of elements of a repeating field we omit the index.
def fieldList = message.NK1(0)[5]()
Getting field values
Use the value property to obtain the value of a primitive field, or simply call toString()
String primitiveValue = message.MSH[3][2].value
primitiveValue = message.MSH[3][2].toString()
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()
Smart navigation
Smart navigation is available starting from IPF 1.6
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.
- Repetitions of groups or segments, e.g. associated parties:
def nextOfKin = message.NK1(0)
- Repetitions of fields, e.g. phone number(s) of associated parties:
def phoneNumber = message.NK1(0)[5](0)[1]
- "Hidden" composites, i.e. fields where most often only the first component is used, e.g. composite type FN used for the family name of associated parties:
def familyName = message.NK1(0)[2][1][1]
To make things worse, the internal structure changes between HL7 versions. In higher versions, e.g. primitive fields are 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 this problem by assuming reasonable defaults when repetitions or component operators are omitted:
- If a repetition is omitted, the first repetition of a group, segment or field is assumed
assert message.NK1(0)[5](0)[1].value == message.NK1[5](0)[1].value assert message.NK1(0)[5](0)[1].value == message.NK1[5][1].value assert message.PATIENT_RESULT(0).PATIENT.PID[5][1] == message.PATIENT_RESULT.PATIENT.PID[5][1]
- If a component is omitted, the first component or subcomponent of a composite is assumed
assert message.NK1(0)[5](0)[1].value == message.NK1[5].value assert message.NK1(0)[5](1)[1].value == message.NK1[5](1).value assert message.NK1(0)[2][1][1].value == message.NK1[2].value
- The treatment of HL7 null values also apply to Smart Navigation Expressions:
assertEquals '', PID[11][1][1].value assertEquals '""', PID[11][1][1].originalValue assertEquals '', PID[11].value assertEquals '""', PID[11].originalValue assertTrue PID[11][1][1].isNullValue() assertTrue PID[11].isNullValue()
Using smart navigation, the expressions are usually shorter and less error-prone. Furthermore, in many cases the same expression can be used for different HL7 versions defining backward-compatible structures.
Change structures
Currently you can change segments only, assignment to groups isn't supported yet. For example
msg1.EVN = msg2.EVN
copies the EVN segment from msg2 over to msg1. We can also use the from() method.
msg1.EVN.from(msg2.EVN)
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 the usual Java/Groovy semantics apply.
def mySegment = ... msg1.EVN = mySegment // mySegment copied onto msg1.EVN def targetSegment = msg1.EVN targetSegment = mySegment // msg1.EVN remains unchanged, targetSegment and mySegment reference the same object
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. Lets say msg1.NK1 refers to a repeating segment:
def mySegment = ... msg1.NK1(0) = 'abc' // syntax error msg1.NK1(0) = mySegment // syntax error msg1.NK1(0).from(mySegment) // works
Change fields
To change a field value we navigate to that field (either by name or index, as shown above) and either assign it a string or another field.
def msh = message.MSH def nk1 = message.NK1(0) msh[5] = nk1[4][4] msh[5] = 'abc'
Composite fields may also be changed by assigning other composite fields or using the from() method.
def msg1 = load('msg-01.hl7')
def msg2 = load('msg-02.hl7')
// alternative 1 using assignment
msg1.NK1(0)[4] = msg2.NK1(0)[4]
// alternative 2 using from()
msg1.NK1(0)[4].from(msg2.NK1(0)[4])
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 the usual Java/Groovy semantics apply.
def myComposite = ... def mySegment = msg1.NK1(0) mySegment[4] = myComposite // myComposite copied onto mySegment[4] def targetComposite = mySegment[4] targetComposite = myComposite // mySegment[4] remains unchanged, targetComposite and myComposite reference the same object
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. Lets say msg1.XYZ[1] refers to a repeating field:
def primitive = msg1.MSH[5] msg1.XYZ[1](0) = 'abc' // syntax error msg1.XYZ[1](0) = primitive // syntax error msg1.XYZ[1](0).value = 'abc' // works for primitives msg1.XYZ[1](0).from(primitive) // works for primitives and composites
The same is true for adding repetitions to a field using the nrp() function.
def field = msg1.XYZ.nrp(1) // adds a repetition to a repeatable field msg1.XYZ.nrp(1) = 'abc' // Syntax error msg1.XYZ.nrp(1).value = 'abc' // works for primitives msg1.XYZ.nrp(1).from(primitive) // works for primitives and composites
Access target objects
Model objects of the HAPI DSL layer reference model objects defined in the HAPI ca.uhn.hl7v2.model package. These can be accessed via the target property.
ca.uhn.hl7v2.model.Segment hapiSegment = message.NK1(0).target
However, this is usually not needed because any property access or method call not applicable to HAPI DSL model objects is dispatched to target objects. For example, to invoke the getMaxCardinality(int) method on a target segment, just write
int cardinality = message.NK1(0).getMaxCardinality(3)
This is equivalent to
int cardinality = message.NK1(0).target.getMaxCardinality(3)
The same is true for properties on the target object.
String segmentName = message.NK1(0).name
is equivalent to
String segmentName = message.NK1(0).target.name
Render messages
Messages can be written to stream using the left-shift (<<) operator. To write a message to stdout:
System.out << message
Understanding repetitions
Any access to a repeating group, segment or field returns a groovy.lang.Closure object that maintains a list of repeating elements.
def repeatingGroup = message.PATIENT_RESULT // closure representing a repeating group
Using parentheses i.e. () calls the closure. Using a positive integer i as argument returns the i-th member of the repetition.
def group = repeatingGroup(0) // first element of the repetition (a group)
Using no argument returns all elements of the repetition.
def groups = repeatingGroup() // all elements of the repetition (a group list)
Repetitions of fields can be counted and appended to.
def count = segment.count(i) // returns the number of repetitions of the i-th field of the segment def field = segment.nrp(i) // adds a repetition to the i-th field, returning the new and empty element
As of IPF 1.7.0, if you try access a repetition of a field, segment or group that does not exist yet, it is automatically added to the repeating element, i.e. nrp(index) is implicitly called. This also works in the context of Smart Navigation when you omit the default index (0) and there's no repetition yet.
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
HAPI Extensions
HAPI extensions is a module that adds methods to the original HAPI library in order to access and manipulate HL7 version 2 messages more conveniently. The library is based on Groovy. It can be used directly with HAPI or together with the HAPI DSL.
Integration with HAPI DSL
HAPI DSL adapters forward all unknown method calls to their underlying HAPI model classes, which are extended by this library. Therefore you can easily access these extensions within the HAPI DSL. The reason for splitting the two libraries is while HAPI DSL focuses on providing a 'fluent' HL7 language, the HAPI Extensions module provides access to operations specific to the HL7 domain, e.g. creating an acknowledgment to a given message. Note that the two libraries do not depend on each other and can therefore be used independently, although they complement each other perfectly.
Getting Started
Maven Setup
For setting up Maven follow the instructions on the IPF development page. If you want to use the the HAPI extensions standalone in your Groovy projects, then you only need to include
<dependency> <groupId>org.openehealth.ipf.modules</groupId> <artifactId>modules-hl7</artifactId> <version>${ipf-version}</version> </dependency>
in your pom.xml file. For using the HAPI DSL inside Camel routes you additionally need to include the following dependency:
<dependency> <groupId>org.openehealth.ipf.platform-camel</groupId> <artifactId>platform-camel-hl7</artifactId> <version>${ipf-version}</version> </dependency>
where ${ipf-version} must be replaced with the IPF version you want to use. For instructions how to activate the HAPI extensions inside your IPF application refer to the configuration section on the HL7 processing page.
As of IPF 1.7-m3, the underlying HAPI library has been updated. The HL7 version-dependent classes have to be included separately:
<dependency> <groupId>ca.uhn.hapi</groupId> <artifactId>hapi-structures-v25</artifactId> <version>0.6</version> </dependency> <dependency> <groupId>ca.uhn.hapi</groupId> <artifactId>hapi-structures-v251</artifactId> <version>0.6</version> </dependency>
Despite the extra configuration, it reduces the overall size of your project as the other version's HL7 classes are not included any more.
HL7 Parser and 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, the HAPI parsers can not be configured to use it. 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)
Custom PipeParser
Use the PipeParser implementation provided by this module (org.openehealth.ipf.modules.hl7.parser.PipeParser), which can be configured with a custom model factory
def customParser = new PipeParser(customFactory)
In Camel integration scenarios, use the custom parser instance by adding a parameter. Also refer to the Camel HL7 extension reference
...
from(...)
.unmarshal()
.ghl7(customParser)
...
Mapping Service
The org.openehealth.ipf.modules.hl7.mapping.MappingService interface deals with the requirement that processing HL7 messages often involves mappings 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 modules-hl7 component extends the java.lang.String and some of the HAPI classes to map between values. Please refer to the API Extensions section below for examples.
The HL7 library provides one MappingService implementation (BidiMappingService), which implements
- bidirectional mapping
- mapping of arbitrary objects
- definitions of mappings using external Groovy Scripts
To use BidiMappingService, you have to initialize it with the external Groovy resource, e.g. using Spring:
<!-- Groovy class that provides the operations on the mappings --> <bean id="hl7MappingService" class="org.openehealth.ipf.modules.hl7.mappings.BidiMappingService"> <property name="mappingScript" value="classpath:example.groovy"/> </bean>
The mapping example is displayed below:
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 OIDs for the key and value code systems. The encounterType mapping has three entries, while the vip and messageType mappings have only one.
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 HL7Exception is thrown.
The services also allow mapping in the backward direction, so that mappingService.getKey('vip', 'VIP') == '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. |
As of IPF 1.6, BidiMappingService also can be initialized using a list of mapping files:
<!-- Groovy class that provides the operations on the mappings --> <bean id="hl7MappingService" class="org.openehealth.ipf.modules.hl7.mappings.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.
As of IPF 1.6, BidiMappingService also 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.
API Extensions
Strings
The String class has been extended to facilitate usage of the Mapping Service. As an example, we assume the mappins defined as shown in the example.groovy definition in the Mapping Service subchapter.
String.map() maps the left to the right side. The identifier for the mapping can either be passed as argument:
assert 'E'.map('encounterType') == 'EMER' assert 'X'.map('encounterType') == null assert 'X'.map('encounterType', 'DEFAULT') == 'DEFAULT'
or as part of the method call. The methods are dynamically added based on the registered mapping identifiers in the defined MappingService instance.
assert 'E'.mapEncounterType() == 'EMER' assert 'X'.mapEncounterType() == null assert 'X'.mapEncounterType('DEFAULT') == 'DEFAULT'
If provided by the MappingService implementation, you can also map in the reverse direction:
assert 'EMER'.mapReverse('encounterType') == 'E' assert 'EMER'.mapReverseEncounterType() == 'E'
Code systems are often associated with a globally unique identifier, usually in form of an ISO Object Identifier (OID). The identifier of both sides of a mapping can be obtained as follows:
assert 'encounterType'.keySystem() == '2.16.840.1.113883.12.4' assert 'encounterType'.valueSystem() == '2.16.840.1.113883.5.4'
Finally, you can check whether a certain key or value is contained in the mapping.
assert 'encounterType'.hasKey('E') == true assert 'encounterType'.hasKey('X') == false assert 'encounterType'.hasValue('EMER') == true assert 'encounterType'.hasValue('XXXX') == false
HAPI Message
You can create positive or negative acknowledgments to messages. The acknowledgments are in the same HL7 version as the original message and is populated as specified in the parameters.
def ack = msg.ack()
def nak1 = msg.nak('Reason for failure')
def nak2 = msg.nak(new HL7Exception('reason for failure', 204)
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 (2.5 in the following example):
def nak = ca.uhn.hl7v2.model.Message.defaultNak(e, '2.5')
Generating acknowledgments is, however, only a special case of generating a response to an original message. If the response is defined as dedicated HL7 message, as with responses to Query messages, you have to use the respond(eventType, triggerEvent) extension. The MSH and MSA segments of the result message are populated as required by the HL7 specification.
def rsp = msg.respond('RSP','K21') // generates a RSP_K21 message
You can 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 }
The matches() method can be used e.g. inside closures for filters and routers in HL7 Camel routes like
...
.choice()
.when { it.in.body.matches('ADT','A01','2.5') }
.to("direct:handle_ADTA01")
.otherwise()
.to("direct:unknown_message")
...
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
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 is possible avoid using this in production environments:
println msg.dump()
HAPI Structures
All HAPI Structures (i.e. not only Messages, but also arbitrary Groups and Segments) can be printed by calling the encode() extension. 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
HAPI Types
All HAPI Types (i.e. Primitives, Composites, and Varies) can be printed by calling the encode() extension.
assert message.MSH.messageType.encode() == 'ADT^A01' // Together with the HL7 DSL, we can also write assert message.MSH[9].encode() == 'ADT^A01'
As described above with java.lang.String objects, the mapping extensions can be applied directly on all HAPI types. The encode() extension is called before the mapping is executed.
// 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 DSL, we 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'
Collection
A further common mapping scenario is having a collection of keys that map to a value or collection of values.
As an example, we assume the following mapping, which is registered under the name 'test':
| key | value |
|---|---|
| A~B~C | X~Y~Z |
| D~E~F | A |
To comply with the rules for Groovy maps, we better don't directly use Collection as keys; instead we use the tilde "~" to separate the collection elements in a single String. You can either use lists or tilde-encoded strings on the value side.
Collection.map() behaves exactly like String.map() shown above:
assert ['A','B','C'].map('test') == ['X','Y','Z'] assert ['A','B','C'].map('test')[0] == 'X' assert ['D','E','F'].map('test') == A assert ['X','Y','Z'].mapReverse('test') == ['A','B','C'] assert 'A'.mapReverse('test') == ['D','E','F']
Note that with the API you work with lists on the key and value side of the mapping.
Collections may also contains HAPI types, in this case Type.encode() is called on each list element before the mapping is executed.
HL7 Validation
Concept
The HAPI library already offers support for validating HL7 messages by definition of rules that check against constraints on type level, message level, and encoded message level. However, definition of these rules is rather cumbersome.
The IPF HL7 module adds support for specifying validation rules in a way that is easy to write and easy to understand. It facilitates the definition of custom validation rules by exploiting features of the Groovy language that is already used in other parts of IPF.
Validation basics
Creating a ValidationContext
Validation rules are defined by instantiating an implementation of the ValidationContext interface of HAPI. IPF comes with the org.openehealth.ipf.modules.hl7.validation.DefaultValidationContext class, which primarily offers two benefits:
- It provides access to a Validation Builder that allows for definition of validation rules using a rather simple Domain Specific Language (DSL)
- It supports Validation rules that constrain their target object by evaluating Groovy closures for maximum flexibility.
import org.openehealth.ipf.modules.hl7.validation.DefaultValidationContext ... DefaultValidationContext context = new DefaultValidationContext() ...
Add rules to the ValidationContext
Although it is possible to manually instantiate the Validation rule classes that come with IPF, you should use the Validation builder.
...
context.configure()
.forVersion('2.5') // following rule applies to HL7 v2.5
...
.forVersion('2.2 2.3 2.4') // following rule applies to HL7 v2.2, 2.3, and 2.4
...
.forVersion().asOf('2.3') // following rule applies to HL7 versions starting with 2.3
...
.forVersion().before('2.3') // following rule applies to HL7 versions older than 2.3
...
.forVersion().except('2.4') // following rule applies to HL7 versions but 2.4
...
.forAllVersions() // following rule applies to all HL7 versions
...
Before actually specifying the validation rule, its application is restricted to one or more HL7 versions. The possibilities are shown in the code example above and are self-describing.
Now the rules can be specified by specifying constraints on one or more of
- primitive type level
- message level
- encoding level
...
context.configure()
.forVersion('2.5') // limit to HL7 v2.5 messages
.type('DT') // constraints for the DT type
...
.message('ADT', 'A01') // constraints for ADT_A01 messages
...
.encoding() // constraints for encoded messages
...
Details on how the validation rules are syntactically defined are shown below in detail.
Reuse a ValidationContext
If there is already an instance of DefaultValidationContext, you can simply add further rules to it:
DefaultValidationContext context = new DefaultValidationContext() // Add a number of rules context.configure() .forVersion() ... // Add some more rules context.configure() .forVersion() ...
However, it is also possible to reuse any ValidationContext instance with a DefaultValidationContext.
DefaultValidationContext context = new DefaultValidationContext() // Use the builder to add Groovy-based validation rules context.configure() .forVersion() ... // Add existing ValidationContext instances context .addContext(additionalValidationContext1) .addContext(additionalValidationContext2) ...
Validate messages
There are two possibilities to execute validation rules
- during parsing
- after parsing
To validate during parsing, simply configure your Parser instance with the ValidationContext.
...
def parser = new PipeParser(context)
Message message = parser.parse(msgText)
...
During parsing, all variants of validation rules (i.e. primitive types, message and encoding; see below) are checked.
To validate after parsing, use the HAPI MessageValidator class
import ca.uhn.hl7v2.validation.MessageValidator ... new MessageValidator(context, true).validate(message)
You can achieve the same by using a bridge implementing the org.openehealth.ipf.commons.core.modules.api.Validator interface:
new HL7Validator().validate(message, context)
This is the variant that IPF's HL7 DSL extension also uses internally. Currently, only message rules are checked when using the HL7Validator.
Validation variants
Validating against primitive type constraints
HL7 Primitive types have no substructure, i.e. they directly contain values. The values are usually restricted by one or more of the following contraints:
- length
- value type (e.g. whether it evaluates to a decimal or number)
- regular expression pattern
Regular expressions can also be used to restrict length and type of the value, but regular expressions are often hard to read and understand.
Each HL7v2 version defines a set of primitive types; over the time the number of types have increased and/or the constraints have been modified.
A type constraint for a range of HL7 versions is defined as follows:
ValidationContext context = new DefaultValidationContext() context.configure() .forVersion().asOf('2.3') .type('DT') .matches(/(\d{4}([01]\d(\d{2})?)?)?/) // YYYY[MM[DD]] .withReference('Version 2.5 Section 2.A.21')
This enforces that all instances of type DT type must match a date pattern, where the year as mandatory and the month the day of month is optional. The example uses a regular expression to specify the date pattern. Please also read about regular expressions in Groovy.
The Validation DSL for primitive types supports the following constraints:
| Constraint type | Method | Example | constraints... |
|---|---|---|---|
| maximum length | .type('XXX').maxSize(int) | maxSize(255) | ... the maximum length to 255 characters |
| length range | .type('XXX')[min..max] | [10..20] | ... the length to be between 10 and 20 |
| existence | .type('XXX').notEmpty() | ... that there must be a value of >= 1 character | |
| matches | .type('XXX').matches(regexp) | matches(/(\d{4}([01]\d(\d{2})?)?)?/) | ... the value be formatted as a date |
| number type | .type('XXX').isNumber() | ... the value to be a decimal number | |
| user defined | .type('XXX').checkIf(Closure) | checkIf { it.size() <= 255 } | ... the maximum length to 255 characters |
The Closure syntax is the most flexible way to define constraints on a type. Internally, all other constraint methods are implemented by calling checkIf using a specific closure.
Additionally, primitive type constraints are "misused" in HAPI for trimming space characters from values. The Validation DSL supports this as well:
| Constraint type | Method | Example | function |
|---|---|---|---|
| trim | .type('XXX').omitLeadingWhitespace() | removes leading whitespace characters | |
| trim | .type('XXX').omitTrailingWhitespace() | removes trailing whitespace characters |
You can combine any of constraint methods to define more than one rule for a type:
...
.type('XXX').omitLeadingWhitespace().isNumber().checkIf { it > 50 && it < 100 }
...
HL7v2 defines constraints for each primitive types for each version. IPF comes with predefined validation rules that enforce these constraints. You can access them using
import ca.uhn.hl7v2.validation.ValidationContext import org.openehealth.ipf.modules.hl7.validation.support.DefaultTypeRulesValidationContext ... ValidationContext context = DefaultTypeRulesValidationContext()
| PipeParser The PipeParser class in IPF is preconfigured with the default type rules. |
| When primitive type rules are checked Note that primitive type rules can only be applied by configuring the parser. Validation then is executed whenever a primitive value is set:
|
Validating against message constraints
By default, the HAPI parsers accept about any message as long as follows the HL7 syntax rules. For messages, groups and segments not defined in the respective HL7 standard, generic structures are instantiated internally. This allows parsing custom messages even without prior definition of Z segments or similar custom structures.
However, any piece of software with an HL7 interface needs to validate whether the received messages comply with either the official specification or specific for a certain customer or project.
| When message type rules are checked Message rules can be enforced by either configuring the parser or by manually validating a parsed message by using e.g. a HL7Validator. |
HL7 Conformance profiles
Conformance profiles have been introduced with HL7 2.5 as a standardized means of defining the static and dynamic properties of a HL7 message. Conformance profiles are encoded in XML and can be seen as a formal specification language. For more details on HL7 conformance profiles, please refer to chapters 2.12 and 2.19 of the HL7v2.5 specification document.
There are tools that facilitate the definition of conformance profiles, most notably the Messaging Workbench, which can be downloaded here.
HAPI supports checks against conformance profiles. In the IPF Validation framework it can be used as follows:
DefaultValidationContext context = new DefaultValidationContext() ProfileStoreFactory.setStore(new ClassPathProfileStore()) context.configure() .forVersion('2.5') .message('QBP', 'Q22').conformsToProfile('IHE-PDQ-QBP-Q22')
In this case, a file with the name IHE-PDQ-QBP-Q22.xml is looked up in the root of the Java classpath. Without the call to ProfileStoreFactory.setStore, the default HAPI ProfileStore is used, which looks into the <hapi.home>/profiles directory.
HL7 Abstract Syntax specification
HL7v2 defines an abstract message syntax (see HL7v2.5 specification, Chapter 2.13), that specifies which groups and segments are expected for a specific message type. Cardinality is indicated by using
- brackets ([...]) for optional groups or segments [0..1]
- braces ({...}) for repeatable groups or segments [1..*]
- a combination of both ({[...]} or [{...}]) for optional and repeatable groups or segments [0..*]
IPF provides support for checking a message instance against such an abstract message syntax definition. The corresponding rule is almost a copy of the Message Syntax, with a few differences:
- segment names are specified in quotes ('')
- group names are specified like function calls inside the cardinality indicators described above
- a choice of one segment from a group of segments is currently not supported.
The following comparison gives an example:
| HL7 Abstract Message Syntax definition | IPF Validation rule |
|---|---|
MSH
[ { SFT } ]
PATIENT_RESULT
PATIENT
{ [ PID
[ PD1 ]
[ { NTE } ]
[ { NK1 } ]
VISIT
[ PV1
[ PV2 ]
]
VISIT
]
PATIENT
ORDER_OBSERVATION
{ [ ORC ]
OBR
[ { NTE } ]
TIMING_QTY
[{ TQ1
[ { TQ2 } ]
}]
TIMING_QTY
[ CTD ]
OBSERVATION
[{ OBX
[ { NTE } ]
}]
OBSERVATION
[ { FT1 } ]
[ { CTI } ]
SPECIMEN
[{ SPM
[ { OBX } ]
}]
SPECIMEN
}
ORDER_OBSERVATION
}
PATIENT_RESULT
[ DSC ]
|
DefaultValidationContext context = new DefaultValidationContext()
context.configure()
.forVersion('2.4')
.message('ORU', 'R01').abstractSyntax(
'MSH',
[ { 'SFT' } ],
{PATIENT_RESULT(
[PATIENT(
'PID',
[ 'PD1' ],
[ { 'NTE' } ],
[ { 'NK1' } ],
[VISIT(
'PV1',
[ 'PV2' ]
)]
)],
{ORDER_OBSERVATION(
[ 'ORC' ],
'OBR',
[{ 'NTE' }],
[{TIMING_QTY(
'TQ1',
[{ 'TQ2' }]
)}],
[ 'CTD' ],
[{OBSERVATION(
'OBX',
[ { 'NTE' } ]
)}],
[{ 'FT1' }],
[{ 'CTI' }],
[{SPECIMEN(
'SPM',
[{ 'OBX' }]
)}]
)}
)},
[ 'DSC' ]
)
|
Note that the fields inside segments can neither be specified nor validated with the abstract message syntax.
Custom validation
You can use message rules also to program you own custom constraints on one or more trigger events. All there is to do is to write a checkIf closure that returns an array of HAPI {{ValidationException}}s. The array is empty, if validation passes.
...
context.configure().forAllVersions()
.message('ADT', 'A01')
.checkIf { msg ->
def validationExceptions = []
// validate and return an (empty) ValidationException array
return validationExceptions
}
.message('ADT', 'A01 A04 A08')
.checkIf { msg ->
// define constraints for all three trigger events
}
.message('ADT', ['A01', 'A04', 'A08'])
.checkIf { msg ->
// same as above but specified as list
}
.message('ADT', '*')
.checkIf { msg ->
// for all trigger events of the ADT message type
}
Validating against encoded messages
These kind of rules can be used to validate the encoded representation of a HL7 message, i.e. their String representation in either ER7 (Pipe) or XML encoding. Besides providing a ClosureEncodingRule class, only a link to HAPI's XMLSchemaRule is defined:
...
.forVersion()
...
.encoding("XML").isValidXML()
...
Settings up validation rules using a Spring application context
To initialize a DefaultValidationContext from a Spring Framework application context, you need a ValidationContextFactoryBean and one or more ValidationContextBuilder beans.
<beans> ... <bean id="parser" class="org.openehealth.ipf.modules.hl7.parser.PipeParser"> <property name="validationContext" ref="context"/> </bean> <bean id="context" class="org.openehealth.ipf.modules.hl7.validation.ValidationContextFactoryBean"/> <bean id="defaultTypeRules" class="org.openehealth.ipf.modules.hl7.validation.builder.DefaultTypeRulesBuilder"/> <bean id="myCustomRules" class="com.my.company.MyValidationContextBuilder"/> ... </beans>
Each ValidationContextBuilder will contribute to the overall set of rules being applied. In the example above, two rule sets are added
- the default set of primitive type rules
- A custom set of rules of either variant (type, message or encoding).
A skeleton implementation for such a custom builder is given in the following example:
import ca.uhn.hl7v2.validation.ValidationContext import ca.uhn.hl7v2.validation.ValidationException import org.openehealth.ipf.modules.hl7.validation.builder.RuleBuilder import org.openehealth.ipf.modules.hl7.validation.builder.ValidationContextBuilder public class MyValidationContextBuilder extends ValidationContextBuilder { public RuleBuilder forContext(ValidationContext context) { context.configure() .forVersion('...') .message(...) ... .type(...) ... .encoding(...) ... } }
DSL extensions
This section describes DSL extensions provided by the platform-camel-hl7 component. For a description of Camel-independent HL7 message processing features visit the pages HAPI DSL, HAPI extensions and HL7 validation. |
HL7 DSL extensions are defined in the org.openehealth.ipf.platform.camel.hl7.extend.Hl7ModelExtension.groovy class. Their main purpose is to make HAPI DSL, HAPI extensions and HL7 validation features available in Camel routes. Extensions provided by this class may well be combined with other extensions that comply with the DSL extension mechanism.
HL7 adapter (un)marshalling
The ghl7() DSL extension allows you to convert between HL7 message strings (or streams) and org.openehealth.ipf.modules.hl7dsl.MessageAdapter objects (the MessageAdapter class implements the HAPI DSL). For example, to unmarshal a message adapter from a string (or stream) use
// ...
from('...')
.unmarshal().ghl7()
.to('...')
// ...
in your Groovy route definitions. To marshal a message adapter to an output stream use
// ...
from('...')
.marshal().ghl7()
.to('...')
// ...
(Un)marshaling options
HL7 message adapter unmarshalling and marshalling can be customized in the following 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 message validation
HL7 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 validation. Here's an example:
DefaultValidationContext context = ... // create and configure a custom HL7 validation 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 HL7 message validation DSL 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 validation context 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(). |
Example
Usually, for processing HL7 messages you might first want to unmarshal a message adapter from an input stream, validate the message using either a default or custom validation context and then use the HAPI DSL (provided by the message adapter) in processors, transmogrifiers or content based routers. Then the processing result is marshalled again for being transported to another endpoint. Here's an example:
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 we make // routing decisions based in the MSH[5] // field value of the HL7 message (using // the HAPI 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
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 component is provided using its own Maven artifact. All Maven artifacts have the same group ID (org.openehealth.ipf.platform-camel). The following table shows a summary of the available components:
| IHE Transaction ID | Description | IPF IHE component | Maven artifact ID |
|---|---|---|---|
| ITI-8 | Patient Identity Feed (PIX Feed) | pix-iti8 xds-iti8 |
platform-camel-ihe-pix-iti8 |
| ITI-9 | PIX Query | pix-iti9 | platform-camel-ihe-pix-iti9 |
| ITI-10 | PIX Update Notification | pix-iti10 | platform-camel-ihe-pix-iti10 |
| ITI-21 | Patient Demographics Query (PDQ) | pdq-iti21 | platform-camel-ihe-pdq-iti21 |
| ITI-22 | Patient Demographics and Visit Query (PDQ) | pdq-iti22 | platform-camel-ihe-pdq-iti22 |
| ITI-14 | XDS.a Register Document Set | xds-iti14 | platform-camel-ihe-xds-iti14 |
| ITI-15 | XDS.a Provide & Register Document Set | xds-iti15 | platform-camel-ihe-xds-iti15 |
| ITI-16 | XDS.a Query Registry | xds-iti16 | platform-camel-ihe-xds-iti16 |
| ITI-17 | XDS.a Retrieve Document | xds-iti17 | platform-camel-ihe-xds-iti17 |
| ITI-18 | XDS.a+b Registry Stored Query | xds-iti18 | platform-camel-ihe-xds-iti18 |
| ITI-41 | XDS.b Provide & Register Document Set | xds-iti41 | platform-camel-ihe-xds-iti41 |
| ITI-42 | XDS.b Register Document Set | xds-iti42 | platform-camel-ihe-xds-iti42 |
| ITI-43 | XDS.b Retrieve Document Set | xds-iti43 | platform-camel-ihe-xds-iti43 |
The package structure of the IHE support is the following:
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 5.0 (December 2008) 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 2 (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 2 (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 2 (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 transaction components to your pom.xml, e.g. for the ITI-18 component:
<dependency> <groupId>org.openehealth.ipf.platform-camel</groupId> <artifactId>platform-camel-ihe-xds-iti18</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://activemq.apache.org/camel/schema/spring" xmlns:cxf="http://activemq.apache.org/camel/schema/cxfEndpoint" xsi:schemaLocation="http://activemq.apache.org/camel/schema/cxfEndpoint http://activemq.apache.org/camel/schema/cxfEndpoint/camel-cxf.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://activemq.apache.org/camel/schema/spring http://activemq.apache.org/camel/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:camelContext> <bean id="producerTemplate" factory-bean="camelContext" factory-method="createProducerTemplate"/> <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.
Secure transport via HTTPS can be configured by adding the URL parameter secure=true. The following sample shows a call to a registry via SOAP 1.1 over HTTPS:
...
.to('xds-iti18://localhost:8080/myWebApp/services/myIti18Service?secure=true&soap11=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:lang="http://www.springframework.org/schema/lang" xmlns:camel="http://activemq.apache.org/camel/schema/spring" xmlns:cxf="http://activemq.apache.org/camel/schema/cxfEndpoint" xmlns:util="http://www.springframework.org/schema/util" xmlns:http="http://cxf.apache.org/transports/http/configuration" xmlns:sec="http://cxf.apache.org/configuration/security" xsi:schemaLocation=" http://activemq.apache.org/camel/schema/cxfEndpoint http://activemq.apache.org/camel/schema/cxfEndpoint/camel-cxf.xsd 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://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd http://activemq.apache.org/camel/schema/spring http://activemq.apache.org/camel/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 5.0 (December 2008) 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, e.g. for PIX Feed (ITI-8):
<dependency> <groupId>org.openehealth.ipf.platform-camel</groupId> <artifactId>platform-camel-ihe-pix-iti8</artifactId> <version>${ipf-version}</version> </dependency>
This will automatically (transitively) involve, in particular, camel-mina, camel-hl7, HAPI and IPF HL7 processing support.
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 components 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.
Differences between the PIX/PDQ components and camel-mina / 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.
Additional URL parameters
Besides the URL parameters pre-defined in camel-mina and camel-hl7, the PIX/PDQ components support additional parameters: secure, audit, and allowIncompleteAudit. They are are in fact common for all IPF IHE components and described here.
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
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()
ATNA
ATNA auditing functionality is fully integrated into the corresponding IPF components. 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 should 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="auditEnterpriseSiteId" 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 (BSD 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 new 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.
URL Parameters' Summary
The following table gives an overview of all URL parameters defined for IPF XDS endpoints.
| Name | Type | Default value | Transactions | Side | Short description |
|---|---|---|---|---|---|
| secure | boolean | false | all except ITI-17 | client only for XDS.a+b; both server and client for PIX/PDQ |
whether Transport Layer Security mechanisms should be applied when sending messages |
| audit | boolean | true | all except ITI-17 | both service and client | whether ATNA auditing should be performed by the corresponding party |
| allowIncompleteAudit | boolean | false | all except ITI-17 | both service and client | whether incomplete audit records should be sent to the ATNA repository as well |
| soap11 | boolean | false | XDS.b | client only | whether requests should be sent using SOAP 1.1 instead of SOAP 1.2 |
| codec | String | "hl7codec" | PIX/PDQ | both service and client | name of HL7 codec factory bean |
CDA support
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://activemq.apache.org/camel/schema/spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://activemq.apache.org/camel/schema/spring http://activemq.apache.org/camel/schema/spring/camel-spring.xsd"> <camel:camelContext id="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.
// ...
from('...')
.unmarshal().gpath()
.transmogrify { gpathResult ->
// process XML ...
}
.to('...')
// ...
To marshal a CDA document to an output stream use
// ...
from('...')
.marshal().cdar2()
.to('...')
// ...
(Un)marshaling options
Unlike HL7 message adapter unmarshalling and marshalling, CDA has no marshalling/unmarshalling options.
CDA document validation
Marshaled CDA documents can be validated in routes with the validate().cdar2() extension. This basically validates whether the document is compliant with the CDA XML schema. Note that you currently can not validate the internal representation of a CDA document
// ...
from('...')
.onException(ValidationException.class)
// handle the validation exception
.end()
.marshal().cdar2()
.validate().xsd().cdar2()
.to('...')
// ...
You can also validate during parsing with XMLSlurper:
...
import static org.openehealth.ipf.modules.cda.CDAR2Constants.CDAR2_SCHEMA
// ...
from('...')
.onException(Exception.class)
// handle the Camel Runtime exception
.end()
.unmarshal().gpath(CDAR2_SCHEMA, true)
...
.to('...')
// ...
CDA Builder Syntax Reference
General Builder 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 |
CDA Schema Names
When creating a CDA document using a single builder statement, you only need to know the top-level element (clinicalDocument for plain CDA, continuityOfCareDocument for CCD). The CDA builder automatically instantiates an object of the correct type for each of the elements below. However, there are situations where you need to know the schema-name for a specific type:
- when you create otherwise nested CDA elements of their own in order to assemble a complete CDA document from them in a later step
- when you need to indicate the type of a variable-typed element (ANY).
For referencing a schema-name and the definition of attributes and nested elements of corresponding schema, for now please look at the respective declaration files in the IPF source repository. We'll provide a more readable reference once the schemas have stabilized.
- Data Types: http://gforge.openehealth.org/svn/ipf/branches/ipf-1.7/modules/cda/src/main/resources/builders/DataTypeBuilder.groovy
- RIM Entities: http://gforge.openehealth.org/svn/ipf/branches/ipf-1.7/modules/cda/src/main/resources/builders/EntityBuilder.groovy
- RIM Roles: http://gforge.openehealth.org/svn/ipf/branches/ipf-1.7/modules/cda/src/main/resources/builders/RoleBuilder.groovy
- RIM Participations: http://gforge.openehealth.org/svn/ipf/branches/ipf-1.7/modules/cda/src/main/resources/builders/ParticipationBuilder.groovy
- RIM Acts: http://gforge.openehealth.org/svn/ipf/branches/ipf-1.7/modules/cda/src/main/resources/builders/ActBuilder.groovy
- RIM ActRelationships: http://gforge.openehealth.org/svn/ipf/branches/ipf-1.7/modules/cda/src/main/resources/builders/ActRelationshipBuilder.groovy
Abstract types are:
- Any: http://gforge.openehealth.org/svn/ipf/branches/ipf-1.7/modules/cda/src/main/resources/builders/AnyBuilder.groovy
- InfrastructureRoot: http://gforge.openehealth.org/svn/ipf/branches/ipf-1.7/modules/cda/src/main/resources/builders/InfrastructureBuilder.groovy
CDA profile support
CDA content profiles define clinical semantics for generic CDA documents. A profile usually constrains the CDA model in one or more of the following areas:
- defining templateIds to uniquely identify the semantics of a CDA element
- defining codes and code systems
- defining CDA body sections
The aim of IPF's CDA profile support is to abstract from the generic CDA representation of a certain profile towards a syntax that reflects the structure and vocabulary of the profile specification. In particular, all CDA "boilerplate" code shall be hidden, e.g. with respect to templateIds, codes and titles. This helps to achieve profile-compliant CDA documents, although there's no guarantee for it as for some restrictions it is not possible to automatically enforce them.
CDA profile support also adds Schematron validation against rules that are often released as informative addendum to the profile specification.
The CDA profile support is not only limited to creating profile-compliant documents, but also for extracting information from existing documents, and for validating existing documents.
CCD
The CCD specification is a constraint on the HL7 Clinical Document Architecture (CDA) standard. CCD was developed as a collaborative effort between ASTM and HL7, combining the benefits of ASTMs Continuity of Care Record (CCR) and the HL7 Clinical Document Architecture (CDA) specifications. It is intended as an alternate implementation to the one specified in ASTM ADJE2369 for those institutions or organizations committed to implementation of the HL7 Clinical Document Architecture.
The CCD specification contains U.S. specific requirements; its use is therefore limited to the U.S. The U.S. Healthcare Information Technology Standards Panel (HITSP) has selected the CCD as one of its standards.
Configuration
Building a CCD document does not require any additional dependencies in the Maven setup. It requires, however even more Groovy metaclass extensions on top of the underlying CDA Object model to facilitate accessing CCD documents. Note that the CCDModelExtensions contain the CDAR2ModelExtensions.
You can register the extensions manually:
import org.openehealth.ipf.modules.ccd.builder.CCDModelExtension ... ExpandoMetaClass.enableGlobally() new CCDModelExtension().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://activemq.apache.org/camel/schema/spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://activemq.apache.org/camel/schema/spring http://activemq.apache.org/camel/schema/spring/camel-spring.xsd"> <camel:camelContext id="camelContext"/> ... <bean id="ccdModelExtension" class="org.openehealth.ipf.modules.ccd.builder.CCDModelExtension"> </bean> <bean id="routeModelExtender" class="org.openehealth.ipf.platform.camel.core.extend.DefaultModelExtender"> <property name="routeModelExtensions"> <list> ... <ref bean="coreModelExtension" /> <ref bean="ccdModelExtension" /> </list> </property> </bean>
Usage
The CCDBuilder is a subclass of the org.openehealth.ipf.modules.cda.builder.CDAR2Builder class. The top-level element is now continuityOfCareDocument instead of clinicalDocument:
import org.openehealth.ipf.modules.cda.builder.content.document.CCDBuilder ... def builder = new CCDBuilder() def ccd = builder.build { continuityOfCareDocument { ... } }
Take care to instantiate CCDBuilder only once and reuse the instance! Note that you use the standard CDAR2Renderer to ender a CCD document to XML.
CCD support also conatins a DSL extension that checks a CCD document in its XML representation against the specified constraints by using a Schematron validator.
import org.openehealth.ipf.commons.xml.SchematronProfile; ... from('direct:input1') .... // get XML CCD into message body .validate().ccd() // equivalent with: // .validate().schematron().staticProfile(new SchematronProfile(CDAR2Constants.CCD_SCHEMATRON_RULES)) ...
You can use the CCDBuilder just like the CDAR2Builder. You can also use it to construct non-CCD documents as its provided functrionality is actually a superset.
However, it offers many additional CCD-specific builder elements and cardinality checks that closely correspond with subchapters of the CCD specification. These special elements facilitate the creation of correct CCD documents by automatically setting static elements and attributes and enforcing the CCD-specific constraints as good as possible.
Below you find a detailed list of how the CCD sections have been mapped into builder elements.
- Purpose section
- Payers section
- Advance Directives section
- Support section
- Functional Status section
- Problems section
- Familiy History section
- Social History section
- Alerts section
- Medications section
- Medical Equipment section
- Immunizations section
- Vital Signs section
- Results section
- Procedures section
- Encounters section
- Plan of Care section
Purpose section
Represents the specific reason for which the summarization was generated, such as in response to a request. The general use case does not require a purpose. Purpose should be utilized when the CCD has a specific purpose such as a transfer, referral, or patient request.
Builder Elements
| Element | CDA Type | Cardinality | Description |
|---|---|---|---|
| purpose | Section | 0..1 | Purpose section |
| purposeActivity | EntryRelationship | 0(1)..* | Reason for creating the document. The target act may be an Observation, Procedure, or some other kind of act, and it may represent an order, an event, etc. |
Example
... // CCD Purpose (Chapter 2.8) purpose { text('Transfer of Care!') purposeActivity { // Example of an Purpose Activity Act act { code(code:'308292007', codeSystem:'2.16.840.1.113883.6.96', displayName:'Transfer of care') statusCode('completed') } } } ...
...
<component>
<structuredBody>
<component>
<section>
<templateId root="2.16.840.1.113883.10.20.1.13"/>
<code code="48764-5" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Summary purpose"/>
<title>Summary purpose</title>
<text>Transfer of Care!</text>
<entry contextConductionInd="true" typeCode="DRIV">
<act classCode="ACT" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.30"/>
<code code="23745001" codeSystem="2.16.840.1.113883.6.96" codeSystemName="SNOMED CT" displayName="Documentation procedure"/>
<statusCode code="completed"/>
<entryRelationship contextConductionInd="true" typeCode="RSON">
<act>
<code code="308292007" codeSystem="2.16.840.1.113883.6.96" displayName="Transfer of care"/>
<statusCode code="completed"/>
</act>
</entryRelationship>
</act>
</entry>
</section>
</component>
</structuredBody>
</component>
...
Payers section
Payers contains data on the patient's payers, whether a 'third party' insurance, self-pay, other payer or guarantor, or some combination of payers, and is used to define which entity is the responsible fiduciary for the financial aspects of a patient's care.
Builder Elements
| Element | CDA Type | Cardinality | Description |
|---|---|---|---|
| payers | Section | 0(1) | Payers section |
| coverageActivity | Act | 0(1)..* | serves to order the payment sources |
| policyActivity | Act | 1..* | the policy or program providing the coverage |
| payer | AssignedEntity | 1 | performer of the policy activity |
| coveredParty | ParticipantRole | 1 | The person for whom payment is being provided |
| subscriber | ParticipantRole | 0..1 | participant that is the holder the coverage |
| authorizationActivity | Act | 0..* | authorizations or pre-authorizations currently active for the patient for the particular payer |
| promise | EntryRelationsship | 0..* |
Example
...
// CCD Payers (Chapter 3.1)
payers {
text {
...
}
coverageActivity {
id('1fe2cdd0-7aad-11db-9fe1-0800200c9a66')
policyActivity {
id('3e676a50-7aac-11db-9fe1-0800200c9a66')
code('EHCPOL')
payer {
id('329fcdf0-7ab3-11db-9fe1-0800200c9a66')
representedOrganization {
name('Good Health Insurance')
}
}
coveredParty {
id('14d4a520-7aae-11db-9fe1-0800200c9a66')
code('SELF')
}
authorizationActivity {
id('f4dce790-8328-11db-9fe1-0800200c9a66')
code(nullFlavor:'NA')
promise {
procedure(moodCode:'PRMS') {
code(code:'73761001',
codeSystem:'2.16.840.1.113883.6.96',
displayName:'Colonoscopy')
}
}
}
}
}
}
...
...
<component>
<structuredBody>
<component>
<section>
<templateId root="2.16.840.1.113883.10.20.1.9"/>
<code code="48768-6" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Payment sources"/>
<title>Payers</title>
<text>...</text>
<entry>
<act classCode="ACT" moodCode="DEF">
<templateId root="2.16.840.1.113883.10.20.1.20"/>
<id root="1fe2cdd0-7aad-11db-9fe1-0800200c9a66"/>
<code xsi:type="CE" code="48768-6" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Payment sources"/>
<statusCode code="completed"/>
<entryRelationship contextConductionInd="true" typeCode="COMP">
<act classCode="ACT" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.26"/>
<id root="3e676a50-7aac-11db-9fe1-0800200c9a66"/>
<code xsi:type="CE" code="EHCPOL" codeSystem="2.16.840.1.113883.5.4"/>
<statusCode code="completed"/>
<performer typeCode="PRF">
<assignedEntity>
<id root="329fcdf0-7ab3-11db-9fe1-0800200c9a66"/>
<representedOrganization>
<name>Good Health Insurance</name>
</representedOrganization>
</assignedEntity>
</performer>
<participant typeCode="COV">
<participantRole>
<id root="14d4a520-7aae-11db-9fe1-0800200c9a66"/>
<code code="SELF" codeSystem="2.16.840.1.113883.5.111"/>
</participantRole>
</participant>
<entryRelationship typeCode="REFR">
<act classCode="ACT" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.19"/>
<id root="f4dce790-8328-11db-9fe1-0800200c9a66"/>
<code nullFlavor="NA"/>
<entryRelationship contextConductionInd="true" typeCode="SUBJ">
<procedure classCode="PROC" moodCode="PRMS">
<code code="73761001" codeSystem="2.16.840.1.113883.6.96" displayName="Colonoscopy"/>
</procedure>
</entryRelationship>
</act>
</entryRelationship>
</act>
</entryRelationship>
</act>
</entry>
</section>
</component>
</structuredBody>
</component>
...
Advance Directives section
This section contains data defining the patient's advance directives and any reference to supporting documentation. The most recent and up-to-date directives are required, if known, and should be listed in as much detail as possible. This section contains data such as the existence of living wills, healthcare proxies, and CPR and resuscitation status. If referenced documents are available, they can be included in the CCD exchange package.
Builder Elements
| Element | CDA Type | Cardinality | Description |
|---|---|---|---|
| advanceDirectives | Section | 0(1) | Advance Directives section |
| advanceDirectiveObservation | Observation | 0(1)..* | Advance Directives observation |
| advanceDirectiveStatus | Observation | 1 | Advance Directive observation status |
| verifier | Participant | 0..* | A verification of an advance directive observation |
| advanceDirectiveReference | ExternalDocument | 0..1 | Referenced advance directive document |
Example
... // CCD Advance Directives (Chapter 3.2) advanceDirectives{ text{ table(border:'1', width:'100%'){ thead{ tr{ th('Directive') th('Description') th('Verification') th('Supporting Document(s)') } } tbody{ tr{ td('Resuscitation status') td('Do not resuscitate') td('Dr. Robert Dolin, Nov 07, 1999') td{ linkHtml(href:'AdvanceDirective.b50b7910-7ffb-4f4c-bbe4-177ed68cbbf3.pdf','Advance directive') } } } } }//text advanceDirectiveObservation{ id(root:'9b54c3c9-1673-49c7-aef9-b037ed72ed27') code(code:'304251008', codeSystem:'2.16.840.1.113883.6.96', displayName:'Resuscitation') value(make{ cd(code:'304253006', codeSystem:'2.16.840.1.113883.6.96', displayName:'Do not resuscitate'){ originalText{ reference(value:'#AD1') } } }) verifier{ time(value:'19991107') participantRole{ id(root:'20cf14fb-b65c-4c8c-a54d-b0cca834c18c') } } advanceDirectiveStatus{ value(code:'15240007', codeSystem:'2.16.840.1.113883.6.96', displayName:'Current and verified') }//advance directive observation status advanceDirectiveReference{ id(root:'b50b7910-7ffb-4f4c-bbe4-177ed68cbbf3') code(code:'371538006', codeSystem:'2.16.840.1.113883.6.96', displayName:'Advance directive') text(mediaType:'application/pdf'){ reference(value:'AdvanceDirective.b50b7910-7ffb-4f4c-bbe4-177ed68cbbf3.pdf') } } } } ...
...
<component>
<structuredBody>
<component>
<section>
<templateId root="2.16.840.1.113883.10.20.1.1"/>
<code code="42348-3" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Advance directives"/>
<title>Advance Directives</title>
<text><table border="1" width="100%">
<thead>
<tr>
<th colspan="1" rowspan="1">Directive</th>
<th colspan="1" rowspan="1">Description</th>
<th colspan="1" rowspan="1">Verification</th>
<th colspan="1" rowspan="1">Supporting Document(s)</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="1" rowspan="1">Resuscitation status</td>
<td colspan="1" rowspan="1">Do not resuscitate</td>
<td colspan="1" rowspan="1">Dr. Robert Dolin, Nov 07, 1999</td>
<td colspan="1" rowspan="1"><linkHtml href="AdvanceDirective.b50b7910-7ffb-4f4c-bbe4-177ed68cbbf3.pdf">Advance directive</linkHtml></td>
</tr>
</tbody>
</table></text>
<entry contextConductionInd="true" typeCode="DRIV">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.17"/>
<id root="9b54c3c9-1673-49c7-aef9-b037ed72ed27"/>
<code xsi:type="CE" code="304251008" codeSystem="2.16.840.1.113883.6.96" displayName="Resuscitation"/>
<statusCode code="completed"/>
<value xsi:type="CD" code="304253006" codeSystem="2.16.840.1.113883.6.96" displayName="Do not resuscitate">
<originalText><reference value="#AD1"/></originalText>
</value>
<participant typeCode="VRF">
<templateId root="2.16.840.1.113883.10.20.1.58"/>
<time value="19991107"/>
<participantRole>
<id root="20cf14fb-b65c-4c8c-a54d-b0cca834c18c"/>
</participantRole>
</participant>
<entryRelationship contextConductionInd="true" typeCode="REFR">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.37"/>
<code xsi:type="CE" code="33999-4" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Status"/>
<statusCode code="completed"/>
<value xsi:type="CE" code="15240007" codeSystem="2.16.840.1.113883.6.96" displayName="Current and verified"/>
</observation>
</entryRelationship>
<reference typeCode="REFR">
<externalDocument classCode="DOC" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.36"/>
<id root="b50b7910-7ffb-4f4c-bbe4-177ed68cbbf3"/>
<code code="371538006" codeSystem="2.16.840.1.113883.6.96" displayName="Advance directive"/>
<text mediaType="application/pdf"><reference value="AdvanceDirective.b50b7910-7ffb-4f4c-bbe4-177ed68cbbf3.pdf"/></text>
</externalDocument>
</reference>
</observation>
</entry>
</section>
</component>
</structuredBody>
</component>
...
Support section
This section represents the patient's sources of support such as immediate family, relatives, and guardian at the time the summarization is generated. Support information also includes next of kin, caregivers, and support organizations. At a minimum, key support contacts relative to healthcare decisions, including next of kin, should be included.
Builder Elements
| Element | CDA Type | Cardinality | Description |
|---|---|---|---|
| gardian | Patient/Guardian | 0(1) | Guardian entry |
| nextOfKin | Participant/associatedEntity | 0..* | Next of kin participant entry |
| emergencyContact | Participant/associatedEntity | 0..* | Emergency contact participant entry |
| caregiver | Participant/associatedEntity | 0..* | Caregiver participant entry |
Example
continuityOfCareDocument {
...
nextOfKin{
id(root:'4ac71514-6a10-4164-9715-f8d96af48e6d')
code(code:'65656005', codeSystem:'2.16.840.1.113883.6.96', displayName:'Biiological mother')
telecom(value:'tel:(999)555-1212')
associatedPerson{
name{
given('Henrietta')
family('Levin')
}
}
}//next of kin
emergencyContact{
id(root:'4ac71514-6a10-4164-9715-f8d96af48e6f')
associatedPerson{
name{
given('Baba')
family('John')
}
}
}//emergency contact
caregiver{
scopingOrganization{
name('Very Good Health Clinic')
}
}//patient caregiver
...
}
.... <participant typeCode="IND"> <associatedEntity classCode="NOK"> <id root="4ac71514-6a10-4164-9715-f8d96af48e6d"/> <code code="65656005" codeSystem="2.16.840.1.113883.6.96" displayName="Biiological mother"/> <telecom value="tel:(999)555-1212"/> <associatedPerson> <name><given>Henrietta</given><family>Levin</family></name> </associatedPerson> </associatedEntity> </participant> <participant typeCode="IND"> <associatedEntity classCode="ECON"> <id root="4ac71514-6a10-4164-9715-f8d96af48e6f"/> <associatedPerson> <name><given>Baba</given><family>John</family></name> </associatedPerson> </associatedEntity> </participant> <participant typeCode="IND"> <associatedEntity classCode="CAREGIVER"> <scopingOrganization> <name>Very Good Health Clinic</name> </scopingOrganization> </associatedEntity> </participant> ...
Functional Status section
Functional Status describes the patient's status of normal functioning at the time the Care Record was created.
Functional Status
| Element | CDA Type | Cardinality | Description |
|---|---|---|---|
| funcationalStatus | Section | 0(1) | Functional Status section |
| problemAct | Act | 0(1)..* | see Problems section |
| functionalStatusStaus | Act | 0..1 | Problem observation may contain a status observation of functional status |
Example
...
// CCD Funtional Stats (Chapter 3.4)
functionalStatus{
text{
...
}
problemAct{
id(root:'6z2fa88d-4174-4909-aece-db44b60a3abb')
code(nullFlavor:'NA')
problemObservation{
id(root:'fd07111a-b15b-4dce-8518-1274d07f142a')
code(code:'ASSERTION', codeSystem:'2.16.840.1.113883.5.4')
effectiveTime{low(value:'1998')}
value( make{
cd(code:'105504002',
codeSystem:'2.16.840.1.113883.6.96',
displayName:'Dependence on cane')
}
)
functionalStatusStatus{
value(code:'55561003',
codeSystem:'2.16.840.1.113883.6.96',
displayName:'Active')
}
}
}
problemAct{
id(root:'64606e86-c080-11db-8314-0800200c9a66')
problemObservation{
id(root:'64606e86-c080-11db-8314-0800200c9a66')
code(code:'ASSERTION', codeSystem:'2.16.840.1.113883.5.4')
value( make{
cd(code:'386807006',
codeSystem:'2.16.840.1.113883.6.96',
displayName:'Memory impairment')
}
)
functionalStatusStatus{
value(code:'55561003',
codeSystem:'2.16.840.1.113883.6.96',
displayName:'Active')
}
}
}
}
...
....
<component>
<structuredBody>
<component>
<section>
<templateId root="2.16.840.1.113883.10.20.1.5"/>
<code code="47420-5" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Functional status assessment"/>
<title>Functional Status</title>
<text>...</text>
<entry contextConductionInd="true" typeCode="DRIV">
<act classCode="ACT" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.27"/>
<id root="6z2fa88d-4174-4909-aece-db44b60a3abb"/>
<code nullFlavor="NA"/>
<entryRelationship contextConductionInd="true" typeCode="SUBJ">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.28"/>
<id root="fd07111a-b15b-4dce-8518-1274d07f142a"/>
<code xsi:type="CE" code="ASSERTION" codeSystem="2.16.840.1.113883.5.4"/>
<statusCode code="completed"/>
<effectiveTime>
<low value="1998"/>
</effectiveTime>
<value xsi:type="CD" code="105504002" codeSystem="2.16.840.1.113883.6.96" displayName="Dependence on cane"/>
<entryRelationship contextConductionInd="true" typeCode="REFR">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.44"/>
<code xsi:type="CE" code="33999-4" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Status"/>
<statusCode code="completed"/>
<value xsi:type="CE" code="55561003" codeSystem="2.16.840.1.113883.6.96" displayName="Active"/>
</observation>
</entryRelationship>
</observation>
</entryRelationship>
</act>
</entry>
<entry contextConductionInd="true" typeCode="DRIV">
<act classCode="ACT" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.27"/>
<id root="64606e86-c080-11db-8314-0800200c9a66"/>
<code nullFlavor="NA"/>
<entryRelationship contextConductionInd="true" typeCode="SUBJ">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.28"/>
<id root="64606e86-c080-11db-8314-0800200c9a66"/>
<code xsi:type="CE" code="ASSERTION" codeSystem="2.16.840.1.113883.5.4"/>
<statusCode code="completed"/>
<value xsi:type="CD" code="386807006" codeSystem="2.16.840.1.113883.6.96" displayName="Memory impairment"/>
<entryRelationship contextConductionInd="true" typeCode="REFR">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.44"/>
<code xsi:type="CE" code="33999-4" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Status"/>
<statusCode code="completed"/>
<value xsi:type="CE" code="55561003" codeSystem="2.16.840.1.113883.6.96" displayName="Active"/>
</observation>
</entryRelationship>
</observation>
</entryRelationship>
</act>
</entry>
</section>
</component>
</structuredBody>
</component>
...
Problems section
This section lists and describes all relevant clinical problems at the time the summary is generated. At a minimum, all pertinent current and historical problems should be listed. CDA R2 represents problems as Observations.
Builder Elements
| Element | CDA Type | Cardinality | Description |
|---|---|---|---|
| problems | Section | 0(1) | Problems section |
| problemAct | Act | 0(1)..* | Problem clinical statement |
| problemObservation | Observation | 1..* | Related problem observations |
| problemStatus | Observation | 0..1 | The status of a given problem observation |
| problemHealthstatus | Observation | 0..1 | Describes overall patient health status as a result of a particular problem |
| episodeObservation | Observation | 0..1 | May be used to indicate that a problem act represents a new episode, distinct from other episodes of a similar concern |
| patientAwareness | Observation | 0..1 | Patient awareness of a problem act or problem observation |
Example
... // CCD Problems (Chapter 3.5) component { structuredBody { problems{ text('Patient Problems Acts') problemAct{ id(root:'d11275e9-67ae-11db-bd13-0800200c9a66') problemObservation{ id(root:'9d3d416d-45ab-4da1-912f-4583e0632000') code(code:'ASSERTION', codeSystem:'2.16.840.1.113883.5.4') value(make{ cd(code:'233604007',codeSystem:'2.16.840.1.113883.6.96',displayName:'Pneumonia') } ) problemStatus{ value(code:'413322009', codeSystem:'2.16.840.1.113883.6.96', displayName:'Resolved') } problemHealthstatus{ value(code:'162467007', codeSystem:'2.16.840.1.113883.6.96', displayName:'Symptom Free') } } episodeObservation{ code(code:'ASSERTION', codeSystem:'2.16.840.1.113883.5.4') value(make{ cd(code:'404684003', codeSystem:'2.16.840.1.113883.6.96', displayName:'Clinical finding') } ) entryRelationship(typeCode:'SAS'){ act(classCode:'ACT', moodCode:'EVN'){ id(root:'ec8a6ff8-ed4b-4f7e-82c3-e98e58b45de7') code(nullFlavor:'NA') }//act } } patientAwareness{ awarenessCode(code:'TEST', codeSystem:'2.16.840.1.113883.5.4') participantRole{ id(root:'c8a6ff8-ed4b-4f7e-82c3-e98e58b45de8') } } } } } } } ...
....
<component>
<structuredBody>
<component>
<section>
<templateId root="2.16.840.1.113883.10.20.1.11"/>
<code code="11450-4" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Problem list"/>
<title>Problems</title>
<text>Patient Problems Acts</text>
<entry contextConductionInd="true" typeCode="DRIV">
<act classCode="ACT" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.27"/>
<id root="d11275e9-67ae-11db-bd13-0800200c9a66"/>
<code nullFlavor="NA"/>
<participant typeCode="SBJ">
<templateId root="2.16.840.1.113883.10.20.1.48"/>
<awarenessCode code="TEST" codeSystem="2.16.840.1.113883.5.4"/>
<participantRole>
<id root="c8a6ff8-ed4b-4f7e-82c3-e98e58b45de8"/>
</participantRole>
</participant>
<entryRelationship contextConductionInd="true" typeCode="SUBJ">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.28"/>
<id root="9d3d416d-45ab-4da1-912f-4583e0632000"/>
<code xsi:type="CE" code="ASSERTION" codeSystem="2.16.840.1.113883.5.4"/>
<statusCode code="completed"/>
<value xsi:type="CD" code="233604007" codeSystem="2.16.840.1.113883.6.96" displayName="Pneumonia"/>
<entryRelationship contextConductionInd="true">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.50"/>
<code xsi:type="CE" code="33999-4" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Status"/>
<statusCode code="completed"/>
<value xsi:type="CE" code="413322009" codeSystem="2.16.840.1.113883.6.96" displayName="Resolved"/>
</observation>
</entryRelationship>
<entryRelationship contextConductionInd="true">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.51"/>
<code xsi:type="CE" code="11323-3" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Health status"/>
<statusCode code="completed"/>
<value xsi:type="CE" code="162467007" codeSystem="2.16.840.1.113883.6.96" displayName="Symptom Free"/>
</observation>
</entryRelationship>
</observation>
</entryRelationship>
<entryRelationship contextConductionInd="true">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.41"/>
<code xsi:type="CE" code="ASSERTION" codeSystem="2.16.840.1.113883.5.4"/>
<statusCode code="completed"/>
<value xsi:type="CD" code="404684003" codeSystem="2.16.840.1.113883.6.96" displayName="Clinical finding"/>
<entryRelationship contextConductionInd="true" typeCode="SAS">
<act classCode="ACT" moodCode="EVN">
<id root="ec8a6ff8-ed4b-4f7e-82c3-e98e58b45de7"/>
<code nullFlavor="NA"/>
</act>
</entryRelationship>
</observation>
</entryRelationship>
</act>
</entry>
</section>
</component>
</structuredBody>
</component>
...
Familiy History section
This section contains data defining the patient's genetic relatives in terms of possible or relevant health risk factors that have a potential impact on the patient's healthcare risk profile.
Builder Elements
| Element | CDA Type | Cardinality | Description |
|---|---|---|---|
| familyHistory | Section | 0(1) | Family history section |
| familyHistoryObservation | Observation | 0(1)..* | Family history observation |
| problemStatus | Observation | 0..1 | The status of a given family history observation (see Problem Observation) |
| causeOfDeath | Observation | 0(1)..* | A special family history observation describing the cause of death |
| cause | Observation | 1 | Family history observation of death |
| familyMember | Organizer | 0(1)..* | Family history organizer in order to group the family history observations related to a family member |
| familyPerson | RelatedSubject | 1 | Subject participant, representing the family member who is the subject of the family history observations |
| age | Observation | 1 | Representation of age |
Example
...
// CCD Family History (Chapter 3.6)
familyHistory {
text('skipped')
familyMember {
familyPerson {
code(code:'9947008', codeSystem:'2.16.840.1.113883.6.96', displayName:'Biological father')
subject {
administrativeGenderCode('M')
birthTime(value:'1912')
}
}
causeOfDeath {
id('d42ebf70-5c89-11db-b0de-0800200c9a66')
code('ASSERTION')
value(make {
ce(code:'22298006',codeSystem:'2.16.840.1.113883.6.96',displayName:'MI')
})
cause {
id('6898fae0-5c8a-11db-b0de-0800200c9a66')
code('ASSERTION', codeSystem:'2.16.840.1.113883.5.4')
statusCode('completed')
value(make {
ce(code:'419099009',codeSystem:'2.16.840.1.113883.6.96',displayName:'Dead')
})
}
age {
value(make { _int(57) })
}
}
familyHistoryObservation{
id('5bfe3ec0-5c8b-11db-b0de-0800200c9a66')
code('ASSERTION', codeSystem:'2.16.840.1.113883.5.4')
value(make {
ce(code:'59621000',codeSystem:'2.16.840.1.113883.6.96',displayName:'HTN')
})
age {
value(make { _int(40) })
}
problemStatus{
value(code:'413322009', displayName:'Resolved')
}
}
}
familyMember {
familyPerson {
code(code:'65656005', codeSystem:'2.16.840.1.113883.6.96', displayName:'Biological mother')
subject {
administrativeGenderCode('F')
birthTime(value:'1912')
}
}
familyHistoryObservation{
id('a13c6160-5c8b-11db-b0de-0800200c9a66')
code('ASSERTION', codeSystem:'2.16.840.1.113883.5.4')
value(make {
ce(code:'195967001',codeSystem:'2.16.840.1.113883.6.96',displayName:'Asthma')
})
age {
value(make { _int(30) })
}
}
}
}
...
...
<component>
<structuredBody>
<component>
<section>
<templateId root="2.16.840.1.113883.10.20.1.4"/>
<code code="10157-6" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="History of family member diseases"/>
<title>Family History</title>
<text>skipped</text>
<entry contextConductionInd="true" typeCode="DRIV">
<organizer classCode="CLUSTER" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.23"/>
<statusCode code="completed"/>
<subject>
<relatedSubject classCode="PRS">
<code xsi:type="CE" code="9947008" codeSystem="2.16.840.1.113883.6.96" displayName="Biological father"/>
<subject>
<administrativeGenderCode code="M" codeSystem="2.16.840.1.113883.5.1"/>
<birthTime value="1912"/>
</subject>
</relatedSubject>
</subject>
<component contextConductionInd="true">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.42"/>
<id root="d42ebf70-5c89-11db-b0de-0800200c9a66"/>
<code xsi:type="CE" code="ASSERTION"/>
<statusCode code="completed"/>
<value xsi:type="CE" code="22298006" codeSystem="2.16.840.1.113883.6.96" displayName="MI"/>
<entryRelationship contextConductionInd="true" typeCode="CAUS">
<observation classCode="OBS" moodCode="INT">
<id root="6898fae0-5c8a-11db-b0de-0800200c9a66"/>
<code xsi:type="CE" code="ASSERTION" codeSystem="2.16.840.1.113883.5.4"/>
<statusCode code="completed"/>
<value xsi:type="CE" code="419099009" codeSystem="2.16.840.1.113883.6.96" displayName="Dead"/>
</observation>
</entryRelationship>
<entryRelationship contextConductionInd="true" inversionInd="true" typeCode="SUBJ">
<observation classCode="OBS" moodCode="INT">
<templateId root="2.16.840.1.113883.10.20.1.38"/>
<code xsi:type="CE" code="397659008" codeSystem="2.16.840.1.113883.6.96" codeSystemName="SNOMED CT" displayName="Age"/>
<statusCode code="completed"/>
<value xsi:type="INT" value="57"/>
</observation>
</entryRelationship>
</observation>
</component>
<component contextConductionInd="true">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.22"/>
<id root="5bfe3ec0-5c8b-11db-b0de-0800200c9a66"/>
<code xsi:type="CE" code="ASSERTION" codeSystem="2.16.840.1.113883.5.4"/>
<statusCode code="completed"/>
<value xsi:type="CE" code="59621000" codeSystem="2.16.840.1.113883.6.96" displayName="HTN"/>
<entryRelationship contextConductionInd="true" inversionInd="true" typeCode="SUBJ">
<observation classCode="OBS" moodCode="INT">
<templateId root="2.16.840.1.113883.10.20.1.38"/>
<code xsi:type="CE" code="397659008" codeSystem="2.16.840.1.113883.6.96" codeSystemName="SNOMED CT" displayName="Age"/>
<statusCode code="completed"/>
<value xsi:type="INT" value="40"/>
</observation>
</entryRelationship>
</observation>
</component>
</organizer>
</entry>
<entry contextConductionInd="true" typeCode="DRIV">
<organizer classCode="CLUSTER" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.23"/>
<statusCode code="completed"/>
<subject>
<relatedSubject classCode="PRS">
<code xsi:type="CE" code="65656005" codeSystem="2.16.840.1.113883.6.96" displayName="Biological mother"/>
<subject>
<administrativeGenderCode code="F" codeSystem="2.16.840.1.113883.5.1"/>
<birthTime value="1912"/>
</subject>
</relatedSubject>
</subject>
<component contextConductionInd="true">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.1.22"/>
<id root="a13c6160-5c8b-11db-b0de-0800200c9a66"/>
<code xsi:type="CE" code="ASSERTION" codeSystem="2.16.840.1.113883.5.4"/>
<statusCode code="completed"/>
<value xsi:type="CE" code="195967001" codeSystem="2.16.840.1.113883.6.96" displayName="Asthma"/>
<entryRelationship contextConductionInd="true" inversionInd="true" typeCode="SUBJ">
<observation classCode="OBS" moodCode="INT">
<templateId root="2.16.840.1.113883.10.20.1.38"/>
<code xsi:type="CE" code="397659008" codeSystem="2.16.840.1.113883.6.96" codeS




