Passa ai contenuti principali

HAPI FHIR - HL7 Java Implementation

 



Introduzione

Il protocollo FHIR si sta accreditando come soluzione standard per la gestione delle risorse e la comunicazione nell'ambito del software sanitario. Qui fornisco alcuni concetti teorici di base e qualche esempio d'uso generico.

HL7

HL7, Health Level 7 (HL7) è un'associazione non profit internazionale che si occupa di gestire standard per la sanità. HL7 è riferito anche ad alcuni degli specifici standard creati da questa associazione (es. FHIR v2.x, v3.0, CDA, ecc.). Fondata nel 1987 e riconosciuta dall'American National Standards Institute nel 1994, riunisce organizzazioni affiliate da oltre 40 paesi.

FHIR

In riferimento al protocollo, FHIR è uno standard che descrive lo scambio in forma elettronica di dati in ambiente sanitario per favorire l'interoperabilità nell'ambiente clinico di software diversi. Descrive le interfacce tra applicazioni, la definizione dei dati, dei tempi e gli errori da scambiare fra applicazioni. Il numero 7, nel nome dello standard, indica il livello nello stack OSI (Open System Interconnection) che occupa. FHIR offre le basi per modellare il database come ontologico dove ogni ontologia è una rappresentazione formale di un certo dominio, in formato adatto alla manipolazione da parte di sistemi automatici e umani, che descrive concetti e relazioni tra questi. Ad esempio: pazienti con malattie che ricevono terapie specifiche.

FHIR è un acronimo e sta per:

F - Fast

H - Healthcare

I - Interoperable

R - Resources

Le "risorse" sono l'elemento fondamentale delle transazioni, con le loro proprietà specifiche. Ogni risorsa è accessibile mediante RESTful API ed ha un'URL per la sua identificazione, questa è l'url della risorsa "STEWARD MEDICAL GROUP, INC", che è una "Organization" in un database demo.

Le risorse sono classificare in 5 sezioni:

Foundation: relative al funzionamento e alla definizione dello stesso protocollo (CodeSystem, Bundle)

Base: entità coinvolte direttamente nel processo assistenziale di base (Patient, Location, Device)

Clinical: fanno riferimento ad un record clinico (Condition, Procedure)

Financial: di supporto a gli aspetti finanziari (Account, Coverage)

Specialized: di supporto alla definizione e specializzazione del protocollo (Measure, MedicalProduct)

