/** * The contents of this file are subject to the OpenMRS Public License * Version 1.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://license.openmrs.org * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * Copyright (C) OpenMRS, LLC. All Rights Reserved. */ package org.openmrs.hl7.handler; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.openmrs.Concept; import org.openmrs.ConceptAnswer; import org.openmrs.ConceptName; import org.openmrs.ConceptProposal; import org.openmrs.Drug; import org.openmrs.Encounter; import org.openmrs.EncounterType; import org.openmrs.Form; import org.openmrs.Location; import org.openmrs.Obs; import org.openmrs.Patient; import org.openmrs.Person; import org.openmrs.PersonAttribute; import org.openmrs.PersonAttributeType; import org.openmrs.Relationship; import org.openmrs.RelationshipType; import org.openmrs.User; import org.openmrs.api.context.Context; import org.openmrs.hl7.HL7Constants; import org.openmrs.hl7.HL7InQueueProcessor; import org.openmrs.hl7.HL7Service; import org.openmrs.util.OpenmrsConstants; import org.openmrs.util.OpenmrsUtil; import org.springframework.util.StringUtils; import ca.uhn.hl7v2.HL7Exception; import ca.uhn.hl7v2.app.Application; import ca.uhn.hl7v2.app.ApplicationException; import ca.uhn.hl7v2.model.DataTypeException; import ca.uhn.hl7v2.model.Message; import ca.uhn.hl7v2.model.Type; import ca.uhn.hl7v2.model.Varies; import ca.uhn.hl7v2.model.v25.datatype.CE; import ca.uhn.hl7v2.model.v25.datatype.CWE; import ca.uhn.hl7v2.model.v25.datatype.CX; import ca.uhn.hl7v2.model.v25.datatype.DLD; import ca.uhn.hl7v2.model.v25.datatype.DT; import ca.uhn.hl7v2.model.v25.datatype.DTM; import ca.uhn.hl7v2.model.v25.datatype.FT; import ca.uhn.hl7v2.model.v25.datatype.ID; import ca.uhn.hl7v2.model.v25.datatype.IS; import ca.uhn.hl7v2.model.v25.datatype.NM; import ca.uhn.hl7v2.model.v25.datatype.PL; import ca.uhn.hl7v2.model.v25.datatype.ST; import ca.uhn.hl7v2.model.v25.datatype.TM; import ca.uhn.hl7v2.model.v25.datatype.TS; import ca.uhn.hl7v2.model.v25.datatype.XCN; import ca.uhn.hl7v2.model.v25.group.ORU_R01_OBSERVATION; import ca.uhn.hl7v2.model.v25.group.ORU_R01_ORDER_OBSERVATION; import ca.uhn.hl7v2.model.v25.group.ORU_R01_PATIENT_RESULT; import ca.uhn.hl7v2.model.v25.message.ORU_R01; import ca.uhn.hl7v2.model.v25.segment.MSH; import ca.uhn.hl7v2.model.v25.segment.NK1; import ca.uhn.hl7v2.model.v25.segment.OBR; import ca.uhn.hl7v2.model.v25.segment.OBX; import ca.uhn.hl7v2.model.v25.segment.ORC; import ca.uhn.hl7v2.model.v25.segment.PID; import ca.uhn.hl7v2.model.v25.segment.PV1; import ca.uhn.hl7v2.parser.EncodingCharacters; import ca.uhn.hl7v2.parser.PipeParser; /** * Parses ORUR01 messages into openmrs Encounter objects Usage: GenericParser parser = new * GenericParser(); MessageTypeRouter router = new MessageTypeRouter(); * router.registerApplication("ORU", "R01", new ORUR01Handler()); Message hl7message = * parser.parse(somehl7string); * * @see HL7InQueueProcessor */ public class ORUR01Handler implements Application { private Log log = LogFactory.getLog(ORUR01Handler.class); /** * Always returns true, assuming that the router calling this handler will only call this * handler with ORU_R01 messages. * * @return true */ public boolean canProcess(Message message) { return message != null && "ORU_R01".equals(message.getName()); } /** * Processes an ORU R01 event message * * @should create encounter and obs from hl7 message * @should create basic concept proposal * @should create concept proposal and with obs alongside * @should not create problem list observation with concept proposals * @should append to an existing encounter * @should create obs group for OBRs * @should create obs valueCodedName * @should fail on empty concept proposals * @should fail on empty concept answers * @should set value_Coded matching a boolean concept for obs if the answer is 0 or 1 and * Question datatype is coded * @should set value as boolean for obs if the answer is 0 or 1 and Question datatype is Boolean * @should set value_Numeric for obs if Question datatype is Numeric and the answer is either 0 * or 1 * @should set value_Numeric for obs if Question datatype is Numeric * @should fail if question datatype is coded and a boolean is not a valid answer * @should fail if question datatype is neither Boolean nor numeric nor coded */ public Message processMessage(Message message) throws ApplicationException { if (!(message instanceof ORU_R01)) throw new ApplicationException("Invalid message sent to ORU_R01 handler"); log.debug("Processing ORU_R01 message"); Message response; try { ORU_R01 oru = (ORU_R01) message; response = processORU_R01(oru); } catch (ClassCastException e) { log.error("Error casting " + message.getClass().getName() + " to ORU_R01", e); throw new ApplicationException("Invalid message type for handler"); } catch (HL7Exception e) { log.error("Error while processing ORU_R01 message", e); throw new ApplicationException(e); } log.debug("Finished processing ORU_R01 message"); return response; } /** * Bulk of the processing done here. Called by the main processMessage method * * @param oru the message to process * @return the processed message * @throws HL7Exception * @should process multiple NK1 segments */ @SuppressWarnings("deprecation") private Message processORU_R01(ORU_R01 oru) throws HL7Exception { // TODO: ideally, we would branch or alter our behavior based on the // sending application. // String sendingApplication = getSendingApplication(oru); // validate message validate(oru); // extract segments for convenient use below MSH msh = getMSH(oru); PID pid = getPID(oru); List<NK1> nk1List = getNK1List(oru); PV1 pv1 = getPV1(oru); ORC orc = getORC(oru); // we're using the ORC assoc with first OBR to // hold data enterer and date entered for now // Obtain message control id (unique ID for message from sending // application) String messageControlId = msh.getMessageControlID().getValue(); if (log.isDebugEnabled()) log.debug("Found HL7 message in inbound queue with control id = " + messageControlId); HL7Service hl7Service = Context.getHL7Service(); // create the encounter Patient patient = getPatient(pid); if (log.isDebugEnabled()) log.debug("Processing HL7 message for patient " + patient.getPatientId()); Encounter encounter = createEncounter(msh, patient, pv1, orc); // do the discharge to location logic try { updateHealthCenter(patient, pv1); } catch (Exception e) { log.error("Error while processing Discharge To Location (" + messageControlId + ")", e); } // process NK1 (relationship) segments for (NK1 nk1 : nk1List) processNK1(patient, nk1); // list of concepts proposed in the obs of this encounter. // these proposals need to be created after the encounter // has been created List<ConceptProposal> conceptProposals = new ArrayList<ConceptProposal>(); // create observations if (log.isDebugEnabled()) log.debug("Creating observations for message " + messageControlId + "..."); // we ignore all MEDICAL_RECORD_OBSERVATIONS that are OBRs. We do not // create obs_groups for them List<Concept> ignoredConcepts = new ArrayList<Concept>(); String ignoreOBRConceptId = Context.getAdministrationService().getGlobalProperty( OpenmrsConstants.GLOBAL_PROPERTY_MEDICAL_RECORD_OBSERVATIONS, "1238"); if (ignoreOBRConceptId.length() > 0) ignoredConcepts.add(new Concept(Integer.valueOf(ignoreOBRConceptId))); // we also ignore all PROBLEM_LIST that are OBRs ignoreOBRConceptId = Context.getAdministrationService().getGlobalProperty( OpenmrsConstants.GLOBAL_PROPERTY_PROBLEM_LIST, "1284"); if (ignoreOBRConceptId.length() > 0) ignoredConcepts.add(new Concept(Integer.valueOf(ignoreOBRConceptId))); ORU_R01_PATIENT_RESULT patientResult = oru.getPATIENT_RESULT(); int numObr = patientResult.getORDER_OBSERVATIONReps(); for (int i = 0; i < numObr; i++) { if (log.isDebugEnabled()) log.debug("Processing OBR (" + i + " of " + numObr + ")"); ORU_R01_ORDER_OBSERVATION orderObs = patientResult.getORDER_OBSERVATION(i); // the parent obr OBR obr = orderObs.getOBR(); // if we're not ignoring this obs group, create an // Obs grouper object that the underlying obs objects will use Obs obsGrouper = null; Concept obrConcept = getConcept(obr.getUniversalServiceIdentifier(), messageControlId); if (obrConcept != null && !ignoredConcepts.contains(obrConcept)) { // maybe check for a parent obs group from OBR-29 Parent ? // create an obs for this obs group too obsGrouper = new Obs(); obsGrouper.setConcept(obrConcept); obsGrouper.setPerson(encounter.getPatient()); obsGrouper.setEncounter(encounter); Date datetime = getDatetime(obr); if (datetime == null) datetime = encounter.getEncounterDatetime(); obsGrouper.setObsDatetime(datetime); obsGrouper.setLocation(encounter.getLocation()); obsGrouper.setCreator(encounter.getCreator()); // set comments if there are any StringBuilder comments = new StringBuilder(); ORU_R01_ORDER_OBSERVATION parent = (ORU_R01_ORDER_OBSERVATION) obr.getParent(); int totalNTEs = parent.getNTEReps(); for (int iNTE = 0; iNTE < totalNTEs; iNTE++) for (FT obxComment : parent.getNTE(iNTE).getComment()) { if (comments.length() > 0) comments.append(" "); comments.append(obxComment.getValue()); } // only set comments if there are any if (StringUtils.hasText(comments.toString())) obsGrouper.setComment(comments.toString()); // add this obs as another row in the obs table encounter.addObs(obsGrouper); } // loop over the obs and create each object, adding it to the encounter int numObs = orderObs.getOBSERVATIONReps(); HL7Exception errorInHL7Queue = null; for (int j = 0; j < numObs; j++) { if (log.isDebugEnabled()) log.debug("Processing OBS (" + j + " of " + numObs + ")"); OBX obx = orderObs.getOBSERVATION(j).getOBX(); try { log.debug("Parsing observation"); Obs obs = parseObs(encounter, obx, obr, messageControlId); if (obs != null) { // if we're backfilling an encounter, don't use // the creator/dateCreated from the encounter if (encounter.getEncounterId() != null) { obs.setCreator(getEnterer(orc)); obs.setDateCreated(new Date()); } // set the obsGroup on this obs if (obsGrouper != null) // set the obs to the group. This assumes the group is already // on the encounter and that when the encounter is saved it will // propagate to the children obs obsGrouper.addGroupMember(obs); else { // set this obs on the encounter object that we // will be saving later log.debug("Obs is not null. Adding to encounter object"); encounter.addObs(obs); log.debug("Done with this obs"); } } } catch (ProposingConceptException proposingException) { Concept questionConcept = proposingException.getConcept(); String value = proposingException.getValueName(); //if the sender never specified any text for the proposed concept if (value != null && !value.equals("")) conceptProposals.add(createConceptProposal(encounter, questionConcept, value)); else { errorInHL7Queue = new HL7Exception(Context.getMessageSourceService().getMessage( "Hl7.proposed.concept.name.empty"), proposingException); break;//stop any further processing of current message } } catch (HL7Exception e) { errorInHL7Queue = e; } finally { // Handle obs-level exceptions if (errorInHL7Queue != null) { throw new HL7Exception("Improperly formatted OBX: " + PipeParser.encode(obx, new EncodingCharacters('|', "^~\\&")), HL7Exception.DATA_TYPE_ERROR, errorInHL7Queue); } } } } if (log.isDebugEnabled()) { log.debug("Finished creating observations"); log.debug("Current thread: " + Thread.currentThread()); log.debug("Creating the encounter object"); } Context.getEncounterService().saveEncounter(encounter); // Notify HL7 service that we have created a new encounter, allowing // features/modules to trigger on HL7-generated encounters. // -This can be removed once we have a obs_group table and all // obs can be created in memory as part of the encounter *before* we // call EncounterService.createEncounter(). For now, making obs groups // requires that one obs be created (in the database) before others can // be linked to it, forcing us to save the encounter prematurely." // // NOTE: The above referenced fix is now done. This method is // deprecated and will be removed in the next release. All modules // should modify their AOP methods to hook around // EncounterService.createEncounter(Encounter). hl7Service.encounterCreated(encounter); // loop over the proposed concepts and save each to the database // now that the encounter is saved for (ConceptProposal proposal : conceptProposals) { Context.getConceptService().saveConceptProposal(proposal); } return oru; } /** * process an NK1 segment and add relationships if needed * * @param patient * @param nk1 * @throws HL7Exception * @should create a relationship from a NK1 segment * @should not create a relationship if one exists * @should create a person if the relative is not found * @should fail if the coding system is not 99REL * @should fail if the relationship identifier is formatted improperly * @should fail if the relationship type is not found */ protected void processNK1(Patient patient, NK1 nk1) throws HL7Exception { // guarantee we are working with our custom coding system String relCodingSystem = nk1.getRelationship().getNameOfCodingSystem().getValue(); if (!relCodingSystem.equals(HL7Constants.HL7_LOCAL_RELATIONSHIP)) throw new HL7Exception("Relationship coding system '" + relCodingSystem + "' unknown in NK1 segment."); // get the relationship type identifier String relIdentifier = nk1.getRelationship().getIdentifier().getValue(); // validate the format of the relationship identifier if (!Pattern.matches("[0-9]+[AB]", relIdentifier)) throw new HL7Exception("Relationship type '" + relIdentifier + "' improperly formed in NK1 segment."); // get the type ID Integer relTypeId = 0; try { relTypeId = Integer.parseInt(relIdentifier.substring(0, relIdentifier.length() - 1)); } catch (NumberFormatException e) { throw new HL7Exception("Relationship type '" + relIdentifier + "' improperly formed in NK1 segment."); } // find the relationship type RelationshipType relType = Context.getPersonService().getRelationshipType(relTypeId); if (relType == null) throw new HL7Exception("Relationship type '" + relTypeId + "' in NK1 segment not found"); // find the relative Person relative = getRelative(nk1); // determine if the patient is person A or B; the relIdentifier indicates // the relative's side of the relationship, so the patient is the inverse boolean patientIsPersonA = relIdentifier.endsWith("B"); boolean patientCanBeEitherPerson = relType.getbIsToA().equals(relType.getaIsToB()); // look at existing relationships to determine if a new one is needed Set<Relationship> rels = new HashSet<Relationship>(); if (relative != null) { if (patientCanBeEitherPerson || patientIsPersonA) rels.addAll(Context.getPersonService().getRelationships(patient, relative, relType)); if (patientCanBeEitherPerson || !patientIsPersonA) rels.addAll(Context.getPersonService().getRelationships(relative, patient, relType)); } // create a relationship if none is found if (rels.isEmpty()) { // check the relative's existence if (relative == null) { // create one based on NK1 information relative = Context.getHL7Service().createPersonFromNK1(nk1); if (relative == null) throw new HL7Exception("could not create a new relative from NK1 segment"); } // create the relationship Relationship relation = new Relationship(); if (patientCanBeEitherPerson || patientIsPersonA) { relation.setPersonA(patient); relation.setPersonB(relative); } else { relation.setPersonA(relative); relation.setPersonB(patient); } relation.setRelationshipType(relType); Context.getPersonService().saveRelationship(relation); } } /** * Not used * * @param message * @throws HL7Exception */ private void validate(Message message) throws HL7Exception { // TODO: check version, etc. } private MSH getMSH(ORU_R01 oru) { return oru.getMSH(); } private PID getPID(ORU_R01 oru) { return oru.getPATIENT_RESULT().getPATIENT().getPID(); } /** * finds NK1 segments in an ORU_R01 message. all HAPI-rendered Messages have at least one NK1 * segment but if the original message truly does not contain an NK1, the setID will be null on * the generated NK1 * * @param oru ORU_R01 message to be parsed for NK1 segments * @return list of not-null NK1 segments * @throws HL7Exception */ public List<NK1> getNK1List(ORU_R01 oru) throws HL7Exception { List<NK1> res = new ArrayList<NK1>(); // there will always be at least one NK1, even if the original message does not contain one for (int i = 0; i < oru.getPATIENT_RESULT().getPATIENT().getNK1Reps(); i++) // if the setIDNK1 value is null, this NK1 is blank if (oru.getPATIENT_RESULT().getPATIENT().getNK1(i).getSetIDNK1().getValue() != null) res.add(oru.getPATIENT_RESULT().getPATIENT().getNK1(i)); return res; } private PV1 getPV1(ORU_R01 oru) { return oru.getPATIENT_RESULT().getPATIENT().getVISIT().getPV1(); } private ORC getORC(ORU_R01 oru) { return oru.getPATIENT_RESULT().getORDER_OBSERVATION().getORC(); } /** * This method does not call the database to create the encounter row. The encounter is only * created after all obs have been attached to it Creates an encounter pojo to be attached * later. This method does not create an encounterId * * @param msh * @param patient * @param pv1 * @param orc * @return * @throws HL7Exception */ private Encounter createEncounter(MSH msh, Patient patient, PV1 pv1, ORC orc) throws HL7Exception { // the encounter we will return Encounter encounter = null; // look for the encounter id in PV1-19 CX visitNumber = pv1.getVisitNumber(); Integer encounterId = null; try { encounterId = Integer.valueOf(visitNumber.getIDNumber().getValue()); } catch (NumberFormatException e) { // pass } // if an encounterId was passed in, assume that these obs are // going to be appended to it. Fetch the old encounter from // the database if (encounterId != null) { encounter = Context.getEncounterService().getEncounter(encounterId); } else { // if no encounter_id was passed in, this is a new // encounter, create the object encounter = new Encounter(); Date encounterDate = getEncounterDate(pv1); Person provider = getProvider(pv1); Location location = getLocation(pv1); Form form = getForm(msh); EncounterType encounterType = getEncounterType(msh, form); User enterer = getEnterer(orc); // Date dateEntered = getDateEntered(orc); // ignore this since we have no place in the data model to store it encounter.setEncounterDatetime(encounterDate); encounter.setProvider(provider); encounter.setPatient(patient); encounter.setLocation(location); encounter.setForm(form); encounter.setEncounterType(encounterType); encounter.setCreator(enterer); encounter.setDateCreated(new Date()); } return encounter; } /** * Creates the Obs pojo from the OBX message * * @param encounter The Encounter object this Obs is a member of * @param obx The hl7 obx message * @param obr The parent hl7 or message * @param uid unique string for this message for any error reporting purposes * @return Obs pojo with all values filled in * @throws HL7Exception if there is a parsing exception * @throws ProposingConceptException if the answer to this obs is a proposed concept * @should add comments to an observation from NTE segments * @should add multiple comments for an observation as one comment * @should add comments to an observation group */ private Obs parseObs(Encounter encounter, OBX obx, OBR obr, String uid) throws HL7Exception, ProposingConceptException { if (log.isDebugEnabled()) log.debug("parsing observation: " + obx); Varies[] values = obx.getObservationValue(); // bail out if no values were found if (values == null || values.length < 1) return null; String hl7Datatype = values[0].getName(); if (log.isDebugEnabled()) log.debug(" datatype = " + hl7Datatype); Concept concept = getConcept(obx.getObservationIdentifier(), uid); if (log.isDebugEnabled()) log.debug(" concept = " + concept.getConceptId()); ConceptName conceptName = getConceptName(obx.getObservationIdentifier()); if (log.isDebugEnabled()) log.debug(" concept-name = " + conceptName); Date datetime = getDatetime(obx); if (log.isDebugEnabled()) log.debug(" timestamp = " + datetime); if (datetime == null) datetime = encounter.getEncounterDatetime(); Obs obs = new Obs(); obs.setPerson(encounter.getPatient()); obs.setConcept(concept); obs.setEncounter(encounter); obs.setObsDatetime(datetime); obs.setLocation(encounter.getLocation()); obs.setCreator(encounter.getCreator()); obs.setDateCreated(encounter.getDateCreated()); // set comments if there are any StringBuilder comments = new StringBuilder(); ORU_R01_OBSERVATION parent = (ORU_R01_OBSERVATION) obx.getParent(); // iterate over all OBX NTEs for (int i = 0; i < parent.getNTEReps(); i++) for (FT obxComment : parent.getNTE(i).getComment()) { if (comments.length() > 0) comments.append(" "); comments = comments.append(obxComment.getValue()); } // only set comments if there are any if (StringUtils.hasText(comments.toString())) obs.setComment(comments.toString()); Type obx5 = values[0].getData(); if ("NM".equals(hl7Datatype)) { String value = ((NM) obx5).getValue(); if (value == null || value.length() == 0) { log.warn("Not creating null valued obs for concept " + concept); return null; } else if (value.equals("0") || value.equals("1")) { concept = concept.hydrate(concept.getConceptId().toString()); obs.setConcept(concept); if (concept.getDatatype().isBoolean()) obs.setValueBoolean(value.equals("1")); else if (concept.getDatatype().isNumeric()) try { obs.setValueNumeric(Double.valueOf(value)); } catch (NumberFormatException e) { throw new HL7Exception("numeric (NM) value '" + value + "' is not numeric for concept #" + concept.getConceptId() + " (" + conceptName.getName() + ") in message " + uid, e); } else if (concept.getDatatype().isCoded()) { Concept answer = value.equals("1") ? Context.getConceptService().getTrueConcept() : Context .getConceptService().getFalseConcept(); boolean isValidAnswer = false; Collection<ConceptAnswer> conceptAnswers = concept.getAnswers(); if (conceptAnswers != null && conceptAnswers.size() > 0) { for (ConceptAnswer conceptAnswer : conceptAnswers) { if (conceptAnswer.getAnswerConcept().equals(answer)) { obs.setValueCoded(answer); isValidAnswer = true; break; } } } //answer the boolean answer concept was't found if (!isValidAnswer) throw new HL7Exception(answer.toString() + " is not a valid answer for obs with uuid " + uid); } else { //throw this exception to make sure that the handler doesn't silently ignore bad hl7 message throw new HL7Exception("Can't set boolean concept answer for concept with id " + obs.getConcept().getConceptId()); } } else { try { obs.setValueNumeric(Double.valueOf(value)); } catch (NumberFormatException e) { throw new HL7Exception("numeric (NM) value '" + value + "' is not numeric for concept #" + concept.getConceptId() + " (" + conceptName.getName() + ") in message " + uid, e); } } } else if ("CWE".equals(hl7Datatype)) { log.debug(" CWE observation"); CWE value = (CWE) obx5; String valueIdentifier = value.getIdentifier().getValue(); log.debug(" value id = " + valueIdentifier); String valueName = value.getText().getValue(); log.debug(" value name = " + valueName); if (isConceptProposal(valueIdentifier)) { if (log.isDebugEnabled()) log.debug("Proposing concept"); throw new ProposingConceptException(concept, valueName); } else { log.debug(" not proposal"); try { Concept valueConcept = getConcept(value, uid); obs.setValueCoded(valueConcept); if (HL7Constants.HL7_LOCAL_DRUG.equals(value.getNameOfAlternateCodingSystem().getValue())) { Drug valueDrug = new Drug(); valueDrug.setDrugId(new Integer(value.getAlternateIdentifier().getValue())); obs.setValueDrug(valueDrug); } else { ConceptName valueConceptName = getConceptName(value); if (valueConceptName != null) { if (log.isDebugEnabled()) { log.debug(" value concept-name-id = " + valueConceptName.getConceptNameId()); log.debug(" value concept-name = " + valueConceptName.getName()); } obs.setValueCodedName(valueConceptName); } } } catch (NumberFormatException e) { throw new HL7Exception("Invalid concept ID '" + valueIdentifier + "' for OBX-5 value '" + valueName + "'"); } } if (log.isDebugEnabled()) log.debug(" Done with CWE"); } else if ("CE".equals(hl7Datatype)) { CE value = (CE) obx5; String valueIdentifier = value.getIdentifier().getValue(); String valueName = value.getText().getValue(); if (isConceptProposal(valueIdentifier)) { throw new ProposingConceptException(concept, valueName); } else { try { obs.setValueCoded(getConcept(value, uid)); obs.setValueCodedName(getConceptName(value)); } catch (NumberFormatException e) { throw new HL7Exception("Invalid concept ID '" + valueIdentifier + "' for OBX-5 value '" + valueName + "'"); } } } else if ("DT".equals(hl7Datatype)) { DT value = (DT) obx5; Date valueDate = getDate(value.getYear(), value.getMonth(), value.getDay(), 0, 0, 0); if (value == null || valueDate == null) { log.warn("Not creating null valued obs for concept " + concept); return null; } obs.setValueDatetime(valueDate); } else if ("TS".equals(hl7Datatype)) { DTM value = ((TS) obx5).getTime(); Date valueDate = getDate(value.getYear(), value.getMonth(), value.getDay(), value.getHour(), value.getMinute(), value.getSecond()); if (value == null || valueDate == null) { log.warn("Not creating null valued obs for concept " + concept); return null; } obs.setValueDatetime(valueDate); } else if ("TM".equals(hl7Datatype)) { TM value = (TM) obx5; Date valueTime = getDate(0, 0, 0, value.getHour(), value.getMinute(), value.getSecond()); if (value == null || valueTime == null) { log.warn("Not creating null valued obs for concept " + concept); return null; } obs.setValueDatetime(valueTime); } else if ("ST".equals(hl7Datatype)) { ST value = (ST) obx5; if (value == null || value.getValue() == null || value.getValue().trim().length() == 0) { log.warn("Not creating null valued obs for concept " + concept); return null; } obs.setValueText(value.getValue()); } else { // unsupported data type // TODO: support RP (report), SN (structured numeric) // do we need to support BIT just in case it slips thru? throw new HL7Exception("Unsupported observation datatype '" + hl7Datatype + "'"); } return obs; } /** * Derive a concept name from the CWE component of an hl7 message. * * @param cwe * @return * @throws HL7Exception */ private ConceptName getConceptName(CWE cwe) throws HL7Exception { ST altIdentifier = cwe.getAlternateIdentifier(); ID altCodingSystem = cwe.getNameOfAlternateCodingSystem(); return getConceptName(altIdentifier, altCodingSystem); } /** * Derive a concept name from the CE component of an hl7 message. * * @param ce * @return * @throws HL7Exception */ private ConceptName getConceptName(CE ce) throws HL7Exception { ST altIdentifier = ce.getAlternateIdentifier(); ID altCodingSystem = ce.getNameOfAlternateCodingSystem(); return getConceptName(altIdentifier, altCodingSystem); } /** * Derive a concept name from the CWE component of an hl7 message. * * @param altIdentifier * @param altCodingSystem * @return */ private ConceptName getConceptName(ST altIdentifier, ID altCodingSystem) throws HL7Exception { if (altIdentifier != null) { if (HL7Constants.HL7_LOCAL_CONCEPT_NAME.equals(altCodingSystem.getValue())) { String hl7ConceptNameId = altIdentifier.getValue(); return getConceptName(hl7ConceptNameId); } } return null; } /** * Utility method to retrieve the openmrs ConceptName specified in an hl7 message observation * segment. This method assumes that the check for 99NAM has been done already and is being * given an openmrs conceptNameId * * @param hl7ConceptNameId internal ConceptNameId to look up * @return ConceptName from the database * @throws HL7Exception */ private ConceptName getConceptName(String hl7ConceptNameId) throws HL7Exception { ConceptName specifiedConceptName = null; if (hl7ConceptNameId != null) { // get the exact concept name specified by the id try { Integer conceptNameId = new Integer(hl7ConceptNameId); specifiedConceptName = new ConceptName(); specifiedConceptName.setConceptNameId(conceptNameId); } catch (NumberFormatException e) { // if it is not a valid number, more than likely it is a bad hl7 message log.debug("Invalid concept name ID '" + hl7ConceptNameId + "'", e); } } return specifiedConceptName; } private boolean isConceptProposal(String identifier) { return OpenmrsUtil.nullSafeEquals(identifier, OpenmrsConstants.PROPOSED_CONCEPT_IDENTIFIER); } private Date getDate(int year, int month, int day, int hour, int minute, int second) { Calendar cal = Calendar.getInstance(); // Calendar.set(MONTH, int) is zero-based, Hl7 is not cal.set(year, month - 1, day, hour, minute, second); return cal.getTime(); } /** * Get an openmrs Concept object out of the given hl7 coded element * * @param codedElement ce to pull from * @param uid unique string for this message for any error reporting purposes * @return new Concept object * @throws HL7Exception if parsing errors occur */ private Concept getConcept(CE codedElement, String uid) throws HL7Exception { String hl7ConceptId = codedElement.getIdentifier().getValue(); String codingSystem = codedElement.getNameOfCodingSystem().getValue(); return getConcept(hl7ConceptId, codingSystem, uid); } /** * Get an openmrs Concept object out of the given hl7 coded with exceptions element * * @param codedElement cwe to pull from * @param uid unique string for this message for any error reporting purposes * @return new Concept object * @throws HL7Exception if parsing errors occur */ private Concept getConcept(CWE codedElement, String uid) throws HL7Exception { String hl7ConceptId = codedElement.getIdentifier().getValue(); String codingSystem = codedElement.getNameOfCodingSystem().getValue(); return getConcept(hl7ConceptId, codingSystem, uid); } /** * Get a concept object representing this conceptId and coding system.<br/> * If codingSystem is 99DCT, then a new Concept with the given conceptId is returned.<br/> * Otherwise, the coding system is looked up in the ConceptMap for an openmrs concept mapped to * that code. * * @param hl7ConceptId the given hl7 conceptId * @param codingSystem the coding system for this conceptid (e.g. 99DCT) * @param uid unique string for this message for any error reporting purposes * @return a Concept object or null if no conceptId with given coding system found * @should return null if codingSystem not found * @should return a Concept if given local coding system * @should return a mapped Concept if given a valid mapping */ protected Concept getConcept(String hl7ConceptId, String codingSystem, String uid) throws HL7Exception { if (HL7Constants.HL7_LOCAL_CONCEPT.equals(codingSystem)) { // the concept is local try { Integer conceptId = new Integer(hl7ConceptId); Concept concept = new Concept(conceptId); return concept; } catch (NumberFormatException e) { throw new HL7Exception("Invalid concept ID '" + hl7ConceptId + "' in hl7 message with uid: " + uid); } } else { // the concept is not local, look it up in our mapping Concept concept = Context.getConceptService().getConceptByMapping(hl7ConceptId, codingSystem); if (concept == null) log.error("Unable to find concept with code: " + hl7ConceptId + " and mapping: " + codingSystem + " in hl7 message with uid: " + uid); return concept; } } /** * Pull the timestamp for this obx out. if an invalid date is found, null is returned * * @param obx the obs to parse and get the timestamp from * @return an obx timestamp or null * @throws HL7Exception * @see {@link #getDatetime(TS)} */ private Date getDatetime(OBX obx) throws HL7Exception { TS ts = obx.getDateTimeOfTheObservation(); return getDatetime(ts); } /** * Pull the timestamp for this obr out. if an invalid date is found, null is returned * * @param obr * @return * @throws HL7Exception */ private Date getDatetime(OBR obr) throws HL7Exception { TS ts = obr.getObservationDateTime(); return getDatetime(ts); } /** * Return a java date object for the given TS * * @param ts TS to parse * @return date object or null * @throws HL7Exception */ private Date getDatetime(TS ts) throws HL7Exception { Date datetime = null; DTM value = ts.getTime(); if (value.getYear() == 0 || value.getValue() == null) return null; try { datetime = getDate(value.getYear(), value.getMonth(), value.getDay(), value.getHour(), value.getMinute(), value .getSecond()); } catch (DataTypeException e) { } return datetime; } private Date getEncounterDate(PV1 pv1) throws HL7Exception { return tsToDate(pv1.getAdmitDateTime()); } private Person getProvider(PV1 pv1) throws HL7Exception { XCN hl7Provider = pv1.getAttendingDoctor(0); Integer providerId = Context.getHL7Service().resolvePersonId(hl7Provider); if (providerId == null) throw new HL7Exception("Could not resolve provider"); Person provider = new Person(providerId); return provider; } private Patient getPatient(PID pid) throws HL7Exception { Integer patientId = Context.getHL7Service().resolvePatientId(pid); if (patientId == null) throw new HL7Exception("Could not resolve patient"); Patient patient = new Patient(); patient.setPatientId(patientId); return patient; } /** * gets a relative based on an NK1 segment * * @param nk1 an NK1 segment from the HL7 request * @return a matching Person or null if not found * @throws HL7Exception */ private Person getRelative(NK1 nk1) throws HL7Exception { // if there are no associated party identifiers, the person will not exist if (nk1.getNextOfKinAssociatedPartySIdentifiers().length < 1) return null; // find the related person via given IDs return Context.getHL7Service().resolvePersonFromIdentifiers(nk1.getNextOfKinAssociatedPartySIdentifiers()); } private Location getLocation(PV1 pv1) throws HL7Exception { PL hl7Location = pv1.getAssignedPatientLocation(); Integer locationId = Context.getHL7Service().resolveLocationId(hl7Location); if (locationId == null) throw new HL7Exception("Could not resolve location"); Location location = new Location(); location.setLocationId(locationId); return location; } private Form getForm(MSH msh) throws HL7Exception { Integer formId = null; try { formId = Integer.parseInt(msh.getMessageProfileIdentifier(0).getEntityIdentifier().getValue()); } catch (Exception e) { throw new HL7Exception("Error parsing form id from message", e); } // must get entire form object in order to get its metadata // (encounterType) later Form form = null; if (formId != null) form = Context.getFormService().getForm(formId); return form; } private EncounterType getEncounterType(MSH msh, Form form) { if (form != null) return form.getEncounterType(); // TODO: resolve encounter type from MSH data - do we need PV1 too? return null; } private User getEnterer(ORC orc) throws HL7Exception { XCN hl7Enterer = orc.getEnteredBy(0); Integer entererId = Context.getHL7Service().resolveUserId(hl7Enterer); if (entererId == null) throw new HL7Exception("Could not resolve enterer"); User enterer = new User(); enterer.setUserId(entererId); return enterer; } //TODO: Debug (and use) methods in HL7Util instead private Date tsToDate(TS ts) throws HL7Exception { // need to handle timezone String dtm = ts.getTime().getValue(); int year = Integer.parseInt(dtm.substring(0, 4)); int month = (dtm.length() >= 6 ? Integer.parseInt(dtm.substring(4, 6)) - 1 : 0); int day = (dtm.length() >= 8 ? Integer.parseInt(dtm.substring(6, 8)) : 1); int hour = (dtm.length() >= 10 ? Integer.parseInt(dtm.substring(8, 10)) : 0); int min = (dtm.length() >= 12 ? Integer.parseInt(dtm.substring(10, 12)) : 0); int sec = (dtm.length() >= 14 ? Integer.parseInt(dtm.substring(12, 14)) : 0); Calendar cal = Calendar.getInstance(); cal.set(year, month, day, hour, min, sec); // if (cal.getTimeZone().getRawOffset() != timeZoneOffsetMillis) { // TimeZone tz = (TimeZone)TimeZone.getDefault().clone(); // tz.setRawOffset(timeZoneOffsetMillis); // cal.setTimeZone(tz); // } return cal.getTime(); } /** * Creates a ConceptProposal object that will need to be saved to the database at a later point. * * @param encounter * @param concept * @param originalText * @return */ private ConceptProposal createConceptProposal(Encounter encounter, Concept concept, String originalText) { // value is a proposed concept, create a ConceptProposal // instead of an Obs for this observation // TODO: at this point if componentSeparator (^) is in text, // we'll only use the text before that delimiter! ConceptProposal conceptProposal = new ConceptProposal(); conceptProposal.setOriginalText(originalText); conceptProposal.setState(OpenmrsConstants.CONCEPT_PROPOSAL_UNMAPPED); conceptProposal.setEncounter(encounter); conceptProposal.setObsConcept(concept); return conceptProposal; } private void updateHealthCenter(Patient patient, PV1 pv1) { // Update patient's location if it has changed if (log.isDebugEnabled()) log.debug("Checking for discharge to location"); DLD dld = pv1.getDischargedToLocation(); log.debug("DLD = " + dld); if (dld == null) return; IS hl7DischargeToLocation = dld.getDischargeLocation(); log.debug("is = " + hl7DischargeToLocation); if (hl7DischargeToLocation == null) return; String dischargeToLocation = hl7DischargeToLocation.getValue(); log.debug("dischargeToLocation = " + dischargeToLocation); if (dischargeToLocation != null && dischargeToLocation.length() > 0) { if (log.isDebugEnabled()) log.debug("Patient discharged to " + dischargeToLocation); // Ignore anything past the first subcomponent (or component) // delimiter for (int i = 0; i < dischargeToLocation.length(); i++) { char ch = dischargeToLocation.charAt(i); if (ch == '&' || ch == '^') { dischargeToLocation = dischargeToLocation.substring(0, i); break; } } Integer newLocationId = Integer.parseInt(dischargeToLocation); // Hydrate a full patient object from patient object containing only // identifier patient = Context.getPatientService().getPatient(patient.getPatientId()); PersonAttributeType healthCenterAttrType = Context.getPersonService().getPersonAttributeTypeByName( "Health Center"); if (healthCenterAttrType == null) { log.error("A person attribute type with name 'Health Center' is not defined but patient " + patient.getPatientId() + " is trying to change their health center to " + newLocationId); return; } PersonAttribute currentHealthCenter = patient.getAttribute("Health Center"); if (currentHealthCenter == null || !currentHealthCenter.equals(newLocationId.toString())) { PersonAttribute newHealthCenter = new PersonAttribute(healthCenterAttrType, newLocationId.toString()); log.debug("Updating patient's location from " + currentHealthCenter + " to " + newLocationId); // add attribute (and void old if there is one) patient.addAttribute(newHealthCenter); // save the patient and their new attribute Context.getPatientService().savePatient(patient); } } log.debug("finished discharge to location method"); } }