HAPI FHIR & MongoDB

2,353 views
Skip to first unread message

muthu.ku...@gmail.com

unread,
Feb 25, 2015, 3:40:12 PM2/25/15
to hapi...@googlegroups.com
I am trying to use HAPI FHIR resources as POJO classes to persist and read back from MongoDB. I can persist to MongoDB using the JSON parser and saving the JSON encoded string into a collection. However, I am unable to read it back and map to a FHIR resource e.g. Patient. Can someone point me to an example of how this can be done? I am trying to avoid going the DBObject route to populate the Patient object.

muthu.ku...@gmail.com

unread,
Feb 25, 2015, 3:42:47 PM2/25/15
to hapi...@googlegroups.com, muthu.ku...@gmail.com
package com.cxd.Patient.Resource;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.stereotype.Repository;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu.composite.HumanNameDt;
import ca.uhn.fhir.model.dstu.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;

/**
 * All resource providers must implement IResourceProvider
 */
@Repository
public class RestfulPatientResourceProvider implements IResourceProvider {

private FhirContext fhirContext;

private MongoOperations mongoOperations;

public RestfulPatientResourceProvider() {
}

public FhirContext getFhirContext() {
return fhirContext;
}

public void setFhirContext(FhirContext fhirContext) {
this.fhirContext = fhirContext;
}

public MongoOperations getMongoOperations() {
return mongoOperations;
}

public void setMongoOperations(MongoOperations mongoOperations) {
this.mongoOperations = mongoOperations;
}

/**
* The getResourceType method comes from IResourceProvider, and must be
* overridden to indicate what type of resource this provider supplies.
*/
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}

/**
* This is the "read" operation. The "@Read" annotation indicates that this
* method supports the read and/or vread operation.
* <p>
* Read operations take a single parameter annotated with the
* {@link IdParam} paramater, and should return a single resource instance.
* </p>
* @param theId
*            The read operation takes one parameter, which must be of type
*            IdDt and must be annotated with the "@Read.IdParam"
*            annotation.
* @return Returns a resource matching this identifier, or null if none
*         exists.
*/
@Read(version = true)
public Patient readPatient(@IdParam IdDt theId) {

System.out.println("Id parameter:" + theId.getIdPart());

Patient patient = mongoOperations.findById(theId.getIdPart(),
Patient.class, "patient");

String jsonEncoded = fhirContext.newJsonParser().setPrettyPrint(true)
.encodeResourceToString(patient);

System.out.println(jsonEncoded);

return patient;
}

/**
* The "@Create" annotation indicates that this method implements
* "create=type", which adds a new instance of a resource to the server.
*/
@Create
public MethodOutcome createPatient(@ResourceParam Patient thePatient)
throws Exception {

UUID uid = UUID.randomUUID();
String idPart = new IdDt(uid.toString()).getValue();
thePatient.setId("http://localhost:8080/fhir/Patient/" + idPart);
thePatient.getIdentifier().get(0)
.setSystem(new UriDt("urn:cxdtest:mrns"));
thePatient.getIdentifier().get(0)
.setValue(thePatient.getIdentifierFirstRep().getValue());

validateResource(thePatient);

String jsonEncoded = fhirContext.newJsonParser().setPrettyPrint(true)
.encodeResourceToString(thePatient);

System.out.println(jsonEncoded);

try {

mongoOperations.insert(jsonEncoded, "patient");

} catch (Exception e) {
throw new Exception(e.getMessage());
}

return new MethodOutcome(thePatient.getId());
}

