Friday, April 21, 2023

Claims Attachments and the Document Rewrite Problem -- 15 years later


In 2005 and 2006, I spent a significant amount of time explaining the "Document Rewrite" problem to the HL7 Claims Attachments (now renamed Payer/Provider Information Exchange) workgroup. 

In short, if you have an existing CDA (or C-CDA) document, and now, for regulatory reasons (for example, to attach a digital signature to it), you must open and rewrite the document, for a subsequent purpose (e.g., to attach an electronic [digital] signature for Claims Attachments), you've introduced a second artifact that must be separately identified, linked to the original, and stored; increasing costs of storage and implementation, and subsequent delays in deployments.  I suspect these additional costs are NOT accounted for in the current proposed rule.

This problem was original introduced in the Attachments AIS back in 2005 in an attempt to insert the attachment linking information into the original artifact, and is now a potential consequence associated with the electronic signature requirements of the currently proposed rule.  At least our institutional memory has had at least one opportunity to notify HHS of this problem again.

Honestly, I'm a supporter of EHRs adding a digital signature to the CDA artifact when the document is SIGNED by the provider creating it, but such technology is NOT readily available in certified EHR technology today. I don't believe that such an imposition should be made for attachments until after such technology is broadly available through certified EHR systems, and then only for documents created after such technology is required for use by healthcare providers under CMS programs.


Monday, April 3, 2023

Using Filebeats on Alpine Linux

