Skip to content

Commit

Permalink
Merge pull request #313 from IBM/johntimm-master
Browse files Browse the repository at this point in the history
FHIR Model Guide
  • Loading branch information
prb112 committed Oct 25, 2019
2 parents 12507a0 + 4f8df9b commit 00b987b
Show file tree
Hide file tree
Showing 8 changed files with 831 additions and 242 deletions.
125 changes: 125 additions & 0 deletions docs/FHIRModelGuide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# FHIR Model Guide

## Overview

The FHIR model component provides Java APIs for parsing, building, generating and validating FHIR resources. Java model classes that represent FHIR resources and data types are generated directly from the structure definitions distributed with the spec. All model objects are thread-safe and immutable. Each model class implements the Java builder pattern (Effective Java, Joshua Bloch) and the visitor pattern (GoF). The classes also implement Java equals, hashCode and toString methods. All date/time processing is done using the Java 8 time library.

Many of the data type classes include additional factory methods to facilitate object construction for common use cases. The model includes generated Javadoc comments complete with excerpts taken directly from the specification. Model classes also include Java annotations for constraints (`@Constraint`), required elements (`@Required`), choice element types (`@Choice`) and value set bindings (`@Binding`). Value set bindings are implemented using Code subclasses with constant fields and nested enumerations. Backbone elements are implemented as Java nested classes to keep them organized.

All schema-level (structure, cardinality, value domain) and global (empty resource, empty element) constraint validation is happens during object construction. This means that it is virtually impossible to build a schema invalid FHIR resource using the APIs. Additional constraint validation (invariants, profile, terminology) is performed using the FHIRValidator class. FHIRParser and FHIRGenerator classes are used to parse and generate both JSON and XML formats. FHIRPathEvaluator is a FHIRPath evaluation engine built on an ANTLR4 generated parser. It implements are large portion of the FHIRPath specification and is used for validation and search parameter value extraction.

## Building a Resource using the FHIR Model API

The FHIR model API implements the Builder pattern for constructing Resource instances.

```
Observation bodyWeight = Observation.builder()
.meta(Meta.builder()
.profile(Canonical.of("http://hl7.org/fhir/StructureDefinition/bodyweight"))
.build())
.status(ObservationStatus.FINAL)
.effective(DateTime.builder()
.value("2019-01-01")
.build())
.category(CodeableConcept.builder()
.coding(Coding.builder()
.system(Uri.of("http://terminology.hl7.org/CodeSystem/observation-category"))
.code(Code.of("vital-signs"))
.build())
.build())
.code(CodeableConcept.builder()
.coding(Coding.builder()
.system(Uri.of("http://loinc.org"))
.code(Code.of("29463-7"))
.build())
.build())
.value(Quantity.builder()
.value(Decimal.of(200))
.system(Uri.of("http://unitsofmeasure.org"))
.code(Code.of("[lb_av]"))
.unit(string("lbs"))
.build())
.build();
```

In the example above, a number of different builder classes are used:

- `Observation.Builder`
- `DateTime.Builder`
- `CodeableConcept.Builder`
- `Quantity.Builder`

Every type in the model that represents a FHIR resource or element has a corresponding nested, static Builder class used for constructing thread-safe, immutable instances.

Several static factory / utility methods are also used:

- `Canonical.of(...)`
- `Uri.of(...)`
- `Code.of(...)`
- `String.string(...)` (via static import)

Many of the primitive data types contain this type of "helper" method.

Fields from an immutable model object may be copied back into a builder object using the `toBuilder()` method:
```
bodyWeight = bodyWeight.toBuilder()
.value(bodyWeight.getValue().as(Quantity.class).toBuilder()
.value(Decimal.of(210))
.build())
.build();
```

## Parsing a Resource from an InputStream or Reader

```
// Parse from InputStream
InputStream in = getInputStream("JSON/bodyweight.json");
Observation observation = FHIRParser.parser(Format.JSON).parse(in);
// Parse from Reader
Reader reader = getReader("JSON/bodyweight.json");
Observation observation = FHIRParser.parser(Format.JSON).parse(reader);
```

## Generating JSON and XML formats from a Resource instance

```
// Generate JSON format
FHIRGenerator.generator(Format.JSON).generate(bodyWeight, System.out);
// Generate XML format
FHIRGenerator.generator(Format.XML).generate(bodyWeight, System.out);
```

The `FHIRGenerator` interface has a separate factory method that takes `boolean prettyPrinting` as a parameter:

```
// Generate JSON format (with pretty printing)
FHIRGenerator.generator(Format.JSON, true).generate(bodyWeight, System.out);
```

## Validating a Resource instance

Schema-level validation occurs during object construction. This includes validation of cardinality constraints and value domains. Additional validation of constraints specified in the model is performed using the `FHIRValidator` class.

