From e8aa0e8de298e5abe2a66a199414002e68f45a69 Mon Sep 17 00:00:00 2001 From: "Keith W. Boone" Date: Wed, 24 Jul 2024 05:10:24 -0400 Subject: [PATCH] Added class to generate QPD from FHIR Query parameters --- .../cdc/izgw/v2tofhir/segment/QPDParser.java | 79 ++-- .../gov/cdc/izgw/v2tofhir/utils/QBPUtils.java | 444 ++++++++++++++++++ .../v2tofhir/MessageParserTests.java | 77 ++- .../gov/cdc/izgateway/v2tofhir/TestBase.java | 5 + src/test/resources/messages.txt | 30 +- 5 files changed, 582 insertions(+), 53 deletions(-) create mode 100644 src/main/java/gov/cdc/izgw/v2tofhir/utils/QBPUtils.java diff --git a/src/main/java/gov/cdc/izgw/v2tofhir/segment/QPDParser.java b/src/main/java/gov/cdc/izgw/v2tofhir/segment/QPDParser.java index c399845..3a38c56 100644 --- a/src/main/java/gov/cdc/izgw/v2tofhir/segment/QPDParser.java +++ b/src/main/java/gov/cdc/izgw/v2tofhir/segment/QPDParser.java @@ -38,6 +38,7 @@ import gov.cdc.izgw.v2tofhir.converter.DatatypeConverter; import gov.cdc.izgw.v2tofhir.converter.MessageParser; import gov.cdc.izgw.v2tofhir.utils.ParserUtils; +import gov.cdc.izgw.v2tofhir.utils.QBPUtils; import lombok.extern.slf4j.Slf4j; /** @@ -85,28 +86,28 @@ public List getFieldHandlers() { private static final String[][] v2QueryNames = { { "Z34", "Request Immunization History", "CDCPHINVS", "Immunization", // QPD Parameter Name, V2 Type to convert from, FHIR Query Parameter, FHIR Type to convert parmater to - "PatientList", "CX", "patient.identifier", Identifier.class.getSimpleName(), + "PatientList", "CX", QBPUtils.PATIENT_LIST, Identifier.class.getSimpleName(), "PatientName", "XPN", "patient.name", HumanName.class.getSimpleName(), - "PatientMotherMaidenName", "XPN", "patient.mothersMaidenName", StringType.class.getSimpleName(), - "PatientDateOfBirth", "TS", "patient.birthdate", DateType.class.getSimpleName(), - "PatientSex", "IS", "patient.gender", CodeType.class.getSimpleName(), - "PatientAddress", "XAD", "patient.address", Address.class.getSimpleName(), - "PatientHomePhone", "XTN", "patient.phone", ContactPoint.class.getSimpleName(), - "PatientMultipleBirthIndicator", "ID", null, BooleanType.class.getSimpleName(), - "PatientBirthOrder", "NM", null, PositiveIntType.class.getSimpleName(), + "PatientMotherMaidenName", "XPN", QBPUtils.PATIENT_MOTHERS_MAIDEN_NAME, HumanName.class.getSimpleName(), + "PatientDateOfBirth", "TS", QBPUtils.PATIENT_BIRTHDATE, DateType.class.getSimpleName(), + "PatientSex", "IS", QBPUtils.PATIENT_GENDER, CodeType.class.getSimpleName(), + "PatientAddress", "XAD", QBPUtils.PATIENT_ADDRESS, Address.class.getSimpleName(), + "PatientHomePhone", "XTN", QBPUtils.PATIENT_HOMEPHONE, ContactPoint.class.getSimpleName(), + "PatientMultipleBirthIndicator", QBPUtils.PATIENT_MULTIPLE_BIRTH_INDICATOR, null, BooleanType.class.getSimpleName(), + "PatientBirthOrder", "NM", QBPUtils.PATIENT_MULTIPLE_BIRTH_ORDER, PositiveIntType.class.getSimpleName(), "ClientLastUpdateDate", "TS", null, InstantType.class.getSimpleName(), "ClientLastUpdateFacility", "HD", null, Identifier.class.getSimpleName() }, { "Z44", "Request Evaluated History and Forecast", "CDCPHINVS", "ImmunizationRecommendation", - "PatientList", "CX", "patient.identifier", Identifier.class.getSimpleName(), + "PatientList", "CX", QBPUtils.PATIENT_LIST, Identifier.class.getSimpleName(), "PatientName", "XPN", "patient.name", HumanName.class.getSimpleName(), - "PatientMotherMaidenName", "XPN", "patient.mothersMaidenName", StringType.class.getSimpleName(), - "PatientDateOfBirth", "TS", "patient.birthdate", DateType.class.getSimpleName(), - "PatientSex", "IS", "patient.gender", CodeType.class.getSimpleName(), - "PatientAddress", "XAD", "patient.address", Address.class.getSimpleName(), - "PatientHomePhone", "XTN", "patient.phone", ContactPoint.class.getSimpleName(), - "PatientMultipleBirthIndicator", "ID", null, BooleanType.class.getSimpleName(), - "PatientBirthOrder", "NM", null, PositiveIntType.class.getSimpleName(), + "PatientMotherMaidenName", "XPN", QBPUtils.PATIENT_MOTHERS_MAIDEN_NAME, HumanName.class.getSimpleName(), + "PatientDateOfBirth", "TS", QBPUtils.PATIENT_BIRTHDATE, DateType.class.getSimpleName(), + "PatientSex", "IS", QBPUtils.PATIENT_GENDER, CodeType.class.getSimpleName(), + "PatientAddress", "XAD", QBPUtils.PATIENT_ADDRESS, Address.class.getSimpleName(), + "PatientHomePhone", "XTN", QBPUtils.PATIENT_HOMEPHONE, ContactPoint.class.getSimpleName(), + "PatientMultipleBirthIndicator", QBPUtils.PATIENT_MULTIPLE_BIRTH_INDICATOR, null, BooleanType.class.getSimpleName(), + "PatientBirthOrder", "NM", QBPUtils.PATIENT_MULTIPLE_BIRTH_ORDER, PositiveIntType.class.getSimpleName(), "ClientLastUpdateDate", "TS", null, InstantType.class.getSimpleName(), "ClientLastUpdateFacility", "HD", null, Identifier.class.getSimpleName() }, @@ -257,7 +258,12 @@ Class resolve(String packageName, String typeName) { } } - private void addQueryParameter(StringBuilder request, Parameters params, String name, org.hl7.fhir.r4.model.Type converted) { + private void addQueryParameter( // NOSONAR: Big switch OK + StringBuilder request, + Parameters params, + String name, + org.hl7.fhir.r4.model.Type converted + ) { if (converted == null) { return; } @@ -268,15 +274,28 @@ private void addQueryParameter(StringBuilder request, Parameters params, String for (StringType line: addr.getLine()) { appendParameter(request, line, name); } - used = appendParameter(request, addr.getCity(), name + "-city") - || appendParameter(request, addr.getState(), name + "-state") - || appendParameter(request, addr.getPostalCode(), name + "-postalCode") - || appendParameter(request, addr.getCountry(), name + "-country"); + used = addParam(request, addr.getCity(), QBPUtils.PATIENT_ADDRESS_CITY); + used |= addParam(request, addr.getState(), QBPUtils.PATIENT_ADDRESS_STATE); + used |= addParam(request, addr.getPostalCode(), QBPUtils.PATIENT_ADDRESS_POSTAL); + used |= addParam(request, addr.getCountry(), QBPUtils.PATIENT_ADDRESS_COUNTRY); break; case "HumanName": HumanName hn = (HumanName) converted; - used = appendParameter(request, hn.getFamily(), StringUtils.replace(name, "name", "family")) || - appendParameter(request, hn.getGivenAsSingleString(), StringUtils.replace(name, "name", "given")); + if (name.equals(QBPUtils.PATIENT_MOTHERS_MAIDEN_NAME)) { + used = addParam(request, hn.getFamily(), QBPUtils.PATIENT_MOTHERS_MAIDEN_NAME); + // support multiple values for given + for (StringType given : hn.getGiven()) { + used |= appendParameter(request, given, QBPUtils.PATIENT_MOTHERS_MAIDEN_NAME + "-given"); + } + used |= addParam(request, hn.getSuffixAsSingleString(), QBPUtils.PATIENT_MOTHERS_MAIDEN_NAME + "-suffix"); + } else { + used = addParam(request, hn.getFamily(), QBPUtils.PATIENT_NAME_FAMILY); + // support multiple values for given + for (StringType given : hn.getGiven()) { + used |= appendParameter(request, given, QBPUtils.PATIENT_NAME_GIVEN); + } + used |= addParam(request, hn.getSuffixAsSingleString(), QBPUtils.PATIENT_NAME_SUFFIX); + } break; case "Identifier": Identifier ident = (Identifier) converted; @@ -284,25 +303,25 @@ private void addQueryParameter(StringBuilder request, Parameters params, String if (ident.hasValue()) { value += ident.getValue(); } - used = appendParameter(request, value, name); + used = addParam(request, value, name); break; case "DateType": DateType dt = new DateType(((BaseDateTimeType)converted).getValue()); - used = appendParameter(request, dt.asStringValue(), name); + used = addParam(request, dt.asStringValue(), name); break; case "ContactPoint": ContactPoint cp = (ContactPoint)converted; if (!cp.hasSystem() || cp.getSystem().toString().equals(name)) { - used = appendParameter(request, cp.getValue(), name); + used = addParam(request, cp.getValue(), name); } break; case "StringType": StringType string = (StringType)converted; - used = appendParameter(request, string.asStringValue(), name); + used = addParam(request, string.asStringValue(), name); break; case "CodeType": CodeType code = (CodeType)converted; - used = appendParameter(request, code.asStringValue(), name); + used = addParam(request, code.asStringValue(), name); break; default: break; @@ -313,9 +332,9 @@ private void addQueryParameter(StringBuilder request, Parameters params, String } private boolean appendParameter(StringBuilder request, StringType value, String name) { - return appendParameter(request, value.asStringValue(), name); + return addParam(request, value.asStringValue(), name); } - private boolean appendParameter(StringBuilder request, String value, String name) { + private boolean addParam(StringBuilder request, String value, String name) { if (!StringUtils.isBlank(value)) { request .append(name) diff --git a/src/main/java/gov/cdc/izgw/v2tofhir/utils/QBPUtils.java b/src/main/java/gov/cdc/izgw/v2tofhir/utils/QBPUtils.java new file mode 100644 index 0000000..14cf5ca --- /dev/null +++ b/src/main/java/gov/cdc/izgw/v2tofhir/utils/QBPUtils.java @@ -0,0 +1,444 @@ +package gov.cdc.izgw.v2tofhir.utils; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.ServiceConfigurationError; + +import org.hl7.fhir.r4.model.Immunization; +import org.hl7.fhir.r4.model.Patient; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.NumberParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.hl7v2.HL7Exception; +import ca.uhn.hl7v2.model.DataTypeException; +import ca.uhn.hl7v2.model.Message; +import ca.uhn.hl7v2.model.Segment; +import ca.uhn.hl7v2.model.Type; +import ca.uhn.hl7v2.model.Varies; +import ca.uhn.hl7v2.model.v251.datatype.CE; +import ca.uhn.hl7v2.model.v251.datatype.CX; +import ca.uhn.hl7v2.model.v251.datatype.ID; +import ca.uhn.hl7v2.model.v251.datatype.IS; +import ca.uhn.hl7v2.model.v251.datatype.NM; +import ca.uhn.hl7v2.model.v251.datatype.TS; +import ca.uhn.hl7v2.model.v251.datatype.XAD; +import ca.uhn.hl7v2.model.v251.datatype.XPN; +import ca.uhn.hl7v2.model.v251.datatype.XTN; +import ca.uhn.hl7v2.model.v251.message.QBP_Q11; +import ca.uhn.hl7v2.model.v251.segment.MSH; +import ca.uhn.hl7v2.model.v251.segment.QPD; +import io.azam.ulidj.ULID; + +/** + * This class supports construction of an HL7 QBP Message following the + * CDC HL7 Version 2.5.1 Implementation Guide for Immunization Messaging + * from HTTP GET parameters retrieved using ServletRequest.getParameters() + * or from a map of Strings to a list of Strings. + * + * NOTE On HAPI DatatypeException and HL7Exception: The HAPI V2 code is pervasive with these + * checked exceptions due to message validation capabilities. Well written HAPI code is + * unlikely to encounter them. As a user of this library, instead of catching the error + * wherever it occurs, you can just simply wrap a whole method that uses it in a try/catch block. + * + * @see CDC HL7 Version 2.5.1 Implementation Guide for Immunization Messaging + * + * @author Audacious Inquiry + */ +public class QBPUtils { + private static final FhirContext R4 = FhirContext.forR4(); + private QBPUtils() {} + /** + * Create a QBP message for an Immunization History (Z34) or Forecast (Z44) + * @param profile The message profile (Z34 or Z44). + * @return The QBP message structure + * @throws DataTypeException If an error occured creating the message. + */ + public static QBP_Q11 createMessage( + String profile + ) throws DataTypeException { + QBP_Q11 qbp = new QBP_Q11(); + MSH msh = qbp.getMSH(); + // Use standard encoding characters + msh.getFieldSeparator().setValue("|"); + msh.getEncodingCharacters().setValue("^~\\&"); + + // Set time of message to now. + msh.getDateTimeOfMessage().getTime().setValue(new Date()); + + // Set message type correctly + msh.getMessageType().getMessageCode().setValue("QBP"); + msh.getMessageType().getTriggerEvent().setValue("Q11"); + msh.getMessageType().getMessageStructure().setValue("QBP_Q11"); + + // Give it a unique identifier + msh.getMessageControlID().setValue(ULID.random()); + + // Set version and protocol flags + msh.getVersionID().getVersionID().setValue("2.5.1"); + msh.getAcceptAcknowledgmentType().setValue("NE"); + msh.getApplicationAcknowledgmentType().setValue("NE"); + + // Set the profile identifer + msh.getMessageProfileIdentifier(0).getEntityIdentifier().setValue(profile); + msh.getMessageProfileIdentifier(0).getNamespaceID().setValue("CDCPHINVS"); + + // Initialize the QPD segment appropriately + QPD qpd = qbp.getQPD(); + CE mqn = qpd.getMessageQueryName(); + mqn.getIdentifier().setValue(profile); + mqn.getText().setValue( + "Z44".equals(profile) ? "Request Evaluated History and Forecast" : "Request Immunization History" + ); + mqn.getNameOfCodingSystem().setValue("CDCPHINVS"); + qpd.getQueryTag().setValue(ULID.random()); + + return qbp; + } + + /** + * Set the sending application for a QBP message. + * @param qbp The QBP message + * @param sendingApp The sending application + * @throws DataTypeException If an error occurs + */ + public static void setSendingApplication(QBP_Q11 qbp, String sendingApp) throws DataTypeException { + qbp.getMSH().getSendingApplication().getHd1_NamespaceID().setValue(sendingApp); + } + + /** + * Set the sending facility for a QBP message. + * @param qbp The QBP message + * @param sendingFacility The sending facility + * @throws DataTypeException If an error occurs + */ + public static void setSendingFacility(QBP_Q11 qbp, String sendingFacility) throws DataTypeException { + qbp.getMSH().getSendingFacility().getHd1_NamespaceID().setValue(sendingFacility); + } + + /** + * Set the receiving application for a QBP message. + * @param qbp The QBP message + * @param receivingApp The receiving application + * @throws DataTypeException If an error occurs + */ + public static void setReceivingApplication(QBP_Q11 qbp, String receivingApp) throws DataTypeException { + qbp.getMSH().getReceivingApplication().getHd1_NamespaceID().setValue(receivingApp); + } + + /** + * Set the receiving facility for a QBP message. + * @param qbp The QBP message + * @param receivingFacility The receiving facility + * @throws DataTypeException If an error occurs + */ + public static void setReceivingFacility(QBP_Q11 qbp, String receivingFacility) throws DataTypeException { + qbp.getMSH().getReceivingFacility().getHd1_NamespaceID().setValue(receivingFacility); + } + + /** + * Add the FHIR Search Request Parameters to the QBP Message. + * + * @param qbp The QBP message to update + * @param map A map such as that returned by ServletRequest.getParameters() + * containing the request parameters + * @return The updated QPD Segment + * @throws HL7Exception If an error occurs + */ + public static QPD addRequestToQPD(QBP_Q11 qbp, Map map) throws HL7Exception { + for (Map.Entry e: map.entrySet()) { + addParameter(qbp.getQPD(), e.getKey(), Arrays.asList(e.getValue())); + } + return qbp.getQPD(); + } + + /** + * Add the FHIR Search Request Parameters to the QBP Message. + * + * @param qbp The QBP message to update + * @param map A map similar to that returned by ServletRequest.getParameters() + * containing the request parameters, save that arrays are lists. NOTE: Only + * patient.identifier can be repeated. + * @return The updated QPD Segment + * @throws HL7Exception If an error occurs + */ + public static QPD addParamsToQPD(QBP_Q11 qbp, Map> map) throws HL7Exception { + for (Map.Entry> e: map.entrySet()) { + addParameter(qbp.getQPD(), e.getKey(), e.getValue()); + } + return qbp.getQPD(); + } + + /** Search parameter for PatientList */ + public static final String PATIENT_LIST = Immunization.SP_PATIENT + "." + Patient.SP_IDENTIFIER; + /** Search parameter for PatientName Family Part */ + public static final String PATIENT_NAME_FAMILY = Immunization.SP_PATIENT + "." + Patient.SP_FAMILY; + /** Search parameter for PatientName Given and Middle Part */ + public static final String PATIENT_NAME_GIVEN = Immunization.SP_PATIENT + "." + Patient.SP_GIVEN; + /** Search parameter for PatientName Suffix Part */ + public static final String PATIENT_NAME_SUFFIX = Immunization.SP_PATIENT + ".suffix"; + /** Search parameter for PatientName Use Part */ + public static final String PATIENT_NAME_USE = Immunization.SP_PATIENT + ".name-use"; + /** Search parameter for Mothers Maiden Name Extension */ + public static final String PATIENT_MOTHERS_MAIDEN_NAME = Immunization.SP_PATIENT + ".mothers-maiden-name"; + /** Search parameter for Birth Date */ + public static final String PATIENT_BIRTHDATE = Immunization.SP_PATIENT + "." + Patient.SP_BIRTHDATE; + /** Search parameter for Gender */ + public static final String PATIENT_GENDER = Immunization.SP_PATIENT + "." + Patient.SP_GENDER; + /** Search parameter for Patient Address Lines */ + public static final String PATIENT_ADDRESS = Immunization.SP_PATIENT + "." + Patient.SP_ADDRESS; + /** Search parameter for Patient Address City */ + public static final String PATIENT_ADDRESS_CITY = Immunization.SP_PATIENT + "." + Patient.SP_ADDRESS_CITY; + /** Search parameter for Patient Address State */ + public static final String PATIENT_ADDRESS_STATE = Immunization.SP_PATIENT + "." + Patient.SP_ADDRESS_STATE; + /** Search parameter for Patient Address Postal Code */ + public static final String PATIENT_ADDRESS_POSTAL = Immunization.SP_PATIENT + "." + Patient.SP_ADDRESS_POSTALCODE; + /** Search parameter for Patient Address Country */ + public static final String PATIENT_ADDRESS_COUNTRY = Immunization.SP_PATIENT + "." + Patient.SP_ADDRESS_COUNTRY; + /** Search parameter for Patient Phone */ + public static final String PATIENT_HOMEPHONE = Immunization.SP_PATIENT + "." + Patient.SP_PHONE; + /** Search parameter for Patient Multiple Birth Indicator */ + public static final String PATIENT_MULTIPLE_BIRTH_INDICATOR = Immunization.SP_PATIENT + ".multipleBirth-indicator"; + /** Search parameter for Patient Multiple Birth Order */ + public static final String PATIENT_MULTIPLE_BIRTH_ORDER = Immunization.SP_PATIENT + ".multipleBirth-order"; + + /** + * Add a given search parameter to the QPD segment. + * @param qpd The QPD segment + * @param fhirParamName The FHIR parameter name + * @param params The list of parameters. + * @throws HL7Exception If an error occurs. + */ + public static void addParameter( // NOSONAR: Giant switch is acceptable + QPD qpd, String fhirParamName, List params + ) throws HL7Exception { + if (fhirParamName == null || fhirParamName.isEmpty()) { + return; + } + XPN name = getField(qpd, 4, XPN.class); + XPN maiden = getField(qpd, 5, XPN.class); + TS birthDate = getField(qpd, 6, TS.class); + XAD address = getField(qpd, 8, XAD.class); + TokenParam token = null; + + for (String param: params) { + switch (fhirParamName) { + case PATIENT_LIST: // QPD-3 PatientList (can repeat) + token = new TokenParam(); + token.setValueAsQueryToken(R4, fhirParamName, null, param); + // get QPD-3 + CX cx = addRepetition(qpd, 3, CX.class); + cx.getIDNumber().setValue(token.getValue()); + cx.getAssigningAuthority().getNamespaceID().setValue(token.getSystem()); + cx.getIdentifierTypeCode().setValue("MR"); // Per CDC Guide + break; + + case PATIENT_NAME_FAMILY: // QPD-4.1 PatientName + name.getFamilyName().getSurname().setValue(param); + break; + case PATIENT_NAME_GIVEN: // QPD-4.2 and QPD-4.3 PatientName + if (name.getGivenName().isEmpty()) { + name.getGivenName().setValue(param); + } else { + name.getSecondAndFurtherGivenNamesOrInitialsThereof().setValue(param); + } + break; + case PATIENT_NAME_SUFFIX: // QPD-4.4 PatientName + name.getSuffixEgJRorIII().setValue(param); + break; + case PATIENT_NAME_USE: // QPD-4.7 PatientName + name.getNameTypeCode().setValue("L"); + break; + case PATIENT_MOTHERS_MAIDEN_NAME: // QPD-5 PatientMotherMaidenName + maiden.getFamilyName().getSurname().setValue(param); + maiden.getNameTypeCode().setValue("M"); + break; + // Given and Suffix won't be used in V2 searches on mother's maiden name, but it allows + // the v2tofhir components to round trip. + case PATIENT_MOTHERS_MAIDEN_NAME + "-given": + if (maiden.getGivenName().isEmpty()) { + maiden.getGivenName().setValue(param); + } else { + maiden.getSecondAndFurtherGivenNamesOrInitialsThereof().setValue(param); + } + break; + case PATIENT_MOTHERS_MAIDEN_NAME + "-suffix": + maiden.getSuffixEgJRorIII().setValue(param); + break; + case PATIENT_BIRTHDATE: // QPD-6 PatientDateOfBirth + DateParam dateParam = new DateParam(); + dateParam.setValueAsQueryToken(R4, fhirParamName, null, param); + birthDate.getTime().setValue(dateParam.getValueAsString().replace("-", "")); + break; + + case PATIENT_GENDER: // QPD-7 PatientSex + token = new TokenParam(); + token.setValueAsQueryToken(R4, fhirParamName, null, param); + if (token.getValueNotNull().length() == 0) { + token.setValue("U"); // Per CDC Guide + } + // HL7 codes are same as first character of FHIR codes. + getField(qpd, 7, IS.class).setValue(token.getValue().substring(0, 1).toUpperCase()); + break; + case PATIENT_ADDRESS: // QPD-8-1 and QPD-8-2 PatientAddress + if (address.getStreetAddress().isEmpty()) { + address.getStreetAddress().getStreetOrMailingAddress().setValue(param); + } else { + address.getOtherDesignation().setValue(param); + } + break; + case PATIENT_ADDRESS_CITY: // QPD-8-3 PatientAddress + address.getCity().setValue(param); + break; + case PATIENT_ADDRESS_STATE: // QPD-8-4 PatientAddress + address.getStateOrProvince().setValue(param); + break; + case PATIENT_ADDRESS_POSTAL: // QPD-8-5 PatientAddress + address.getZipOrPostalCode().setValue(param); + break; + case PATIENT_ADDRESS_COUNTRY: // QPD-8-5 PatientAddress + address.getCountry().setValue(param); + break; + case PATIENT_HOMEPHONE: // QPD-9 PatientHomePhone + token = new TokenParam(); + token.setValueAsQueryToken(R4, PATIENT_HOMEPHONE, null, param); + getField(qpd, 9, XTN.class).getTelephoneNumber().setValue(token.getValue()); + break; + case PATIENT_MULTIPLE_BIRTH_INDICATOR: // QPD-10 PatientMultipleBirthIndicator + token = new TokenParam(); + token.setValueAsQueryToken(R4, PATIENT_MULTIPLE_BIRTH_INDICATOR, null, param); + String mbi = token.getValueNotNull(); + if (mbi.length() != 0) { + switch (mbi.toLowerCase().charAt(0)) { + case 'y', 't': + getField(qpd, 10, ID.class).setValue("Y"); break; + case 'n', 'f': + getField(qpd, 10, ID.class).setValue("N"); break; + default: + break; + } + } + break; + case PATIENT_MULTIPLE_BIRTH_ORDER: // QPD-11 PatientBirthOrder + NumberParam number = new NumberParam(); + number.setValueAsQueryToken(R4, PATIENT_MULTIPLE_BIRTH_ORDER, null, param); + getField(qpd, 10, NM.class).setValue(number.getValue().toPlainString()); + break; + default: + break; + } + } + if (!name.isEmpty()) { + name.getNameTypeCode().setValue("L"); + } + if (!maiden.isEmpty()) { + maiden.getNameTypeCode().setValue("M"); + } + if (!address.isEmpty()) { + address.getAddressType().setValue("L"); + } + } + + /** + * Return the first repetition if it is empty, or create a new repetition + * at the end. + * + * @param segment The segment + * @param fieldNo The field + * @return An empty repetition of the field + * @throws HL7Exception If an error occurs + */ + public static Type addRepetition(Segment segment, int fieldNo) throws HL7Exception { + Type[] t = segment.getField(fieldNo); + if (t.length == 1 && t[0].isEmpty()) { + return t[0]; + } + return addRepetition(segment, fieldNo, t.length); + } + + /** + * A convenience method to get a field from a segment of a specific type + * for fields with Varies content. + * + * NOTE: This method works well on GenericSegments and segments that have + * Varies content for the given field, -- OR -- if the type specified is + * the type that the segment model reports for the field (or a superclass of it). + * + * @param The name of the type + * @param segment The segment to get it from + * @param fieldNo The field number + * @param theClass The type of field to get. + * @return The first field of that type in the segment. + * @throws HL7Exception If an error occurs + */ + private static T getField(Segment segment, int fieldNo, Class theClass) throws HL7Exception { + Type type = segment.getField(fieldNo, 0); + if (theClass.isInstance(type)) { + return theClass.cast(type); + } + return convertType(theClass, type); + } + + /** + * Add a repetition of the field, with the given type. + * + * @param The type of the field + * @param segment The segment containing the field + * @param fieldNo The field number + * @param theClass The class for the type + * @return The requested field + * @throws HL7Exception If an error occurs + */ + public static T addRepetition(Segment segment, int fieldNo, Class theClass) throws HL7Exception { + Type[] t = segment.getField(fieldNo); + Type type; + if (t.length == 1 && t[0].isEmpty()) { + type = t[0]; + } else { + type = addRepetition(segment, fieldNo, t.length); + } + return convertType(theClass, type); + } + + private static T convertType(Class theClass, Type type) + throws ServiceConfigurationError, DataTypeException { + if (theClass.isInstance(type)) { + return theClass.cast(type); + } + + if (type instanceof Varies v) { + T data; + try { + data = theClass.getDeclaredConstructor(Message.class).newInstance((Object)null); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | NoSuchMethodException | SecurityException e) { + throw new ServiceConfigurationError("HAPI V2 is not Happy creating " + theClass.getName(), e); + } + v.setData(data); + return data; + } + + throw new IllegalArgumentException("Field is not an instance of Varies or of " + theClass.getSimpleName()); + } + + /** + * Add a repetition of the field at the specified position + * @param segment The segment + * @param fieldNo The one-based field number + * @param rep The repetition number (zero-based) + * @return The new field. + * @throws HL7Exception If an error occurs + */ + public static Type addRepetition(Segment segment, int fieldNo, int rep) throws HL7Exception { + Type[] t = segment.getField(fieldNo); + while (t.length <= rep) { + segment.getField(fieldNo, t.length); + t = segment.getField(fieldNo); + } + return t[rep]; + } +} diff --git a/src/test/java/test/gov/cdc/izgateway/v2tofhir/MessageParserTests.java b/src/test/java/test/gov/cdc/izgateway/v2tofhir/MessageParserTests.java index b7a9420..52ca732 100644 --- a/src/test/java/test/gov/cdc/izgateway/v2tofhir/MessageParserTests.java +++ b/src/test/java/test/gov/cdc/izgateway/v2tofhir/MessageParserTests.java @@ -8,12 +8,15 @@ import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.text.ParseException; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; @@ -40,6 +43,9 @@ import ca.uhn.hl7v2.model.Segment; import ca.uhn.hl7v2.model.Type; import ca.uhn.hl7v2.model.v251.datatype.ST; +import ca.uhn.hl7v2.model.v251.message.QBP_Q11; +import ca.uhn.hl7v2.model.v251.segment.MSH; +import ca.uhn.hl7v2.model.v251.segment.QPD; import ca.uhn.hl7v2.util.Terser; import gov.cdc.izgw.v2tofhir.converter.DatatypeConverter; import gov.cdc.izgw.v2tofhir.converter.MessageParser; @@ -52,6 +58,7 @@ import gov.cdc.izgw.v2tofhir.segment.StructureParser; import gov.cdc.izgw.v2tofhir.utils.Mapping; import gov.cdc.izgw.v2tofhir.utils.ParserUtils; +import gov.cdc.izgw.v2tofhir.utils.QBPUtils; import gov.cdc.izgw.v2tofhir.utils.TextUtils; import lombok.extern.slf4j.Slf4j; @@ -321,6 +328,65 @@ void testSegmentConversions(NamedSegment segment) throws HL7Exception, ClassNotF } } + @ParameterizedTest + @MethodSource("getTestZIMs") + void testQBPUtils(NamedSegment segment) throws HL7Exception { + Type messageQueryName = segment.segment().getField(1, 0); + String zim = ParserUtils.toString(messageQueryName); + + if (!"Z34".equals(zim) && !"Z44".equals(zim)) { + return; // This test is only for specific message profiles + } + + Bundle b1 = new MessageParser().createBundle(Collections.singleton(segment.segment())); + Parameters parameters = b1.getEntry().stream() + .map(e -> e.getResource()) + .filter(r -> r instanceof Parameters) + .map(r -> (Parameters)r) + .findFirst().orElse(null); + + StringType search = (StringType)parameters.getParameter("_search").getValue(); + + QBP_Q11 qbp = QBPUtils.createMessage(zim); + MSH msh = qbp.getMSH(); + + QBPUtils.setSendingApplication(qbp, "SendingApp"); + assertEquals("SendingApp", msh.getSendingApplication().getNamespaceID().getValue()); + + QBPUtils.setSendingFacility(qbp, "SendingFacility"); + assertEquals("SendingFacility", msh.getSendingFacility().getNamespaceID().getValue()); + + QBPUtils.setReceivingApplication(qbp, "ReceivingApp"); + assertEquals("ReceivingApp", msh.getReceivingApplication().getNamespaceID().getValue()); + + QBPUtils.setReceivingFacility(qbp, "ReceivingFacility"); + assertEquals("ReceivingFacility", msh.getReceivingFacility().getNamespaceID().getValue()); + + String[] searchParams = StringUtils.substringAfter(search.asStringValue(), "?").split("&"); + Map> m = new LinkedHashMap<>(); + for (String searchParam: searchParams) { + String name = StringUtils.substringBefore(searchParam, "="); + String value = StringUtils.substringAfter(searchParam, "="); + List values = m.computeIfAbsent(name, k -> new ArrayList<>()); + values.add(URLDecoder.decode(value, StandardCharsets.UTF_8)); + } + + QBPUtils.addParamsToQPD(qbp, m); + + // Copy over the query tag. + Type queryTag = segment.segment().getField(2, 0); + QPD qpd = qbp.getQPD(); + qpd.getQueryTag().setValue(ParserUtils.toString(queryTag)); + if (!StringUtils.equals(segment.segment().encode(), qpd.encode())) { + log.info("=============== FAILURE ==============="); + log.info("Search: \n{}", parameters.getParameter("_search").getValue()); + log.info("Comparing: \nORIGINAL: {}\n REBUILT: {}", segment.segment().encode(), qpd.encode()); + } + // Assert that the round trip worked. + assertEquals(segment.segment().encode().replace("|", "|\n "), qpd.encode().replace("|", "|\n ")); + } + + private String verify(Segment segment, Resource res, ComesFrom from) { // So now we have a path and a segment and field. // We should be able to compare these using TextUtils.toString() methods. @@ -433,14 +499,7 @@ private String getProducedValue(List l, String value, String v2Type) { log.info("Produced {} <> value {}", l, value); return null; } - /** - * Return true if base is of a primitive type - * @param base The base - * @return true if a FHIR primitive type, false otherwise - */ - private boolean isPrimitive(IBase base) { - return Character.isLowerCase(base.fhirType().charAt(0)); - } + private void getFields(Segment segment, ComesFrom from) { fieldValues = null; fieldName = segment.getName() + "-" + from.field(); diff --git a/src/test/java/test/gov/cdc/izgateway/v2tofhir/TestBase.java b/src/test/java/test/gov/cdc/izgateway/v2tofhir/TestBase.java index 261e432..d600140 100644 --- a/src/test/java/test/gov/cdc/izgateway/v2tofhir/TestBase.java +++ b/src/test/java/test/gov/cdc/izgateway/v2tofhir/TestBase.java @@ -251,6 +251,11 @@ static List getTestSegments(String ...names) { static List getTestRCPs() { return getTestSegments("RCP"); } static List getTestRXAs() { return getTestSegments("RXA"); } static List getTestRXRs() { return getTestSegments("RXR"); } + static Stream getTestZIMs() { + List allowed = Arrays.asList("Z34", "Z44"); + return getTestSegments("QPD").stream() + .filter(n -> allowed.contains(ParserUtils.toString(n.segment(), 1))); + } static Message parse(String message) { diff --git a/src/test/resources/messages.txt b/src/test/resources/messages.txt index c0fb380..512b1fb 100644 --- a/src/test/resources/messages.txt +++ b/src/test/resources/messages.txt @@ -996,8 +996,10 @@ ERR||PID|100^required segment missing^HL70357|E||||PID is required segment. Mess #Complete Example Of Evaluation And Forecasting: MSH|^~\&|MYEHR|DCS|||200910311452-0500||RSP^K11^RSP_K11|3533469|P|2.5.1|||NE|NE|||||Z42^CDCPHINVS|DCS MSA|AA|793543 -QAK|37374859|OK|Z44^request evaluated Immunization history^CDCPHINVS -QPD|Z44^Request Evaluated Immunization History^CDCPHINVS|37374859|123456^^^MYEHR^MR|Child^Bobbie^Q^^^^L|Que^Suzy^^^^^M|20090214|M|10 East Main St^^Myfaircity^GA^^^L +QAK|999|OK|Z44^Request Evaluated History and Forecast^CDCPHINVS +QPD|Z44^Request Evaluated History and Forecast^CDCPHINVS|\ + 999|\ + 123456^^^MYEHR^MR|Child^Bobbie^Q^^^^L|Que^Suzy^^^^^M|20090214|M|10 East Main St^^Myfaircity^GA^^^L PID|1||123456^^^MYEHR^MR||Child^Bobbie^Q^^^^L||20090214|M|||10 East Main St^^Myfaircity^GA^^^L ORC|RE||197023^DCS|||||||^Clerk^Myron|||||||DCS^Dabig Clinical System^StateIIS RXA|0|1|20090415132511|20090415132511|31^Hep B Peds NOS^CVX|999|||01^historical record^NIP0001|||||||||||CP|A @@ -1036,8 +1038,8 @@ OBX|24|DT|30980-7^Date vaccination due^LN|1|20091015||||||F # Sample message for multiple recommendations: MSH|^~\&|MYIIS|StatePH|MyEHR|DCS|20150131145233-0500||RSP^K11^RSP_K11|3533469|P|2.5.1|||NE|NE|||||Z42^CDCPHINVS|DCS^^^^^DCS^XX^^^6439432|StatePH MSA|AA|793543 -QAK|37374859|OK|Z44^request evaluated Immunization history^CDCPHINVS -QPD|Z44^Request Evaluated History and Forecast^CDCPHINVS|37374859|123456^^^MYEHR^MR|Child^Bobbie^Q^^^^L|Que^Suzy^^^^^M|20110214|M|10 East Main St^^Myfaircity^GA^^^L +QAK|1039|OK|Z44^Request Evaluated History and Forecast^CDCPHINVS +QPD|Z44^Request Evaluated History and Forecast^CDCPHINVS|1039|123456^^^MYEHR^MR|Child^Bobbie^Q^^^^L|Que^Suzy^^^^^M|20110214|M|10 East Main St^^Myfaircity^GA^^^L PID|1||123456^^^MYEHR^MR~34500907^^^MyIIS^SR||Child^Bobbie^Q^^^^L||20110214|M|||10 East Main St^^Myfaircity^GA^^^L ORC|RE|8788^IIS|197023^IIS RXA|0|1|20150131|20150131|998^no vaccine admin^CVX|999||||||||||||||NA @@ -1054,14 +1056,14 @@ OBX|30|DT|30980-7^Date vaccination due^LN|3|20150214||||||F|||20150131 # Send Request for Complete Immunization History (QBP/RSP) # Process for requesting Immunization History MSH|^~\&|||||201405150010-0500||QBP^Q11^QBP_Q11|793543|P|2.5.1|||||||||Z34^CDCPHINVS -QPD|Z34^Request Immunization History^CDCPHINVS|37374859|123456^^^MYEHR^MR|Child^Bobbie^Q^^^^L|Que^Suzy^^^^^M|20050512|M|10 East Main St^^Myfaircity^GA^^^L +QPD|Z34^Request Immunization History^CDCPHINVS|1057|123456^^^MYEHR^MR|Child^Bobbie^Q^^^^L|Que^Suzy^^^^^M|20050512|M|10 East Main St^^Myfaircity^GA^^^L RCP|I|5^RD&records&HL70126 # Returning a list of candidate clients in response to QBP^Q11 query -MSH|^~\&|SOME_SYSTEM|A_Clinic |MYIIS|MyStateIIS|200911051000-0500||RSP^K11^RSP_K11|37374859|P|2.5.1|||NE|NE|||||Z31^CDCPHINVS|A_Clinic +MSH|^~\&|SOME_SYSTEM|A_Clinic |MYIIS|MyStateIIS|200911051000-0500||RSP^K11^RSP_K11|1061|P|2.5.1|||NE|NE|||||Z31^CDCPHINVS|A_Clinic MSA|AA|793543 -QAK|37374859|OK -QPD|Z34^Request Immunization History^CDCPHINVS|37374859|123456^^^MYEHR^MR|Child^Bobbie^Q^^^^L|Que^Suzy^^^^^M|20050512|M|10 East Main St^^Myfaircity^GA^^^L +QAK|1061|OK +QPD|Z34^Request Immunization History^CDCPHINVS|1061|123456^^^MYEHR^MR|Child^Bobbie^Q^^^^L|Que^Suzy^^^^^M|20050512|M|10 East Main St^^Myfaircity^GA^^^L PID|1||99445566^^^MYStateIIS^SR||Child^Robert^^^^^L||20050512|M NK1|1|Child^Susan|MTH^Mother^HL70063|^^Myfaircity^GA PID|2||123456^^^MYStateIIS^SR||Child^Robert^^^^^L||20050512|M @@ -1069,8 +1071,8 @@ PID|2||123456^^^MYStateIIS^SR||Child^Robert^^^^^L||20050512|M # Returning an immunization history in response to a Request for Immunization History query MSH|^~\&|MYIIS|MyStateIIS|MYEHR|Myclinic|200911300200-0500||RSP^K11^RSP_K11|7731029|P|2.5.1|||NE|NE|||||Z32^CDCPHINVS|MyStateIIS|Myclinic MSA|AA|793543 -QAK|37374859|OK|Z34^Request Immunization History^CDCPHINVS -QPD|Z34^Request Immunization History^CDCPHINVS|37374859|123456^^^MYEHR^MR|Child^Bobbie^Q^^^^L|Que^Suzy^^^^^M|20050512|M|10 East Main St^^Myfaircity^GA^^^L