Using the IBM FHIR Server and Implementation Guide as Java Modules

The IBM FHIR Server is an extensible HL7 FHIR Server. The IBM FHIR server supports complicated ImplementationGuides (IGs), a set of rules of how a particular problem is solved using FHIR Resources. The implementation guides include a set of Profiles, ValueSets, CodeSystems and supporting resources (Examples, CapabilityStatements).

The IBM FHIR Server supports the loading of NPM packages – stored in the (package.tgz). You see the package at the https://www.hl7.org/fhir/us/core/package.tgz (One just appends package.tgz to any IG site).

The IBM FHIR Server includes a number of IGs built-tested-released with each tag.

The IGs are Java modules which are specially formed to support the resources in the NPM packages. The Java modules use a ServiceLoader (activated at startup when the Java Module is in the classpath).

The best way to start is to copy and existing fhir-ig, such as fhir-ig-us-core, and modify as needed (package rename and updated files).

The service loader uses the META-INF/services/com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider interface to activate the list of classes in the file.

Each of the corresponding classes need to be in src/main/java under the corresponding package (com.ibm.fhir.ig.us.core as above).

The PackageRegistryResourceProvider navigates the src/main/resources to find the folder hl7/fhir/us/core/311 and loads the files referenced in the NPM packages index (.index.json).

You might not see the .index.json file by default in Eclipse, and you should unhide the .resource file at the Enterprise Explorer View > Filters and Customization, Select .*resources, Click OK

When you open .index.json, you should see:

These are the resources which will be loaded when the fhir-ig-us-core is added to the userlib folder.

The US-Core and CarinBB support multiple versions of the same IG – v3.1.1 and v4.0.0 in the same Java module. To control this behavior, one needs to set the configuration in order to map to a default version (the default is always the latest or newest version).  With cross IG dependencies, they are updated to point to the correct one, or the latest one as the IG specifies.

To make these files viewable, we do like to format the contents of these folders so they are pretty JSON. When the IGs are built and released, the JSON files are compressed and saves a good chunk of Memory. 

We also like to remove Narrative texts:

"text": {
     "status": "empty",
     "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Redacted for size</div>"
},

There is an examples folder included with most package.tgz files.  You should copy this into the src/test/resources/JSON folder and update the index.txt so you have the latest examples in place, and these will be loaded the ProfileTest (ConformaceTest is used for usecase specific tests).  The below is an example of loading the 400/index.txt and failing when issues exceed a limit or a severity of error.

public class ProfileTest {

    private static final String INDEX = "./src/test/resources/JSON/400/index.txt";

    private String path = null;

    public ProfileTest() {
        // No Operation
    }

    public ProfileTest(String path) {
        this.path = path;
    }

    @Test
    public void testUSCoreValidation() throws Exception {
        try (Reader r = Files.newBufferedReader(Paths.get(path))) {
            Resource resource = FHIRParser.parser(Format.JSON).parse(r);
            List<Issue> issues = FHIRValidator.validator().validate(resource);
            issues.forEach(item -> {
                if (item.getSeverity().getValue().equals("error")) {
                    System.out.println(path + " " + item);
                }
            });
            assertEquals(countErrors(issues), 0);
        } catch (Exception e) {
            System.out.println("Exception with " + path);
            fail(e.toString());
        }
    }

    @Factory
    public Object[] createInstances() {
        List<Object> result = new ArrayList<>();

        try (BufferedReader br = Files.newBufferedReader(Paths.get(INDEX))) {
            String line;
            while ((line = br.readLine()) != null) {
                result.add(new ProfileTest(line));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result.toArray();
    }

}

The ProviderTest checks the provider loads successfully, and that the number of resources returned is correct, and tests one exemplary resource is returned.

@Test
    public void testRegistry() {
        StructureDefinition definition = FHIRRegistry.getInstance()
                .getResource("http://hl7.org/fhir/us/core/StructureDefinition/pediatric-bmi-for-age", StructureDefinition.class);
        assertNotNull(definition);
        assertEquals(definition.getVersion().getValue(), "4.0.0");
    }

    @Test
    public void testUSCoreResourceProvider() {
        FHIRRegistryResourceProvider provider = new USCore400ResourceProvider();
        assertEquals(provider.getRegistryResources().size(), 148);
    }

There is a very important test which is testConstraintGenerator any issue in the structure definition will be reported when the Constraints are compiled, and you’ll get some really good warnings.

    @Test
    public static void testConstraintGenerator() throws Exception {
        FHIRRegistryResourceProvider provider = new USCore400ResourceProvider();
        for (FHIRRegistryResource registryResource : provider.getRegistryResources()) {
            if (StructureDefinition.class.equals(registryResource.getResourceType())) {
                assertEquals(registryResource.getVersion().toString(), "4.0.0");
                String url = registryResource.getUrl() + "|4.0.0";
                System.out.println(url);
                Class<?> type = ModelSupport.isResourceType(registryResource.getType()) ? ModelSupport.getResourceType(registryResource.getType()) : Extension.class;
                for (Constraint constraint : ProfileSupport.getConstraints(url, type)) {
                    System.out.println("    " + constraint);
                    if (!Constraint.LOCATION_BASE.equals(constraint.location())) {
                        compile(constraint.location());
                    }
                    compile(constraint.expression());
                }
            }
        }
    }

For example, you might get a pediatric BMI set of constraints:

http://hl7.org/fhir/us/core/StructureDefinition/pediatric-bmi-for-age|4.0.0
    @com.ibm.fhir.model.annotation.Constraint(id="vs-1", level="Rule", location="Observation.effective", description="if Observation.effective[x] is dateTime and has a value then that value shall be precise to the day", expression="($this as dateTime).toString().length() >= 8", source="http://hl7.org/fhir/StructureDefinition/vitalsigns", modelChecked=false, generated=false, validatorClass=interface com.ibm.fhir.model.annotation.Constraint$FHIRPathConstraintValidator)
    @com.ibm.fhir.model.annotation.Constraint(id="vs-2", level="Rule", location="(base)", description="If there is no component or hasMember element then either a value[x] or a data absent reason must be present.", expression="(component.empty() and hasMember.empty()) implies (dataAbsentReason.exists() or value.exists())", source="http://hl7.org/fhir/StructureDefinition/vitalsigns", modelChecked=false, generated=false, validatorClass=interface com.ibm.fhir.model.annotation.Constraint$FHIRPathConstraintValidator)

The above constraints can be tested using the FHIRPath expression against a failing test resource, and confirm the validity of the StructureDefinition.

I hope this document help you build your own IGs for the IBM FHIR Server.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.