The importance of XML
XML has established itself as the de facto information exchange format and technology in many fields of software. It certainly has its problems, but without going into comparisons with alternatives or discussions of what works fine and what does not, it it possible to say that XML provides a lot of tooling along with a massive adoption across programming languages and platforms.
XML Schema, XSLT, XPath, XQuery are some of the powerful tools available to users of XML, and they have been widely implemented with strong tooling support just like XML parsers.
A key requirement for almost all clinical applications build on openEHR is to be able to talk to other systems and layers. Other system here does not necessarily mean a piece of software that belongs to another party. It is quite common that same clinical system may use different technology stacks for various reasons and the assumption of a homogeneous, monolithic application is not realistic anymore in today’s software development environment. Therefore, the requirement to move data across layers in an application or across applications is a significant one and Ocean stack provides strong support for XML to respond to this requirement.
The use of XML for representing openEHR data is not an Ocean Informatics specific approach. XML is strongly supported by the openEHR foundation and there is a set of XML schemas published by the foundation. These schemas allow well defined and shared representation of many important components of the openEHR specification. Both during the communications amongst implementers and in this text, this foundation supported and released XML format is called the canonical XML form. The canonical XML guarantees that as long as implementers are able to produce and consume this XML (which complies with the canonical schemas), information can be shared across systems. Moreover, if you develop a clever piece of technology such as an XSLT transformation that creates an HTML display out of a Composition’s XML form represented as canonical XML, your implementation will work on outputs of all openEHR implementations. If you write a transformation from an HL7 message to an openEHR Composition in canonical XML form, all systems would know how to consume the outputs of your transformation. There are many use cases where having an agreed XML schema set helps.
Given that it can be quite useful, there are various ways the stack provides support for XML, ranging from serialisation to full artefact generation for software development and the following sections will cover these options.
XML serialisation and deserialisation
Let’s start with the basic requirement: we have an openEHR Composition at hand and we would like to send it to another point. The Composition is a key type in the openEHR specification as we’ve discussed. It can almost be described as the de-facto document/unit that represents a clinical concept, so it makes sense that it may be passed around. For example, you may be sending clinical data of a patient to some other software which will display them on the screen. Both systems agree to use the canonical XML as the data representation format and you are given a patient id for which you’re supposed to provide a Composition. We’ve seen that we can easily run AQL or use an API call to get back a number of Compositions, so the only step that we would need to take would be to produce canonical XML output of those compositions. Let’s see how we can do this.
XML and Compositions
The XML capabilities of Ocean stack are seamlessly integrated into various points within the implementation, so using them usually does not even require a separate utility type. Here is an example function which will perform a series of actions all of which have been explained before, with a few final steps to show the XML serialisation and deserialisation works:
1: public void SerialiseDeserialiseComposition()
2: {
3: var ehrId = lmSvc.CreateEHR(patId);
4: var tdo = CreateTDO();
5:
6: var compId = lmSvc.CommitNewTDO(ehrId, tdo);
7:
8: var query = "SELECT c FROM EHR e[ehr_id='" + ehrId.Value.ToString() + "'] " +
9: "CONTAINS COMPOSITION c[openEHR-EHR-COMPOSITION.encounter.v1] " +
10: "WHERE c/name/value matches {'Examination'}";
11: var result = lmSvc.ExecuteAQL(query);
12: Composition c = result.Rows[0][0] as Composition;
13: //serialize
14: StringBuilder sb = new StringBuilder();
15: var sWriter = new StringWriter(sb);
16: var xmlWriter = XmlWriter.Create(sWriter);
17: Composition.Serialize(xmlWriter, c);
18: var xml = sb.ToString();
19: var xmlDoc = new XmlDocument();
20: xmlDoc.LoadXml(xml);
21: xmlDoc.Save(@"c:\work\data\temp\tempComp.xml");
22:
23: var xmlDocForRead = new XmlDocument();
24: xmlDocForRead.Load(@"c:\work\data\temp\tempComp.xml");
25: using (XmlNodeReader reader = new XmlNodeReader(xmlDocForRead.DocumentElement))
26: {
27: Composition cDeserialized = Composition.Deserialize(reader);
28: }
29: }
The story up until line 13 should be familiar to you by now. Create an EHR for a patient. Get a TDO instance with some data, persist it, then read it back into a variable ( c ) This is a mini version of a major use case for your system: creating data and reading it back. Now we want to get an XML representation of the variable c. Line 17 is where the magic happens. All we need is an XmlWriter instance, which is a build in .NET framework type. This instance is created between lines 14 and 16 and then the variable c is provided along with XmlWriter instance to a static method that is provided by the Composition type’s implementation in the Ocean stack. at line 18, we get the XML we’re looking for as a string.
Then at line 19 we create an XmlDocument instance, again a type provided by the .NET framework, and load it with the string form of XML (line 20), which we then save to disk at line 21. We could have used the string directly in many cases, but handling XML data properly can be tricky. Writing it to disk in a pretty format, or sending it over the wire as response to a web service call may require some attention to make sure XML stays as valid XML. XmlDocument is a convenient type that lets us do many things safely, and both Ocean’s implementation of XML related functionality and XmlDocument’s functionality are really fast (a few milliseconds at worst for many operations) so its use is encouraged.
The file we’ve created in line 21 is canonical XML. It would pass validation using the published XML schemas from the
Line 23 demonstrates what the recipient of the XML output could have done if they were using the Ocean SDK: other than the call in line 27, this is all standard .NET XML processing and Line 27 is simply calling the Deserialise static method on Composition type to arrive at a Composition instance.
This function actually shows a lot: creating the EHR, the clinical data, persistence to CDR, querying back, how information can leave your system in a well defined format (the act of saving XML to disk) and how you can pull it back in (reading from the disk). Now let’s see other use cases around XML capabilities of Ocean stack.
XML and TDOs
The TDO is a convenient tool under the developer’s belt and for the reasons we’ve discussed in the context of XML and Compositions, it makes sense to have some XML capability for TDOs as well. Luckily for us, this capability has been implemented in the Ocean stack, and there is a method that exposes it in the SDK. Before we see how this method is used, let’s take a look at its implementation as usual:
1: public String SerializeTDO(IComposition pTdo)
2: {
3: var xml = "";
4: IKnowledgeRepository knowledgeRepository = null;
5:
6: try
7: {
8: knowledgeRepository = GetContainer().Resolve<IKnowledgeRepository>();
9: using (var sw = new StringWriter())
10: {
11: XmlWriter xmlWriter = XmlWriter.Create(sw);
12: pTdo.Serialize(knowledgeRepository, xmlWriter);
13: xmlWriter.Close();
14: xml = sw.ToString();
15: }
16: return xml;
17: }
18: catch (Exception ex)
19: {
20: Logger.LogException(ex);
21: throw ex;
22: }
23: finally
24: {
25: GetContainer().Release(knowledgeRepository);
26: }
27: }
The knowledge repository service from Ocean stack is instantiated at line 8 and it is used at line 12 alongside an XmlWriter from the .NET framework standard library to let TDO instance pTdo create an XML representation of itself. The rest of the method is responsible with resource management and error handling.
The deserialisation method for TDO in the Ocean SDK is even simpler and we won’t cover it here. Let’s see how these functions could be used if you’re using the SDK.
1: public void SerializeDeserializeTDO()
2: {
3: var tdo = CreateTDO();
4: var xml = lmSvc.SerializeTDO(tdo);
5: var xmlDoc = new XmlDocument();
6: xmlDoc.LoadXml(xml);
7: Composition comp = null;
8: using (var xNR = new XmlNodeReader(xmlDoc.DocumentElement))
9: {
10: comp = Composition.Deserialize(xNR);
11: }
12:
13: //deserialize
14: var deserTDO = lmSvc.DeserialiseTDO(xml, typeof(ExaminationComposition));
15: }
Line 1 calls the now familiar CreateTDO method that has been giving us our test TDO instance all along the tutorial. Line 2 is calling the SDK method we’ve just discussed and as in the case of Compositions and XML related functionality, we’re using an XmlDocument instance up until line 10 where we call the Deserialize function from the SDK.
Note that we’re deserialising the xml payload from the TDO serialisation into a Composition instance comp (declared at line 7). This is just a minor reminder to show that a TDO is a convenient form of a Composition and we can switch between the two using either API functions as we’ve done before or even XML serialisation/deserialisation. If you go back to XML and Compositions section and take a look at the use case we’ve discussed there, you can see that thanks to XML based deserialisation to TDOs, you can actually work with TDOs on both sides of the communication in that scenario and pass around XML which is actually a Composition. So same XML payload can be deserialised to a Composition or to a TDO.
Line 14 demonstrates this by deserialising the XML payload into a TDO. So the real benefit of XML support for TDOs is that it lets you work with your TDO based interface and when it is time to share that information with another system, you can simply use the XML serialisation and deserialisation features to keep using your interface, regardless of what happens to data outside of your system. Speaking of communicating with the outside world, it makes sense that we discuss the extend of XML functionality supported in the Ocean stack in the context of web services.
XML and Web Services
For the last few sections of this tutorial, we’ve been discussing the idea of sharing information with other systems using XML and our examples so far have been rather simplistic with the goal of focusing on a particular feature and not complicating things with discussions of how XML gets shared, that is the mechanics of moving XML around.
XML Web services have risen to strong dominance in the information systems implementation domain ever since 2000s and even though there is a big shift towards use of simpler implementation methods such as REST, there is still a large user base and tooling for XML Web services. Web services integration of the Ocean SDK will be covered in its own section but there is one aspect of Ocean Stack that enables this integration which we’d like to mention at this point where we’re talking about XML serialisation for Compositions and TDOs.
A key feature of the Ocean stack is that its result set from AQL queries can be cast directly to a ResultsTable type. The ResultsTable type is capable of seamlessly deserialising itself when used as a return type of an XML web service. This means that you can write code that will use AQL and the results can be directly serialized to XML and send over web services to any client that is capable of consuming XML web services. As of today, all major programming languages and platforms have some support for this, which means that you can provide the results of your queries to many other technologies and stacks out there with very little effort. Moreover, if your result set is returning compositions or other complex types from the openEHR reference model (RM), the XML that is generated for web services communication is canonical XML, which means it will be valid according to published XML schemas. This brings all the advantages of XML discussed before in a distributed, platform independent communication setting, which will be covered in detail later in the tutorial.
XML, Template Data Schema (TDS) and Template Data Document (TDD)
Up until now, we’ve focussed on TDO mechanism as an option for representing clinical models in the software development context. It is true that the TDO option helps developers manage the clinical logic, but it is not the only option the Ocean stack provides. The Template Designer has a mechanism called the Template Data Schema (TDS from now on) which is similar to TDO, but uses XML instead of C’# as the target for autogeneation of artefacts.
Template Data Schema (TDS) and Template Data Document (TDD)
XML Schema is a mechanism that enables XML documents to conform to a set of constraints. The schema (XSD) allows definition of valid documents using a type system which includes basic data types and also the capability to compose complex types out of basic types.
This capability has enabled XML to become a convenient data representation formalism for many types of data used across different systems. The Template Designer uses this capability of XML to create a mini type system within an XSD that represents an openEHR template. Template Designer has an option to export a template to a Template Data Schema (TDS from now on) which is an XSD which is automatically generated based on the template.
Just like we’ve done for the TDO mechanism, let’s see the template and the TDS structures. First the Template:
Then the TDS (in the Visual Studio.NET designer view):
Focus on the Comments and DrinksEveryNight fields. The TDS is a good old XML Schema (XSD) but you can see that it looks more comprehensive and detailed compared to the template in the template designer. This difference is due to UIs of the two different tools used. The Template Designer does not display everything on the main UI area and it also does not expose every element and attribute of the RM.
Still, you can see that the TDS is an XSD based on the template. Every TDS creates its own mini type system using the XSD type system capabilities. The obvious question is why?
The TDS is designed to be a messaging convenience. It lets you turn a template into a self contained XML document with its schema so that it can be validated. So the original design goal of the TDS is to establish a messaging mechanism between openEHR implementations.
You may be concerned about the per template based XSD type system, but it is not entirely disconnected from the canonical XSDs from openEHR foundation. Whenever it is possible to do so, the TDS type system reuses types from the canonical XML representation of openEHR specification. There is also another tool at your disposal which makes TDS an option that can go well beyond messaging between openEHR systems in terms of its benefits.
An actual instance of a TDS is an XML file. This XML file, which must be valid according the TDS, is called a template data document (TDD). There is a publicly available XSLT transformation that can transform a TDD to an XML serialisation of a Composition. This mechanism is the answer to the obvious question that you were probably just about to ask: what happens when a TDD arrives at an openEHR implementation? We need a method to go from the message specific TDS type system to openEHR type system. The XSLT transformation to canonical XML solves this problem for us. If you remember what we’ve discussed before in the context of XML, Compositions and TDOs, once you have a canonical XML form of a Composition, you can simply deserialise it to create a Composition object implementation from the Ocean stack.
The hidden benefit of a TDS + TDD based approach to getting information into an openEHR system is that it allows creation of openEHR data without having to access any openEHR implementation or any other tools. The only tool one would need to create a TDS is the Template Designer from Ocean Informatics, which is freely available. After we have a TDS, we are in the XML technology domain where there are a lot of tools which would let us create a TDD that will be valid based on the TDS. Then if we apply the XSLT transformation to canonical XML, we have an XML file that could have been created through the use of a TDO.
This approach allows non openEHR systems to create an openEHR based representation of their data without implementing openEHR for their system functionality. The large tooling base for XML and its platform independent nature allows TDS+TDD approach to be used by a large number of external systems. Given that XML is only about data, this approach can not provide the rich functionality of a full openEHR implementation. TDS based validation is also just XSD validation, it would not be able to provide the full validation capabilities of the Ocean stack on its own which is based on the information in the template which in turn is based on the information in the archetypes. Just because a TDD is valid according to its TDS, it does not mean that it is valid according to all constraints in the openEHR model (archetype + template). Therefore the TDD is not a full replacement for an RM implementation or TDO. Still, there is a significant number of use cases where TDD is a good option.
Using TDD to commit to CDR
Let’s now take a look at the process for taking a TDD and committing it into the CDR. Let’s start with the TDD, which is a valid XML document according the sample TDS which belongs to our sample Template that we’ve been using for this tutorial:
1: <?xml version="1.0" encoding="UTF-8"?>
2: <Examination archetype_node_id="openEHR-EHR-COMPOSITION.encounter.v1"
3: type="COMPOSITION"
4: template_id="LMTestTemp"
5: xmlns="http://schemas.oceanehr.com/templates"
6: xmlns:oe="http://schemas.openehr.org/v1"
7: xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
8: xsi:schemaLocation="http://schemas.oceanehr.com/templates LMTestTemp.xsd"
9: >
10: <name>
11: <value>Examination</value>
12: </name>
13: <language>
14: <terminology_id>
15: <value>ISO_639-1</value>
16: </terminology_id>
17: <code_string>en</code_string>
18: </language>
19: <territory>
20: <terminology_id>
21: <value>ISO_3166-1</value>
22: </terminology_id>
23: <code_string>AU</code_string>
24: </territory>
25: <category>
26: <value>event</value>
27: <defining_code>
28: <terminology_id>
29: <value>openehr</value>
30: </terminology_id>
31: <code_string>433</code_string>
32: </defining_code>
33: </category>
34: <composer xsi:type="oe:PARTY_SELF" />
35: <context other_context_node_id="at0001" other_context_type="ITEM_TREE">
36: <start_time>
37: <oe:value>2005-07-07T00:00:00</oe:value>
38: </start_time>
39: <setting>
40: <oe:value>clinical setting</oe:value>
41: <oe:defining_code>
42: <oe:terminology_id>
43: <oe:value>openehr</oe:value>
44: </oe:terminology_id>
45: <oe:code_string>228</oe:code_string>
46: </oe:defining_code>
47: </setting>
48: </context>
49: <Lmtest archetype_node_id="openEHR-EHR-OBSERVATION.lmtest.v1" type="OBSERVATION" >
50: <name>
51: <value>Lm test event</value>
52: </name>
53: <language>
54: <terminology_id>
55: <value>ISO_639-1</value>
56: </terminology_id>
57: <code_string>en</code_string>
58: </language>
59: <encoding>
60: <terminology_id>
61: <value>IANA_character-sets</value>
62: </terminology_id>
63: <code_string>UTF-8</code_string>
64: </encoding>
65: <subject xsi:type="oe:PARTY_SELF" />
66: <data archetype_node_id="at0001">
67: <PatientEval_as_Point_Event archetype_node_id="at0002" type="POINT_EVENT">
68: <name>
69: <value>PatientEval</value>
70: </name>
71: <time>
72: <oe:value>2005-07-07T00:00:00</oe:value>
73: </time>
74: <data archetype_node_id="at0003" type="ITEM_TREE">
75: <Comments archetype_node_id="at0004" type="ELEMENT" valueType="DV_TEXT">
76: <name>
77: <value>Comments</value>
78: </name>
79: <value xsi:type="oe:DV_TEXT">
80: <oe:value>oe:value</oe:value>
81: </value>
82: </Comments>
83: <DrinksEveryNight archetype_node_id="at0005" type="ELEMENT" valueType="DV_QUANTITY">
84: <name>
85: <value>DrinksEveryNight</value>
86: </name>
87: <value>
88: <magnitude>5.0</magnitude>
89: <units>kg</units>
90: </value>
91: </DrinksEveryNight>
92: </data>
93: </PatientEval_as_Point_Event>
94: </data>
95: </Lmtest>
96: </Examination>
You can see that this TDD is a plain XML file with some namespace definitions and a reference to the TDS (LmTestTemp.xsd) which would allow validation of this TDD. This is a test TDD so not all the values in the document make sense. It is possible that there may be a constraint that can not be expressed with the XSD and XML features, and the problem can only be discovered when the TDD has gone through all transformations up until the point there is an attempt to persist it as a Composition. This TDD has been tested though, so we know that it will work through the next steps we’ll discuss.
As usual, we’ll use simple function to demonstrate the steps: the following function would perform the whole import of a TDD to CDR:
1: public void TransformTDD()
2: {
3: XDocument xDoc = XDocument.Load(".\\Data\\LMTestTemp.xml");
4:
5: XDocument transformedDoc = new XDocument();
6: using (XmlWriter writer = transformedDoc.CreateWriter())
7: {
8: XslCompiledTransform transform = new XslCompiledTransform();
9: transform.Load(XmlReader.Create(new StreamReader(@"C:\work\data\temp\templates\DemoApps\LM\LMServiceLayer\LMartinServiceLayer\Data\TDD_to_openEHR_Canonical-leeds.xslt")));
10: transform.Transform(xDoc.CreateReader(), writer);
11: }
12: var fileName = @"c:\work\data\temp\LMTransformedComposition.xml";
13: transformedDoc.Save(fileName);
14:
15: XmlDocument compDoc = new XmlDocument();
16: compDoc.Load(fileName);
17: Composition comp = null;
18: using (XmlNodeReader reader = new XmlNodeReader(compDoc.DocumentElement))
19: {
20: comp = Composition.Deserialize(reader);
21: }
22:
23: var tdo = comp.AsTemplateObject<ExaminationComposition>();
24:
25: var ehrId = lmSvc.CreateEHR(patId);
26: var compId = lmSvc.CommitNewTDO(ehrId, tdo);
27: }
The XDocument type used in line 3 is another standard .NET type, which loads our TDD from the data directory of the skeleton project.
Up until line 13, all we’re doing is applying the XSLT transformation to TDD so that we end up with a canonical XML document which we save to disk at line 13. We then load this canonical XML file from the disk to a Composition instance at line 20 and create a TDO from the composition at line 23. This TDO is then committed to a brand new EHR created for a patient with id patId (assigned to a random string before this function is called).
This whole process could have been a lot shorter but the code of the function is intentionally explicit and almost bureaucratic to demonstrate steps that might have taken place at different layers. The TDD may have arrived at a web service as an incoming parameter then the web service may apply the XSLT transform and call another web service which may expect a composition so that it can be persisted. Maybe just before the commit a piece of code that checks for a condition for decision support may be required to run, but the code has been written for a TDO which has been generated from the same template that has created the TDS and consequently the TDD.
The point here is to show how different options for handling openEHR data can play together in the SDK with support from the Ocean stack.
Use of these features with all the options of the XML and related technologies provide a flexible and powerful software development environment. There are many options available through use of XML such as generating summary views, user interfaces, or other representations of data. Downstream generation of JSON, YAML, or different clinical information standards such as HL7 messages are some examples of the possible uses for XML based representation of openEHR data.
Now that we have covered the XML related topics, we are ready to take a look at its use to build distributed systems using the SDK through use of web services.
Using Web Services to Develop openEHR based applications with Ocean SDK
An inevitable requirement in today’s software development world is to develop distributed applications. Targeting a service oriented architecture is a well established industry practice and especially in healthcare where there exists a large number of technologies that must be used together, web services are used frequently to connect these technologies. Ocean’s stack’s XML support enables development of applications based on XML web services, with other variations being possible as well (Restfull, Json or Yaml based etc)
The web service skeleton project included with the SDK and its client project demonstrate how functionality discussed before can be exposed and consumed through web services. The web services interface does not expose every method in the SDK, but it allows access to all fundamental functionality one would need to develop openEHR based applications. In the future more capability will be added to this project
Let’s now take a second look at the key use cases we’ve covered previously, this time in a web services based context.
Creating a Knowledge Repository
As before, let’s first take a look at the function(s) we will be calling from the client side. The web service function that creates the knowledge repository is as follows:
1: public string CreateKnowledgeRepository(string pKnowledgeRepositoryName)
2: {
3: var svc = new OceanService();
4: var id = svc.CreateNewKnowledgeRepository(pKnowledgeRepositoryName);
5: return id.Value;
6: }
As before, we’re making sure that we are creating a repository with a name that matches the one in the application configuration file (MEHR) in this case. The relevant section in the app.config/web.config is given below, to make sure you can check and change this value:
1: <knowledgeRepositoryFactoryConfiguration>
2: ...
3: <repositoryProviders>
4: <add name="OperationalTemplates" type="Ocean.KnowledgeRepositoryClient.OptRepository, KnowledgeRepositoryClient.Operational" krsFactoryProvider="KnowledgeRepository" krsRepositoryName="MeHR"/>
5: </repositoryProviders>
krsRepositoryName is the attribute we need match in our knowledge repository creation code.
Going back to the function CreateKnowledgeRepository from the SDKWebService project above, svc is our main SDK service class we’ve used before. The web service is just a thin wrapper around it, and it uses it to create the repository and returns the id.
The code on the client side where the web service is called is even simpler:
1: private SdkWServiceClient _svc = new SdkWServiceClient();
2: ...
3: public void CreateKnowledgeRepository()
4: {
5: var id = _svc.CreateKnowledgeRepository("MEHR");
6: Console.WriteLine(id);
7: }
_svc is the client side proxy to web service, automatically generated by Visual Studio, and all it takes for us is to call the method with the name that matches the configuration on the server (this is mostly a one time configuration step)
With the repository in place, the next step is to insert the opt, this time using the web service.
Inserting an OPT to repository using the web service
The web service function code first:
1: public string InsertOpt(XmlElement pOpt, string pKnowledgeRepoName)
2: {
3: var svc = new OceanService();
4: var opt = svc.InsertOpt(pOpt, pKnowledgeRepoName);
5: return opt.Uid.Value;
6: }
Again, it is a thin wrapper, nothing more. The parameter pOpt with type XmlElement is simply an XML document. The advantage of OPT being an XML based artefact is it can travel over SOAP calls easily, which is what makes it easy to upload an OPT to server using a web service. This is what the client side code does:
1: public void InsertOpt()
2: {
3: var optXML = new XmlDocument();
4: optXML.Load(@"C:\work\data\temp\templates\LMTestTemp.opt");
5: var id = _svc.InsertOpt(optXML.DocumentElement, "MEHR");
6: Console.WriteLine(id);
7: }
The client side code is using the same reference _svc as before and it is just sending the OPT to the web service.
With our fundamental clinical model infrastructure configured, we can again move to creating EHRs for patients.
Creating an EHR using the web service
The web service function:
1: public string CreateEHR(string pPatientId)
2: {
3: var svc = new OceanService();
4: var id = svc.CreateEHR(pPatientId);
5: return id.Value;
6: }
called by the client side code (a one liner actually):
1: public void CreateEHR()
2: {
3: var id = _svc.CreateEHR(GetPatientId());
4: Console.WriteLine(id);
5: }
creates the EHR and returns its id. GetPatientId in line 3 just a utility method that generates a random patient id string every time it is called. Finding the EHR using the patient id which is associated with it is easy.
Finding the EHR of a patient using the web service
The web service expects the patient id as string:
1: public string FindEHR(string pPatientId)
2: {
3: var svc = new OceanService();
4: var id = svc.FindEHR(pPatientId);
5: return id.Value;
6: }
and returns the EHR id. The client side call shows the both the creation of the EHR and its querying:
1: public void FindEHR()
2: {
3: var patId = GetPatientId();
4: //we're just making sure that there is an
5: //EHR for the pat id. Assume that the call below took place
6: //before and we don't know the EHR id
7: var ehrId = _svc.CreateEHR(patId);
8:
9: var foundEhrId = _svc.FindEHR(patId);
10: Console.WriteLine(ehrId.Equals(foundEhrId));
11: }
Pushing data to server: TDDs and Compositions
In case of web services, we will not use TDO option to send data to server. The TDO is a .NET based solution for creating clinical data. The web services provide the advantage of using XML as the data representation format and we’ll assume that the clients of the web service have no capability to work with .NET. Even though this may not be the case, assuming TDD based commits addresses a much larger client base in terms of technology so we will use this option. Compositions are also instances of the openEHR Reference Model (RM) Composition class but remember that there is a canonical XML schema for Compositions, which means that even if you don’t have access to an RM implementation, you can still create an XML file that represents a Composition and pretty much every openEHR implementation should be able to deserialize this to an actual Composition object implementation. The following examples cover TDDs and Compositions as means of committing data using web services
Inserting a TDD using the web service
The web service call uses XSLT to transform TDD to canonical XML before deserialising it to a Composition object as discussed in the previous paragraph:
1: public string InsertTDD(XmlElement pTDD, string pPatientId)
2: {
3: var svc = new OceanService();
4: var compDocument = svc.TDDToComposition(pTDD);
5: Composition comp = svc.DeserialiseComposition(compDocument);
6: var compId = svc.CommitComposition(svc.FindEHR(pPatientId), comp);
7: return compId.Root.Value;
8: }
TDDToComposition called at line 4 is just a utility method that wraps the XSLT transformation. Note that we’re returning compId.Root.Value and not compId. This is because compId is not a string, and even if it would serialize itself to XML automatically to play nice with SOAP, it contains more information that we’d like to send over the wire, such as version id of composition and system id. So we simply return the unique id portion at line 7. Finding the EHR at line 6 so that we can pass it as a parameter to CommitComposition in the same line is a standard operation for us now, having seen it used many times. It would make sense though to check FindEHR for null return value before the call at line 6, to make sure we can throw an exception that clearly tells what the problem is (there is no EHR for a patient with this id!)
The client code is simply loading an XML file from the disk and sending it to web service, though it demonstrates the complete scenario by creating an EHR first:
1: public void InsertTDD()
2: {
3: var patId = GetPatientId();
4: //we're just making sure that there is an
5: //EHR for the pat id. Assume that the call below took place
6: //before and we don't know the EHR id
7: var ehrId = _svc.CreateEHR(patId);
8:
9: var xDoc = new XmlDocument();
10: xDoc.Load(@"C:\work\data\git-ocean-sdk\ocean-sdk\src\OceanSDK-Service-Layer\SDKService\Data\LMTestTemp.xml");
11: var compositionId = _svc.InsertTDD(xDoc.DocumentElement, patId);
12: Console.WriteLine(compositionId);
13: }
Inserting multiple TDDs under the same contribution using the web service
As discussed before, a single template and consequently a single TDD may not be enough to cover all the information that will be persisted together. Instead of creating mega models, we would rather commit multiple TDDs based on multiple templates together and using a single contribution is the way to achieve this outcome.
The web service function uses XSLT transformation to canonical XML again, creating a number of Composition XMLs:
1: public string[] InsertTDDs(XmlElement[] pTDDs, string pPatientId)
2: {
3: var svc = new OceanService();
4: var lstTdds = new List<Composition>();
5: foreach (var xmlElement in pTDDs)
6: {
7: lstTdds.Add(svc.DeserialiseComposition(svc.TDDToComposition(xmlElement)));
8: }
9: var ids = svc.CreateCompositions(svc.FindEHR(pPatientId), lstTdds);
10: return ids.ToArray();
11: }
the OceanService instance uses a single contribution to commit all Compositions together and returns the ids.
The client side code is very similar to previous examples, here we simply use the same TDD file for a number of times, but it could have been different TDD files.
1: public void InsertTDDs()
2: {
3: var patId = GetPatientId();
4: //we're just making sure that there is an
5: //EHR for the pat id. Assume that the call below took place
6: //before and we don't know the EHR id
7: var ehrId = _svc.CreateEHR(patId);
8:
9: //emulate multiple TDDs using same file numerous times
10: var lst = new List<XmlElement>();
11: foreach (var i in Enumerable.Range(0,5))
12: {
13: var xDoc = new XmlDocument();
14: xDoc.Load(@"C:\work\data\git-ocean-sdk\ocean-sdk\src\OceanSDK-Service-Layer\SDKService\Data\LMTestTemp.xml");
15: lst.Add(xDoc.DocumentElement);
16: }
17:
18: var ids = _svc.InsertTDDs(lst.ToArray(), patId);
19: foreach (var id in ids)
20: {
21: Console.WriteLine(id);
22: }
23: }
Composition XML based use cases are very similar to TDDs, with the extra benefit of not needing XSLT based transforms on the server side.
Inserting single Composition and multiple compositions through the web service
The web service method expects a Composition XLM document which must be in the the canonical XML format:
1: public string InsertComposition(XmlElement pComposition, string pPatientId)
2: {
3: var svc = new OceanService();
4: var id = svc.CommitComposition(svc.FindEHR(pPatientId), pComposition);
5: return id.Root.Value;
6: }
The client side could have created a Composition in XML form from scratch, but it is more likely that there will be a TDD available, so we convert this TDD using XSLT to canonical XML and pass that to web service:
1: public void InsertComposition()
2: {
3: var patId = GetPatientId();
4: //we're just making sure that there is an
5: //EHR for the pat id. Assume that the call below took place
6: //before and we don't know the EHR id
7: var ehrId = _svc.CreateEHR(patId);
8:
9: var xmlDoc = TDDToCanonical(@".\Data\LmTestTemp.xml");
10: var id = _svc.InsertComposition(xmlDoc.DocumentElement, patId);
11: Console.WriteLine(id);
12: }
It may seem appear redundant to perform an XSLT transformation when you already have a TDD at hand and there exists a web service method that accepts a TDD, but you may end up applying the XSLT transformation for some other reason.
Remember that a TDD is related to a template, and there may be some generic logic that addresses a Composition such as a patient safety check or a decision support rule. In this case, you may need to perform a transformation to a Composition and even modify its contents and it would be convenient to use that Composition to persist data.
Just like the TDD case, there may be multiple compositions you’d like to insert together under the same transaction (Contribution from an openEHR point of view) so here is the web service method and corresponding client call:
1: public string[] InsertCompositions(XmlElement[] pCompositions, string pPatientId)
2: {
3: var svc = new OceanService();
4: var lstTdds = new List<Composition>();
5: foreach (var xmlElement in pCompositions)
6: {
7: lstTdds.Add(svc.DeserialiseComposition(xmlElement));
8: }
9: var ids = svc.CreateCompositions(svc.FindEHR(pPatientId), lstTdds);
10: return ids.ToArray();
11: }
1: public void InsertCompositions()
2: {
3: var patId = GetPatientId();
4: //we're just making sure that there is an
5: //EHR for the pat id. Assume that the call below took place
6: //before and we don't know the EHR id
7: var ehrId = _svc.CreateEHR(patId);
8:
9: //mimic multiple compositions using same file
10: var lst = new List<XmlElement>();
11: foreach (var i in Enumerable.Range(0,5))
12: {
13: var xmlDoc = TDDToCanonical(@".\Data\LmTestTemp.xml");
14: lst.Add(xmlDoc.DocumentElement);
15: }
16:
17: var ids = _svc.InsertCompositions(lst.ToArray(), patId);
18: foreach (var id in ids)
19: {
20: Console.WriteLine(id);
21: }
22: }
Updating existing Compositions with TDDs using the web service
Now that we have covered how to insert data through both TDDs and Compositions, let’s see how we can perform updates using the web service. First, the web service method that performs the TDD update:
1: public int UpdateTDD(String pPatientId, XmlElement pTDD, string pCompositionId)
2: {
3: var svc = new OceanService();
4: return svc.UpdateTDD(svc.FindEHR(pPatientId),pTDD, pCompositionId);
5: }
Just a redirection to SDK service type method. Note that the pCompositionId parameter means that for an update to an existing Composition you need to provide its id. There are multiple ways of getting access to that id based on your use case but an AQL query may be one possible way. This method returns the latest version after the update. That is, a minimum of 2 since an insert creates a version id of 1 automatically.
The client code is self explanatory and it uses the same TDD multiple times. There is no change in TDD content, but each commit creates a new version and writing the returned version ids to console lets us see this:
1: public void UpdateTDD()
2: {
3: var patId = GetPatientId();
4: //we're just making sure that there is an
5: //EHR for the pat id. Assume that the call below took place
6: //before and we don't know the EHR id
7: var ehrId = _svc.CreateEHR(patId);
8:
9: var xDoc = new XmlDocument();
10: xDoc.Load(@"C:\work\data\git-ocean-sdk\ocean-sdk\src\OceanSDK-Service-Layer\SDKService\Data\LMTestTemp.xml");
11: var compositionId = _svc.InsertTDD(xDoc.DocumentElement, patId);
12: Console.WriteLine(compositionId);
13:
14: //send the TDD again, as an update to previous insert
15: //asssume that something has changed and the TDD has been created with this different information
16: var newVersionId = _svc.UpdateTDD(patId,xDoc.DocumentElement, compositionId);
17: //should be 2
18: Console.WriteLine(newVersionId);
19:
20: newVersionId = _svc.UpdateTDD(patId, xDoc.DocumentElement, compositionId);
21: //should be 3
22: Console.WriteLine(newVersionId);
23:
24: }
The use case for multiple TDDs is quite similar to inserts for multiple TDDs; we want things to happen under the same transaction. The client use case here is obviously an update to a set of template driven components, maybe a UI update or arrival of a set of records for a patient which must be added to their lifetime records. The server side code is:
1: public CompositionVersionInfo[] UpdateTDDs(String pPatientId, TDDUpdateRequest[] pUpdateRequest)
2: {
3: var svc = new OceanService();
4: return svc.UpdateTDDs(svc.FindEHR(pPatientId), pUpdateRequest);
5: }
Note that the pUpdateRequest parameter is asking for an array of TDDUpdateRequest, a quite simple type which basically contains TDD content and existing Composition’s id so that the content can be used to update it. An array of this type therefore contains all the content for update along with the Composition ids to find the existing compositions.
The client side call shows how update requests are put together:
1: public void UpdateTDDs()
2: {
3: var patId = GetPatientId();
4: //we're just making sure that there is an
5: //EHR for the pat id. Assume that the call below took place
6: //before and we don't know the EHR id
7: var ehrId = _svc.CreateEHR(patId);
8:
9: var tdd1 = new XmlDocument();
10: tdd1.Load(@"C:\work\data\git-ocean-sdk\ocean-sdk\src\OceanSDK-Service-Layer\SDKService\Data\LMTestTemp.xml");
11: var compositionId1 = _svc.InsertTDD(tdd1.DocumentElement, patId);
12: Console.WriteLine(compositionId1);
13:
14: var tdd2 = new XmlDocument();
15: tdd2.Load(@"C:\work\data\git-ocean-sdk\ocean-sdk\src\OceanSDK-Service-Layer\SDKService\Data\LMTestTemp.xml");
16: var compositionId2 = _svc.InsertTDD(tdd2.DocumentElement, patId);
17: Console.WriteLine(compositionId2);
18:
19: var tdd3 = new XmlDocument();
20: tdd3.Load(@"C:\work\data\git-ocean-sdk\ocean-sdk\src\OceanSDK-Service-Layer\SDKService\Data\LMTestTemp.xml");
21: var compositionId3 = _svc.InsertTDD(tdd3.DocumentElement, patId);
22: Console.WriteLine(compositionId3);
23:
24: //send multiple TDDs this time, as an update to previous inserts
25: //asssume that something has changed and the TDDs have been updated/recreated from UI with different information
26: TDDUpdateRequest[] updateRequests = new TDDUpdateRequest[3];
27:
28: updateRequests[0] = new TDDUpdateRequest();
29: updateRequests[0].CompositionId = compositionId1;
30: updateRequests[0].TDDContent = tdd1.DocumentElement;
31:
32: updateRequests[1] = new TDDUpdateRequest();
33: updateRequests[1].CompositionId = compositionId2;
34: updateRequests[1].TDDContent = tdd2.DocumentElement;
35:
36: updateRequests[2] = new TDDUpdateRequest();
37: updateRequests[2].CompositionId = compositionId3;
38: updateRequests[2].TDDContent = tdd3.DocumentElement;
39:
40: CompositionVersionInfo[] results = _svc.UpdateTDDs(patId, updateRequests);
41: foreach (var result in results)
42: {
43: Console.WriteLine(result.CompositionId + ": " + result.CurrentVersion.ToString());
44: }
45: }
Updating existing Compositions with XML Compositions using the web service
This use case is build on the discussions and justifications of TDD and Composition update scenarios discussed above, so we won’t go over these again, let’s see the source code to both server side method and client side method anyway:
1: public CompositionVersionInfo[] UpdateCompositions(String pPatientId, CompositionUpdateRequest[] pUpdateRequests)
2: {
3: var svc = new OceanService();
4: return svc.UpdateCompositions(svc.FindEHR(pPatientId), pUpdateRequests.ToList()).ToArray();
5: }
The client side code uses TDDToCanonical utility method to apply XSLT transform to canonical XML:
1: public void UpdateCompositions()
2: {
3: var patId = GetPatientId();
4: //we're just making sure that there is an
5: //EHR for the pat id. Assume that the call below took place
6: //before and we don't know the EHR id
7: var ehrId = _svc.CreateEHR(patId);
8:
9: var xmlDoc1 = TDDToCanonical(@".\Data\LmTestTemp.xml");
10: var id1 = _svc.InsertComposition(xmlDoc1.DocumentElement, patId);
11: Console.WriteLine(id1);
12:
13: var xmlDoc2 = TDDToCanonical(@".\Data\LmTestTemp.xml");
14: var id2 = _svc.InsertComposition(xmlDoc2.DocumentElement, patId);
15: Console.WriteLine(id2);
16:
17: var xmlDoc3 = TDDToCanonical(@".\Data\LmTestTemp.xml");
18: var id3 = _svc.InsertComposition(xmlDoc3.DocumentElement, patId);
19: Console.WriteLine(id3);
20:
21: var upReqs = new CompositionUpdateRequest[3];
22:
23: upReqs[0] = new CompositionUpdateRequest();
24: upReqs[0].CompositionContent = xmlDoc1.DocumentElement;
25: upReqs[0].CompositionId = id1;
26:
27: upReqs[1] = new CompositionUpdateRequest();
28: upReqs[1].CompositionContent = xmlDoc2.DocumentElement;
29: upReqs[1].CompositionId = id2;
30:
31: upReqs[2] = new CompositionUpdateRequest();
32: upReqs[2].CompositionContent = xmlDoc3.DocumentElement;
33: upReqs[2].CompositionId = id3;
34:
35: //send the Compositions again, as an update to previous inserts
36: //asssume that something has changed and the Compositions has been updated/recreated from UI with different information
37: var results = _svc.UpdateCompositions(patId, upReqs);
38: foreach (var result in results)
39: {
40: Console.WriteLine(result.CompositionId + ": " + result.CurrentVersion.ToString());
41: }
42:
43: }
With inserts and updates covered, it is time we look at how we get our data back using the web services.
Running AQL queries using the web service
As discussed and demonstrated before, there is strong support for XML in the Ocean stack, and this support allows XML serialisation of AQL query results so that they can be sent over the wire as web service call responses.
The ExecuteAQL method which is used by the SDK service class returns a type of IResultSet and Ocean stack has a ResultsTable type which implements this interface. The good thing about ResultsTable is that it can be serialized over SOAP, which means we can connect the AQL engine to a web service method with just two lines of code as follows:
1: public ResultsTable ExecuteAQL(string pAqlQuery)
2: {
3: var svc = new OceanService();
4: return svc.ExecuteAQL(pAqlQuery) as ResultsTable;
5: }
The client side code simply passes the AQL query as the parameter and retrieves a table structure which contains rows. The rows have columns which correspond to selected items declared through the SELECT section of the AQL query. The following is a self contained client side method that creates an EHR, inserts a Composition and queries back its UID attribute using AQL:
1: public void QueryComposition()
2: {
3: var patId = GetPatientId();
4: //we're just making sure that there is an
5: //EHR for the pat id. Assume that the call below took place
6: //before and we don't know the EHR id
7: var ehrId = _svc.CreateEHR(patId);
8:
9: var xmlDoc = TDDToCanonical(@".\Data\LmTestTemp.xml");
10: var id = _svc.InsertComposition(xmlDoc.DocumentElement, patId);
11: Console.WriteLine(id);
12:
13: var query = "SELECT c/uid/value FROM EHR e[ehr_id='" + ehrId + "'] " +
14: "CONTAINS COMPOSITION c[openEHR-EHR-COMPOSITION.encounter.v1] " +
15: "CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.lmtest.v1] " +
16: "WHERE c/name/value matches {'Examination'}";
17: var result = _svc.ExecuteAQL(query);
18: var nm = result.name;
19: if (result.rows.Length > 0)
20: {
21: var row1 = result.rows[0];
22: //this should be UID
23: var cell1 = row1[0];
24: }
25: }
When using AQL over the web services, the results for the web service call are compatible with the result set schema designed by Ocean. The xml serialisation of this schema provides a practical view of data when the query is returning fine grained data such as integer or string value at a particular path in the Composition. The example above which returns ids of compositions that match the criteria in the WHERE section of AQL query is another example.
These type of highly granular results are easy to map to native data types of the programming environment that is used to access the web service, such as integers or strings in C# or Java. When there is a need to get Complex objects it may be easier to get the whole Composition that fits the criteria as XML as ExecuteAQL method’s output may be a bit cumbersome to transform into an XML document (though it is possible).
In this case, the recommended approach is to use AQL to get ids of the Compositions of interest and then pull all the compositions fully. Let’s first see how we can get a single composition using its id. First, the web service method:
1: public XmlElement GetComposition(string pCompositionId)
2: {
3: var svc = new OceanService();
4: var composition = svc.FindComposition(new HierObjectId(pCompositionId));
5: return svc.SerialiseComposition(composition).DocumentElement;
6: }
As you can see this method simply uses the composition id to fetch the Composition and serializes to XML. The returned results is canonical XML for an openEHR Composition. Here is a client side method that is self contained like previous examples:
1: public void GetComposition()
2: {
3: var patId = GetPatientId();
4: //we're just making sure that there is an
5: //EHR for the pat id. Assume that the call below took place
6: //before and we don't know the EHR id
7: var ehrId = _svc.CreateEHR(patId);
8:
9: var xmlDoc = TDDToCanonical(@".\Data\LmTestTemp.xml");
10: var id = _svc.InsertComposition(xmlDoc.DocumentElement, patId);
11: Console.WriteLine(id);
12:
13: //this is canonical XML
14: var result = _svc.GetComposition(id);
15: }
The client side code demonstrates the case where you have a single composition id at hand (say, as a result of an AQL query) but it is likely that you’ll also have cases where multiple compositions will match your AQL criteria and you’ll have multiple ids. In this case, you’ll want to get back all the Compositions with the ids you have, so let’s see how a web service method can be written that provides us these compositions:
1: public XmlElement[] GetCompositions(string[] pCompositionIds, string pPatientId)
2: {
3: if (pCompositionIds == null || pCompositionIds.Length < 1)
4: return null;
5: var svc = new OceanService();
6: var ehrId = svc.FindEHR(pPatientId);
7: if(ehrId == null)
8: throw new Exception("No EHR found for patient id " + pPatientId);
9: var criteria = new List<string>();
10: foreach (var c in pCompositionIds)
11: {
12: criteria.Add("'" + c + "'");
13: }
14: var criteriaTxt = String.Join(",", criteria);
15: var query = "SELECT c FROM EHR e[ehr_id='" + ehrId + "'] " +
16: "CONTAINS COMPOSITION c " +
17: "WHERE c/uid/root/value matches {" + criteriaTxt + "}";
18: var compositions = svc.ExecuteAQL(query);
19: if (compositions == null || compositions.Rows.Length < 1)
20: return null;
21: var compXmls = new List<XmlElement>();
22: foreach (var c in compositions.Rows)
23: {
24: var cXML = svc.SerialiseComposition(c[0] as Composition);
25: compXmls.Add(cXML.DocumentElement);
26: }
27: return compXmls.ToArray();
28: }
As you can see, this web service method is simply building an AQL query that uses the composition ids as criteria. Since we know that the returned rows will contain only compositions (SELECT c FROM EHR…) if there are any matches, we simply serialize them to XML, create an array and return it. The client side method that calls this web service method is self contained again, in the sense that it does not use an AQL query to find the composition ids but uses the ids from the results of insert operations. This is just a simplification of course, the ids we’re using could have been obtained from an AQL query. The client method is as follows:
1: public void GetCompositions()
2: {
3: var patId = GetPatientId();
4: //we're just making sure that there is an
5: //EHR for the pat id. Assume that the call below took place
6: //before and we don't know the EHR id
7: var ehrId = _svc.CreateEHR(patId);
8:
9: var xmlDoc1 = TDDToCanonical(@".\Data\LmTestTemp.xml");
10: var id1 = _svc.InsertComposition(xmlDoc1.DocumentElement, patId);
11: Console.WriteLine(id1);
12:
13: var xmlDoc2 = TDDToCanonical(@".\Data\LmTestTemp.xml");
14: var id2 = _svc.InsertComposition(xmlDoc2.DocumentElement, patId);
15: Console.WriteLine(id2);
16:
17: var ids = new string[2];
18: ids[0] = id1;
19: ids[1] = id2;
20:
21: var res = _svc.GetCompositions(ids, patId);
22: }
The result objet res in line 21 is just an array of XML content where each array element is canonical XML of a Composition.
Using an AQL query to get the ids of compositions of interest and then fetching the Compositions in XML form is a flexible pattern which can be used in many settings.