(vedi : https://www.hl7.org/fhir/resourcelist.html )


Schema E/R stralcio di Patient:

da : https://wiki.hl7.org/index.php?title=File:Fhir-clinical-uml.png


HAPI FHIR

HAPI FHIR è un'implementazione, Open Source sotto licenza Apache 2.0, della specifica HL7 FHIR per Java. La implementazione è fatta ispirandosi alle API JAXB e JAX-WS che sono notoriamente delle API ben sviluppate.

Ogni tipo di risorsa definita in FHIR ha una classe corrispondente con i propri get e set.

HAPI FHIR è una applicazione Spring quindi integrabile in infrastrutture cloud facilmente.

Uso pratico

Istanza pubblica

Per fare dei test di uso ma anche di sviluppo si può usare una istanza messa a disposizione dalla community di sviluppo qui.

Istanza locale

Come al solito per avere la disponibilità locale di una componente risulta comodo usare docker:
$ docker run -p 8080:8080
-e hapi.fhir.default_encoding=json hapiproject/hapi:latest

Si otterrà una istanza del db disponibile sulla porta 8080, se si vuole una porta diversa basta cambiare la porta esterna, nel parametro "-p" che è quella a sinistra dei ":". Dopo l'avvio puntare http://localhost:8080:


Già su questo server è possibile fare delle ricerche o data-entry dall'interfaccia web.

Preparazione dell'ambiente di sviluppo

Mediante Eclipse "Maven > Add Dependency", o modificando a mano il pom.xml del progetto che deve usare HL7, bisogna aggiungere le dipendenze che seguono:

<!-- https://mvnrepository.com/artifact/ca.uhn.hapi.fhir/hapi-fhir-base -->
<dependency>
    <groupId>ca.uhn.hapi.fhir</groupId>
    <artifactId>hapi-fhir-base</artifactId>
    <version>5.3.0</version>
</dependency>


<!-- https://mvnrepository.com/artifact/ca.uhn.hapi.fhir/hapi-fhir-structures-r5 -->
<dependency>
    <groupId>ca.uhn.hapi.fhir</groupId>
    <artifactId>hapi-fhir-structures-r5</artifactId>
    <version>5.3.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/ca.uhn.hapi.fhir/hapi-fhir-client -->
<dependency>
    <groupId>ca.uhn.hapi.fhir</groupId>
    <artifactId>hapi-fhir-client</artifactId>
    <version>5.3.0</version>
</dependency>


Operazioni di CRUD

Create (Creazione)

HAPI consente il popolamento di ogni oggetto in maniera semplificata grazie ad una serie di metodi pensati appositamente. Ad esempio la risorsa "Osservation" ha una proprietà che si chiama "issued" che di tipo "instant" per FHIR (un timestamp per intendersi). Ci sono metodi per usare il tipo di dato FHIR, ma ci sono anche metodi per usare i tipi propri di Java:

Observation obs = new Observation();

// Istruzioni tutte equivalenti
obs.setIssuedElement(new InstantType(new Date()));
obs.setIssuedElement(new InstantType(new Date(), TemporalPrecisionEnum.MILLI));               //FHIR data type
obs.setIssued(new Date());  //Java data type

// Il tipo InstantType, di FHIR, consente di lavorare con instant come Java Date o come una stringa FHIR .

Date date = obs.getIssuedElement().getValue(); // Oggetto Data
String dateString = obs.getIssuedElement().getValueAsString(); 
// "2014-03-08T12:59:58.068-05:00"


Valori codificati ed enumerativi

In FHIR molto spesso vengono usate stringhe codificate. Questo vuol dire che un codice va scelto da una lista specifica di valori disponibili.
Ad esempio in FHIR ci sono dei "ValueSets" che possono assumere alcuni specifici valori come Patient.gender. Questo campo può essere vuoto oppure popolato con un valore preso da una lista di valori definiti da FHIR. HAPI fornisce un set di tipi Enums che semplifica la gestione di questi campi:
Patient patient = new Patient();

// E' possibile settare il valore di questi campi usando 
// una stringa libera
patient.getGenderElement().setValueAsString("male");

// HAPI fornisce tipi Java enumerativi che semplificano 
// l'uso di queste stringhe per ottenere lo stesso risultato 
//di quella precedente
patient.setGender(Enumerations.AdministrativeGender.MALE);

// Si può recuperare il valore allo stesso modo
String genderString = patient.getGenderElement().getValueAsString();
Enumerations.AdministrativeGender genderEnum = patient.getGenderElement().getValue();

Numerosità


Per semplificare l'uso di strutture complesse come quelle del dominio di FHIR si è deciso di fornire supporto a oggetti con numerosità come può essere il nome di una persona costituito da un cognome, da più nomi e da soprannomi. In questo contesto anche usare queste strutture è stato semplice, vediamo come è possibile inserire il nome di un paziente in due forme equivalenti:

Patient patient = new Patient();
HumanName name = patient.addName();
name.setFamily("Smith");
StringType firstName = name.addGivenElement();
firstName.setValue("Rob");
StringType secondName = name.addGivenElement();
secondName.setValue("Bruce");

Che è equivalente a:

Patient patient = new Patient();
patient.addName().setFamily("Smith").addGiven("Rob").addGiven("Bruce");


Integrabilità

Un aspetto molto interessante di questo protocollo è l'apertura verso una facile integrazione in sistemi pre-esistenti. A conferma di questa tesi esiste un approccio molto duttile per la integrazione in sistemi diversi che devono comunicare fra di loro. Lo dimostro con un esempio.

Diciamo che la clinica abc.it voglia adottare una soluzione software basata su FHIR ma abbia in esercizio altri software integrati pre-esistenti che non usano FHIR. Vuole usare FHIR perché deve comunicare con altri sistemi esterni che lo adottano. Un problema sono le anagrafiche. Cioè come identifico Mario Rossi con codice fiscale RSSMRA80A01H501U che nel sistema pre-esistente è identificato da un codice paziente RSSMRA804242344 se devo comunicare, mediante FHIR, con la clinica xyz.com dove Mario Rossi con codice fiscale RSSMRA80A01H501U lo identifica in anagrafica con il codice 4534D432 ?

In FHIR la risorsa Patient ha un campo "identifier" con cardinalità 0..*. Questo identifier nel db dell'azienda abc.it sarà del tipo:

"identifier": [ { "use": "official", "system": "http://www.abc.it/paz", "value": "RSSMRA804242344" } 

]

Mentre nell'azienda xyz.com:

"identifier": [ { "use": "official", "system": "http://www.xyz.com/patient", "value": "4534D432" } ]

Se le cliniche abc e e xyz dovessero aver bisogno di tenere i dati nello stesso database si avrà il paziente Mario Rossi con codice fiscale RSSMRA80A01H501U con un record che lo riguarda strutturato con due identifier:

{
  "resourceType": "Patient",
  "id": "1977038",
  "meta": {
    "versionId": "1",
    "lastUpdated": "2021-03-27T17:29:42.203+00:00",
    "source": "#F58PhsKS87Ov3xn9"
  },
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\">Mario <b>ROSSI </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Identifier</td><td>RSSMRA804242344</td></tr></tbody></table></div>"
  },
  "identifier": [ {
    "system": "http://abc.it/paz",
    "value": "RSSMRA804242344"
  }, {
    "system": "http://xyz.com/patient",
    "value": "4534D432"
  } ],
  "name": [ {
    "family": "Rossi",
    "given": [ "Mario" ]
  } ]
}