```
Observation observation = getObservation();
List<Issue> issues = FHIRValidator.validator().validate(observation);
for (Issue issue : issues) {
if (IssueSeverity.ERROR.equals(issue.getSeverity())) {
// handle error
}
}
```

## Evaluating FHIRPath expressions on a Resource instance

```
EvaluationContext evaluationContext = new EvaluationContext(bodyWeight);
Collection<FHIRPathNode> result = FHIRPathEvaluator.evaluator().evaluate(evaluationContext, "Observation.value.as(Quantity).value >= 200");
assert(FHIRPathUtil.isTrue(result));
```

The `EvaluationContext` class builds a `FHIRPathTree` from a FHIR resource or element. A `FHIRPathTree` is a tree of labeled nodes that wrap FHIR elements and are used by the FHIRPath evaluation engine (`FHIRPathEvaluator`).
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public static void main(java.lang.String[] args) throws Exception {
Patient patient = Patient.builder()
.id(id)
.active(Boolean.TRUE)
.deceased(Boolean.FALSE)
.multipleBirth(Integer.of(2))
.meta(meta)
.name(name)
Expand All @@ -89,7 +90,7 @@ public void close() {
EvaluationContext evaluationContext = new EvaluationContext(patient);

FHIRPathEvaluator.DEBUG = true;
Collection<FHIRPathNode> result = evaluator.evaluate(evaluationContext, "Patient.name.given.first().as(System.String)");
Collection<FHIRPathNode> result = evaluator.evaluate(evaluationContext, "Patient.deceased.exists() and Patient.deceased != false");

System.out.println("result: " + result);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* (C) Copyright IBM Corp. 2019
*
* SPDX-License-Identifier: Apache-2.0
*/

package com.ibm.fhir.model.test;

import static com.ibm.fhir.model.type.String.string;

import com.ibm.fhir.model.format.Format;
import com.ibm.fhir.model.generator.FHIRGenerator;
import com.ibm.fhir.model.resource.Observation;
import com.ibm.fhir.model.resource.Observation.Component;
import com.ibm.fhir.model.type.Code;
import com.ibm.fhir.model.type.CodeableConcept;
import com.ibm.fhir.model.type.Coding;
import com.ibm.fhir.model.type.Decimal;
import com.ibm.fhir.model.type.Element;
import com.ibm.fhir.model.type.Quantity;
import com.ibm.fhir.model.type.Uri;
import com.ibm.fhir.model.type.code.ObservationStatus;

public class FoodIntakeObservationTest {
private static final String SYSTEM_SNOMED_CT = "http://snomed.info/sct";
private static final String SYSTEM_UNITS_OF_MEASURE = "http://www.unitsofmeasure.org";

public static void main(String[] args) throws Exception {
Observation foodIntakeObservation = Observation.builder()
.status(ObservationStatus.FINAL)
.code(codeableConcept(coding(SYSTEM_SNOMED_CT, "226379006", "Food intake")))
.component(component(codeableConcept(coding(SYSTEM_SNOMED_CT, "226441002", "Fish intake")),
quantity(250, "grams", SYSTEM_UNITS_OF_MEASURE, "g")))
.component(component(codeableConcept(coding(SYSTEM_SNOMED_CT, "226404003", "Milk intake")),
quantity(1, "cup", SYSTEM_UNITS_OF_MEASURE, "[cup_us]")))
.build();
FHIRGenerator.generator(Format.JSON, true).generate(foodIntakeObservation, System.out);
}

public static Component component(CodeableConcept code, Element value) {
return Component.builder()
.code(code)
.value(value)
.build();
}

public static CodeableConcept codeableConcept(Coding... coding) {
return CodeableConcept.builder()
.coding(coding)
.build();
}

public static Coding coding(String system, String code, String display) {
return Coding.builder()
.system(Uri.of(system))
.code(Code.of(code))
.display(string(display))
.build();
}

public static Quantity quantity(Number value, String unit, String system, String code) {
return Quantity.builder()
.value(Decimal.of(value))
.unit(string(unit))
.system(Uri.of(system))
.code(Code.of(code))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ public Resource getResource() {

@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Expand Down Expand Up @@ -121,9 +121,6 @@ public static Version from(String version) {
return new Version(version);
}
try {
if (version.startsWith("v") || version.startsWith("V")) {
version = version.substring(1);
}
Integer major = Integer.parseInt(tokens[0]);
Integer minor = (tokens.length >= 2) ? Integer.parseInt(tokens[1]) : 0;
Integer patch = (tokens.length == 3) ? Integer.parseInt(tokens[2]) : 0;
Expand All @@ -135,12 +132,12 @@ public static Version from(String version) {

@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Expand Down
Loading

0 comments on commit 00b987b

Please sign in to comment.