dib0/NHapiTools

Custom segment

Closed this issue · 19 comments

Hi,
How does NHapiTools support to

  1. Add non standard z segment
  2. Add custom properties to newly added z segment

can you provide some example ?

dib0 commented

Hi!

NHapiTools doesn't support this exactly, but NHapi doe support this (NHapiTools is just a support package the the NHapi implementation of HL7).

You can add custom (or z) segments by adding the following to the application config:

<HL7PackageConfiguration>
	<HL7PackageCollection>
		<HL7Package name="TestApp.CustomImplementation.V23." version="2.3"/>
	</HL7PackageCollection>
</HL7PackageConfiguration>

The TestApp.CustomImplementation containts the custom segments etc. They need to have exaclty the same namespace structure as the standard NHapi assemblies. So message implementations need ot go in *.Messages., segments into *.Segments. and so on.

The only thing is that if you need to add a z segment to an existing message you need to create a completely custom implementation of this message structure with the added custom segment.

If you don't want to do that (which is understandable) you can the GenericMessageWrapper provided with NHapiTools. This allows you to just parse the standard messages and also gives you the means to retreive custom, non-standard, segments.

See also:
https://github.com/dib0/NHapiTools/blob/master/Documentation/NHapiTools%20Developer%20guide.doc
(look for paragraph 4.3.1)

And for an example the test application in this project:
https://github.com/dib0/NHapiTools/blob/master/TestApp/Program.cs

Hi Thanks for quick reply. i need to build the new HL7 message using application data, so that i can send newly generated HL7 message to endpoints. So far nhapi and tools are coming very handy. The only piece i m struggling is that

  • create new Z segment ZTA in 2.7 version order (repeating)
  • in ZTA there will be multiple fields
  • Create new Z segment parallel to MSH called ZSD, ZPM and

can i support this functionality using standard nhapi and using local configuration ?

Thanks

dib0 commented

Yes you can.