Ricerche

Una ricerca molto semplice è per cognome:


IGenericClient client = fhirClientR5Configuration.clientFhihrR5();
Bundle results = client.search().forResource(Patient.class).where(Patient.FAMILY.matches().value(name))
// Lancia una ricerca .returnBundle(Bundle.class).execute();
log.debug("Trovati " + results.getEntry().size() + " pazienti di cognome '" + name + "'");

Mentre la ricerca per il paziente inserito prima con un identifier in due system diversi avverrebbe con un metodo come questo (ometto la parte di definizione del client che è sempre la stessa, come sopra):

// Lancia una ricerca Identifier id = new Identifier(); id.setSystem(system).setId(code); Bundle results = client.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode(system, code)) .returnBundle(Bundle.class).execute(); log.debug("Trovati " + results.getEntry().size() + " pazienti con system "+system + " codice '" + code + "'");


Lettura

Le operazioni di lettura degli oggetti supportano le strutture complesse che questi possono modellare. Gli oggetti FHIR prevedono dei metodi get che costruiscono automaticamente gli oggetti figli di quello che si sta leggendo:


Observation observation = new Observation();

// Nessuna di queste chiamare restituisce null, 
// perchè costruisce i rispettivi oggetti figli
List<Identifier> identifierList = observation.getIdentifier();
CodeableConcept code = observation.getCode();
StringType textElement = observation.getCode().getTextElement();

// DateTimeDt è una primitiva FHIR tuttavia,
// ciò che segue restituisce null
// A meno che non ci sia stato inserito un valore.
Date active = observation.addIdentifier().getPeriod().getStartElement().getValue();


Parsing (deserializzazione)


FHIR non ha strumenti separati per la serializzazione ed il parsing ma è tutto accorpato dall'oggetto "Parser":

// Crea un contesto FHIR 
FhirContext ctx = FhirContext.forR4();

// Questo è un esempio di Paziente serializzato da parsare
String input = "{" +
   "\"resourceType\" : \"Patient\"," +
   "  \"name\" : [{" +
   "    \"family\": \"Simpson\"" +
   "  }]" +
   "}";

// Instanzia un nuovo parser
IParser parser = ctx.newJsonParser();

// Parsa
Patient parsed = parser.parseResource(Patient.class, input);
System.out.println(parsed.getName().get(0).getFamily());


Decodifica (serializzazione)

// Crea un contesto FHIR FhirContext ctx = FhirContext.forR4(); // Crea una risorsa Patient da serializzare Patient patient = new Patient(); patient.addName().setFamily("Simpson").addGiven("James"); // Istanzia un nuovo parser JSON IParser parser = ctx.newJsonParser(); // Lo serializza String serialized = parser.encodeResourceToString(patient); System.out.println(serialized); // Uso di XML serialized = ctx.newXmlParser().encodeResourceToString(patient); System.out.println(serialized);



Client

FHIR fornisce due tipi di client già pronti per l'accesso ai server:

Generic client: molto flessibile e facile da usare.

Annotation client: miglior controllo in fase di compilazione con un maggior numero di funzioni esposte. Richiede maggior sforzo per essere usato.

Ricerche

Usiamo il Generic per cercare un "Patient" per cognome:

FhirContext ctx = FhirContext.forR5(); String serverBase = "http://localhost:8080/fhir"; IGenericClient client = ctx.newRestfulGenericClient(serverBase); // Lancia una ricerca Bundle results = client.search().forResource(Patient.class).where(Patient.FAMILY.matches().value(name)) .returnBundle(Bundle.class).execute(); System.out.println("Trovati " + results.getEntry().size() + " pazienti con nome '" + name + "'");


Attenzione: creare una istanza di FhirContext è molto costoso a livello di risorse quindi bisogna trovare il modo di centralizzare questa creazione e mantenere sempre la stessa istanza per tutta la vita dell'applicazione (singleton). I client invece sono leggeri e se ne può creare una istanza ogni volta che serve.
Per rendere facilmente riproducibili gli esempi qui usiamo ripetere tutto il codice necessario.

Per una ricerca con valori multipli in "OR" (vedi name1 e name2):

FhirContext ctx = FhirContext.forR5(); String serverBase = "http://localhost:8080/fhir"; IGenericClient client = ctx.newRestfulGenericClient(serverBase); // Lancia una ricerca Bundle results = client.search().forResource(Patient.class)
                .where(Patient.FAMILY.matches().value(name1, name2)) .returnBundle(Bundle.class).execute(); System.out.println("Trovati " + results.getEntry().size() + " pazienti con nome '" + name1 + "' oppure '"+name2);


