/// <summary> /// Create a PDQ search message /// </summary> /// <param name="filters">The parameters for query</param> private QBP_Q21 CreatePDQSearch(int offset, int count, object state, params KeyValuePair <String, String>[] filters) { // Search - Construct a v2 message this is found in IHE ITI TF-2:3.21 QBP_Q21 message = new QBP_Q21(); this.UpdateMSH(message.MSH, "QBP_Q21", "QBP", "Q22"); //message.MSH.VersionID.VersionID.Value = "2.3.1"; // Message query message.QPD.MessageQueryName.Identifier.Value = "Patient Demographics Query"; message.DSC.ContinuationPointer.Value = state?.ToString(); message.RCP.QuantityLimitedRequest.Quantity.Value = count.ToString(); message.RCP.QuantityLimitedRequest.Units.Identifier.Value = "RD"; // Sometimes it is easier to use a terser Terser terser = new Terser(message); terser.Set("/QPD-2", Guid.NewGuid().ToString()); // Tag of the query terser.Set("/QPD-1", "Patient Demographics Query"); // Name of the query for (int i = 0; i < filters.Length; i++) { terser.Set(String.Format("/QPD-3({0})-1", i), filters[i].Key); terser.Set(String.Format("/QPD-3({0})-2", i), filters[i].Value); } return(message); }
/// <summary> /// Process a response message doing query continuation if needed /// </summary> private void PullPatientsAsync(Object state) { // Cast request QBP_Q21 request = state as QBP_Q21; // Send the PDQ message try { var response = this.m_sender.SendAndReceive(request) as RSP_K21; AuditUtil.SendPDQAudit(request, response); if (response == null || response.MSA.AcknowledgmentCode.Value != "AA") { foreach (var err in response.ERR.GetErrorCodeAndLocation()) { Trace.TraceError("{0}: CR ERR: {1} ({2})", this.m_context.JobId, err.CodeIdentifyingError.Text, err.CodeIdentifyingError.AlternateText); } // Kill! Trace.TraceError("Stopping sync"); this.m_errorState = true; } // Is there a continuation pointer? if (!String.IsNullOrEmpty(response.DSC.ContinuationPointer.Value)) { Trace.TraceInformation("{0}: Need to continue query", this.m_context.JobId); request.DSC.ContinuationPointer.Value = response.DSC.ContinuationPointer.Value; this.UpdateMSH(request.MSH, "QBP_Q21", "QBP", "Q22"); this.m_waitThread.QueueUserWorkItem(this.PullPatientsAsync, request); } // Process the patients in this response lock (this.m_syncState) for (int i = 0; i < response.QUERY_RESPONSERepetitionsUsed; i++) { var responseData = response.GetQUERY_RESPONSE(i); this.m_workerItems.Push(responseData); } // Relieve memorypressure lock (this.m_syncState) while (this.m_workerItems.Count > 0) { this.m_waitThread.QueueUserWorkItem(this.ProcessPIDAsync, this.m_workerItems.Pop()); } } catch (Exception e) { Trace.TraceError(e.ToString()); this.m_errorState = true; } }
/// <summary> /// Create a PIX search message /// </summary> private QBP_Q21 CreatePIXSearch(PatientIdentifier localId, string targetDomain) { QBP_Q21 retVal = new QBP_Q21(); this.UpdateMSH(retVal.MSH, "QBP_Q21", "QBP", "Q23"); Terser terser = new Terser(retVal); terser.Set("/QPD-1", "IHE PIX Query"); terser.Set("/QPD-2", Guid.NewGuid().ToString().Substring(0, 8)); terser.Set("/QPD-3-1", localId.Value); terser.Set("/QPD-3-4-1", localId.Domain); terser.Set("/QPD-4-4-1", targetDomain); return(retVal); }
/// <summary> /// Send query to master target /// </summary> private List <Patient> SendQuery(NameValueCollection originalQuery, int count, out int totalResults) { // Map reverse var parmMap = s_map.Map.FirstOrDefault(o => o.Trigger == "Q22"); List <KeyValuePair <Hl7QueryParameterMapProperty, object> > parameters = new List <KeyValuePair <Hl7QueryParameterMapProperty, object> >(); foreach (var kv in originalQuery) { var rmap = parmMap.Parameters.Find(o => o.ModelName == kv.Key); if (rmap == null) { // Is the value a UUID? If so, it may be an identifier we can use if (Guid.TryParse(kv.Value.First(), out Guid uuid)) { // What is the type of this property var property = QueryExpressionParser.BuildPropertySelector <Patient>(kv.Key); if (property == null) { throw new InvalidOperationException($"{kv.Key} is not valid on Patient"); } // Is there a classifier? We need it for querying a guaranteed unique property var preferred = property.Body.Type.GetCustomAttribute <ClassifierAttribute>()?.ClassifierProperty; if (String.IsNullOrEmpty(preferred)) { throw new InvalidOperationException($"{property.Body.Type} does not have a ClassifierAttribute"); } var idp = typeof(IDataPersistenceService <>).MakeGenericType(property.Body.Type); var ids = ApplicationServiceContext.Current.GetService(idp) as IDataPersistenceService; if (ids == null) { throw new InvalidOperationException($"{idp} not found"); } var value = ids.Get(uuid); var match = property.Body.Type.GetProperty(preferred).GetValue(value); preferred = property.Body.Type.GetProperty(preferred).GetSerializationName(); // Get the parmaeter map for this classifier rmap = parmMap.Parameters.Find(o => o.ModelName == $"{kv.Key}.{preferred}"); if (rmap != null) { parameters.Add(new KeyValuePair <Hl7QueryParameterMapProperty, object>(rmap, match)); } else { continue; } } else { continue; } } else { parameters.Add(new KeyValuePair <Hl7QueryParameterMapProperty, object>(rmap, kv.Value)); } } if (parameters.Count == 0) { parameters.Add(new KeyValuePair <Hl7QueryParameterMapProperty, object>(parmMap.Parameters.FirstOrDefault(o => o.Hl7Name == "@PID.33"), DateTime.MinValue.AddDays(10))); } // Construct the basic QBP_Q22 QBP_Q21 queryRequest = new QBP_Q21(); var endpoint = this.Configuration.Endpoints.First(); queryRequest.MSH.SetDefault(endpoint.ReceivingDevice, endpoint.ReceivingFacility, endpoint.SecurityToken); queryRequest.MSH.MessageType.MessageStructure.Value = "QBP_Q21"; queryRequest.MSH.MessageType.TriggerEvent.Value = "Q22"; queryRequest.MSH.MessageType.MessageCode.Value = "QBP"; queryRequest.GetSFT(0).SetDefault(); queryRequest.RCP.QuantityLimitedRequest.Units.Identifier.Value = "RD"; queryRequest.RCP.QuantityLimitedRequest.Quantity.Value = (count).ToString(); queryRequest.QPD.MessageQueryName.Identifier.Value = "Q22"; queryRequest.QPD.MessageQueryName.Text.Value = "Find Candidates"; queryRequest.QPD.MessageQueryName.NameOfCodingSystem.Value = "HL7"; Terser tser = new Terser(queryRequest); int q = 0; foreach (var qp in parameters) { List <String> filter = qp.Value as List <String> ?? new List <String>() { qp.Value.ToString() }; foreach (var val in filter) { Terser.Set(queryRequest.QPD, 3, q, 1, 1, qp.Key.Hl7Name); string dval = val; while (new String[] { "<", ">", "!", "=", "~" }.Any(o => dval.StartsWith(o))) { dval = dval.Substring(1); } switch (qp.Key.ParameterType) { case "date": var dt = DateTime.Parse(dval); switch (dval.Length) { case 4: Terser.Set(queryRequest.QPD, 3, q, 2, 1, dt.Year.ToString()); break; case 7: Terser.Set(queryRequest.QPD, 3, q, 2, 1, dt.ToString("yyyyMM")); break; case 10: Terser.Set(queryRequest.QPD, 3, q, 2, 1, dt.ToString("yyyyMMdd")); break; default: Terser.Set(queryRequest.QPD, 3, q, 2, 1, dt.ToString("yyyyMMddHHmmss.fffzzzz").Replace(":", "")); break; } break; default: Terser.Set(queryRequest.QPD, 3, q, 2, 1, dval); break; } q++; } } // TODO: Send the query and then maps results try { RSP_K21 response = endpoint.GetSender().SendAndReceive(queryRequest) as RSP_K21; // Iterate and create responses totalResults = Int32.Parse(response.QAK.HitCount.Value ?? response.QUERY_RESPONSERepetitionsUsed.ToString()); List <Patient> overr = new List <Patient>(); // Query response for (int i = 0; i < response.QUERY_RESPONSERepetitionsUsed; i++) { var ar = response.GetQUERY_RESPONSE(i); // Create patient Bundle patientData = MessageUtils.Parse(ar); patientData.Reconstitute(); // Does this patient "really" exist? if (!ar.PID.GetPatientIdentifierList().Any(o => o.AssigningAuthority.NamespaceID.Value == this.m_configuration.LocalAuthority.DomainName) && !this.m_retrieveHacks.ContainsKey(patientData.Item.OfType <Patient>().First().Key.Value)) { var key = this.m_retrieveHacks.FirstOrDefault(o => o.Value.Any(x => x.Value == ar.PID.GetPatientIdentifierList()[0].IDNumber.Value)); var patient = patientData.Item.OfType <Patient>().First(); if (key.Key != Guid.Empty) { patient.Key = key.Key; } else { this.m_retrieveHacks.Add(patient.Key.Value, patient.Identifiers); } } // Now we extract the patient var pat = patientData.Item.OfType <Patient>().First(); pat.VersionKey = pat.Key; overr.Add(pat); } return(overr); } catch (Exception ex) { totalResults = 0; this.m_tracer.TraceEvent(EventLevel.Error, "Error dispatching HL7 query {0}", ex); throw new HL7ProcessingException("Error dispatching HL7 query", null, null, 0, 0, ex); } }
/// <summary> /// Handle a PIX query. /// </summary> /// <param name="request">The request.</param> /// <param name="eventArgs">The <see cref="Hl7MessageReceivedEventArgs" /> instance containing the event data.</param> /// <returns>Returns the message result from the query.</returns> /// <exception cref="System.InvalidOperationException"></exception> internal IMessage HandlePixQuery(QBP_Q21 request, Hl7MessageReceivedEventArgs eventArgs) { var patientRepositoryService = ApplicationContext.Current.GetService <IPatientRepositoryService>(); var details = new List <IResultDetail>(); MessageUtil.Validate(request, details); IMessage response = null; // Control if (request == null) { return(null); } try { // Create Query Data var query = MessageUtil.CreateIDQuery(request.QPD); if (query == null) { throw new InvalidOperationException(ApplicationContext.Current.GetLocaleString("MSGE00A")); } var count = int.Parse(request?.RCP?.QuantityLimitedRequest?.Quantity?.Value ?? "0"); var offset = 0; var totalCount = 0; var result = patientRepositoryService.Find(query, offset, count, out totalCount); // Now process the result response = MessageUtil.CreateRSPK23(result, details); try { (response as RSP_K23).QPD.MessageQueryName.Identifier.Value = request.QPD.MessageQueryName.Identifier.Value; Terser reqTerser = new Terser(request), rspTerser = new Terser(response); rspTerser.Set("/QPD-1", reqTerser.Get("/QPD-1")); rspTerser.Set("/QPD-2", reqTerser.Get("/QPD-2")); rspTerser.Set("/QPD-3-1", reqTerser.Get("/QPD-3-1")); rspTerser.Set("/QPD-3-4-1", reqTerser.Get("/QPD-3-4-1")); rspTerser.Set("/QPD-3-4-2", reqTerser.Get("/QPD-3-4-2")); rspTerser.Set("/QPD-3-4-3", reqTerser.Get("/QPD-3-4-3")); rspTerser.Set("/QPD-4-1", reqTerser.Get("/QPD-4-1")); rspTerser.Set("/QPD-4-4-1", reqTerser.Get("/QPD-4-4-1")); rspTerser.Set("/QPD-4-4-2", reqTerser.Get("/QPD-4-4-2")); rspTerser.Set("/QPD-4-4-3", reqTerser.Get("/QPD-4-4-3")); } catch (Exception e) { this.traceSource.TraceEvent(TraceEventType.Error, 0, e.ToString()); } MessageUtil.UpdateMSH(new Terser(response), request); } catch (Exception e) { this.traceSource.TraceEvent(TraceEventType.Error, 0, e.ToString()); response = MessageUtil.CreateNack(request, details, typeof(RSP_K23)); var errTerser = new Terser(response); // HACK: Fix the generic ACK with a real ACK for this message errTerser.Set("/MSH-9-2", "K23"); errTerser.Set("/MSH-9-3", "RSP_K23"); errTerser.Set("/QAK-2", "AE"); errTerser.Set("/MSA-1", "AE"); errTerser.Set("/QAK-1", request.QPD.QueryTag.Value); } return(response); }
/// <summary> /// Pull clients /// </summary> public void PullClients() { Trace.TraceInformation("{0}: -- Starting PULL of patients from CR --", this.m_context.JobId); QBP_Q21 request = null; // Get the last sync to be completed using (SyncData dao = new SyncData()) { // Last modified filter var lastSync = dao.GetLastSync(); DateTime?lastModifiedFilter = lastSync == null ? null : (DateTime?)lastSync.StartTime; // Create a PDQ message if (lastModifiedFilter.HasValue) { Trace.TraceInformation("{0}: Last sync was on {1}", this.m_context.JobId, lastModifiedFilter.Value ); //request = this.CreatePDQSearch(new KeyValuePair<string, string>("@PID.33", new TS(lastModifiedFilter.Value))) as QBP_Q21; ////Trace.TraceInformation("{0}: Only PULL patients modified on {1:yyyy-MMM-dd}", this.m_context.JobId, new TS(this.m_context.StartTime.AddDays(-i), DatePrecision.Day).DateValue); //this.m_waitThread.QueueUserWorkItem(this.PullPatientsAsync, request); // Create a series of OR parameters representing days we're out of sync for (int i = 0; i <= this.m_context.StartTime.Subtract(lastModifiedFilter.Value).TotalDays; i++) { request = this.CreatePDQSearch(new KeyValuePair <string, string>("@PID.33", new TS(this.m_context.StartTime.AddDays(-i), DatePrecision.Day)), new KeyValuePair <string, string>("@PID.8", "M")) as QBP_Q21; Trace.TraceInformation("{0}: Only PULL MALE patients modified on {1:yyyy-MMM-dd}", this.m_context.JobId, new TS(this.m_context.StartTime.AddDays(-i), DatePrecision.Day).DateValue); this.m_waitThread.QueueUserWorkItem(this.PullPatientsAsync, request); request = this.CreatePDQSearch(new KeyValuePair <string, string>("@PID.33", new TS(this.m_context.StartTime.AddDays(-i), DatePrecision.Day)), new KeyValuePair <string, string>("@PID.8", "F")) as QBP_Q21; Trace.TraceInformation("{0}: Only PULL FEMALE patients modified on {1:yyyy-MMM-dd}", this.m_context.JobId, new TS(this.m_context.StartTime.AddDays(-i), DatePrecision.Day).DateValue); this.m_waitThread.QueueUserWorkItem(this.PullPatientsAsync, request); } } else // No last modification date, we have to trick the CR into giving us a complete list { for (int i = DateTime.Now.Year - 3; i <= DateTime.Now.Year; i++) { request = this.CreatePDQSearch(new KeyValuePair <String, String>("@PID.8", "F"), new KeyValuePair <String, String>("@PID.7", string.Format("{0:0000}", i))) as QBP_Q21; this.m_waitThread.QueueUserWorkItem(this.PullPatientsAsync, request); request = this.CreatePDQSearch(new KeyValuePair <String, String>("@PID.8", "M"), new KeyValuePair <String, String>("@PID.7", string.Format("{0:0000}", i))) as QBP_Q21; this.m_waitThread.QueueUserWorkItem(this.PullPatientsAsync, request); } } } // The wtp worker for query contiuation // This is when the response is not complete but there are more results waiting this.m_waitThread.WaitOne(); if (this.m_errorState) { throw new InvalidOperationException("Sync resulted in error state"); } // Work items while (this.m_workerItems.Count > 0) { this.m_waitThread.QueueUserWorkItem(this.ProcessPIDAsync, this.m_workerItems.Pop()); } this.m_waitThread.WaitOne(); if (this.m_errorState) { throw new InvalidOperationException("Sync resulted in error state"); } }
/// <summary> /// Handle the pdq query /// </summary> private IMessage HandlePdqQuery(QBP_Q21 request, Hl7MessageReceivedEventArgs evt) { // Get config var config = this.Context.GetService(typeof(ISystemConfigurationService)) as ISystemConfigurationService; var locale = this.Context.GetService(typeof(ILocalizationService)) as ILocalizationService; var dataService = this.Context.GetService(typeof(IClientRegistryDataService)) as IClientRegistryDataService; // Create a details array List <IResultDetail> dtls = new List <IResultDetail>(); // Validate the inbound message MessageUtil.Validate((IMessage)request, config, dtls, this.Context); IMessage response = null; // Control if (request == null) { return(null); } // Data controller //DataUtil dataUtil = new DataUtil() { Context = this.Context }; AuditUtil auditUtil = new AuditUtil() { Context = this.Context }; // Construct appropriate audit AuditData audit = null; try { // Create Query Data ComponentUtility cu = new ComponentUtility() { Context = this.Context }; DeComponentUtility dcu = new DeComponentUtility() { Context = this.Context }; var data = cu.CreateQueryComponentsPdq(request, dtls); if (data == null) { Trace.TraceError("{0} problems mapping message:", dtls.Count); foreach (var itm in dtls) { Trace.TraceError($"\t{itm.Type} : {itm.Message}"); } throw new InvalidOperationException();// locale?.GetString("MSGE00A") ?? "Could not process components"); } // Is this a continue or new query? RegistryQueryResult result = dataService.Query(data); audit = auditUtil.CreateAuditData("ITI-21", ActionType.Execute, OutcomeIndicator.Success, evt, result); // Now process the result response = dcu.CreateRSP_K21(result, data, dtls); //MessageUtil.CopyQPD((response as RSP_K21).QPD, request.QPD, data); MessageUtil.UpdateMSH(new NHapi.Base.Util.Terser(response), request, config); Terser ters = new Terser(response); ters.Set("/MSH-9-2", "K22"); } catch (Exception e) { Trace.TraceError(e.ToString()); if (!dtls.Exists(o => o.Message == e.Message || o.Exception == e)) { if (dtls.Count == 0) { dtls.Add(new ResultDetail(ResultDetailType.Error, e.Message, e)); } } // HACK: Only one error allowed in nHAPI for some reason : // TODO: Fix NHapi dtls.RemoveAll(o => o.Type != ResultDetailType.Error); while (dtls.Count > 1) { dtls.RemoveAt(1); } response = MessageUtil.CreateNack(request, dtls, this.Context, typeof(RSP_K21)); Terser errTerser = new Terser(response); // HACK: Fix the generic ACK with a real ACK for this message errTerser.Set("/MSH-9-2", "K22"); errTerser.Set("/MSH-9-3", "RSP_K21"); errTerser.Set("/QAK-2", "AE"); errTerser.Set("/MSA-1", "AE"); errTerser.Set("/QAK-1", request.QPD.QueryTag.Value); audit = auditUtil.CreateAuditData("ITI-21", ActionType.Execute, OutcomeIndicator.EpicFail, evt, new List <VersionedDomainIdentifier>()); } finally { IAuditorService auditSvc = this.Context.GetService(typeof(IAuditorService)) as IAuditorService; if (auditSvc != null) { auditSvc.SendAudit(audit); } } return(response); }