Spring Data Commons implementation for HAPI FHIR (Part 3)

Kostiantyn Ivanov
6 min readSep 3, 2023

--

Previous part:
Spring Data Commons implementation for HAPI FHIR (Part 2)

Core features implementation map:

Dependencies

We add spring dependencies that will bring us spring data commons interfaces and spring context. Beside that we add HAPI FHIR dependencies to build our internal FHIR client.

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>spring-data-hapifhir</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>3.0.3</version>
</parent>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<hapi-fhir-.version>6.4.3</hapi-fhir-.version>
<spring-data-commons.version>3.0.3</spring-data-commons.version>
</properties>

<dependencies>
<!--Spring-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>${spring-data-commons.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>3.0.3</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>

<!--HAPI FHIR-->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r4</artifactId>
<version>${hapi-fhir-.version}</version>
</dependency>

<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</artifactId>
<version>${hapi-fhir-.version}</version>
</dependency>

</dependencies>

</project>

Simple Repository

Not we make a simple CRUD implementation of a repository. This implementation will be provided if we will specify empty Spring Data repositories in our client application.

public class SimpleFhirRepository<T extends  Resource, ID> implements CrudRepository<T, ID> {
private final IGenericClient fhirClient;
private final Class<T> modelType;

public SimpleFhirRepository(IGenericClient fhirClient, Class<T> modelType) {
this.fhirClient = fhirClient;
this.modelType = modelType;
}

@Override
public <S extends T> S save(S entity) {
if (entity.hasId()) {
fhirClient.update().resource(entity).execute();
} else {
entity.setId(fhirClient.create().resource(entity).execute().getId());
}

return entity;
}
...

@Override
public Optional<T> findById(ID id) {
IBaseResource result = fhirClient.read().resource(getModelType().getSimpleName())
.withId((String) id).execute();
return Optional.ofNullable((T)result);
}

private Class<T> getModelType() {
return modelType;
}

...

@Override
public Iterable<T> findAll() {
Class<T> modelType = getModelType();
return BundleUtil
.toListOfResourcesOfType(
fhirClient.getFhirContext(),
fhirClient.search().forResource(modelType.getSimpleName()).returnBundle(Bundle.class).execute(),
modelType
);
}

...

@Override
public void deleteById(ID id) {
fhirClient.delete().resourceById(getModelType().getSimpleName(), (String)id).execute();
}

...
}

Repository Factory

Repository factory will provide an information about default repository class to a context. It also will create an instance of this class using reflation.

NOTE 1: We specified an empty implementation of getEntityInformation since we will not use it yet. You can provide additional data for your default repositories here.

NOTE 2: We left a default getQueryLookupStrategy implementation for now. We will come back to this method later, when will be implementing query methods. And it’s locked anyway in our map =)

public class FhirRepositoryFactory extends RepositoryFactorySupport {
private final IGenericClient fhirClient;

public FhirRepositoryFactory(IGenericClient fhirClient) {
this.fhirClient = fhirClient;
}

@Override
public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
return new AbstractEntityInformation<>(domainClass) {
@Override
public ID getId(T entity) {
return null;
}

@Override
public Class<ID> getIdType() {
return null;
}
};
}

@Override
protected Object getTargetRepository(RepositoryInformation metadata) {
return getTargetRepositoryViaReflection(metadata, fhirClient, metadata.getDomainType());
}

@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return SimpleFhirRepository.class;
}

@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(QueryLookupStrategy.Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return super.getQueryLookupStrategy(key, evaluationContextProvider);
}
}

FHIR Client Config

We create a default FHIR client bean which can be overridden by client. It give us a lot of flexibility in order to use different implementations of FHIR Client with different configurations (timeouts, retries, serialization/ deserialization properties).

@Configuration
public class FhirClientConfiguration {
@Value("${spring.data.fhir.base-url}")
private String fhirBaseUrl;

@Bean
@ConditionalOnMissingBean
public IGenericClient fhirClient() {
return FhirContext.forR4()
.newRestfulGenericClient(fhirBaseUrl);
}
}

Repository Factory Bean

