From 49e672b8c11a3d81a3558147d350f78bfcd38ac0 Mon Sep 17 00:00:00 2001 From: Yannik Warnecke Date: Mon, 21 Oct 2024 10:44:00 +0200 Subject: [PATCH 1/4] Implemented new retriever that requests data by PID and creates an encounter accordingly --- ...L7v22PatientInformationRetrieverByPID.java | 362 ++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetrieverByPID.java diff --git a/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetrieverByPID.java b/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetrieverByPID.java new file mode 100644 index 00000000..0879dd21 --- /dev/null +++ b/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetrieverByPID.java @@ -0,0 +1,362 @@ +package de.imi.mopat.helper.controller; + +import ca.uhn.hl7v2.DefaultHapiContext; +import ca.uhn.hl7v2.HapiContext; +import ca.uhn.hl7v2.app.Connection; +import ca.uhn.hl7v2.app.Initiator; +import ca.uhn.hl7v2.llp.MinLowerLayerProtocol; +import ca.uhn.hl7v2.model.Message; +import ca.uhn.hl7v2.model.v22.group.ADR_A19_QUERY_RESPONSE; +import ca.uhn.hl7v2.model.v22.message.ADR_A19; +import ca.uhn.hl7v2.model.v22.message.QRY_Q01; +import ca.uhn.hl7v2.model.v22.segment.MSH; +import ca.uhn.hl7v2.model.v22.segment.PID; +import ca.uhn.hl7v2.model.v22.segment.QRD; +import ca.uhn.hl7v2.parser.PipeParser; +import de.imi.mopat.dao.AuditEntryDao; +import de.imi.mopat.dao.ConfigurationDao; +import de.imi.mopat.model.Configuration; +import de.imi.mopat.model.Encounter; +import de.imi.mopat.model.dto.EncounterDTO; +import de.imi.mopat.model.enumeration.AuditEntryActionType; +import de.imi.mopat.model.enumeration.AuditPatientAttribute; +import de.imi.mopat.model.enumeration.Gender; +import de.imi.mopat.model.user.User; +import org.slf4j.Logger; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Looks up patient data using HL7 v2.2 messages (QRY^01 to query and ADR_A19 to retrieve). Tries to + * connect to a system using a given hostname (see + * {@link HL7v22PatientInformationRetrieverByPID#getHL7v22PatientInformationRetrieverHostname() }) and + * port (see {@link HL7v22PatientInformationRetrieverByPID#getHL7v22PatientInformationRetrieverPort() }) + * that have to be provided in the configuration.
Different to the official HL7 specification, + * this HL7 patient data retriever does not only allow the value 'AA' as an MSA acknowledgement + * code, but also the value 'CA'. If either one of these values is present as an MSA acknowledgement + * code in the communication server's answer, this patient data retriever tries to get patient data + * out of this very answer and populate the {@link Encounter}. + * + * @version 1.0 + */ +public class HL7v22PatientInformationRetrieverByPID extends PatientDataRetriever { + + private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger( + HL7v22PatientInformationRetrieverByPID.class); + /** + * Codes from HL7 v2 specification telling that the message sent to a communication server + * resulted in acceptance and thus valid data sent back, not error or reject + */ + public static final String MSA_APPLICATION_ACCEPT_CODE = "AA"; + public static final String MSA_COMMIT_ACCEPT_CODE = "CA"; + + // Use the same properties for this exporter + private final String className = "de.imi.mopat.helper.controller.HL7v22PatientInformationRetriever"; + private final String hostnameProperty = "HL7v22PatientInformationRetrieverHostname"; + private final String portProperty = "HL7v22PatientInformationRetrieverPort"; + + public HL7v22PatientInformationRetrieverByPID() { + LOGGER.info("[SETUP] To configure this PatientDataRetriever, please set " + + "HL7 hostname ({}) and port ({}) in the " + "configuration...", hostnameProperty, + portProperty); + } + + @Override + public EncounterDTO retrievePatientData(String patientNumber) { + LOGGER.debug("patientNumber is: {}", patientNumber); + assert patientNumber != null : "The given patientNumber was null"; + patientNumber = patientNumber.trim(); + EncounterDTO result = null; + String hostname = getHL7v22PatientInformationRetrieverHostname(); + Integer port = getHL7v22PatientInformationRetrieverPort(); + if (hostname != null && port != null) { + LOGGER.info("[SETUP] hostname is: {}", hostname); + LOGGER.info("[SETUP] port is: {}", port); + // [bt] bean was able to retrieve a hostname and port + // (configuration), + // thus setting up a connection is worth it + QRY_Q01 hl7message = new QRY_Q01(); + Connection connection = null; + Set patientAttributes = new HashSet(); + try { + LOGGER.debug("Creating a HL7 message to retrieve patient data" + "..."); + // [bt] We sent a QRY^01 message (see MoPat1, + // orbis_interface.rb:create_hl7_msg(patientID)) and set the + // processingID to "P" for PRODUCTION (again, see MoPat1, + // orbis_interface.rb:create_hl7_msg(patientID) and HAPI + // AbstractMessage.initQuickstart()) + hl7message.initQuickstart("QRY", "01", "P"); + // [bt] set the 2 important segments, 'MSH' and 'QRD' + MSH msh = hl7message.getMSH(); + msh.getSendingApplication().setValue("PATB"); + msh.getReceivingApplication().setValue("ORBIS"); + QRD qrd = hl7message.getQRD(); + qrd.getQrd8_WhoSubjectFilter(0).setValue(patientNumber); + + qrd.getQrd9_WhatSubjectFilter(0).setValue("MRO"); + + // [bt] set up a context (well, factory for connections and + // parsers and so on + HapiContext context = new DefaultHapiContext(); + MinLowerLayerProtocol mllp = new MinLowerLayerProtocol(); + mllp.setCharset("ISO-8859-1"); + context.setLowerLayerProtocol(mllp); + // [bt] let the default Pipe parser parse our message + PipeParser parser = context.getPipeParser(); + LOGGER.debug("HL7 message created: {}", parser.encode(hl7message)); + + LOGGER.debug("Opening a Connection for HL7 messaging..."); + // [bt] open a new connection with the given hostname, port, and + // don't use TLS + connection = context.newClient(hostname, port, false); + Initiator initiator = connection.getInitiator(); + initiator.setTimeout(30, TimeUnit.SECONDS); + LOGGER.debug("Opening a Connection for HL7 messaging...[DONE]"); + LOGGER.debug("Sending HL7 message..."); + Message response = initiator.sendAndReceive(hl7message); + patientAttributes.add(AuditPatientAttribute.CASE_NUMBER); + this.getAuditEntryDao() + .writeAuditEntry(this.getClass().getSimpleName(), "retrievePatientData(String)", + patientNumber, patientAttributes, AuditEntryActionType.SENT, + "HL7 Communication server as given in " + "configuration"); + LOGGER.debug("Sending HL7 message...[DONE]"); + + ADR_A19 hl7response = (ADR_A19) response; + + String acknowledgementCode = hl7response.getMSA().getMsa1_AcknowledgementCode() + .getValue(); + + if (acknowledgementCode.equalsIgnoreCase(MSA_APPLICATION_ACCEPT_CODE) + || acknowledgementCode.equalsIgnoreCase(MSA_COMMIT_ACCEPT_CODE)) { + ADR_A19_QUERY_RESPONSE queryResponse = hl7response.getQUERY_RESPONSE(); + + PID pid = queryResponse.getPID(); + if (pid != null) { + patientAttributes.clear(); + result = new EncounterDTO(); + + //Try to set case number from PID4.1 + try { + String caseNumber = pid.getPid4_AlternatePatientID().getValue(); + + if (caseNumber != null) { + result.setCaseNumber(caseNumber); + patientAttributes.add(AuditPatientAttribute.CASE_NUMBER); + } + } catch(Throwable t) { + LOGGER.error("Although a HL7 response was sent, " + + "setting the case number did " + "not work, because of: {}. Use " + + "debug output for stacktrace.", t.getMessage()); + LOGGER.debug("Case ID could no be set because of:" + " {}", t.getMessage()); + } + + + // [bt] separation of tries and error handling for each + // of + // the encounter's fields + try { + if (pid.getDateOfBirth() != null + && pid.getDateOfBirth().getTimeOfAnEvent() != null) { + // [bt] getting the values for the day of birth. + // CAVE: + // Month is from 0 to 11 in GregorianCalendar + int year = pid.getDateOfBirth().getTimeOfAnEvent().getYear(); + int month = pid.getDateOfBirth().getTimeOfAnEvent().getMonth() - 1; + int day = pid.getDateOfBirth().getTimeOfAnEvent().getDay(); + + GregorianCalendar calendar = new GregorianCalendar(year, month, + day); + java.sql.Date dayOfBirth = new java.sql.Date( + calendar.getTimeInMillis()); + + result.setBirthdate(dayOfBirth); + patientAttributes.add(AuditPatientAttribute.DATE_OF_BIRTH); + LOGGER.debug("Date of Birth was available and" + " set"); + } + } catch (Throwable t) { + LOGGER.error("Although a HL7 response was sent, " + + "setting the day of birth did " + "not work, because of: {}. Use " + + "debug output for stacktrace.", t.getMessage()); + LOGGER.debug("Day of birth could no be set because of:" + " {}", t); + } + + try { + if (pid.getPatientName() != null + && pid.getPatientName().getGivenName() != null + && pid.getPatientName().getGivenName().getValue() != null) { + result.setFirstname(pid.getPatientName().getGivenName().getValue()); + patientAttributes.add(AuditPatientAttribute.FIRST_NAME); + LOGGER.debug("Patient's first name was " + "available and set"); + } + } catch (Throwable t) { + LOGGER.error("Although a HL7 response was sent, " + + "setting the first name did not" + " work, because of: {}. Use " + + "debug output for stacktrace.", t.getMessage()); + LOGGER.debug("First name could no be set because of: {}", t); + } + + try { + if (pid.getPatientName() != null + && pid.getPatientName().getFamilyName() != null + && pid.getPatientName().getFamilyName().getValue() != null) { + result.setLastname(pid.getPatientName().getFamilyName().getValue()); + patientAttributes.add(AuditPatientAttribute.LAST_NAME); + LOGGER.debug("Patient's last name was " + "available and set"); + } + } catch (Throwable t) { + LOGGER.error("Although a HL7 response was sent, " + + "setting the last name did not " + "work, because of: {}. Use " + + "debug output for stacktrace.", t.getMessage()); + LOGGER.debug("Last name could no be set because of: {}", t); + } + + try { + if (pid.getSex() != null && pid.getSex().getValue() != null) { + switch (pid.getSex().getValue()) { + case "M": { + result.setGender(Gender.MALE); + patientAttributes.add(AuditPatientAttribute.GENDER); + LOGGER.debug( + "Patient's gender was " + "available and" + " set"); + break; + } + case "F": // [bt] although the word document + // lists + // 'W', the communication server + // sends + // 'F' (see MoPat1, additionally + // tested + // with a real life case at 08th of + // August, 2013) + { + result.setGender(Gender.FEMALE); + patientAttributes.add(AuditPatientAttribute.GENDER); + LOGGER.debug( + "Patient's gender was " + "available and" + " set"); + break; + } + } + } + } catch (Throwable t) { + LOGGER.error( + "Although a HL7 response was sent, " + "setting the gender did not " + + "work, because of: {}. Use " + "debug output for stacktrace.", + t.getMessage()); + LOGGER.debug("Gender could no be set because of: {}", t); + } + + try { + if (pid.getPatientIDInternalID(0) != null + && pid.getPatientIDInternalID(0).getIDNumber() != null + && pid.getPatientIDInternalID(0).getIDNumber().getValue() != null) { + result.setPatientID(Long.valueOf( + pid.getPatientIDInternalID(0).getIDNumber().getValue())); + patientAttributes.add(AuditPatientAttribute.PATIENT_ID); + LOGGER.debug("Patient's ID was available and " + "set"); + } + } catch (Throwable t) { + LOGGER.error("Although a HL7 response was sent, " + + "setting the patient ID did not" + " work, because of: {}. Use " + + "debug output for stacktrace.", t.getMessage()); + LOGGER.debug("Patient ID could no be set because of: {}", t); + } + + this.getAuditEntryDao().writeAuditEntry(this.getClass().getSimpleName(), + "retrievePatientData(String)", patientNumber, patientAttributes, + AuditEntryActionType.RECEIVED, + "HL7 communication server as given in " + "configuration"); + } + } + } catch (Exception e) { + Long currentUserId = null; + if (SecurityContextHolder.getContext().getAuthentication() + .getPrincipal() instanceof User) { + currentUserId = ((User) SecurityContextHolder.getContext().getAuthentication() + .getPrincipal()).getId(); + } + + String loggingAttributes = null; + if (result != null) { + loggingAttributes = result.getLoggingAttributes(); + } + + if (loggingAttributes != null && loggingAttributes.isEmpty()) { + LOGGER.error("User ID {} requested patient data via HL7 for " + + "case number {}. Something went wrong " + + "while retrieving patient data. No " + + "patient data has successfully been " + + "retrieved. Because of the error, no " + + "patient data will be listet for the " + + "given case number. Use debug output " + "for more information", + currentUserId, patientNumber); + } else { + this.getAuditEntryDao().writeAuditEntry(this.getClass().getSimpleName(), + "retrievePatientData(String)", patientNumber, patientAttributes, + AuditEntryActionType.RECEIVED, + "HL7 communication server as given in " + "configuration"); + LOGGER.error("User ID {} requested patient data via HL7 for " + + "case number {}. Something went wrong " + + "while retrieving patient data. " + "Nonetheless, the following data has " + + "successfully been retrieved: {}. " + + "Because of the error, no patient data " + + "will be listet for the given case " + + "number. Use debug output for more " + "information", currentUserId, + patientNumber, loggingAttributes); + } + LOGGER.debug("Retrieving patient data failed because of: {}", e); + + } finally { + if (connection != null) { + LOGGER.debug("Closing the connection..."); + connection.close(); + LOGGER.debug("Closing the connection...[DONE]"); + } + } + } else { + LOGGER.error("hostname is {}, port is {}, thus no HL7 communication is" + + " set up. Returning null", hostname, port); + } + return result; + } + + /** + * Returns the {@link AuditEntryDao} from the ApplicationContext. + * + * @return The {@link AuditEntryDao} from the ApplicationContext. + */ + private AuditEntryDao getAuditEntryDao() { + return ApplicationContextService.getApplicationContext().getBean(AuditEntryDao.class); + } + + /** + * Returns the HL7v22PatientRetriever hostname from the {@link ConfigurationDao} by using the + * name of this class and the appropriate attribute name. + * + * @return The HL7v22PatientRetriever hostname string. + */ + private String getHL7v22PatientInformationRetrieverHostname() { + ConfigurationDao configurationDao = ApplicationContextService.getApplicationContext() + .getBean(ConfigurationDao.class); + Configuration configuration = configurationDao.getConfigurationByAttributeAndClass( + hostnameProperty, className); + return configuration.getValue(); + } + + /** + * Returns the HL7v22PatientRetriever port from the {@link ConfigurationDao} by using the name + * of this class and the appropriate attribute name. + * + * @return The HL7v22PatientRetriever port number. + */ + private Integer getHL7v22PatientInformationRetrieverPort() { + ConfigurationDao configurationDao = ApplicationContextService.getApplicationContext() + .getBean(ConfigurationDao.class); + Configuration configuration = configurationDao.getConfigurationByAttributeAndClass( + portProperty, className); + return Integer.parseInt(configuration.getValue()); + } +} From 87c774475d440355f3272a761397dd4c3474bb96 Mon Sep 17 00:00:00 2001 From: Yannik Warnecke Date: Mon, 21 Oct 2024 10:44:14 +0200 Subject: [PATCH 2/4] Added update script to add new retriever to configuration options --- db/update/v3.2.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 db/update/v3.2.sql diff --git a/db/update/v3.2.sql b/db/update/v3.2.sql new file mode 100644 index 00000000..a52718c3 --- /dev/null +++ b/db/update/v3.2.sql @@ -0,0 +1,5 @@ +USE moPat; + +INSERT INTO moPat.SelectConfiguration_OPTIONS +(SelectConfiguration_id, `options`) +VALUES(33, 'de.imi.mopat.helper.controller.HL7v22PatientInformationRetrieverByPID'); \ No newline at end of file From 7490e8c9afe81a243e292ae8e6b353281b5730bf Mon Sep 17 00:00:00 2001 From: Hussain Sabir Date: Mon, 25 Nov 2024 16:26:24 +0100 Subject: [PATCH 3/4] merged 88->99 --- .../controller/HL7v22PatientInformationRetrieverByPID.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetrieverByPID.java b/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetrieverByPID.java index 0879dd21..1663d6c1 100644 --- a/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetrieverByPID.java +++ b/src/main/java/de/imi/mopat/helper/controller/HL7v22PatientInformationRetrieverByPID.java @@ -15,6 +15,7 @@ import ca.uhn.hl7v2.parser.PipeParser; import de.imi.mopat.dao.AuditEntryDao; import de.imi.mopat.dao.ConfigurationDao; +import de.imi.mopat.model.Clinic; import de.imi.mopat.model.Configuration; import de.imi.mopat.model.Encounter; import de.imi.mopat.model.dto.EncounterDTO; @@ -66,7 +67,7 @@ public HL7v22PatientInformationRetrieverByPID() { } @Override - public EncounterDTO retrievePatientData(String patientNumber) { + public EncounterDTO retrievePatientData(Clinic clinic, String patientNumber) { LOGGER.debug("patientNumber is: {}", patientNumber); assert patientNumber != null : "The given patientNumber was null"; patientNumber = patientNumber.trim(); From e6a992c5525cb9d3300d6b17b9a8ca75512933f0 Mon Sep 17 00:00:00 2001 From: Hussain Sabir Date: Mon, 25 Nov 2024 16:27:39 +0100 Subject: [PATCH 4/4] db updates --- db/installationInit.sql | 1 + db/installationInitTest.sql | 1 + 2 files changed, 2 insertions(+) diff --git a/db/installationInit.sql b/db/installationInit.sql index 3a1078a1..66ddebbf 100644 --- a/db/installationInit.sql +++ b/db/installationInit.sql @@ -216,6 +216,7 @@ INSERT INTO `SelectConfiguration_OPTIONS` (`SelectConfiguration_id`, `options`) (81, 'de.imi.mopat.helper.controller.RandomPatientDataRetrieverImpl'), (81, 'de.imi.mopat.helper.controller.HL7v22PatientInformationRetriever'), (81, 'de.imi.mopat.helper.controller.DummyPatientDataRetrieverImpl'), +(81, 'de.imi.mopat.helper.controller.HL7v22PatientInformationRetrieverByPID'), (38, 'de_DE'), (38, 'en_GB'); diff --git a/db/installationInitTest.sql b/db/installationInitTest.sql index 1dd5f5fb..cfeae3e2 100644 --- a/db/installationInitTest.sql +++ b/db/installationInitTest.sql @@ -222,6 +222,7 @@ INSERT INTO `SelectConfiguration_OPTIONS` (`SelectConfiguration_id`, `options`) (81, 'de.imi.mopat.helper.controller.RandomPatientDataRetrieverImpl'), (81, 'de.imi.mopat.helper.controller.HL7v22PatientInformationRetriever'), (81, 'de.imi.mopat.helper.controller.DummyPatientDataRetrieverImpl'), +(81, 'de.imi.mopat.helper.controller.HL7v22PatientInformationRetrieverByPID'), (38, 'de_DE'), (38, 'en_GB');