Pages

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.