-
Notifications
You must be signed in to change notification settings - Fork 26
FHIR server notes
This FHIR server is based on the HAPI open source server. These notes discuss how it was modified to link in toolkit and more specifically toolkit simulators.
HAPI is a FHIR server. Toolkit is a collection of servers. We call those individual servers simulators. They have their own context maintained in the external cache in simdb. Here we are trying to convert HAPI to serve the same high level structure where the endpoint (URL) has within it coded the simulator identity. So first a little HAPI.
We start with ExampleRestfulServlet which is, in the HAPI distribution, an example of their RestfulServer which itself extends HttpServlet. (ExampleRestfulServlet extends RestfulServer extends HttpServlet). We have to modify both ExampleRestfulServlet and the base RestfulServer to manage our simulator mappings.
The first issue to overcome is managing the SimId, the identifier for a specific simulator. There is no place in the RestfulServer to hide it. So we introduce a hack. It is the class
class SimTracker static Map<Long, SimContext> contextMap = new HashMap<Long, SimContext>(); static public void setContext(SimContext cxt) { Long threadId = Thread.currentThread().getId(); contextMap.put(threadId, cxt); } static public SimContext getContext() { Long threadId = Thread.currentThread().getId(); return contextMap.get(threadId); }
It relies on the fact that each request is managed on its own thread. A thread has a thread ID which is used as an index above. This is not so different from depending on a Request Context within the servlet standard.
Nearly the first thing that happens at the start of an incoming request is that
ExampleRestfulServlet:initializeRequestHandler(String requestFullPath)
is called. Here we capture the SimId off the URI and stash it using SimTracker:setContext(SimContext cxt). SimContext contains the SimId. Later when we need it we use SimTracker:getContext() to retrieve the SimId. Obviously this could be used to manage more context than just SimId.
Next we must acknowledge that HAPI has a lot of magic going on related to managing Resource. The short story is that for each resource type managed by the server there is a class which looks like this:
class PatientResourceProvider implements IResourceProvider
this class is linked to a resource type by the overridden method
@Override public Class<Patient> getResourceType() { return Patient.class; }
At system startup time HAPI scans known class that implement IResourceProvider, builds an instance and calls this method. It maintains a table linking the name of the resource (actually the class) to the Provider. The Provider contains all the actions you ask for on a resource.
So the
where SimContext is a simple container for the simId. Later in when implementing an operation in HAPI (READ for example)
@Read(version = true) public Patient readPatient(@IdParam IdDt theId) {
we use
SimContext simContext = SimTracker.getContext(); SimId simId = simContext.getSimId();
to get back the simId (contained in SimContext). The other request types are
@Create() @Search() @Read(version = true) @Update()
There are a lot of others. They are defined in ca.uhn.fhir.rest.annotation.
HAPI is a full FHIR server (as best I can tell) and actually does too much for us. It is an implementation and we need to build a test tool. The needs are different. We started with ExampleRestfulServlet because it bypasses a lot of the automatic things HAPI could do for us. That’s the good news. The bad news is there are now some things we have to do on our own. What? Basically the entire back end. But that’s were we want to plug in toolkit so that’s ok.
ResDb is the FHIR equivalent of SimDb for our SOAP based simulators. It is separate because it has different requirements. It has enough in common that the class ResDb extends SimDb so the resource DB is actually an extension of the sim (SOAP) DB.
FHIR simulators (or resource sims - haven’t settle on a name yet) have the same top level naming structure. A sim is named
testsession__name
note that like simdb, that is a double underscore a separator. Name must be unique within the test session. It will likely be enforced that a SOAP sim and a FHIR sim cannot have the same name just for better understanding but there is not yet a technical reason for that restriction.
The top level structure of the db similar to simdb:
fhir/ sim_type.txt simid.txt simindex/
where
fhir directory holds the main contents of the simulator<br /> sim_type.txt is the simulator type<br /> simid.txt is the simulator id<br /> simindex is new. It is the storage area for the search engine Lucene which we use to index and search FHIR resources. Our code does not manage the contents of this directory, Lucene does. More on this later.
Inside the fhir directory, in a fashion similar to SOAP simulators, there is a directory per operation. One obvious example is PUT. This directory holds all the submitted content.
Each directory under PUT is a toolkit event:
2017_03_22_11_57_43_324/ date.ser Patient1.json
which is timestamp name of a submission event. Inside it is the date of submission (really the same information that is on the directory but in an easy format for code to pull) and the stored resource in JSON format. The file name is the resource type followed by the index which is assigned locally when it is stored.
A retrieve (or more specifically a GET) uses the Lucene index for the simulator even if the ID of the resource is provided. This is because we do not store resources by id we store them by submission event. BTW, the index shown above (the 1 on Patient1.json) is not the id of the resource. It is just a descriminator so we can store a bunch of Patient resources in the directory. It has no other significance. The resource contents and the Lucene index hold the real ID.
We now look to the HAPI Provider interface, more specifically to
@Read(version = true) public Patient readPatient(@IdParam IdDt theId) {
A Provider implementation has a @Read method with one parameter, the ID. We use Lucene to lookup the resource. First we need the Patient ID
String patientId = theId.getIdPart(); // in the FHIR sense
and through our hack the simid
SimContext simContext = SimTracker.getContext(); SimId simId = simContext.getSimId();
and the Lucene lookup
SimIndexer simIndexer = new SimIndexer(simId).open(); List<String> paths = simIndexer.lookupByTypeAndId("Patient", patientId);
Lucene always returns a list of paths since it is usually used with large data sets and queries returning many results. We pull the file out of the ResDb with
resourceString = Io.stringFromFile(new File(paths.get(0)));
and return it through HAPI invocation
return ourCtx.newJsonParser().parseResource(Patient.class, resourceString);
To look at what this means in terms of Lucene we have to open up the code where we indexed the resource.
To be able to search for a Resource we must index it. There are a number of per-resource-type files that must be created to create and use an index.
gov.nist.toolkit.fhirServer.XResourceProvider - this is the main class managing Resource X<br /> gov.nist.toolkit.fhir.resourceIndexer.X - this is the index builder for Resource X<br />
A good place to start is the class gov.nist.toolkit.fhir.resourceIndexer.Base which is the base class for resource indexers. It’s build method looks like:
ResourceIndex build(def json, SimResource simResource) { ResourceIndex ri = new ResourceIndex() String resourceType = json.resourceType ri.add(new ResourceIndexItem('type', resourceType)) String id = json.id ri.add(new ResourceIndexItem('id', id)) return ri }
The ResourceIndex has two items added to it, type and id. These are the two most basic indexes and are necessary for basic lookup. Notice they look a lot like properties. ResourceIndex is the primary data structure for interfacing with Lucene. It contains
List<ResourceIndexItem> items = [] String path
where items is the list of properties we are defining above when we describe Resources and path which is the location in the ResDb where the Resource is stored. New ResourceIndex objects get written to and read from the Lucene database via the class ResDbIndexer. This database is stored in the ResDb under the directory simindex/ that was mentioned above.
Toolkit
Downloads
Installing Toolkit
Configuring Toolkit for Imaging Tests
Reporting Toolkit Installation Problems
Environment
Test Session
Conformance Test Tool
Writing Conformance Tests
Overview of Imaging Tests
Test Context Definition
Launching Conformance Tool from Gazelle
Inspector
External Cache
Support Tools
Test Organization
Configuring Test Kits
Managing Multiple Test Kits
SAML Validation against Gazelle
Renaming Toolkit
Toolkit API
Managing system configurations
Configuring Toolkit for Connectathon
Developer's blog