Beginner's guide to Apache Camel and IPF for the Swiss EPR



Last modified on September 29, 2024

Introduction#

In this article, I will focus on using these libraries with the Spring Boot integration. While it is not mandatory to use Spring Boot, it is a good choice, as it makes configuring the application much easier. IPF provides starters dependencies that will automatically configure the Camel context and the IPF components for you. I will present the manual configuration of the Camel context and the IPF components, as it is useful to understand how everything works.

About Apache Camel#

Apache Camel is a powerful open-source integration framework based on known Enterprise Integration Patterns (EIP). It allows you to define routing and mediation rules in a variety of domain-specific languages, including a Java-based Fluent API, Spring or Blueprint XML Configuration files, and a Scala DSL. If you are still unsure about what Camel is, you can refer to this nice StackOverflow thread.

I will quickly describe some components of Camel that we will need to use in this article.

The registry#

The Camel context contains a bean registry, a component that Spring also provides. It allows to set (binding) and retrieve (lookup) beans in the current context, and we will use it directly when setting the endpoint parameters. When both Camel and Spring are used together, the Camel context will automatically delegate its bean registry to Spring. This is quite helpful, as you can simply define your beans in the Spring context and refer to them in the Camel endpoint URIs.

The type converters#

Camel implements a type conversion mechanism that allows you to convert a message from one type to another. For example, converting a String to a byte[]. There are two types of type converters in Camel: the built-in type converters and the custom type converters. The built-in type converters are automatically registered by Camel and are available for all routes, providing conversions between common types (like String, byte[], streams, readers/writers, etc.).

IPF and Husky also provide some custom type converters, which are automatically registered in the Camel context, for all models that are implemented for specific transactions. For example, if you want to send a PIXv3 Query message, you can send a PIXv3Query object, and Camel will automatically convert it to any type it natively supports for the HTTP transaction. This means you can work with any type you will find in the IPF or Husky documentation without having to worry about using the converter.

About IPF#

The Open eHealth Integration Platform, commonly known as IPF, is a Java framework that provides an extension to Apache Camel to support common interfaces for health-care related data exchange, mainly around IHE and HL7 specifications. It contains a set of components, type converters, and utilities that will make easy for us to implement the Swiss EPR in a Java application.

Setting up your application#

As said, we will focus here on a Spring Boot application, as it is the most common way to use IPF and Camel together. You can use IPF without Spring Boot, but it will be more difficult to set up and use, requiring you to manually configure the Camel context and some of the IPF components.

Adding the dependencies#

To import IPF dependencies in the project, we will use the Spring Boot starter artifacts, providing all necessary dependencies and auto-configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <properties>
    <ipf-version>4.8.0</ipf-version>
  </properties>
  
  <dependencies>
    <dependency>
      <groupId>org.openehealth.ipf.boot</groupId>
      <artifactId>ipf-xds-spring-boot-starter</artifactId>
      <version>${ipf-version}</version>
    </dependency>
    <dependency>
      <groupId>org.openehealth.ipf.boot</groupId>
      <artifactId>ipf-hl7v3-spring-boot-starter</artifactId>
      <version>${ipf-version}</version>
    </dependency>
    <dependency>
      <groupId>org.openehealth.ipf.boot</groupId>
      <artifactId>ipf-hpd-spring-boot-starter</artifactId>
      <version>${ipf-version}</version>
    </dependency>
    <dependency>
      <groupId>org.openehealth.ipf.commons</groupId>
      <artifactId>ipf-commons-ihe-xua</artifactId>
      <version>${ipf-version}</version>
    </dependency>
  </dependencies>
</project>

Configuration at startup#

The first step to send a message with Camel is to instantiate the client that will send the message. The client is an instance of the ProducerTemplate class, which is instantiated from the Camel context. The Camel context is automatically instantiated by org.apache.camel:camel-spring and you can autowire it with Spring Boot.

java Camel client instantiation
1
2
3
4
5
6
7
8
@Config
public class AppConfig {

    @Bean
    public ProducerTemplate createProducerTemplate(final CamelContext camelContext) {
        return camelContext.createProducerTemplate();
    }
}

A ProducerTemplate can be resource-intensive, because it creates a new thread pool for each instance and keeps a cache of initialized transactions (i.e. when you send a message for the first time, Camel initializes the component needed by the transaction type, which can take a few seconds). Camel recommends to create it once and reuse it; you are not meant to create a new instance for each message you want to send.

Sending a request#

Once you have the ProducerTemplate, you can use it to send a message to a Camel endpoint. Camel uses URIs to defines the destination of the message, the protocol to use, and the options to apply. An example of endpoint sending a PIXv3 Query message to an HTTP endpoint would be: pixv3-iti45://www.example.com/pix-manager?secure=true&audit=true&sslContextParameters=#pixContext It is made of:

The endpoint URI#

