Skip to content
This repository has been archived by the owner on Jan 21, 2022. It is now read-only.

Connecting OpenMRS (EMR) to a FHIR Server

Volker edited this page Jun 13, 2019 · 2 revisions

This page describes how to push data entered in OpenMRS (EMR = Electronic Medical Record System) to a FHIR Server by using the new OpenMRS Sync2 Module. This is just a simple approach that may be used in simple cases. This approach may not be suitable for environments with a high load or may require additional hardware in such a case for processing the data.

The OpenMRS Atomfeed Module notifies the OpenMRS Sync2 Module about created, updated and deleted data. The Sync2 Module can retrieve the changed data by OpenMRS REST requests or by OpenMRS FHIR requests from the OpenMRS system. After conflict handling the retrieved data is pushed to a configured client. The invocations that push FHIR data to a configured client may neither be FHIR compliant (e.g. use the correct content type), nor reference only existing FHIR resources, nor be able to transform the OpenMRS specific FHIR profile to a country specific FHIR profile that is expected on a SHR.

Sync2 to FHIR Server Adapter

The following code snippet is used to receive the FHIR resources from OpenMRS (Sync2 Module), transform them and store them on a FHIR server. This adapter can be seen as a simple mediator. The code uses a slightly modified OpenMRS FHIR profile to store the data.

@Controller
@RequestMapping( path = "/ws/fhir" )
public class TransformationController
{
    private static final String FHIR_CONTENT_TYPE = "application/fhir+json; charset=UTF-8";

    private final FhirContext fhirContext = FhirContext.forDstu3();

    @Value( "${fhir-openmrs-mediator.fhir.server.baseUrl}" )
    private String fhirServerBaseUrl;

    @Value( "${fhir-openmrs-mediator.open-mrs.fhir.baseUrl}" )
    private String openMrsFhirBaseUrl;

    @Value( "${fhir-openmrs-mediator.open-mrs.fhir.user}" )
    private String openMrsUser;

    @Value( "${fhir-openmrs-mediator.open-mrs.fhir.password}" )
    private String openMrsPassword;

    @RequestMapping( path = "/{resourceType}/{id}", method = { RequestMethod.GET } )
    public ResponseEntity<byte[]> get( @PathVariable( "resourceType" ) String resourceType, @PathVariable( "id" ) String id )
    {
        final IGenericClient openMrsClient = fhirContext.newRestfulGenericClient( openMrsFhirBaseUrl );
        openMrsClient.registerInterceptor( new BasicAuthInterceptor( openMrsUser, openMrsPassword ) );
        final IBaseResource openMrsResource;
        try
        {
            openMrsResource = openMrsClient.read().resource( resourceType ).withId( id ).execute();
        }
        catch ( ResourceNotFoundException | ResourceGoneException e )
        {
            return new ResponseEntity<>( HttpStatus.NOT_FOUND );
        }
        
        push( openMrsResource );

        final HttpHeaders headers = new HttpHeaders();
        headers.add( HttpHeaders.CONTENT_TYPE, FHIR_CONTENT_TYPE );
        return new ResponseEntity<>( getResourceAsBytes( openMrsResource ), headers, HttpStatus.OK );
    }

    @RequestMapping( path = "/*/{id}", method = { RequestMethod.POST, RequestMethod.PUT } )
    public ResponseEntity<byte[]> push( @PathVariable( "id" ) String id, RequestEntity<byte[]> request )
    {
        final IBaseResource resource = fhirContext.newJsonParser().parseResource(
            new InputStreamReader( new ByteArrayInputStream( Objects.requireNonNull( request.getBody() ) ), StandardCharsets.UTF_8 ) );

        final HttpHeaders headers = new HttpHeaders();
        headers.add( HttpHeaders.CONTENT_TYPE, FHIR_CONTENT_TYPE );
        return new ResponseEntity<>( getResourceAsBytes( push( resource ) ), headers, HttpStatus.OK );
    }

    @RequestMapping( path = "/{resourceType}/{id}", method = { RequestMethod.DELETE } )
    public ResponseEntity<Void> delete( @PathVariable( "resourceType" ) String resourceType, @PathVariable( "id" ) String id )
    {
        final IGenericClient serverClient = fhirContext.newRestfulGenericClient( fhirServerBaseUrl );
        try
        {
            serverClient.delete().resourceById( resourceType, id ).execute();
        }
        catch ( ResourceNotFoundException | ResourceGoneException e )
        {
            return new ResponseEntity<>( HttpStatus.NOT_FOUND );
        }
        return new ResponseEntity<>( HttpStatus.NO_CONTENT );
    }

