Add HashMapResourcePrvider

This commit is contained in:
James Agnew 2018-02-25 13:16:02 -05:00
parent ef6ee83744
commit 84c72203b7
3 changed files with 357 additions and 0 deletions

View File

@ -0,0 +1,173 @@
package ca.uhn.fhir.rest.server.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* This class is a simple implementation of the resource provider
* interface that uses a HashMap to store all resources in memory.
* <p>
* This class currently supports the following FHIR operations:
* </p>
* <ul>
* <li>Create</li>
* <li>Update existing resource</li>
* <li>Update non-existing resource (e.g. create with client-supplied ID)</li>
* <li>Delete</li>
* <li>Search by resource type with no parameters</li>
* </ul>
*
* @param <T> The resource type to support
*/
public class HashMapResourceProvider<T extends IBaseResource> implements IResourceProvider {
private static final Logger ourLog = LoggerFactory.getLogger(HashMapResourceProvider.class);
private final Class<T> myResourceType;
private final FhirContext myFhirContext;
private final String myResourceName;
private Map<String, TreeMap<Long, T>> myIdToVersionToResourceMap = new HashMap<>();
private long myNextId;
/**
* Constructor
*
* @param theFhirContext The FHIR context
* @param theResourceType The resource type to support
*/
@SuppressWarnings("WeakerAccess")
public HashMapResourceProvider(FhirContext theFhirContext, Class<T> theResourceType) {
myFhirContext = theFhirContext;
myResourceType = theResourceType;
myResourceName = myFhirContext.getResourceDefinition(theResourceType).getName();
clear();
}
/**
* Clear all data held in this resource provider
*/
public void clear() {
myNextId = 1;
myIdToVersionToResourceMap.clear();
}
@Create
public MethodOutcome create(@ResourceParam T theResource) {
long idPart = myNextId++;
String idPartAsString = Long.toString(idPart);
Long versionIdPart = 1L;
IIdType id = store(theResource, idPartAsString, versionIdPart);
return new MethodOutcome()
.setCreated(true)
.setId(id);
}
@Delete
public MethodOutcome delete(@IdParam IIdType theId) {
TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart());
if (versions == null || versions.isEmpty()) {
throw new ResourceNotFoundException(theId);
}
long nextVersion = versions.lastEntry().getKey() + 1L;
IIdType id = store(null, theId.getIdPart(), nextVersion);
return new MethodOutcome()
.setId(id);
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return myResourceType;
}
private TreeMap<Long, T> getVersionToResource(String theIdPart) {
if (!myIdToVersionToResourceMap.containsKey(theIdPart)) {
myIdToVersionToResourceMap.put(theIdPart, new TreeMap<Long, T>());
}
return myIdToVersionToResourceMap.get(theIdPart);
}
@Read(version = true)
public IBaseResource read(@IdParam IIdType theId) {
TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart());
if (versions == null || versions.isEmpty()) {
throw new ResourceNotFoundException(theId);
}
if (theId.hasVersionIdPart()) {
Long versionId = theId.getVersionIdPartAsLong();
if (!versions.containsKey(versionId)) {
throw new ResourceNotFoundException(theId);
} else {
T resource = versions.get(versionId);
if (resource == null) {
throw new ResourceGoneException(theId);
}
return resource;
}
} else {
return versions.lastEntry().getValue();
}
}
@Search
public List<IBaseResource> search() {
List<IBaseResource> retVal = new ArrayList<>();
for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) {
if (next.isEmpty() == false) {
retVal.add(next.lastEntry().getValue());
}
}
return retVal;
}
private IIdType store(@ResourceParam T theResource, String theIdPart, Long theVersionIdPart) {
IIdType id = myFhirContext.getVersion().newIdType();
id.setParts(null, myResourceName, theIdPart, Long.toString(theVersionIdPart));
TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart);
versionToResource.put(theVersionIdPart, theResource);
ourLog.info("Storing resource with ID: {}", id.getValue());
return id;
}
@Update
public MethodOutcome update(@ResourceParam T theResource) {
String idPartAsString = theResource.getIdElement().getIdPart();
TreeMap<Long, T> versionToResource = getVersionToResource(idPartAsString);
Long versionIdPart;
boolean created;
if (versionToResource.isEmpty()) {
versionIdPart = 1L;
created = true;
} else {
versionIdPart = versionToResource.lastKey() + 1L;
created = false;
}
IIdType id = store(theResource, idPartAsString, versionIdPart);
return new MethodOutcome()
.setCreated(created)
.setId(id);
}
}