Per una ricerca con valori multipli in "AND" (vedi name1 e name2):

FhirContext ctx = FhirContext.forR5(); String serverBase = "http://localhost:8080/fhir"; IGenericClient client = ctx.newRestfulGenericClient(serverBase); // Lancia una ricerca Bundle results = client.search().forResource(Patient.class)
                .where(Patient.ADDRESS.matches().values("Toronto"))                 .and(Patient.ADDRESS.matches().values("Ontario"))                 .and(Patient.ADDRESS.matches().values("Canada"))                 .returnBundle(Bundle.class)                 .execute(); System.out.println("Trovati " + results.getEntry().size() + " pazienti residenti nella città di 'Toronto', della provincia 'Ontario' nello stato del 'Canada'");


Paginazione:

Se il server supporta i risultati paginati ci sarà uno specifico metodo nel client che restituisce le pagine successive:

FhirContext ctx = FhirContext.forDstu2();
String serverBase = "http://fhirtest.uhn.ca/baseDstu2"; IGenericClient client = ctx.newRestfulGenericClient(serverBase);
// Perform a search Bundle resultBundle = client.search() .forResource(Patient.class) .where(Patient.NAME.matches().value("Smith")) .returnBundle(Bundle.class) .execute(); if (resultBundle.getLink(Bundle.LINK_NEXT) != null) { // load next page Bundle nextPage = client.loadPage().next(resultBundle).execute(); }




Fonti:

https://github.com/hapifhir/hapi-fhir/

https://hapifhir.io/hapi-fhir/docs/model/working_with_resources.html




Commenti

Post popolari in questo blog

Telecamere Ip con accesso "nascosto"

Telecamere Ip con accesso "nascosto" Storia triste di un auto-hacking obbligato che mi ha fatto scoprire come la nostra privacy è realmente messa a rischi. Storia Ho acquistato dal mercatino/fiera del Radioamatore di Fasano quattro telecamere IP. La scatola riporta "Smart Camera" LF4810. Ne ho montata una e testata in tutte le sue funzionalità per oltre un mese. Chiaramente la manualistica scarsissima, come da tradizione in questi prodotti cinesi di costo molto concorrenziale, consiste in un "pieghevole" di 4 facciate. Chiaramente non erano documentate le impostazioni necessarie per attivare i protocolli ONVIF e RTSP che mi sono indispensabili per l'uso che ne devo fare. Nonostante questa scarsa documentazione dopo l'installazione base fatta con l'apposita app: tutto sembrava corretto. Chiaramente la prima azione che ho compiuto è stata quella di cambiare la password che di default è "123". Subito dopo h

Alzatapparella con Shelly 2 e Alexa

Alzatapparella con Shelly 2 e Alexa Qui spiego tutti i passaggi per installare lo Shelly 2 per automatizzare una tapparella e come configurarlo in Alexa. Impianto attuale Collegamenti da effettuare: Schema teorico: Collegamenti reali: Impostazioni: Dopo aver collegato tutto si procede alla configurazione. Prima di tutto installare la App "Shelly" cercandola nel proprio store e create un account: Il dispositivo verrà visto come doppio interruttore. Andare sulla "i" "Impostazioni": Nella sezione "MODO" ed impostare "Roller Shutter" Poi su "APRI/CHIUDI TEMPO DI LAVORO" ed impostare tipo 20 secondi. Altro passaggio importante in caso di tapparelle è quello relativo alla calibrazione (schermata relativa a "Shelly 2.5"). Se si omette questo passaggio non sarà possibile vedere in che stato è (aperta/chiusa

UPS Monitor via USB

Collegamenti Ho collegato l'UPS Mustek PowerMust 800 mediante il cavo USB al server con Ubuntu Linux 18.04. Software Ho installato: $ sudo apt-get install nut nut-cgi Configurazione Ho impostato i permessi di accesso: $ sudo chown root:nut /etc/nut/* $ sudo chown 640 /etc/nut/* Ho impostato nel file ups.conf (il file di configurazione del servizio) [mustek] driver = blazer_usb port = auto desc = "Musteck Power 800 usb" Ho impostato nel file upsd.conf (il file di configurazione del demone del servizio) LISTEN 127.0.0.1 3493 ACL all 0.0.0.0/0 ACL localhost 127.0.0.1/32 ACCEPT localhost REJECT all Ho impostato nel file upsd.users (il file di configurazione degli utenti del servizio) [mustek] password = 123456 allowfrom = localhost upsmon master Ho impostato nel file upsmon.conf (il file di configurazione del servizio di monitoraggio) MONITOR mustek@localhost 1 local_mon 123456 master Ho abilitato il servizio di moni