    private IBaseResource push( IBaseResource resource )
    {
        final List<BundleEntryComponent> bundleEntryComponents = convertResourceFromMrs( (Resource) resource );

        final Bundle bundle = new Bundle().setType( Bundle.BundleType.TRANSACTION );
        bundleEntryComponents.forEach( bundle::addEntry );

        final IGenericClient client = fhirContext.newRestfulGenericClient( fhirServerBaseUrl );
        client.transaction().withBundle( bundle ).execute();
        return resource;
    }

    private byte[] getResourceAsBytes( IBaseResource resource )
    {
        final ByteArrayOutputStream os = new ByteArrayOutputStream();
        try
        {
            final OutputStreamWriter writer = new OutputStreamWriter( os, StandardCharsets.UTF_8 );
            fhirContext.newJsonParser().encodeResourceToWriter( resource, writer );
            writer.close();
            os.close();
        }
        catch ( IOException e )
        {
            throw new RuntimeException( "Error while writing FHIR resource.", e );
        }
        return os.toByteArray();
    }


    private List<BundleEntryComponent> convertResourceFromMrs( Resource resource )
    {
        if ( resource instanceof Encounter )
        {
            return convertFromMrs( (Encounter) resource );
        }
        if ( resource instanceof Location )
        {
            return convertFromMrs( (Location) resource );
        }
        if ( resource instanceof Observation )
        {
            return convertFromMrs( (Observation) resource );
        }
        if ( resource instanceof Patient )
        {
            return convertFromMrs( (Patient) resource );
        }
        return Collections.singletonList( createBundleEntryComponent( resource ) );
    }

    private List<BundleEntryComponent> convertFromMrs( Encounter encounter )
    {
        encounter.setParticipant( null );
        encounter.setPartOf( null );
        return Collections.singletonList( createBundleEntryComponent( encounter ) );
    }

    private List<BundleEntryComponent> convertFromMrs( Location location )
    {
        final BundleEntryComponent organizationComponent = createOrganization();
        location.setManagingOrganization( new Reference( organizationComponent.getResource().getId() ) );
        return Arrays.asList( organizationComponent, createBundleEntryComponent( location ) );
    }

    private List<BundleEntryComponent> convertFromMrs( Observation observation )
    {
        observation.setPerformer( null );
        return Collections.singletonList( createBundleEntryComponent( observation ) );
    }

    private List<BundleEntryComponent> convertFromMrs( Patient patient )
    {
        final BundleEntryComponent organizationComponent = createOrganization();
        final String identifierValue = getOpenMrsIdentifierValue( patient.getIdentifier() );
        if ( identifierValue != null )
        {
            patient.getIdentifier().add( new Identifier()
                .setSystem( "http://example.sl/patients" ).setValue( "OMRS01-" + identifierValue ) );
        }
        patient.setManagingOrganization( new Reference( organizationComponent.getResource().getId() ) );
        return Arrays.asList( organizationComponent, createBundleEntryComponent( patient ) );
    }

    private BundleEntryComponent createOrganization()
    {
        final Organization organization = new Organization();
        organization.setId( IdType.newRandomUuid() );
        organization.setName( "George Brook Health Centre" );
        organization.setActive( true );
        organization.addType().addCoding()
            .setSystem( "http://terminology.hl7.org/CodeSystem/organization-type" )
            .setCode( "prov" ).setDisplay( "Healthcare Provider" );
        organization.addIdentifier()
            .setSystem( "http://example.sl/organizations" )
            .setValue( "OU_278330" );

        final BundleEntryComponent bundleEntryComponent = new BundleEntryComponent();
        bundleEntryComponent.setResource( organization ).setFullUrl( organization.getId() )
            .getRequest().setMethod( Bundle.HTTPVerb.PUT )
            .setUrl( organization.fhirType() + "?identifier=http://example.sl/organizations|OU_278330" );
        return bundleEntryComponent;
    }

    private BundleEntryComponent createBundleEntryComponent( Resource resource )
    {
        final BundleEntryComponent bundleEntryComponent = new BundleEntryComponent();
        bundleEntryComponent.setResource( resource ).setFullUrl( resource.getId() )
            .getRequest().setMethod( Bundle.HTTPVerb.PUT )
            .setUrl( resource.fhirType() + "/" + resource.getIdElement().getIdPart() );
        return bundleEntryComponent;
    }
    
    @Nullable
    private String getOpenMrsIdentifierValue( @Nullable Collection<Identifier> identifiers )
    {
        if ( identifiers == null )
        {
            return null;
        }
        for ( Identifier identifier : identifiers )
        {
            if ( "OpenMRS ID".equals( identifier.getSystem() ) )
            {
                return identifier.getValue();
            }
        }
        return null;
    }
}