/**
* The "@Search" annotation indicates that this method supports the search
* operation. You may have many different method annotated with this
* annotation, to support many different search criteria. This example
* searches by family name.
* @param theFamilyName
*            This operation takes one parameter which is the search
*            criteria. It is annotated with the "@Required" annotation.
*            This annotation takes one argument, a string containing the
*            name of the search criteria. The datatype here is StringDt,
*            but there are other possible parameter types depending on the
*            specific search criteria.
* @return This method returns a list of Patients. This list may contain
*         multiple matching resources, or it may also be empty.
*/
@Search()
public List<Patient> findPatientsByName(
@RequiredParam(name = Patient.SP_FAMILY) StringDt theFamilyName) {
List<Patient> patients = new ArrayList<Patient>();

/*
* Look for all patients matching the family name
*/

patients = mongoOperations.findAll(Patient.class, "patient");

for (Patient patient : patients)

{
NAMELOOP: for (HumanNameDt nextName : patient.getName()) {
for (StringDt nextFamily : nextName.getFamily()) {
if (theFamilyName.equals(nextFamily)) {
patients.add(patient);
break NAMELOOP;
}
}
}
}

return patients;
}

/**
* This method just provides simple business validation for resources we are
* storing.
* @param thePatient
*            The patient to validate
*/
private void validateResource(Patient thePatient) {
/*
* Our server will have a rule that patients must have a family name or
* we will reject them
*/
if (thePatient.getNameFirstRep().getFamilyFirstRep().isEmpty()) {
OperationOutcome outcome = new OperationOutcome();
outcome.addIssue()
.setSeverity(IssueSeverityEnum.FATAL)
.setDetails(
"No family name provided, Patient resources must have at least one family name.");
throw new UnprocessableEntityException(outcome);
}
}

}


This is what I have right now. Obviously the Create API works but not the Search or Read operations.

James Agnew

unread,
Feb 26, 2015, 11:01:53 AM2/26/15
to muthu.ku...@gmail.com, hapi...@googlegroups.com
Hi Muthu,

I haven't worked with Mongo personally, so this may not be helpful at all.... but here goes. :)

It looks to me in your "read" code like you're trying to retrieve HAPI's FHIR Java objects from the database instead of retrieving the FHIR "wire format" JSON documents, which is presumably what you want.


@Read(version = true)
public Patient readPatient(@IdParam IdDt theId) { 
            String patientString = mongoOperations.findById(theId.getIdPart()); // Read a JSON document from the DB
            Patient retVal = fhirContext.newJsonParser().parseResource(Patient.class, patientString);
            return retVal;
}

That would be my guess. Bear in mind this is just typed in GMail, I have no idea if it will work.

Cheers,
James


--
You received this message because you are subscribed to the Google Groups "HAPI FHIR" group.
To unsubscribe from this group and stop receiving emails from it, send an email to hapi-fhir+...@googlegroups.com.
To post to this group, send email to hapi...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/hapi-fhir/a1d2861a-5a96-4431-9755-64a7c4df324e%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

muthu.ku...@gmail.com

unread,
Feb 26, 2015, 11:50:53 AM2/26/15
to hapi...@googlegroups.com, muthu.ku...@gmail.com
Thank you James, I think you may have pointed me in the right direction. However, it did not parse the resource back correctly. What's being read in patientString is shown below... It may be due to the internal ObjectID assigned to this document by MongoDB.

Patient patient = fhirContext.newJsonParser().parseResource(Patient.class, patientString);

