Skip to content

Commit

Permalink
v2 -> v1 plugin (#748)
Browse files Browse the repository at this point in the history
Added new eCR module with eRSD V2 to V1 conversion operation

---------

Co-authored-by: taha.attari@smilecdr.com <taha.attari@smilecdr.com>
Co-authored-by: Adam Stevenson <stevenson_adam@yahoo.com>
  • Loading branch information
3 people authored Nov 25, 2023
1 parent dd28edd commit 5ef7709
Show file tree
Hide file tree
Showing 16 changed files with 11,644 additions and 7 deletions.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@

---

The cqf-ruler is based on the [HAPI FHIR JPA Server Starter](https://github.com/hapifhir/hapi-fhir-jpaserver-starter) and adds a set of plugins that provide an implementation of FHIR's [Clinical Reasoning Module](
http://hl7.org/fhir/clinicalreasoning-module.html), serve as a
The cqf-ruler is based on the [HAPI FHIR JPA Server Starter](https://github.com/hapifhir/hapi-fhir-jpaserver-starter) and adds a set of plugins that provide an implementation of FHIR's [Clinical Reasoning Module](http://hl7.org/fhir/clinicalreasoning-module.html), serve as a
knowledge artifact repository, and a [cds-hooks](https://cds-hooks.org/) compatible clinical decision support service. The cqf-ruler provides an [extensibility API](#plugins) to allow adding custom FHIR operations without the need to fork or clone the entire project.

See the [wiki](https://github.com/DBCG/cqf-ruler/wiki/Home) for more information
Expand All @@ -30,6 +29,7 @@ The public sandbox is not persistent, has no authentication, and is regularly re

The easiest way to get started with the cqf-ruler is to pull and run the docker image.
For avoiding to run docker container by default root user permission, the container from this image will run with a user named `cqfruler`

```bash
docker pull alphora/cqf-ruler
docker run -p 8080:8080 alphora/cqf-ruler
Expand Down Expand Up @@ -75,8 +75,7 @@ to clean up any unneeded or unused files, use:

#### Java

Go to [http://www.oracle.com/technetwork/java/javase/downloads/](
http://www.oracle.com/technetwork/java/javase/downloads/) and download the
Go to [http://www.oracle.com/technetwork/java/javase/downloads/](http://www.oracle.com/technetwork/java/javase/downloads/) and download the
latest (version 11 or higher) JDK for your platform, and install it.

#### Apache Maven
Expand Down
3 changes: 3 additions & 0 deletions ecr/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM contentgroup/cqf-ruler:latest

COPY ./target/cqf-ruler-*.jar plugin
15 changes: 15 additions & 0 deletions ecr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Plugin

This is a cqf-ruler plugin which takes an eRSD V2 Bundle and converts it to an eRSD V1 Bundle.

## Build

Use `mvn package` to build the jar files

## Docker

The Dockerfile builds on top of the base cqf-ruler image and simply copies the jar into the `plugin` directory of the image.

## Setup

A V1 PlanDefinition skeleton must be uploaded with ID : `plandefinition-ersd-skeleton`
66 changes: 66 additions & 0 deletions ecr/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.opencds.cqf.ruler</groupId>
<artifactId>cqf-ruler</artifactId>
<version>0.15.0-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>

<groupId>com.ecr</groupId>
<artifactId>cqf-ruler-ecr</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.opencds.cqf.ruler</groupId>
<artifactId>cqf-ruler-core</artifactId>
<version>0.15.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.opencds.cqf.ruler</groupId>
<artifactId>cqf-ruler-test</artifactId>
<version>0.15.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<configuration>
<skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
21 changes: 21 additions & 0 deletions ecr/src/main/java/com/transform/TransformConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.transform;

import org.opencds.cqf.external.annotations.OnR4Condition;
import org.opencds.cqf.ruler.api.OperationProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TransformConfig {
@Bean
public TransformProperties transformProperties() {
return new TransformProperties();
}

@Bean
@Conditional(OnR4Condition.class)
public OperationProvider transformProvider() {
return new TransformProvider();
}
}
28 changes: 28 additions & 0 deletions ecr/src/main/java/com/transform/TransformProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.transform;
import org.hl7.fhir.r5.model.IdType;
import org.opencds.cqf.ruler.behavior.DaoRegistryUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;

import ca.uhn.fhir.jpa.api.dao.DaoRegistry;

@ConfigurationProperties(prefix = "transform")
public class TransformProperties implements DaoRegistryUser {
@Autowired
private DaoRegistry myDaoRegistry;

public static final IdType v1PlanDefinitionId = new IdType("PlanDefinition", "plandefinition-ersd-skeleton");
public static final String usPHTriggeringVSProfile = "http://hl7.org/fhir/us/ecr/StructureDefinition/us-ph-triggering-valueset";
public static final String usPHTriggeringVSLibProfile = "http://hl7.org/fhir/us/ecr/StructureDefinition/us-ph-triggering-valueset-library";
public static final String ersdVSLibProfile = "http://hl7.org/fhir/us/ecr/StructureDefinition/ersd-valueset-library";
public static final String ersdVSProfile = "http://hl7.org/fhir/us/ecr/StructureDefinition/ersd-valueset";
public static final String ersdPlanDefinitionProfile = "http://hl7.org/fhir/us/ecr/StructureDefinition/ersd-plandefinition";
public static final String usPHSpecLibProfile = "http://hl7.org/fhir/us/ecr/StructureDefinition/us-ph-specification-library";
public static final String usPHUsageContextType = "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type";
public static final String hl7UsageContextType = "http://terminology.hl7.org/CodeSystem/usage-context-type";
public static final String usPHUsageContext = "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context";

public DaoRegistry getDaoRegistry() {
return myDaoRegistry;
}
}
169 changes: 169 additions & 0 deletions ecr/src/main/java/com/transform/TransformProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package com.transform;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.MetadataResource;
import org.hl7.fhir.r4.model.PlanDefinition;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.ResourceType;
import org.hl7.fhir.r4.model.UsageContext;
import org.hl7.fhir.r4.model.ValueSet;
import org.opencds.cqf.ruler.api.OperationProvider;
import org.springframework.beans.factory.annotation.Autowired;

import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;

public class TransformProvider implements OperationProvider {
@Autowired
TransformProperties transformProperties;

/**
* Implements the $ersd-v2-to-v1-transform operation which transforms an ersd v2 Bundle
* into an ersd v1 compatible bundle
* @param requestDetails the incoming request details
* @param maybeBundle the v2 bundle to transform
* @param maybePlanDefinition the v1 PlanDefinition to include
* @return the v1 compatible bundle
*/
@Description(shortDefinition = "Converts a v2 ERSD bundle into a v1 ERSD bundle", value = "Converts a v2 ERSD bundle into a v1 ERSD bundle")
@Operation(idempotent = true, name = "$ersd-v2-to-v1-transform")
public Bundle convert_v1(
RequestDetails requestDetails,
@OperationParam(name = "bundle") IBaseResource maybeBundle,
@OperationParam(name = "planDefinition") IBaseResource maybePlanDefinition
) throws UnprocessableEntityException {
if (maybeBundle == null) {
throw new UnprocessableEntityException("Resource is missing");
}
if (!(maybeBundle instanceof IBaseBundle )) {
throw new UnprocessableEntityException("Resource is not a bundle");
}
if (maybePlanDefinition != null && !(maybePlanDefinition instanceof PlanDefinition)) {
throw new UnprocessableEntityException("Provided v1 PlanDefinition is not a PlanDefinition resource");
}
Bundle v2 = (Bundle) maybeBundle;
removeRootSpecificationLibrary(v2);
final PlanDefinition v1PlanDefinition = maybePlanDefinition != null ? (PlanDefinition) maybePlanDefinition : getV1PlanDefinition(requestDetails);
v2.getEntry().stream()
.forEach(entry -> {
if (entry.getResource() instanceof MetadataResource) {
MetadataResource resource = (MetadataResource) entry.getResource();
checkAndUpdateV2PlanDefinition(entry, v1PlanDefinition);
updateV2GroupersUseContext(resource,v1PlanDefinition.getIdElement());
updateV2TriggeringValueSets(resource, v1PlanDefinition.getUrl());
updateV2TriggeringValueSetLibrary(resource);
resource.setExperimentalElement(null);
}
});
return v2;
}
private void updateV2GroupersUseContext(MetadataResource resource, IIdType planDefinitionId) {
// if resourc is a vs
if (resource.getResourceType() == ResourceType.ValueSet) {
ValueSet valueSet = (ValueSet) resource;
// if vs is a grouper
if (valueSet.hasCompose()
&& valueSet.getCompose().getIncludeFirstRep().getValueSet().size() > 0) {
List<UsageContext> usageContexts = valueSet.getUseContext();
UsageContext program = usageContexts.stream().filter(useContext -> useContext.getCode().getSystem().equals(TransformProperties.hl7UsageContextType) && useContext.getCode().getCode().equals("program")).findFirst().orElseGet(() -> {
UsageContext retval = new UsageContext(new Coding(TransformProperties.hl7UsageContextType, "program", null), null);
usageContexts.add(retval);
return retval;
});
program.setValue(new Reference(planDefinitionId));
}
}
}
private void removeRootSpecificationLibrary(Bundle v2) {
List<BundleEntryComponent> filteredRootLib = v2.getEntry().stream()
.filter(entry -> entry.hasResource())
.filter(entry -> !(entry.getResource().hasMeta() && entry.getResource().getMeta().hasProfile(TransformProperties.usPHSpecLibProfile))).collect(Collectors.toList());
v2.setEntry(filteredRootLib);
}
private void checkAndUpdateV2PlanDefinition(BundleEntryComponent entry, PlanDefinition v1PlanDefinition) throws UnprocessableEntityException{
if (entry.getResource().getResourceType() == ResourceType.PlanDefinition
&& entry.getResource().hasMeta()
&& entry.getResource().getMeta().getProfile().stream().anyMatch(canonical -> canonical.getValue().contains("/ersd-plandefinition"))) {
entry.setResource(v1PlanDefinition);
String url = Optional.ofNullable(v1PlanDefinition.getUrl()).orElseThrow(() -> new UnprocessableEntityException("URL missing from PlanDefinition"));
String version = Optional.ofNullable(v1PlanDefinition.getVersion()).orElseThrow(() -> new UnprocessableEntityException("Version missing from PlanDefinition"));
entry.setFullUrl(url + "|" + version);
}
}
/**
* Remove all instances of an old profile and add one instance of a new profile
* @param meta the meta object to update
* @param oldProfile the profile URL to remove
* @param newProfile the profile URL to add
*/
private void replaceProfile(Meta meta, String oldProfile, String newProfile) {
// remove all instances of old profile
List<CanonicalType> updatedProfiles = meta.getProfile().stream()
.filter(profile -> !profile.getValue().equals(oldProfile)).collect(Collectors.toList());
// add the new profile if it doesn't exist
if (!updatedProfiles.stream().anyMatch(profile -> profile.getValue().equals(newProfile))) {
updatedProfiles.add(new CanonicalType(newProfile));
}
meta.setProfile(updatedProfiles);
}
private void updateV2TriggeringValueSetLibrary(MetadataResource resource) {
if (resource.getResourceType() == ResourceType.Library
&& resource.hasMeta()
&& resource.getMeta().hasProfile(TransformProperties.usPHTriggeringVSLibProfile)
) {
replaceProfile(resource.getMeta(), TransformProperties.usPHTriggeringVSLibProfile, TransformProperties.ersdVSLibProfile);
List<UsageContext> filteredUseContexts = resource.getUseContext().stream()
.filter(useContext ->
!(useContext.getCode().getCode().equals("reporting")
&& useContext.getValueCodeableConcept().hasCoding(TransformProperties.usPHUsageContext, "triggering"))
&& !(useContext.getCode().getCode().equals("specification-type")
&& useContext.getValueCodeableConcept().hasCoding(TransformProperties.usPHUsageContext, "value-set-library")))
.collect(Collectors.toList());
resource.setUseContext(filteredUseContexts);
}
}
private void updateV2TriggeringValueSets(MetadataResource resource, String v1PlanDefinitionUrl) {
if (resource.getResourceType() == ResourceType.ValueSet
&& resource.hasMeta()
&& resource.getMeta().hasProfile(TransformProperties.usPHTriggeringVSProfile)) {
replaceProfile(resource.getMeta(), TransformProperties.usPHTriggeringVSProfile, TransformProperties.ersdVSProfile);
List<UsageContext> filteredUseContexts = resource.getUseContext().stream()
.filter(useContext ->
!(useContext.getCode().getCode().equals("reporting")
&& useContext.getValueCodeableConcept().hasCoding(TransformProperties.usPHUsageContext, "triggering"))
&& !(useContext.getCode().getCode().equals("priority")
&& useContext.getValueCodeableConcept().hasCoding(TransformProperties.usPHUsageContext, "routine")))
.collect(Collectors.toList());
resource.setUseContext(filteredUseContexts);
}
}
private PlanDefinition getV1PlanDefinition(RequestDetails requestDetails) throws ResourceNotFoundException {
Optional<PlanDefinition> maybePlanDefinition = Optional.ofNullable(null);
try {
PlanDefinition v1PlanDefinition = (PlanDefinition) transformProperties
.getDaoRegistry()
.getResourceDao(TransformProperties.v1PlanDefinitionId.getResourceType())
.read(TransformProperties.v1PlanDefinitionId, requestDetails);
maybePlanDefinition = Optional.of(v1PlanDefinition);
} catch (ResourceNotFoundException | ResourceGoneException e) {
throw new ResourceNotFoundException("Could not find V1 PlanDefinition");
}
return maybePlanDefinition.orElseThrow(() -> new ResourceNotFoundException("Could not find V1 PlanDefinition"));
}
}
2 changes: 2 additions & 0 deletions ecr/src/main/resources/META-INF/spring.factories
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.transform.TransformConfig
495 changes: 495 additions & 0 deletions ecr/src/main/resources/ersd-v1-plandefinition-skeleton.json

Large diffs are not rendered by default.

Loading

0 comments on commit 5ef7709

Please sign in to comment.