Required Software

  • OpenMRS Platform 2.1.4 WAR
  • OpenMRS Reference Application 2.8.0 Addons
  • OpenMRS FHIR Module 1.16.0
  • OpenMRS Atomfeed Module 1.0.8
  • OpenMRS Sync2 Module 1.4.0
  • HAPI FHIR JPA Server 3.6.0, or any other FHIR Server that is FHIR Release 3 (STU) compliant

Configuration

Atomfeed

The following configuration snippet contains only the feed configurations that must be enabled. The existing feed configuration should not be replaced. Just the specified feed configurations should be enabled.

{
  "feedFilterBeans": [],
  "feedConfigurations": [
    {
      "openMrsClass": "org.openmrs.Obs",
      "enabled": true,
      "title": "Observation",
      "category": "observation",
      "linkTemplates": {
        "rest": "/ws/rest/v1/obs/{uuid}?v=full",
        "fhir": "/ws/fhir/Observation/{uuid}"
      },
      "feedWriter": null
    },
    {
      "openMrsClass": "org.openmrs.Encounter",
      "enabled": true,
      "title": "Encounter",
      "category": "encounter",
      "linkTemplates": {
        "rest": "/ws/rest/v1/encounter/{uuid}?v=full",
        "fhir": "/ws/fhir/Encounter/{uuid}"
      },
      "feedWriter": null
    },
    {
      "openMrsClass": "org.openmrs.Patient",
      "enabled": true,
      "title": "Patient",
      "category": "patient",
      "linkTemplates": {
        "rest": "/ws/rest/v1/patient/{uuid}?v=full",
        "fhir": "/ws/fhir/Patient/{uuid}"
      },
      "feedWriter": null
    },
    {
      "openMrsClass": "org.openmrs.Location",
      "enabled": true,
      "title": "Location",
      "category": "location",
      "linkTemplates": {
        "rest": "/ws/rest/v1/location/{uuid}?v=full",
        "fhir": "/ws/fhir/Location/{uuid}"
      },
      "feedWriter": null
    }
  ]
}

Sync2

First the following global property must be changed to the specified value. This should ensure that the content of the OpenMRS instance will not be overridden by the content of the FHIR server and no manual conflict resolution is required. The newest resource (will be the resource on OpenMRS instance) will override the older resource every time.

sync2.mergeBehavior = sync2.newIsTheBestMergeBehaviour

The following configuration assumes that the OpenMRS instance is available on port 8090 of localhost and the Sync2 to FHIR Server Adapter can be reached on port 9067 of localhost. The push is performed any 30 seconds (as long as there is changed data to push).