View File

@ -0,0 +1,173 @@
package ca.uhn.fhir.rest.server.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import javax.servlet.ServletException;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.Assert.*;
public class HashMapResourceProviderTest {
private static MyRestfulServer ourRestServer;
private static Server ourListenerServer;
private static IGenericClient ourClient;
private static FhirContext ourCtx = FhirContext.forR4();
@Before
public void before() {
ourRestServer.clearData();
}
@Test
public void testCreateAndRead() {
// Create
Patient p = new Patient();
p.setActive(true);
IIdType id = ourClient.create().resource(p).execute().getId();
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("1", id.getVersionIdPart());
// Read
p = (Patient) ourClient.read().resource("Patient").withId(id).execute();
assertEquals(true, p.getActive());
}
@Test
public void testDelete() {
// Create
Patient p = new Patient();
p.setActive(true);
IIdType id = ourClient.create().resource(p).execute().getId();
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("1", id.getVersionIdPart());
ourClient.delete().resourceById(id.toUnqualifiedVersionless()).execute();
// Read
ourClient.read().resource("Patient").withId(id.withVersion("1")).execute();
try {
ourClient.read().resource("Patient").withId(id.withVersion("2")).execute();
fail();
} catch (ResourceGoneException e) {
// good
}
}
@Test
public void testSearchAll() {
// Create
for (int i = 0; i < 100; i++) {
Patient p = new Patient();
p.addName().setFamily("FAM" + i);
IIdType id = ourClient.create().resource(p).execute().getId();
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("1", id.getVersionIdPart());
}
// Search
Bundle resp = ourClient.search().forResource("Patient").returnBundle(Bundle.class).execute();
assertEquals(100, resp.getTotal());
assertEquals(100, resp.getEntry().size());
}
@Test
public void testUpdate() {
// Create
Patient p = new Patient();
p.setActive(true);
IIdType id = ourClient.create().resource(p).execute().getId();
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("1", id.getVersionIdPart());
// Update
p = new Patient();
p.setId(id);
p.setActive(false);
id = ourClient.update().resource(p).execute().getId();
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("2", id.getVersionIdPart());
// Read
p = (Patient) ourClient.read().resource("Patient").withId(id.withVersion("1")).execute();
assertEquals(true, p.getActive());
p = (Patient) ourClient.read().resource("Patient").withId(id.withVersion("2")).execute();
assertEquals(false, p.getActive());
try {
ourClient.read().resource("Patient").withId(id.withVersion("3")).execute();
fail();
} catch (ResourceNotFoundException e) {
// good
}
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourListenerServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void startListenerServer() throws Exception {
int ourListenerPort = PortUtil.findFreePort();
ourRestServer = new MyRestfulServer();
String ourBase = "http://localhost:" + ourListenerPort + "/";
ourListenerServer = new Server(ourListenerPort);
ourClient = ourCtx.newRestfulGenericClient(ourBase);
ServletContextHandler proxyHandler = new ServletContextHandler();
proxyHandler.setContextPath("/");
ServletHolder servletHolder = new ServletHolder();
servletHolder.setServlet(ourRestServer);
proxyHandler.addServlet(servletHolder, "/*");
ourListenerServer.setHandler(proxyHandler);
ourListenerServer.start();
}
private static class MyRestfulServer extends RestfulServer {
MyRestfulServer() {
super(ourCtx);
}
void clearData() {
for (IResourceProvider next : getResourceProviders()) {
if (next instanceof HashMapResourceProvider) {
((HashMapResourceProvider) next).clear();
}
}
}
@Override
protected void initialize() throws ServletException {
super.initialize();
registerProvider(new HashMapResourceProvider<>(ourCtx, Patient.class));
registerProvider(new HashMapResourceProvider<>(ourCtx, Observation.class));
}
}
}

View File

@ -165,6 +165,17 @@
if a parameter being used also matched that type. Thanks
to Dave Carlson for reporting!
</action>
<action type="add">
A new IResourceProvider implementation called
<![CDATA[
<code>HashMapResourceProvider</code>
]]>
has been added. This is a complete resource provider
implementation that uses a HashMap as a backing store. This class
is probably of limited use in real production systems, but it
cam be useful for tests or for static servers with small amounts
of data.
</action>
</release>
<release version="3.2.0" date="2018-01-13">
<action type="add">