package ca.uhn.example.provider;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.dstu2.composite.HumanNameDt;
import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.valueset.AdministrativeGenderEnum;
import ca.uhn.fhir.model.dstu2.valueset.IssueSeverityEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.parser.DataFormatException;
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.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
/**
* This is a resource provider which stores Patient resources in memory using a HashMap. This is obviously not a production-ready solution for many reasons,
* but it is useful to help illustrate how to build a fully-functional server.
*/
public class PatientResourceProvider implements IResourceProvider {
/**
* This map has a resource ID as a key, and each key maps to a Deque list containing all versions of the resource with that ID.
*/
private Map<Long, Deque<Patient>> myIdToPatientVersions = new HashMap<Long, Deque<Patient>>();
/**
* This is used to generate new IDs
*/
private long myNextId = 1;
/**
* Constructor, which pre-populates the provider with one resource instance.
*/
public PatientResourceProvider() {
long resourceId = myNextId++;
Patient patient = new Patient();
patient.setId(Long.toString(resourceId));
patient.addIdentifier();
patient.getIdentifier().get(0).setSystem(new UriDt("urn:hapitest:mrns"));
patient.getIdentifier().get(0).setValue("00002");
patient.addName().addFamily("Test");
patient.getName().get(0).addGiven("PatientOne");
patient.setGender(AdministrativeGenderEnum.FEMALE);
LinkedList<Patient> list = new LinkedList<Patient>();
list.add(patient);
myIdToPatientVersions.put(resourceId, list);
}
/**
* Stores a new version of the patient in memory so that it can be retrieved later.
*
* @param thePatient
* The patient resource to store
* @param theId
* The ID of the patient to retrieve
*/
private void addNewVersion(Patient thePatient, Long theId) {
InstantDt publishedDate;
if (!myIdToPatientVersions.containsKey(theId)) {
myIdToPatientVersions.put(theId, new LinkedList<Patient>());
publishedDate = InstantDt.withCurrentTime();
} else {
Patient currentPatitne = myIdToPatientVersions.get(theId).getLast();
Map<ResourceMetadataKeyEnum<?>, Object> resourceMetadata = currentPatitne.getResourceMetadata();
publishedDate = (InstantDt) resourceMetadata.get(ResourceMetadataKeyEnum.PUBLISHED);
}
/*
* PUBLISHED time will always be set to the time that the first version was stored. UPDATED time is set to the time that the new version was stored.
*/
thePatient.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, publishedDate);
thePatient.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, InstantDt.withCurrentTime());
Deque<Patient> existingVersions = myIdToPatientVersions.get(theId);
// We just use the current number of versions as the next version number
String newVersion = Integer.toString(existingVersions.size());
// Create an ID with the new version and assign it back to the resource
IdDt newId = new IdDt("Patient", Long.toString(theId), newVersion);
thePatient.setId(newId);
existingVersions.add(thePatient);
}
/**
* 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) {
validateResource(thePatient);
// Here we are just generating IDs sequentially
long id = myNextId++;
addNewVersion(thePatient, id);
// Let the caller know the ID of the newly created resource
return new MethodOutcome(new IdDt(id));
}
/**
* 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) {
LinkedList<Patient> retVal = new LinkedList<Patient>();
/*
* Look for all patients matching the name
*/
for (Deque<Patient> nextPatientList : myIdToPatientVersions.values()) {
Patient nextPatient = nextPatientList.getLast();
NAMELOOP: for (HumanNameDt nextName : nextPatient.getName()) {
for (StringDt nextFamily : nextName.getFamily()) {
if (theFamilyName.equals(nextFamily)) {
retVal.add(nextPatient);
break NAMELOOP;
}
}
}
}
return retVal;
}
@Search
public List<Patient> findPatientsUsingArbitraryCtriteria() {
LinkedList<Patient> retVal = new LinkedList<Patient>();
for (Deque<Patient> nextPatientList : myIdToPatientVersions.values()) {
Patient nextPatient = nextPatientList.getLast();
retVal.add(nextPatient);
}
return retVal;
}
/**
* 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) {
Deque<Patient> retVal;
try {
retVal = myIdToPatientVersions.get(theId.getIdPartAsLong());
} catch (NumberFormatException e) {
/*
* If we can't parse the ID as a long, it's not valid so this is an unknown resource
*/
throw new ResourceNotFoundException(theId);
}
if (theId.hasVersionIdPart() == false) {
return retVal.getLast();
} else {
for (Patient nextVersion : retVal) {
String nextVersionId = nextVersion.getId().getVersionIdPart();
if (theId.getVersionIdPart().equals(nextVersionId)) {
return nextVersion;
}
}
// No matching version
throw new ResourceNotFoundException("Unknown version: " + theId.getValue());
}
}
/**
* The "@Update" annotation indicates that this method supports replacing an existing
* resource (by ID) with a new instance of that resource.
*
* @param theId
* This is the ID of the patient to update
* @param thePatient
* This is the actual resource to save
* @return This method returns a "MethodOutcome"
*/
@Update()
public MethodOutcome updatePatient(@IdParam IdDt theId, @ResourceParam Patient thePatient) {
validateResource(thePatient);
Long id;
try {
id = theId.getIdPartAsLong();
} catch (DataFormatException e) {
throw new InvalidRequestException("Invalid ID " + theId.getValue() + " - Must be numeric");
}
/*
* Throw an exception (HTTP 404) if the ID is not known
*/
if (!myIdToPatientVersions.containsKey(id)) {
throw new ResourceNotFoundException(theId);
}
addNewVersion(thePatient, id);
return new MethodOutcome();
}
/**
* 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);
}
}
}