{
  "general" : {
    "parentFeedLocation" : "http://localhost:8091/openmrs",
    "localFeedLocation" : "http://localhost:8090/openmrs",
    "localInstanceId" : "test",
    "persistSuccessAudit" : true,
    "persistFailureAudit" : true,
    "clients" : {
      "fhir" : {
        "hostAddress" : "http://localhost:9067",
        "login" : null,
        "password" : null
      }
    }
  },
  "push" : {
    "enabled" : true,
    "schedule" : 30,
    "classes" : [ {
      "classTitle" : "Location",
      "category" : "location",
      "openMrsClass" : "org.openmrs.Location",
      "enabled" : true,
      "preferredClient" : "fhir"
    }, {
      "classTitle" : "Allergy",
      "category" : "allergy",
      "openMrsClass" : "org.openmrs.Allergy",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Observation",
      "category" : "observation",
      "openMrsClass" : "org.openmrs.Obs",
      "enabled" : true,
      "preferredClient" : null
    }, {
      "classTitle" : "Encounter",
      "category" : "encounter",
      "openMrsClass" : "org.openmrs.Encounter",
      "enabled" : true,
      "preferredClient" : null
    }, {
      "classTitle" : "Visit",
      "category" : "visit",
      "openMrsClass" : "org.openmrs.Visit",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Provider",
      "category" : "provider",
      "openMrsClass" : "org.openmrs.Provider",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Drug",
      "category" : "drug",
      "openMrsClass" : "org.openmrs.Drug",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Form",
      "category" : "form",
      "openMrsClass" : "org.openmrs.Form",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Order",
      "category" : "order",
      "openMrsClass" : "org.openmrs.Order",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Program Enrollment",
      "category" : "programenrollment",
      "openMrsClass" : "org.openmrs.PatientProgram",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Person",
      "category" : "person",
      "openMrsClass" : "org.openmrs.Person",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Patient",
      "category" : "patient",
      "openMrsClass" : "org.openmrs.Patient",
      "enabled" : true,
      "preferredClient" : "fhir"
    }, {
      "classTitle" : "Relationship",
      "category" : "relationship",
      "openMrsClass" : "org.openmrs.Relationship",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Cohort",
      "category" : "cohort",
      "openMrsClass" : "org.openmrs.Cohort",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Visit Type",
      "category" : "visittype",
      "openMrsClass" : "org.openmrs.VisitType",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Concept",
      "category" : "concept",
      "openMrsClass" : "org.openmrs.Concept",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "User",
      "category" : "user",
      "openMrsClass" : "org.openmrs.User",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Program",
      "category" : "program",
      "openMrsClass" : "org.openmrs.Program",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Privilege",
      "category" : "privilege",
      "openMrsClass" : "org.openmrs.Privilege",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "AuditMessage",
      "category" : "audit_message",
      "openMrsClass" : "org.openmrs.module.sync2.api.model.audit.AuditMessage",
      "enabled" : false,
      "preferredClient" : null
    } ]
  },
  "pull" : {
    "enabled" : false,
    "schedule" : 43200,
    "classes" : [ {
      "classTitle" : "Location",
      "category" : "location",
      "openMrsClass" : "org.openmrs.Location",
      "enabled" : true,
      "preferredClient" : null
    }, {
      "classTitle" : "Allergy",
      "category" : "allergy",
      "openMrsClass" : "org.openmrs.Allergy",
      "enabled" : true,
      "preferredClient" : null
    }, {
      "classTitle" : "Observation",
      "category" : "observation",
      "openMrsClass" : "org.openmrs.Obs",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Encounter",
      "category" : "encounter",
      "openMrsClass" : "org.openmrs.Encounter",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Visit",
      "category" : "visit",
      "openMrsClass" : "org.openmrs.Visit",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Provider",
      "category" : "provider",
      "openMrsClass" : "org.openmrs.Provider",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Drug",
      "category" : "drug",
      "openMrsClass" : "org.openmrs.Drug",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Form",
      "category" : "form",
      "openMrsClass" : "org.openmrs.Form",
      "enabled" : true,
      "preferredClient" : null
    }, {
      "classTitle" : "Order",
      "category" : "order",
      "openMrsClass" : "org.openmrs.Order",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "DrugOrder",
      "category" : "drug_order",
      "openMrsClass" : "org.openmrs.DrugOrder",
      "enabled" : true,
      "preferredClient" : null
    }, {
      "classTitle" : "TestOrder",
      "category" : "test_order",
      "openMrsClass" : "org.openmrs.TestOrder",
      "enabled" : true,
      "preferredClient" : null
    }, {
      "classTitle" : "Program Enrollment",
      "category" : "programenrollment",
      "openMrsClass" : "org.openmrs.PatientProgram",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Person",
      "category" : "person",
      "openMrsClass" : "org.openmrs.Person",
      "enabled" : true,
      "preferredClient" : null
    }, {
      "classTitle" : "Patient",
      "category" : "patient",
      "openMrsClass" : "org.openmrs.Patient",
      "enabled" : true,
      "preferredClient" : null
    }, {
      "classTitle" : "Relationship",
      "category" : "relationship",
      "openMrsClass" : "org.openmrs.Relationship",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Cohort",
      "category" : "cohort",
      "openMrsClass" : "org.openmrs.Cohort",
      "enabled" : true,
      "preferredClient" : null
    }, {
      "classTitle" : "Visit Type",
      "category" : "visittype",
      "openMrsClass" : "org.openmrs.VisitType",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Concept",
      "category" : "concept",
      "openMrsClass" : "org.openmrs.Concept",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "User",
      "category" : "user",
      "openMrsClass" : "org.openmrs.User",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Program",
      "category" : "program",
      "openMrsClass" : "org.openmrs.Program",
      "enabled" : false,
      "preferredClient" : null
    }, {
      "classTitle" : "Privilege",
      "category" : "privilege",
      "openMrsClass" : "org.openmrs.Privilege",
      "enabled" : true,
      "preferredClient" : null
    }, {
      "classTitle" : "AuditMessage",
      "category" : "audit_message",
      "openMrsClass" : "org.openmrs.module.sync2.api.model.audit.AuditMessage",
      "enabled" : true,
      "preferredClient" : null
    } ]
  },
  "whitelist" : {
    "enabled" : false,
    "instanceIds" : [ ]
  }
}
Clone this wiki locally