You can create a custom HL7 package (like I showed in the config example above, you can also see the testapplication from NHapiTool to see how it's done) and add this as *.V27. namespace.

NHapi will then locate the custom implementation. What you also need to do is implement the message that needs the ZTA segement as a custom message (I would inherit the original message and override the constructor). In that case the parser can parse the message to the custom implementation classes (that are given priority over the standard classes). and work with that by constructing the message and adding the custom Z segments as you need.

Hello. I have a similar need. We need to extend an ORM 2.5.1 message to include a custom Z segment (we call it a ZRT for Z- . Unfrotunately, I was working with the "TestGenericMessageWrapper" test method and it doesn't seem to be working. I can see the custom Segment (PID.cs) included under the Segment directory. However, the following call always results in "segmentOverridden=false".

ISegment pid = gcw.GetSegment("PID");
if (pid is TestApp.CustomImplementation.V23.Segment.PID)
segmentOverridden = true;

I was wondering if there was something I was failing to do? I have made no changes to the latest test project.

If I am understanding you correctly, if I wanted to override my default ORM 2.5.1 message, I would derive it from the standard base class and then add a custom ZRT segment to it. The current example shows how to override a segment but it doesn't show how to extend a message. I suppose I would create a Message directory and then add my derived class into there. An example would be quite useful but I'm sure you are busy.

If you can provide some insight into any of this, it would be most appreciated.

Thank you very much for developing this useful extension to nHapi. Great work.

-Tom

As a follow up to this, I did some experimenting with the GenericMessageWrapper. At first, I thought it was doing exactly what it needed to do: I could strictly cast the IMessage produced by the "Unwrap" function and then use custom "GetSegment" methods to get at the non-standard elements. What I found was that it appeared to be working but really wasn't. It woudn't correctly parse all of the message:
var orm = (NHapi.Model.V251.Message.ORM_O01) strictIM;

Oddly, using the AutomatedContext, that works fine (resulting in a fully parsed ORM message). Am I stuck with using the AutomatedContext to attempt to properly parse the message while putting in a flag or something to look for non-standard segments? That was would result in two parsing attempts... first with Automated and then with the Generic to get the custom fields. I was hoping it could have been just one pass.

Thanks.

dib0 commented

Hi Tom!

Sorry for the delay in my reaction. The way NHapi works is that you can't just implement a specific of custom segment. It also has to be included in the message class (otherwise it will never be recognized).

So if you use the DefaultContext or the AutomatedContext the message will be interpreted fine (if the message is according to standards), but the custom segment will be left out. In order to add the segment you have to make a custom implementation of the message class and the Z-segment in order for the parser to add the custom segment.

Since this requires some effort, I solved this by implementing the GenericMessageWrapper. What this does is that it let's the parser parse the message as the original message structure and all unknown segments should be parsed as either a custom implementation or the default implementation. In that way the Unwrap method should give you the standard message (like the defaultContext would) and the specific segment should be obtainable by calling the GetSegment.

I did some testing yesterday and you are right: it doesn't work. Somehow the custom HL7 packages aren't picked up from the configuration. This is something NHapi does and I'm not sure why it doesn't find the specific (in this case V23) segment. Maybe something changed in how NHapi handles the config or something like that. I'll have to dive further in to that. Sorry for that.

It should be just one pass.

Thanks Bas. Although not optimal, I can probably get around the fact the the custom configuration doesn't work as expected. My bigger issue now is that the GenericMessageWrapper appears to have limited functionality when parsing the underlying class. At first, I was thinking it would work exactly as you described but then found that the Unwrap missed some things when casting to a real object. Oddly though, using the AutomatedContext, it is able to cast the message perfectly. I had hoped that inside the code, they may have been doing a similar unbundling of the the structure into an object. Perhaps there is a different context I can use for the GenericMessageWrapper which makes the parsing behave more like the AutomatedContext?

Thanks so much for your quick reply. Your efforts are much appreciated. I'm happy to contribute back if I can be of assistance.

-Tom

dib0 commented

Hi Tom,

Today I had some time to dive into the custom segment problem. There was a configuration change in NHapi, therefore the custom segment configuration didn't work. I fixed that. Also I used the CopyUtil.DeepCopy to copy the message in the GenericMessageWrapper. I changed it to avoid using the DeepCopy and, with the quick tests that I did, that seems to work fine.

So in that case you should be able to use the GenericMessageWrapper without a problem and be able to get the custom segments out of it without having to override the Message class(es).

I haven't had time to create a new release, but did commit the changes to the repository. Will make a new release including NuGet packages asap.

Please contribute any improvement you find! That will be much appreciated!!

Bas

Hi Bas. It seems that you have fixed that error with the configuration. Unfortunately, now if has uncovered a problem with the "IsEqual" extension. You should see a failure here:
Console.WriteLine("PID from original message {0} custom PID.", pid1.IsEqual(pid2) ? "is the same as" : "isn't the same as");

I wanted to share this code snippet with you to show you the GenericMessageWrapper problem. You'll see that if you run this with an automated context, the orm.PATIENT.PID.AdministrativeSex.Value field will resolve. When using the GenericMessageWrapper, it will be null.

Here is the test case that you should be able to copy/paste into Program.cs:
private static void ZRTTest()
{
string txtMessage = "MSH|^\&|ABC|DEF|DAL|QUEST|20160907113230||ORM^O01^ORM_O01|201609071132304107|P|2.5.1\rPID|1||[PATIENTID]^^^^MR||[LASTNAME]^[FIRSTNAME]^M^^^^L|SMITH|[DOB]|[GENDER]||2106-3^White^HL70005|123 Main St.^^Fischer^TX^78623^USA^H||^PRN^PH^anyone@mycompany.com^^704^5551212^^CP^^^980^5551414|^PRN^PH^^^704^5551313|eng^English^ISO639|S^Single|||123-45-6789|||N^Not Hispanic or Latino^HL70189\rPV1|1|O|^^^ZZZZZ00002^^C^^^Bleeding Edge Trauma Center||||NPIDA^Allthework^Dew^^^Mr^PA^^^^^^NPI^^^^^^^^PA||||||||||||ZZZZZ000EJ^^^^VN|||||||||||||||||||||||||20110511132339-0400\rORC|SN|[PON]||||||||||^Jekyll^Burt^^^^^^NPI^L\rOBR||[PON]||87590^Neisseria gonorrhoeae detection by nucleic acid, direct probe^C4|||201609071108|||||||| ^^^ |^Jekyll^Burt^^^^^^NPI^L||||||||LAB\rDG1|1||I25700^Atherosclerosis of CABG, unsp, w unstable angina pectoris^ICD\rZRT|1|Inbound Processor - Prod|ConfigID=1406;MessageID=16971290;MessageType=ORM;LabOrderID=32271;PatientID=1234;|SPLINTER|";

        var emch = new EnhancedModelClassFactory();
        var parser = new PipeParser(emch);
        emch.ValidationContext = parser.ValidationContext;

        IMessage genericIM = parser.Parse(txtMessage);

        if (genericIM is GenericMessageWrapper gcw)
        {
            var strictIM = gcw.Unwrap();
            var structureName = strictIM.GetStructureName();

            if (structureName.Contains("ORM_O01"))
            {
                var orm = (NHapi.Model.V251.Message.ORM_O01) strictIM;
                var sex = orm.PATIENT.PID.AdministrativeSex.Value;
                Console.WriteLine(sex);
            }

            XMLParser xmlParser = new DefaultXMLParser();
            var xmlMessageStrict = xmlParser.Encode(strictIM);
            Console.WriteLine(xmlMessageStrict);

            var xmlMessageGeneric = xmlParser.Encode(genericIM);
            Console.WriteLine(xmlMessageGeneric);

            try
            {
                var zrt = gcw.GetSegment<ISegment>("ZRT");

                //if (zrt is TestApp.CustomImplementation.V251.Segment.ZRT)
                //{
                //  Console.WriteLine("Got a ZRT!!!");
                //}

                int numFields = zrt.NumFields();

                for (int i = 1; i <= numFields; i++)
                {
                    IType field = zrt.GetField(i, 0);
                    var value = ((Varies) field).Data;
                    Console.WriteLine(value);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }

As a final thought, I was having some issues with using DeepCopy in the past. I wrote a recursive method named DeepCopy2 which also deals with Groups:

    public static void DeepCopy2(ref IStructure sourceStructure, ref IStructure targetStructure)
    {
        var targetStructureType = targetStructure.GetType().Namespace;

        if (targetStructureType.Contains("Segment"))
        {
            DeepCopy.copy((ISegment)sourceStructure, (ISegment)targetStructure);
        }
        else if (targetStructureType.Contains("Group"))
        {
            var sourceGroup = (IGroup)sourceStructure;
            var targetGroup = (IGroup)targetStructure;

            foreach (var segmentName in targetGroup.Names)
            {
                var subSourceSegment = sourceGroup.GetStructure(segmentName);
                var subTargetSegment = targetGroup.GetStructure(segmentName);

                DeepCopy2(ref subSourceSegment, ref subTargetSegment);
            }
        }
    }

Perhaps that will be of use to you.

Thanks!
-Tom

dib0 commented

Hi Tom,

Thank you!
I fixed it... DeepCopy (even your version) didn't solve the problem. The fix I created is a bit dirty, but works and that's the most important part.

I pushed the changes to GitHub and uploaded a new NuGet packages.

Hi Bas. I was just getting back into this for a different project. My original one required parsing a non-standard message. I was never able to cast the entire message successfully (to my own derived type) but I was able to get at the individual custom segments (using GetSegment and a cast). I would have really liked the entire message (in this case, ORM_O01 version 2.5.1) to cast, but I think something may have changed in nHapi.

My new project requires me to add these non-standard ZRT segments on the fly. It also requires me to add a non-standard NK1 segment below the PID. I was looking into addNonstandard segment but it didn't seem to render properly when I used the parser to Encode it again. Any thoughts on an easy way to add a bunch of non-stanadard segments?

Thanks in advance,
Tom

dib0 commented

Hi Tom,

I'm not aware of anything that has been changed within NHapi on this (but, I'm not using non-standard segments everyday, so it might have slipped by).
As far as I know still the only way to do this is to implement your own derived type and add the non-standard segments to that. That is, if the previously discussed genericmessagewrapper is too limited for your usecase.

What way did you use to try and get NHapi to cast/parse the message to you ownb derived type? (This can be a hassle, but once you get it working it works well.)

Bas

dib0 commented

Hi!

If you'd like, I don't mind looking at your project.

If you want the whole message to be your custom implementation, I'd say you wouldn't need the generic wrapper approach. That is specifically build to prevent the need of implementing the complete message and being able to add custom segments.

So if you have the complete custom message structure, NHapi will do the parsing to the custom message implementation if added correctly to the config (which you seem to have done). So if you parse it with the NHapi default ModelClassFactory it should be the right (e.g. custom) type.
Did you try that already?

dib0 commented

Hi Tom,

Glad you got it working. NHapi is really strict about using the namespaces. The problem is in the way they build it. Sadly NHapi does the reflection by finding the assembly with the specific name. That way it is impossible to avoid creating an assembly per HL7 version. To change that, NHapi should be changed.

I'll dive into that, maybe there's a way to use NHapiTools to do this or create a pullrequest for NHapi to change their code.