One of the critical components for any interoperability component is monitoring.  I've played around with using ElasticSearch with SANER and used it for other projects. One of the important parts of this monitoring component is filebeat, which ships the logs to the Elastic cloud implementation.  Recently, I found that one of my AWS installation scripts stopped working over the April Fool's weekend.  All the more fool I for relying on SBEC (somebody else's code) to keep working.

We use Alpine Linux as the base for many of our Docker image deployments, like many others.  If, like me, you are also using filebeat, and have been relying on the Alpine testing APK repository, you probably noticed it no longer contains filebeat because of this commit.

If you've done as I have, your filebeat installation in your Dockerfile probably looks like this today and isn't working:

RUN echo http://dl-cdn.alpinelinux.org/alpine/edge/testing >> /etc/apk/repositories
RUN apk update
RUN apk add filebeat

To make it work with the penultimate release (using 8.6.2, since 8.7.0 was JUST released a few days ago), you'd replace that with the following.

RUN apk update
RUN apk add curl libc6-compat
ENV FILEBEAT_VERSION=8.6.2
RUN curl https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-${FILEBEAT_VERSION}-linux-x86_64.tar.gz \
  -o /filebeat.tar.gz && \
  tar xzvf filebeat.tar.gz && \
  rm filebeat.tar.gz && \
  mv filebeat-${FILEBEAT_VERSION}-linux-x86_64 filebeat && \
  cd filebeat && \
  cp filebeat /usr/bin

NOTE: If you leave out the libc6-compat, filebeat won't run. That took me a bit to track down



This may only be a short-lived problem, as I made a request to find out what it would take to get Filebeats back into the APK repository, and at least one contributed has indicated that they are willing to provide support for it (and possibly other beats applications).


Tuesday, March 28, 2023

Translating CDA to FHIR: SNOMED CT Codes


One of the challenges in translating from CDA (or C-CDA) to FHIR is related to variations in how CDA and FHIR store complex SNOMED-CT codes using qualifiers.  As you might expect, these differences are especially significant when the concept being encoded uses the SNOMED CT Expressions. 

In CDA, these codes are stored using the ConceptDescriptor (CD) data type, and unpacks the concept using the Qualifier component of the concept description when the code is a SNOMED CT expression.  This is arguably a flaw in HL7 Version 3 standards, as the qualifier concept is variably expressed in SNOMED CT and other terminology systems differently through their own expression languages, and shouldn't be syntactically unpacked in the CD data type.  It's overengineering the terminology component to meet the needs of a data model, and overgeneralizing (in the favor of SNOMED) how this may be handled in other terminologies.

HL7 FHIR uses the CodableConcept data type, and doesn't unpack the expression (fixing this flaw).  If we take the SNOMED CT example provided at that link and shown below:

Example

And look at it in CDA, we would see:

<code xsi:type="CD" code="284196006" codeSystem="2.16.840.1.113883.6.96" 
      displayName="burn of skin">
    <qualifier>
        <name code="363698007" codeSystem="2.16.840.1.113883.6.96" displayName="finding site" />
        <value code="770850006" codeSystem="2.16.840.1.113883.6.96" 
               displayName="Skin structure of left index finger"/>
    </qualifier>
</code>

But in FHIR:

<code>
  <coding>
    <code value='284196006|burn of skin|:363698007|finding site|=770850006|Skin structure of left index finger|' 
          system='http://snomed.info/sct'/>
  </coding>
</code>
                    <!-- OR Better yet (IMNSHO*) -->
<code>
  <coding>
    <code value='284196006:363698007=770850006' system='http://snomed.info/sct'
          display='|burn of skin|:|finding site|=|Skin structure of left index finger|'/>
  </coding>
</code>
NOTE, the latter example is just a simplified version of the former without any display name values.

This use of SNOMED CT Expressions is advanced, and doesn't show up often in the real world, but when it does, it is a real challenge for implementers.

To convert from a SNOMED CT code with qualifiers in CDA to a SNOMED CT Code in FHIR, apply the following algorithm:

  1. Generate new nested <fhir:code>, <fhir:coding> and <fhir:code> elements.
  2. If there is a cda:code/@code attribute, generate a value attribute in the final fhir:code element as follows:
    1. Set fhir:code/@value attribute in FHIR to the value of cda:code/@code
    2. (Optional and legal, but not recommended): If there is a cda:code/@displayName attribute in the CDA, append "|" + @displayName + "|" to fhir:code/@value.  NOTE: This captures displayNames, which you should only trust from your own terminology service, and it also appends a string to fhir:code/@value which will complicate FHIR Search operations in most FHIR implementations.
    3. For each cda:qualifier element from SNOMED CT in the cda:code element. 
      1. Append ":" + cda:qualifer/cda:name/@code to fhir:code/@value
      2. Again, optional, legal and not recommended: append "|" + @displayName + "|" to fhir:code/@value
      3. Append "=" + cda:qualifer/cda:value/@code to fhir:code/@value
  3. If there isn't a code/@code attribute, generate an appropriate uncoded value in FHIR (possibly using null flavors depending on the IG).  NOTE: If there's NO @code, but there are cda:qualifier elements, this is bogus, and should be reported as a validation error on input, at least for SNOMED CT.  You can't qualify an uncoded value.
Some comments:
  1. SNOMED CT expressions allow display name values to be appended to a code between vertical bars | (we call these pipes in HL7 V2).  I don't recommend putting the display names into the code even though it's legal and semantically correct.  Pragmatically, it's going to cause your FHIR implementation to have problems in search.
    1. This is legal:
      <code value='284196006|burn of skin|:363698007|finding site|=770850006|Skin structure of left index finger|'  system='http://snomed.info/sct'/>
    2. But this is MUCH better:
      <code value='284196006:363698007=770850006'
            display='|burn of skin|:|finding site|=|Skin structure of left index finger|'
            system='http://snomed.info/sct'/>

  2. Don't trust display name values from external systems.  Use the display name values from your own, validated terminology service.

  3. Consider how incorporating qualifiers into codes will impact your search.  In general, qualifiers reflect refinement in SNOMED CT, which implies subclasses of the core code.  You might consider coding it twice, once without the qualifiers, and a second time with the qualifiers:
    <code>
      <coding>
    
        <code value='284196006' system='http://snomed.info/sct'
              display='burn of skin'/>
        <code value='284196006:363698007=770850006' system='http://snomed.info/sct'
    display='|burn of skin|:|finding site|=|Skin structure of left index finger|'/> </coding> </code>
    I like this better because a search for fhir/Condition?code=
    http://snomed.info/sct|284196006 is going to find what the end user expects, all Condition resources representing a burn of skin.

  4. If you go with #3 above, you may be concerned (and rightly so) about negation.  There are a couple of recommendations you might consider:
    1. Insert a human into the mix and ensure that qualifiers related to negation or other situations with explicit context are correctly interpreted in workflows that involve importing data from CDA into the patient chart.
    2. Read through and understand SNOMED CT documentation on this topic.
    3. Few systems today use SNOMED CT expressions.  Fewer yet use them with negation.  That doesn't mean you can safely ignore it. Make it an exceptional process in your conversions, AND don't try to automate processing it, just detect it and call for (human) assistance in interpretation.  The number of times that happens is likely be small enough to avoid user complaint, yet remain safe for patient care.  And note, my expert opinion does not excuse you from making your own risk assessments and code accordingly.

Keith

P.S. To make it even uglier, technically, the FHIR representation would also be legal in CDA, if about as rare as a snowball in Central Africa.

<code xsi:type="CD" value='284196006:363698007=770850006'
      displayName='|burn of skin|:|finding site|=|Skin structure of left index finger|'
      codeSystem="2.16.840.1.113883.6.96">
* IMNSHO = In My Not So Humble Opinion

Friday, March 17, 2023

One more hack for TUS for 0 Content

Image of A Zero (Source)

I've been playing around with the TUS Protocol lately as I've mentioned in previous posts.  I implemented both a TUS Server and a TUS Client using open-source libraries.  I've also been testing with an external system that uses TUSD underneath.

One of the challenges I ran into with the client implementation had to do with not having a Content-Length field on requests such as GET, OPTIONS or HEAD with no body, that shouldn't actually require the Content-Length field.

What I wound up doing in this case was to:

  1. Set Content-Length explicitly to 0 in the HttpURLConnection class:
    con.setAttribute("Content-Length", 0);
  2. And when that didn't work, made sure that I actually retrieved the output stream from the connection even though nothing was written to it:
    if (con.getDoOutput()) {
      con.getOutputStream();
    }
    This is what seems to have the impact on the Content-Length value sent by the underlying HttpsURLConnectionImpl class, although I can't really see what it is doing because as I said previously, good luck finding the source for that.  It's in one of the sun packages that doesn't ship with the Java JRE source code.  I suppose I could decompile those classes, but for the most part, it's not worth the effort.
NOTE: Both of these activities are done in my overridden: prepareConnection() method in a subclassed TusClient class.

Once again, I'm using URLConnection because that's what's in use by the TUS Java Client, not because it's my first choice.  If I had a choice, I'd probably be using the Apache HTTP Client.

TUS is an interesting protocol because it allows for resumable uploads.  I think it could be better if it were able to handle uploads of multiple chunks over separate network connections, so that you could take better advantage of the resumability AND available network bandwidth and CPU.  The protocol would have to be adjusted to deal with that.

     Keith

P.S.  The folks behind TUS would prefer if we wrote it tus, but somehow my fingers keep capitalizing it.  Since it is a TLA, I'll probably keep writing it in all caps.


Wednesday, March 15, 2023

Patching PATCH for JDK 11


Last post I talked about HttpsURLConnection, and how one could intercept calls to the underlying socket to report on the HTTP protocol messages.  In this post, I'll explain how you can work around another shortcoming in Java where HttpURLConnection (and thus HttpsURLConnection which derives from it) fail to support the HTTP PATCH method defined in RFC5789.

The critical problem comes from the fact that this doesn't work:

  URL url = new URL("https://localhost");
  HttpURLConnection con = (HttpURLConnection)url.openConnection();
  con.setRequestMethod("PATCH");

It will throw a ProtocolException.  Here's the JDK Source code for the static variables and method in question:

  /* valid HTTP methods */
  private static final String[] methods = {
    "GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"
  };

  /**
    * Set the method for the URL request, one of:
    * <UL>
    * <LI>GET
    * <LI>POST
    * <LI>HEAD
    * <LI>OPTIONS
    * <LI>PUT
    * <LI>DELETE
    * <LI>TRACE
    * </UL> are legal, subject to protocol restrictions. The default
    * method is GET.
    * @param method the HTTP method
    * @exception ProtocolException if the method cannot be reset or if
    * the requested method isn't valid for HTTP.
    * @exception SecurityException if a security manager is set and the
    * method is "TRACE", but the "allowHttpTrace"
    * NetPermission is not granted.
    * @see #getRequestMethod()
  */

  public void setRequestMethod(String method) throws ProtocolException {
    if (connected) {
      throw new ProtocolException("Can't reset method: already connected");
    }
    // This restriction will prevent people from using this class to
    // experiment w/ new HTTP methods using java. But it should
    // be placed for security - the request String could be
    // arbitrarily long.
    for (int i = 0; i < methods.length; i++) {
      if (methods[i].equals(method)) {
        if (method.equals("TRACE")) {
          SecurityManager s = System.getSecurityManager();
          if (s != null) {
            s.checkPermission(new NetPermission("allowHttpTrace"));
          }
        }
        this.method = method;
        return;
      }
    }
    throw new ProtocolException("Invalid HTTP method: " + method);
}

What's a developer to do?  Well, you could use an alternate implementation to connect instead of HttpsURLConnection.  Some alternatives include the Apache HTTP Client, and Spring's RestTemplate.

Sometimes using an alternative isn't a choice (for example, when an underlying library uses it and you can't change the implementation).  In my particular case, I was experimenting with the tus-java-client to talk to a TUS Server to experiment with sending data to a DEX endpoint.  