You should have received from your EPR community the HTTP endpoints for all the transactions. Now, you need to build the endpoint URI to send a message to that endpoint. All needed schemes are defined by IPF and can be found in its documentation:

TransactionURI scheme
ITI-45pixv3-iti45
ITI-47pdqv3-iti47
ITI-44pixv3-iti44
ITI-18xds-iti18
ITI-41xds-iti41
ITI-43xds-iti43
ITI-57xds-iti57
ITI-58hpd-iti58
ITI-59hpd-iti59

mTLS configuration#

The Swiss EPR environment requires the use of mTLS for all transactions. To configure mTLS in Camel, you need to set the two following options in the endpoint URI:

While the first setting is a simple boolean value, the second one is a reference to a bean. You need to define a named bean in your application for your instance of SSLContextParameters. That bean will contain a keystore, which contains the private key of your client certificate to authenticate to the server, and a truststore, which contains the public key of the server certificate that you can use to authenticate the server (ensuring you are connected to a legitimate host).

java SSLContextParameters bean definition
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Config
public class AppConfig {

    @Bean("eprTlsContext")
    public SSLContextParameters createSSLContextParameters() {
        // The keystore contains the private key of your client certificate
        final var keyStoreParameters = new KeyStoreParameters();
        keyStoreParameters.setResource("file:/path/to/your/keystore.jks");
        keyStoreParameters.setPassword("your-keystore-password");

        // The truststore contains the public key of the server certificate
        final var trustStoreParameters = new KeyStoreParameters();
        trustStoreParameters.setResource("file:/path/to/your/truststore.jks");
        trustStoreParameters.setPassword("your-truststore-password");

        final var sslContextParameters = new SSLContextParameters();
        sslContextParameters.setKeyManagers(new KeyManagersParameters());
        sslContextParameters.setTrustManagers(new TrustManagersParameters());
        sslContextParameters.setSecureSocketProtocol("TLSv1.2");
        sslContextParameters.setKeyStore(keyStoreParameters);
        sslContextParameters.setTrustStore(trustStoreParameters);

        return sslContextParameters;
    }
}

With this example, you can now send a message with the configured mTLS context by referencing it in the endpoint URI: pixv3-iti45://www.example.com?secure=true&<strong>sslContextParameters=#eprTlsContext</strong>. For more information, you can refer to the ‘Web Service Security’ IPF documentation.

ATNA audit configuration#

Another requirement of the Swiss EPR is to audit all transactions, meaning to send a DICOM Audit Message to the community, detailing the transaction. IPF provides a component to automatically audit all transactions, with all information required by the Swiss EPR. Two options are used to enable the audit of a transaction: The

java AuditContext bean definition
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
public class AppConfig {

    @Bean("eprAuditContext")
    public AuditContext auditContext() {
        final var tlsParameters = this.createAtnaTlsParameters();
        final var auditContext = new DefaultAuditContext();
        auditContext.setTlsParameters(tlsParameters);
        auditContext.setAuditTransmissionProtocol(new TLSSyslogSenderImpl(tlsParameters));
        auditContext.setAuditSourceId("1.2.3");
        auditContext.setAuditEnterpriseSiteId("1.2.3");
        auditContext.setAuditRepositoryHost("arr.example.com");
        auditContext.setAuditRepositoryPort(514);
        return auditContext;
    }

    @Bean
    public AuditMetadataProvider auditMetadataProvider() {
        final var provider = new DefaultAuditMetadataProvider();
        provider.setSendingApplication("MyApplication");
        provider.setHostName("my-application-dev-01-srv");
        return provider;
    }
}

You can refer to the ‘ATNA Auditing’ IPF documentation for more details.

Other options#

Other options can be set in the endpoint URI to configure the transaction. Some of the most common ones are:

Writing the requests and reading the responses#

Now that we know how to build the endpoint URI, assuming you know the particular hosts and paths to use in your EPR community, and that we know how to configure the mTLS and audit context, we need to write the requests and read the response.

The IPF documentation provides information about the data models that can be used for each transaction. I am listing here the models I recommend to use for the Swiss EPR transactions:

Transaction messageRecommended data object
ITI-45 requestPixV3QueryRequest
ITI-45 responsePixV3QueryResponse
ITI-47 requestPRPAIN201305UV02Type
ITI-47 responsePRPAIN201306UV02Type
ITI-44 requestPRPAIN201301UV02Type, PRPAIN201302UV02Type, PRPAIN201304UV02Type
ITI-44 responseMCCIIN000002UV01Type
ITI-18 requestQueryRegistry
ITI-18 responseQueryResponse
ITI-41 requestProvideAndRegisterDocumentSet
ITI-41 responseResponse
ITI-43 requestRetrieveDocumentSet
ITI-43 responseRetrievedDocumentSet
ITI-57 requestRegisterDocumentSet
ITI-57 responseResponse
ITI-58 requestBatchRequest
ITI-58 responseBatchResponse

