Skip to main content

Implement the Publisher

Publisher Application Overview​

The Publisher is any application that a patient can use to create medical data. In this example, it is a CLI that creates samples simulating a device that measures the sugar level in blood.

Since patient users cannot be created directly through the cockpit and to keep this tutorial self-contained, the application will first log in as a healthcare party to create the patient user. This newly created patient user will then be used for the rest of the example.

Create the Patient User​

First, the user will be prompted to insert the credentials of a healthcare party created through the cockpit. An SDK instance will be initialized for that healthcare party.

private const val CARDINAL_URL = "https://api.icure.cloud"

print("Login: ")
val username = readln().trim()
print("Password: ")
val password = readln().trim()
val sdk = CardinalSdk.initialize(
applicationId = null,
baseUrl = CARDINAL_URL,
authenticationMethod = AuthenticationMethod.UsingCredentials(
UsernamePassword(username, password)
),
baseStorage = FileStorageFacade("./scratch/storage")
)

Npw, the healthcare party user can create a Patient entity.

val newPatient = DecryptedPatient(
id = UUID.randomUUID().toString(),
firstName = "Edmond",
lastName = "Dantes",
)
val patientWithMetadata = sdk.patient.withEncryptionMetadata(newPatient)
val createdPatient = sdk.patient.createPatient(patientWithMetadata)

Then, it creates a user associated to the patient. The healthcare party also creates a temporary token that will allow the new user to log in.

val login = "edmond.dantes.${UUID.randomUUID().toString().substring(0, 6)}@icure.com"
val patientUser = User(
id = UUID.randomUUID().toString(),
patientId = createdPatient.id,
login = login,
email = login
)
val createdUser = sdk.user.createUser(patientUser)
val loginToken = sdk.user.getToken(createdUser.id, "login")

Now, the patient user can log in and instantiate a new sdk. By doing so, they will also initialize their cryptographic key.

CardinalSdk.initialize(
applicationId = null,
baseUrl = CARDINAL_URL,
authenticationMethod = AuthenticationMethod.UsingCredentials(
UsernamePassword(login, loginToken)
),
baseStorage = FileStorageFacade("./scratch/storage")
)

However, the Patient (entity) is an encrypted entity. The healthcare party user could not share it with the Patient (user), because their key were not initialized yet. Now that they are, the healthcare party can share the patient with itself.

note

A user that acts as a patient must be able to access their own patient entity to create, read, and share data through Cardinal.

val patientSecretIds = sdk.patient.getSecretIdsOf(createdPatient)
val patient = sdk.patient.shareWith(
delegateId = createdPatient.id,
patient = createdPatient,
options = PatientShareOptions(
shareSecretIds = patientSecretIds,
shareEncryptionKey = ShareMetadataBehaviour.IfAvailable,
requestedPermissions = RequestedPermission.MaxWrite
)
)

Now that the patient has the permission to access their own information, it can log in and create data.

val patientSdk = CardinalSdk.initialize(
applicationId = null,
baseUrl = CARDINAL_URL,
authenticationMethod = AuthenticationMethod.UsingCredentials(
UsernamePassword(login, loginToken)
)
caution

This registration flow makes sense only in the context of this example, to make it self-contained. To learn how to register a Patient in a real context, check this how to.

Create Medical Data​

The patient user is now completely initialized and can start creating data. The Publisher simulates a medical device capable of measuring glycemia. This measurement is stored through the Cardinal SDK as a Service inside of a Contact, as explained in the previous tutorial.

val glycemiaValue = Random.nextInt(60, 160).toDouble()
val formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
val contact = DecryptedContact(
id = UUID.randomUUID().toString(),
openingDate = LocalDateTime.now().format(formatter).toLong(),
services = setOf(
DecryptedService(
id = UUID.randomUUID().toString(),
content = mapOf(
"en" to DecryptedContent(
measureValue = Measure(
value = glycemiaValue,
unitCodes = setOf(
CodeStub(
id = "UCUM|mmol/L|1",
type="UCUM",
code="mmol/L",
version="1"
)
)
)
)
),
tags = setOf(
CodeStub(
id="LOINC|2339-0|1",
type="LOINC",
code="2339-0",
version="1"
),
CodeStub(
id="CARDINAL|TO_BE_ANALYZED|1",
type="CARDINAL",
code="TO_BE_ANALYZED",
version="1"
)
)
)
)
)

Three different codifications were used in the Service:

  • One (UCUM|mmol/L|1) was used in the Measure to associate to the number the UCUM code for mmol/L, the standard unit for glycemia tests.
  • Another (LOINC|2339-0|1) was used to include the information that the service represents a glycemia test. For that, a LOINC code (2339-0) was used.
  • The last one (CARDINAL|TO_BE_ANALYZED|1) was also used to annotate the service, but this time is an internal code that will be used by the subscriber to recognize the entity that it has to analyze.

Now, the Publisher can initialize the encryption metadata of the entity, sharing it with the healthcare party at the same time, and create it.

For simplicity's sake, the Publisher is getting the id of the healthcare party to share the data with from the responsible field of the patient. This is a field automatically filled at patient creating with the id of the healthcare party who created it. For more information about sharing data, check our tutorial on data sharing.

val recipientHcp = patient.responsible ?: throw IllegalStateException("Patient has no responsible")
val contactWithEncryptionMetadata = patientSdk.contact.withEncryptionMetadata(
base = contact,
patient = patient,
delegates = mapOf(recipientHcp to AccessLevel.Write)
)
patientSdk.contact.createContact(contactWithEncryptionMetadata)

At the same time, the Subscriber will immediately receive the created entity and will perform its analysis.

Review the Analysis Result​

In the full code example available on the repository, the user will be able to register multiple glycemia measurements. After that, they will be asked if they want to display the results of the analysis performed by the Subscriber, that will add a SNOMED-CT code for the diagnosis and an internal code to signal that a Service has been analyzed.

To retrieve the Service where the Subscriber completed the analysis, the Publisher creates a filter.

val filter = ServiceFilters.byTagAndValueDateForSelf(
tagType = "CARDINAL",
tagCode = "ANALYZED"
)
val serviceIterator = patientSdk.contact.filterServicesBy(filter)

This filter will retrieve all the services shared with the Patient that have in the tag field a CodeStub with type CARDINAL and code ANALYZED.

Finally, for each retrieved service, the first CodeStub with type SNOMED is taken, the full Code is retrieved from the cloud and the label is shown.

while(serviceIterator.hasNext()) {
val service = serviceIterator.next(1).first()
val diagnosisOrNull = service.tags.firstOrNull { it.type == "SNOMED" }
if (diagnosisOrNull != null) {
val code = patientSdk.code.getCode(diagnosisOrNull.id!!)
println("The diagnosis for sample ${service.id} is ${code.label?.getValue("en")}")
} else {
println("No diagnosis for this sample")
}
}