The Java tus client uses HttpURLConnection, and some servers may not like the X-HTTP-Method-Override header that has been included to work around this problem with the expected use of PATCH.

A bit of Googling turns up a number of different solutions, one of which I implemented in a static method called during application startup.

private static void patchThePatch() {
  try {
    String[] methods = { "PATCH" };
    Field methodsField = HttpURLConnection.class.getDeclaredField("methods");
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(methodsField, methodsField.getModifiers() & ~Modifier.FINAL);
    methodsField.setAccessible(true);
    String[] oldMethods = (String[]) methodsField.get(null);
    Set<String> methodsSet = new LinkedHashSet<>(Arrays.asList(oldMethods));
    methodsSet.addAll(Arrays.asList(methods));
    String[] newMethods = methodsSet.toArray(new String[0]);
    methodsField.set(null/*static field*/, newMethods);
    URL url = new URL("https://localhost");
    HttpURLConnection con = (HttpURLConnection)url.openConnection();
    con.setRequestMethod("PATCH");
    // Turn off caching
    con.setDefaultUseCaches(false);
    LOGGER.info("PATCH updated in HttpURLConnection");
  } catch (Exception ex) {
    LOGGER.info("Could not adjust HttpURLConnection to accept PATCH: {}", ex.getMessage(), ex);
  }
}