{ "_id" : { "$oid" : "54ecf6ffd8780569f5465296"} , "resourceType" : "Patient" , "text" : { "status" : "generated" , "div" : "<div><div class=\"hapiHeaderText\"> Donald <b>DUCK </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Identifier</td><td>UHN MRN 7000135</td></tr><tr><td>Address</td><td><span>10 Raut Street </span><br /><span>VICTORIA </span><span>BC </span><span>Can </span></td></tr><tr><td>Date of birth</td><td><span>01 June 1980</span></td></tr></tbody></table></div>"} , "identifier" : [ { "use" : "official" , "label" : "UHN MRN 7000135" , "system" : "urn:cxdtest:mrns" , "value" : "7000135" , "assigner" : { "reference" : "Organization/1.3.6.1.4.1.12201"}}] , "name" : [ { "family" : [ "Duck"] , "given" : [ "Donald"]}] , "telecom" : [ { "system" : "phone" , "use" : "home"} , { "system" : "phone" , "use" : "work"} , { "system" : "phone" , "use" : "mobile"} , { "system" : "email" , "use" : "home"}] , "gender" : { "coding" : [ { "system" : "http://hl7.org/fhir/v3/AdministrativeGender" , "code" : "M"}]} , "birthDate" : "1980-06-01T00:00:00" , "address" : [ { "use" : "home" , "line" : [ "10 Raut Street"] , "city" : "VICTORIA" , "state" : "BC" , "zip" : "V8N 1Y4" , "country" : "Can"}] , "managingOrganization" : { "reference" : "Organization/1.3.6.1.4.1.12201"}}
2015-02-26 08:36:41.227  INFO 8628 --- [nio-8080-exec-1] ca.uhn.fhir.context.ModelScanner         : Done scanning FHIR library, found 6 model entries in 6ms
2015-02-26 08:36:41.236  INFO 8628 --- [nio-8080-exec-1] ca.uhn.fhir.context.ModelScanner         : Done scanning FHIR library, found 2 model entries in 3ms
2015-02-26 08:36:41.240 ERROR 8628 --- [nio-8080-exec-1] ca.uhn.fhir.rest.server.RestfulServer    : Failure during REST processing

ca.uhn.fhir.rest.server.exceptions.InternalErrorException: Failed to call access method
at ca.uhn.fhir.rest.method.BaseMethodBinding.invokeServerMethod(BaseMethodBinding.java:195)
at ca.uhn.fhir.rest.method.ReadMethodBinding.invokeServer(ReadMethodBinding.java:192)
at ca.uhn.fhir.rest.method.BaseResourceReturningMethodBinding.invokeServer(BaseResourceReturningMethodBinding.java:239)
at ca.uhn.fhir.rest.method.ReadMethodBinding.invokeServer(ReadMethodBinding.java:53)
at ca.uhn.fhir.rest.server.RestfulServer.handleRequest(RestfulServer.java:614)
at ca.uhn.fhir.rest.server.RestfulServer.doGet(RestfulServer.java:127)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:618)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)

James Agnew

unread,
Feb 26, 2015, 5:16:21 PM2/26/15
to muthu.ku...@gmail.com, hapi...@googlegroups.com
I would guess that you are correct. With that "_id" element, you no longer have a valid resource so the parser is probably rejecting it.

There are two things here:
1. I think HAPI should probably give a warning but not fail outright when it finds that unexpected element. This seems like a useful enhancement for a future version.
2. If you use a JSON library to remove that element (or even a bit of string manipulation) it should parse correctly and things should start working. You could use the mongo assigned ID (54ecf6ffd8780569f5465296) as the resource in the returned resource too.

patient.setId(mongoId);

Cheers,
James

muthu.ku...@gmail.com

unread,
Feb 26, 2015, 7:47:13 PM2/26/15
to hapi...@googlegroups.com, muthu.ku...@gmail.com
James, I agree with both your points. Just for others benefit, I had to strip out the internal id "_id" attribute and the "text" attribute which contains the HTML narrative. But thanks again for getting me on track here. I was worried that I would have to write custom mappers to map MongoDB objects for all the FHIR resources.

              patientString = "{\"resourceType\":\"Patient\",\"identifier\":[{\"system\":\"urn:system\",\"value\":\"12345\"}],\"name\":[{\"family\":[\"Smith\"],\"given\":[\"John\"]}]}";

              Patient patient = fhirContext.newJsonParser().parseResource(Patient.class, patientString);

Kevin Mayfield

unread,
Mar 9, 2015, 6:55:01 AM3/9/15
to Muthu Kuttalingam, hapi...@googlegroups.com

I'm going to have a look at using suggestions in the above link. The Jackson link looks to be useful for mapping the date types correctly.

p.s. Probably moving over to service Mix at some point.

Reply all
Reply to author
Forward
0 new messages