Our Factory Bean will extend RepositoryFactoryBeanSupport thus it will be able to create our Repository Factory. Also it implements BeanFactoryAware so we can take needed dependencies (for example FHIR client bean from the previous section) from the context. These dependencies can be populated into our default Repository implementation.

public class FhirRepositoryFactoryBean<T extends Repository<S, String>, S>
extends RepositoryFactoryBeanSupport<T, S, String> implements BeanFactoryAware {
private static final String FHIR_CLIENT_BEAN_NAME = "fhirClient";
private IGenericClient fhirClient;

protected FhirRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
super(repositoryInterface);
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
super.setBeanFactory(beanFactory);
this.fhirClient = beanFactory.getBean(FHIR_CLIENT_BEAN_NAME, IGenericClient.class);
}
@Override
protected RepositoryFactorySupport createRepositoryFactory() {
Assert.notNull(fhirClient, "fhirClient bean should be created!");
return new FhirRepositoryFactory(fhirClient);
}
}

Repository Config Extension

Config extension provides extensions and overrides to an enabling annotation (we will overview it in the next section). However it has “extension” in its name its implementation is mandatory and we need to provide it. In the other case we will face with: “java.lang.IllegalArgumentException: RepositoryConfigurationExtension must not be null…”

public class FhirRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport {
private static final String MODULE_NAME = "Fhir";

@Override
protected String getModulePrefix() {
return getModuleIdentifier();
}

@Override
public String getModuleName() {
return MODULE_NAME;
}

@Override
public String getRepositoryFactoryBeanClassName() {
return FhirRepositoryFactoryBean.class.getName();
}
}

Enabling annotation and Repositories Registrar

We already implemented almost everything. The only thing left is how to attach all this beautiful code to our client application context. And now enabling annotation and registrar take their turn.

Enabling annotation attaches our default FHIR client config and the registrar into a context and also provides the basic configuration properties (with ability to specify default values which can be overridden using extension from the previous section).

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({FhirClientConfiguration.class, FhirRepositoriesRegistrar.class})
public @interface EnableFhirRepositories {
...
String repositoryImplementationPostfix() default "Impl";
QueryLookupStrategy.Key queryLookupStrategy() default QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND;
Class<?> repositoryFactoryBeanClass() default FhirRepositoryFactoryBean.class;

Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;

...
}

Registrar provides the enabling annotation class (yes, it’s a bidirectional dependency) and the extension instance.

public class FhirRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport {
@Override
protected Class<? extends Annotation> getAnnotation() {
return EnableFhirRepositories.class;
}

@Override
protected RepositoryConfigurationExtension getExtension() {
return new FhirRepositoryConfigExtension();
}
}

Our first Spring Data FHIR Repository

As you can see we implemented a lot of thigs and our journey was pretty long (but I hope it was interesting). But why do we need all of this? For sure, to make our application client code clean and short. Let’s just take a look, how our client can start using our new framework and enjoy their life:

Dependencies:

<dependency>
<groupId>com.example</groupId>
<artifactId>spring-data-hapifhir</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

Configuration:

spring:
data:
fhir:
base-url: http://hapi.fhr.org/baseR4

Main class:

@SpringBootApplication
@EnableFhirRepositories
public class FhirDemoApplication {

public static void main(String[] args) {
SpringApplication.run(FhirDemoApplication.class, args);
}

}

Repository:

public interface PatientRepository extends CrudRepository<Patient, String> {
}

Controller (Service layer skipped):

@RestController
@RequestMapping("/patients")
public class PatientController {
@Autowired
private PatientRepository repository;

@GetMapping
public List<Patient> getAll() {
return (List<Patient>) repository.findAll();
}

@PostMapping
public Patient save(@RequestBody Patient patient) {
return repository.save(patient);
}

@GetMapping("/{id}")
public Patient getOne(@PathVariable String id) {
return repository.findById(id).orElseThrow(() -> new RuntimeException("Patient not found"));
}

@DeleteMapping("/{id}")
public void deleteOne(@PathVariable String id) {
repository.deleteById(id);
}
}

I believe our developers will be happy to use it. Let’s discuss how to bring even more useful functionality to them in the next part.

Next part:
Spring Data Commons implementation for HAPI FHIR (Part 4)

--

--