This code was inspired by:

https://stackoverflow.com/questions/25163131/httpurlconnection-invalid-http-method-patch/40606633#40606633

If you are using a JDK later than JDK-11, you might want to look into:

https://stackoverflow.com/questions/56039341/get-declared-fields-of-java-lang-reflect-fields-in-jdk12



Tuesday, March 14, 2023

Intercepting Secure Socket Calls

Transport Layer Security is something I've been working with since the very first publication of Cross Enterprise Document Sharing (XDS).  For that very first Connectathon, we had to demonstrate that we could secure the XDS communications channels, and I struggled with that part.  So much so that I wrote what might be considered my first blog post, the ATNA FAQ.

Since then, I've become somewhat expert in implementing, diagnosing, and resolving issues with TLS implementations in Java.  One of my most recent challenges involved having to diagnose problems with a protocol where the usual methods in the ATNA FAQ don't work, because the underlying implementation is on Bouncy Castle FIPS, and that doesn't respond to the usual Java -Djavax.net.debug options I first wrote about 20 years ago.  There's a good reason for that, but it makes debugging protocol implementations a bit of a beast when you cannot see what is happening.  Sure, you can wireshark it, but that doesn't always work when you aren't in control (as I wasn't) the responding service.

The solution is rather ugly, and it involves interceptors.  What I wound up doing was getting a hold of the client socket factory, wrapping it with my own implementation, intercepting calls to the Socket creation code, wrapping them in interceptors which catch calls get underlying Input and Output streams, and wrapping each of those in an Interceptor which filters the input or output stream to duplicate the output onto my console.

This is brutually tedious code, because it involves overriding EVERY implemented public method of these classes, calling the original intercepted object's method and returning the result (or slightly modifying it in a very few cases).  Fortunately, modern IDEs will write MOST of this code for you and search and replace will finish it (except for the special cases).  I won't reproduce every line here, but I'll explain the technique.

First, understand that I'm dealing with client calls to a server, and these are built on HttpsURLConnection.  For those of you who've been programming in Java, you may already understand that this is not an interface, but rather an abstract class, and the finicky details are found underneath it in sun.net.www.protocol.http.HttpsURLConnectionImpl (BTW: Good luck finding the source for that).

When you have your URL Connection, you will need to wrap the SSL Socket Factory it is using with a new socket factory that intercepts calls to the existing one.

    HttpsURLConnection conx = (HttpsURLConnection)url.openConnection();
    if (debug) {
      conx.setSSLSocketFactory(new MySSLSocketFactory(conx.getSSLSocketFactory()));
    }

Next you are going to create this new SSLSocketFactory:

public class MySSLSocketFactory {
  private final SSLSocketFactory base;
  MySSLSocketFactory(SSLSocketFactory base) {
    this.base = base;
  }
   ...
}

Inside this class you will override every method implemented in the class (Eclipse's Source | Override Implement Methods menu makes this easy).

Each override follows one of two patterns:

  @Override
  public void methodX(Type0 arg0, Type1 arg1) {
    base.methodX(arg0, arg1);
  }

Or for methods returning a value:

  @Override
  public TypeR methodX(Type0 arg0, Type1 arg1) {
    return base.methodX(arg0, arg1);
  }

For the createSocket methods, you will vary this slightly:

  @Override
  public Socket createSocket(Type0 arg0, Type1 arg1) {
    return new MySocketWrapper(base.methodX(arg0, arg1));
  }

Next, you will create MySocketWrapper following similar instructions.  This time there are 60-70 methods you must override (I did say this was tedious, did I not).

public class MySSLSocketWrapper extends SSLSocket {
  private final SSLSocket base;
  public MySSLSocketWrapper(SSLSocket base) {
    this.base = base;
  }

  @Override
  public InputStream getInputStream() throws IOException {
    return new MyInterceptingInputStream(base.getInputStream());
  }

  @Override
  public OutputStream getOutputStream() throws IOException {
    return new MyInterceptingOutputStream(base.getOutputStream());
  }

  // ... Add other Overrides here just calling the base method of the same name
}

Finally, create the MyInterceptingInputStream and MyInterceptingOutputStream classes just as before, overriding the read or write methods to copy the input or output to another place of your choosing.  I just write what was read or written to System.err, but in the case of the InputStream, I also write a couple of escape sequences before and after to change the color of the output in my console so I can distinguish which stream I'm seeing.  Here's an example of what I mean.

  private static final byte[] BLUE = { '\033', '[', '3', '4', 'm' };
  private static final byte[] NORMAL = { '\033', '[', '0', 'm' };
  @Override
  public int read(byte[] b, int off, int len) throws IOException {
    int val = in.read(b, off, len);
    if (val > 0) {
      try {
        out.write(BLUE);
        out.write(b, off, val);
        out.write(NORMAL);
      } catch (IOException ex) {
        // Swallow it.
      }
    }
    return val;
}

Now, when I set debug to true, I'll get the HTTP communications appearing in my console.

Later this week I'll show you another hack I implemented to make HttpURLConnection support PATCH in JDK-11.

Wednesday, December 7, 2022

New CMS Proposed Rule Released on PatientAccess and a Feature Request for the @FedRegister Web Site

 CMS Announced the updates to the Patient Access Rule yesterday.  So another tweet tweet through and summary are coming by EOW (my time zone).  You can find the preview here.  My comments on the prior rule are here, and you might read that first before starting on this one.

I'm not going to get through this one right away because I have other high priority tasks in front of that (releasing is a very important feature).

I've been a Federal Register user for quite some time.  I really appreciate all the work that has gone into it, especially for reviewing regulation.  My biggest use of it is for providing feedback on regulations, and that leads me to a feature request or three that I'd love to see.

  1. Provide users with the capability to make notes on individual paragraphs and sentences in a publication requesting feedback (e.g., a rule or proposed rule).
  2. Enable users to coordinate with other users so that multiple users can make notes in the same note repository.
  3. Enable users to access and download that feedback in a machine readable, preferably standardized format.
  4. Enable users to upload/merge feedback from other exports into a note repository.
  5. Enable users to share a link to that feedback with others for read or write access.
  6. Enable users to automatically submit a repository with additional attachments as feedback on a proposed rule, RFI or other publication.
  7. Annotations should take the format: Link to original text (a span of text in the rule), proposed wording, comments, and author.  See the last paragraph below for why this is the format.
What this would do is enable users to comment on regulations, provide a standardized format that would enable federal agencies to automate much of the work of coordinating comments from different users, and enable feedback to be tied directly back to required sections.  This could vastly improve response times for feedback.

I try to provide feedback on regulations the same way as I do for standards: Here's the original text, here's how it should read, and here's why.  When writing the "how it should read", I try to use the same voice as the original proposed rule so that the text can just be copy-pasted into the final rule. It makes everyone's day easier, and that's how I've gotten verbatim text I and my colleagues have written to appear in at least three different rules (yes, you too can write text that appears in a final rule).

     Keith