Skip to main content

Create and Encrypt Medical Data

The example in this section demonstrates how to register a medical examination, where some medical exams are performed and a diagnosis is elaborated.

Initiating a Medical Examination​

In Cardinal, the concept of a medical examination is represented through the Contact entity. Generally, a Contact represents a moment when medical data are produced and involves a patient and, usually, one or more healthcare actors. For more details, check the Contact explanation.

As the first step, the user can choose to use an existing Patient or create a new one:

print("Insert the id of a Patient (blank to create a new one): ")
val patientId = readlnOrNull()
val patient = if (patientId.isNullOrBlank()) {
sdk.patient.createPatient(
DecryptedPatient(
id = UUID.randomUUID().toString(),
firstName = "Annabelle",
lastName = "Hall",
).let { sdk.patient.withEncryptionMetadata(it) }
)
} else {
sdk.patient.getPatient(patientId)
}

Next, a new Contact is instantiated with a custom description provided by the user:

val formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
print("Examination description: ")
val description = readln().trim()
val contact = DecryptedContact(
id = UUID.randomUUID().toString(),
descr = description,
openingDate = LocalDateTime.now().format(formatter).toLong()
)

A Contact is an encryptable entity, so a DecryptedContact is used when instantiating it. Besides the description, both the id and openingDate must be set. The openingDate represents the moment when the medical examination starts. Generally, it marks the beginning of the event during which medical data are created.

Being an encryptable entity, the encryption metadata need to be initialized before creating the Contact, just as with the Patient:

val contactWithMetadata = sdk.contact.withEncryptionMetadata(contact, patient)

In this case, the initialization step differs slightly from that of the patient. The function takes two parameters as input: the contact itself and a patient. This is because the metadata will also include a link to the Patient who is the subject of this examination. To prevent data leaks, this link is encrypted, and only users with access to the contact will be able to decipher it.

Finally, the Contact can be encrypted and stored in the cloud:

val createdContact = sdk.contact.createContact(contactWithMetadata)

Registering Medical Data​

After the Contact is created, you can add medical data to it. Medical information is registered using a Service encryptable entity nested within a Contact. In the following examples, data in different formats will be added to the contact that was just created.

Creating Scalar Medical Data (Blood Pressure)​

The first piece of information added to the contact is a blood pressure measurement. In the following snippet, a Service (using its DecryptedService variation, since Service is an encryptable entity) is instantiated with the result of the exam:

val bloodPressureService = DecryptedService(
id = UUID.randomUUID().toString(),
label = "Blood pressure",
identifier = listOf(Identifier(system = "cardinal", value = "bloodPressure")),
content = mapOf(
"en" to DecryptedContent(
measureValue = Measure(
value = Random.nextInt(80, 120).toDouble(),
unit = "mmHg"
)
)
)
)

In this case, a free-text label provides a description for the Service, and an identifier allows for a more structured labeling.

warning

When adding sensitive information to an encryptable entity, always remember that not all fields are encrypted. You can customize the encrypted fields as explained in this how to.

The actual measurement is stored in the content of the Service. This field is a map that associates an ISO language code with Content. In this case, the content contains a measure value that holds the blood pressure result and its unit.

The Service can now be added to the existing Contact:

val contactWithBloodPressure = sdk.contact.modifyContact(
createdContact.copy(
services = setOf(bloodPressureService)
)
)

It is worth noting that even though Service is an encryptable entity, there is no need to call the withEncryptionMetadata method because the entity is nested within another encryptable entity and will inherit its encryption metadata. This means that if the enclosing entity was shared with another user, the nested entity will automatically be shared as well.

Creating Signal-like Medical Data (Electrocardiography)​

A Service can also hold time-series data, signals, and, in general, vector-like data. In the following example, the resulting signal from an ECG (Electrocardiography) exam is added to the Contact through a Service:

val ecgSignal = List(10) { Random.nextInt(0, 100) / 100.0 }
val heartRateService = DecryptedService(
id = UUID.randomUUID().toString(),
identifier = listOf(Identifier(system = "cardinal", value = "ecg")),
label = "Heart rate",
content = mapOf(
"en" to DecryptedContent(
timeSeries = TimeSeries(
samples = listOf(ecgSignal)
)
)
)
)
val contactWithECG = sdk.contact.modifyContact(
contactWithBloodPressure.copy(
services = contactWithBloodPressure.services + heartRateService
)
)