When the request body is created, we can now initiate an instance of the transaction, set the request, send it and read the response. In Camel, a transaction instance is an instance of the Exchange class, which contains the request and the response, plus other properties.

java Creating an Exchange
1
Exchange exchange = new DefaultExchange(producerTemplate.getCamelContext(), ExchangePattern.InOut);

The first parameter is used to bind the exchange to the Camel context (to access its bean registry), and the second one is the exchange pattern: it informs Camel we are expecting a response.

We can then set the request in the Exchange body. In that exchange pattern, the request is called In, and the response is called Out, which can be confusing when implementing the client actor.

java Setting the request in the Exchange
1
2
3
4
5
final var pixRequest = new PixV3QueryRequest();
pixRequest.setQueryPatientId(new II("2.756.1.2.3.4", "patient-12345"));
// Set other request parameters

exchange.getIn().setBody(pixRequest);

The request is now ready to be sent to the endpoint. This is done through the ProducerTemplate instance:

java Sending the request
1
2
final var endpointUri = "pixv3-iti45://www.example.com?secure=true&sslContextParameters=#eprTlsContext";
exchange = producerTemplate.send(endpointUri, exchange);

The method is synchronous and will block until the response is received. The response is now in the Out body, and it must be read not with the getOut() method, but with the getMessage() for some reason. But before reading the response, you should check if the exchange has been successful, as the response can be null if the transaction failed. In that case, an exception can be found in the exchange properties.

java Checking the response
1
2
3
4
5
if (exchange.getException() !== null) {
  throw result.getException();
}
final PixV3QueryResponse response = exchange.getMessage().getBody(PixV3QueryResponse.class);
response.getPatientIds(); // Do something with the response

Using a XUA assertion#

For the XDS transactions, you will need to insert a XUA assertion in the message, to be authorized to access the patient record. The XUA assertion is a SAML token that you get from your EPR community.

As we have seen earlier, when using IPF or Husky types in SOAP transactions, you only control the SOAP body, and the SOAP envelope is generated automatically. This means that you cannot directly insert the XUA assertion in the SOAP envelope, but you can instruct IPF to insert it for you in the following way:

java Inserting the XUA assertion
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
 * Creates a Web Services Security (WSS) header and adds it to an outgoing message.
 *
 * @param messageOut   The outgoing message.
 * @param xuaAssertion The XUA Assertion to set.
 */
void addWssHeader(final Message messageOut,
                  final Element xuaAssertion) throws ParserConfigurationException {
    List<SoapHeader> soapHeaders = CastUtils.cast((List<?>) messageOut.getHeader(AbstractWsEndpoint.OUTGOING_SOAP_HEADERS));
    if (soapHeaders == null) {
        soapHeaders = new ArrayList<>(1);
        messageOut.setHeader(AbstractWsEndpoint.OUTGOING_SOAP_HEADERS, soapHeaders);
    }

    final var wssDocument = this.newSafeDocumentBuilder().newDocument();
    wssDocument.appendChild(wssDocument.createElementNS(WSSecurityConstants.WSSE_NS, "Security"));
    wssDocument.getDocumentElement().appendChild(wssDocument.adoptNode(xuaAssertion));

    final SoapHeader wssHeader;
    try {
        wssHeader = new SoapHeader(new QName(WSSecurityConstants.WSSE_NS, "Security", WSSecurityConstants.WSSE_PREFIX),
                                   wssDocument.getDocumentElement());
        wssHeader.setDirection(Header.Direction.DIRECTION_OUT);
        soapHeaders.add(wssHeader);
    } catch (final Exception exception) {
        log.error("Error while creating the outgoing WSS header", exception);
        // Handle the exception
    }
}

/**
 * Initializes and configures a {@link DocumentBuilder} that is not vulnerable to XXE injections (XInclude, Billions
 * Laugh Attack, ...).
 *
 * @return a configured {@link DocumentBuilder}.
 * @throws ParserConfigurationException if the parser is not Xerces2 compatible.
 * @see <a
 * href="https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#jaxp-documentbuilderfactory-saxparserfactory-and-dom4j">XML
 * External Entity Prevention Cheat Sheet</a>
 */
DocumentBuilder newSafeDocumentBuilder() throws ParserConfigurationException {
    final var factory = DocumentBuilderFactory.newDefaultInstance();
    factory.setNamespaceAware(true);
    factory.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true);
    factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    factory.setFeature("http://apache.org/xml/features/xinclude", false);
    factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
    factory.setXIncludeAware(false);
    factory.setExpandEntityReferences(false);
    factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
    factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
    return factory.newDocumentBuilder();
}

IPF is looking for a specific message header whose name is contained in the constant AbstractWsEndpoint.OUTGOING_SOAP_HEADERS, and will automatically insert it in the outgoing SOAP envelope.

Warning: when dealing with SAML assertions, you should be aware that the assertion is a signed XML element, and that you need to keep the original XML format when inserting it in the message. Otherwise, the signature will be invalid, and the message will be rejected by the server.