The structure of this Service is almost identical to that of the previous example, with the only difference being that the medical data are stored as timeSeries in the Content instead of measureValue. A TimeSeries entity can contain both 1-dimensional and 2-dimensional signals, as well as aggregated data such as minimum, maximum, and average values.

Creating Medical Image Data​

Due to their larger size, the process of uploading medical images (such as those from X-Ray or CT exams, as well as simple photos) differs from uploading single measurements or signals. This difference is intended to avoid performance loss when querying and retrieving entities that contain large files.

note

A Content has a binaryData field that can be used to store binary data, but for the aforementioned reasons, it should not be used to store large amounts of data.

The first step in uploading a medical image (or another large file) is to create a new Document entity. A Document is an encryptable entity that represents medical documents (e.g., reports, certificates, images) in any format.

val document = DecryptedDocument(
id = UUID.randomUUID().toString(),
documentType = DocumentType.Labresult
)

In this example, a new DecryptedDocument is instantiated with the type set to a laboratory result. Since a Document is encryptable, the encryption metadata must be initialized before it is created on the cloud.

val createdDocument = sdk.document.createDocument(
sdk.document.withEncryptionMetadata(document, null)
)

Note that in this case, the withEncryptionMetadata method takes a null second parameter. This is because there is no need to link the Document directly to the Patient, as the document will be linked to a Service that, in turn, will be linked to the Patient.

Next, you can load the image as an attachment to the Document. A Document can have a single main attachment and multiple secondary attachments. In this case, an "image" is loaded as the main attachment to the document.

val xRayImage = Random.nextBytes(100)
val documentWithAttachment = sdk.document.encryptAndSetMainAttachment(
document = createdDocument,
utis = listOf("public.tiff"),
attachment = xRayImage
)

The bytes composing the image are encrypted and set as the attachment of the Document. Information about the UTI of the attachment is also set.

Finally, it is possible to link this Document with a new Service representing the X-Ray image and add it to the Contact.

val xRayService = DecryptedService(
id = UUID.randomUUID().toString(),
label = "X-Ray image",
identifier = listOf(Identifier(system = "cardinal", value = "xRay")),
content = mapOf(
"en" to DecryptedContent(
documentId = documentWithAttachment.id
)
)
)
val contactWithImage = sdk.contact.modifyContact(
contactWithECG.copy(
services = contactWithECG.services + xRayService
)
)

Adding a Diagnosis​

Diagnoses and other medical contexts that define the health condition of a patient are represented by a HealthElement encryptable entity. In this example, the user will create a HealthElement containing the diagnosis elaborated after the examination.

print("What is the diagnosis?: ")
val diagnosis = readln().trim()
val healthElement = DecryptedHealthElement(
id = UUID.randomUUID().toString(),
descr = diagnosis
)
val createdDiagnosis = sdk.healthElement.createHealthElement(
sdk.healthElement.withEncryptionMetadata(healthElement, patient)
)

A HealthElement is an encryptable entity, so like a Contact, first a DecryptedHealthElement is instantiated with the desired information. Then, the encryption metadata are initialized, linking the HealthElement to a Patient and sharing the entity with the current user. Finally, the entity is created.

It is possible to associate the HealthElement with a Contact by linking it to a SubContact:

val contactWithDiagnosis = sdk.contact.modifyContact(
contactWithImage.copy(
subContacts = setOf(DecryptedSubContact(
descr = "Diagnosis",
healthElementId = createdDiagnosis.id
))
)
)

Closing the Examination​

To indicate that the medical examination has concluded, you can set the closingDate on the corresponding Contact. This action signifies that the data collection session is finished and, ideally, that the Contact will not be modified further.

val finalContact = sdk.contact.modifyContact(
contactWithDiagnosis.copy(
closingDate = LocalDateTime.now().format(formatter).toLong()
)
)

Like openingDate, the closingDate is an instant, precise to the second, represented in the YYYYMMDDhhmmss format.