Adding tester module - not nearly complete though

This commit is contained in:
jamesagnew 2014-04-28 18:04:26 -04:00
parent 2287011601
commit 5615f9b7d7
14 changed files with 664 additions and 116 deletions

View File

@ -24,6 +24,12 @@ import ca.uhn.fhir.model.primitive.InstantDt;
public enum ResourceMetadataKeyEnum { public enum ResourceMetadataKeyEnum {
/**
* The value for this key is the version ID of the resource object.
* <p>
* Values for this key are of type <b>{@link IdDt}</b>
* </p>
*/
VERSION_ID, VERSION_ID,
/** /**
@ -31,7 +37,7 @@ public enum ResourceMetadataKeyEnum {
* is defined by FHIR as "Time resource copied into the feed", which is generally * is defined by FHIR as "Time resource copied into the feed", which is generally
* best left to the current time. * best left to the current time.
* <p> * <p>
* Values for this key are of type {@link InstantDt} * Values for this key are of type <b>{@link InstantDt}</b>
* </p> * </p>
* <p> * <p>
* <b>Server Note</b>: In servers, it is generally advisable to leave this * <b>Server Note</b>: In servers, it is generally advisable to leave this
@ -49,7 +55,7 @@ public enum ResourceMetadataKeyEnum {
* used for populating the "Last-Modified" header in the case of methods * used for populating the "Last-Modified" header in the case of methods
* that return a single resource (read, vread, etc.) * that return a single resource (read, vread, etc.)
* <p> * <p>
* Values for this key are of type {@link InstantDt} * Values for this key are of type <b>{@link InstantDt}</b>
* </p> * </p>
* *
* @see InstantDt * @see InstantDt

View File

@ -29,7 +29,7 @@ import ca.uhn.fhir.model.api.annotation.DatatypeDef;
import ca.uhn.fhir.model.api.annotation.SimpleSetter; import ca.uhn.fhir.model.api.annotation.SimpleSetter;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
@DatatypeDef(name="instant") @DatatypeDef(name = "instant")
public class InstantDt extends BaseDateTimeDt { public class InstantDt extends BaseDateTimeDt {
/** /**
@ -38,12 +38,34 @@ public class InstantDt extends BaseDateTimeDt {
public static final TemporalPrecisionEnum DEFAULT_PRECISION = TemporalPrecisionEnum.MILLI; public static final TemporalPrecisionEnum DEFAULT_PRECISION = TemporalPrecisionEnum.MILLI;
/** /**
* Constructor * Constructor which creates an InstantDt with <b>no timne value</b>. Note that unlike the default constructor for the Java {@link Date} or {@link Calendar} objects, this constructor does not
* initialize the object with the current time.
*
* @see #withCurrentTime() to create a new object that has been initialized with the current time.
*/ */
public InstantDt() { public InstantDt() {
super(); super();
} }
/**
* Create a new DateTimeDt
*/
public InstantDt(Calendar theCalendar) {
setValue(theCalendar.getTime());
setPrecision(DEFAULT_PRECISION);
setTimeZone(theCalendar.getTimeZone());
}
/**
* Create a new DateTimeDt
*/
@SimpleSetter(suffix = "WithMillisPrecision")
public InstantDt(@SimpleSetter.Parameter(name = "theDate") Date theDate) {
setValue(theDate);
setPrecision(DEFAULT_PRECISION);
setTimeZone(TimeZone.getDefault());
}
/** /**
* Constructor which accepts a date value and a precision value. Valid precisions values for this type are: * Constructor which accepts a date value and a precision value. Valid precisions values for this type are:
* <ul> * <ul>
@ -58,30 +80,11 @@ public class InstantDt extends BaseDateTimeDt {
setTimeZone(TimeZone.getDefault()); setTimeZone(TimeZone.getDefault());
} }
/**
* Create a new DateTimeDt
*/
@SimpleSetter(suffix="WithMillisPrecision")
public InstantDt(@SimpleSetter.Parameter(name="theDate") Date theDate) {
setValue(theDate);
setPrecision(DEFAULT_PRECISION);
setTimeZone(TimeZone.getDefault());
}
/**
* Create a new DateTimeDt
*/
public InstantDt(Calendar theCalendar) {
setValue(theCalendar.getTime());
setPrecision(DEFAULT_PRECISION);
setTimeZone(theCalendar.getTimeZone());
}
/** /**
* Create a new InstantDt from a string value * Create a new InstantDt from a string value
* *
* @param theString The string representation of the string. Must be in * @param theString
* a valid format according to the FHIR specification * The string representation of the string. Must be in a valid format according to the FHIR specification
* @throws DataFormatException * @throws DataFormatException
*/ */
public InstantDt(String theString) { public InstantDt(String theString) {
@ -100,13 +103,19 @@ public class InstantDt extends BaseDateTimeDt {
} }
/** /**
* Sets the value of this instant to the current time (from the system clock) * Sets the value of this instant to the current time (from the system clock) and the local/default timezone (as retrieved using {@link TimeZone#getDefault()}. This TimeZone is generally obtained
* and the local/default timezone (as retrieved using {@link TimeZone#getDefault()}. This * from the underlying OS.
* TimeZone is generally obtained from the underlying OS.
*/ */
public void setToCurrentTimeInLocalTimeZone() { public void setToCurrentTimeInLocalTimeZone() {
setValue(new Date()); setValue(new Date());
setTimeZone(TimeZone.getDefault()); setTimeZone(TimeZone.getDefault());
} }
/**
* Factory method which creates a new InstantDt and initializes it with the current time.
*/
public static InstantDt withCurrentTime() {
return new InstantDt(new Date());
}
} }

View File

@ -30,21 +30,30 @@ import ca.uhn.fhir.rest.client.api.IBasicClient;
import ca.uhn.fhir.rest.client.api.IRestfulClient; import ca.uhn.fhir.rest.client.api.IRestfulClient;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
/**
* RESTful method annotation to be used for the FHIR <a href="http://hl7.org/implement/standards/fhir/http.html#read">read</a> and <a
* href="http://hl7.org/implement/standards/fhir/http.html#vread">vread</a> method.
*
* <p>
* If this method has a parameter annotated with the {@link IdParam} annotation and a parameter annotated with the {@link VersionIdParam} annotation, the method will be treated as a vread method. If
* the method has only a parameter annotated with the {@link IdParam} annotation, it will be treated as a read operation.
* the
* </p>
* <p>
* If you wish for your server to support both read and vread operations, you will need
* two methods annotated with this annotation.
* </p>
*/
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) @Target(ElementType.METHOD)
public @interface Read { public @interface Read {
/** /**
* The return type for this method. This generally does not need * The return type for this method. This generally does not need to be populated for {@link IResourceProvider resource providers} in a server implementation, but often does need to be populated in
* to be populated for {@link IResourceProvider resource providers} in a server implementation, * client implementations using {@link IBasicClient} or {@link IRestfulClient}, or in plain providers on a server.
* but often does need to be populated in client implementations using {@link IBasicClient} or
* {@link IRestfulClient}, or in plain providers on a server.
* <p> * <p>
* This value also does not need to be populated if the return type for a method annotated with * This value also does not need to be populated if the return type for a method annotated with this annotation is sufficient to determine the type of resource provided. E.g. if the method returns
* this annotation is sufficient to determine the type of resource provided. E.g. if the * <code>Patient</code> or <code>List&lt;Patient&gt;</code>, the server/client will automatically determine that the Patient resource is the return type, and this value may be left blank.
* method returns <code>Patient</code> or <code>List&lt;Patient&gt;</code>, the server/client
* will automatically determine that the Patient resource is the return type, and this value
* may be left blank.
* </p> * </p>
*/ */
// NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere

View File

@ -40,9 +40,11 @@ import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.Metadata; import ca.uhn.fhir.rest.annotation.Metadata;
import ca.uhn.fhir.rest.method.BaseMethodBinding; import ca.uhn.fhir.rest.method.BaseMethodBinding;
import ca.uhn.fhir.rest.method.ReadMethodBinding;
import ca.uhn.fhir.rest.method.SearchMethodBinding; import ca.uhn.fhir.rest.method.SearchMethodBinding;
import ca.uhn.fhir.rest.param.IParameter; import ca.uhn.fhir.rest.param.IParameter;
import ca.uhn.fhir.rest.param.BaseQueryParameter; import ca.uhn.fhir.rest.param.BaseQueryParameter;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.ResourceBinding; import ca.uhn.fhir.rest.server.ResourceBinding;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.util.ExtensionConstants; import ca.uhn.fhir.util.ExtensionConstants;
@ -65,6 +67,8 @@ public class ServerConformanceProvider {
Conformance retVal = new Conformance(); Conformance retVal = new Conformance();
retVal.getSoftware().setName(myRestfulServer.getServerName()); retVal.getSoftware().setName(myRestfulServer.getServerName());
retVal.getSoftware().setVersion(myRestfulServer.getServerVersion()); retVal.getSoftware().setVersion(myRestfulServer.getServerVersion());
retVal.addFormat(Constants.CT_FHIR_XML);
retVal.addFormat(Constants.CT_FHIR_JSON);
Rest rest = retVal.addRest(); Rest rest = retVal.addRest();
rest.setMode(RestfulConformanceModeEnum.SERVER); rest.setMode(RestfulConformanceModeEnum.SERVER);

View File

@ -0,0 +1,111 @@
package ca.uhn.fhir.rest.server.tester;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.TemplateProcessingParameters;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.resourceresolver.IResourceResolver;
import org.thymeleaf.standard.StandardDialect;
import org.thymeleaf.templateresolver.TemplateResolver;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu.resource.Conformance;
import ca.uhn.fhir.rest.annotation.Metadata;
import ca.uhn.fhir.rest.client.api.IBasicClient;
public class PublicTesterServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PublicTesterServlet.class);
private TemplateEngine myTemplateEngine;
private HashMap<String, String> myStaticResources;
private String myServerBase;
private FhirContext myCtx;
public void setServerBase(String theServerBase) {
myServerBase = theServerBase;
}
public PublicTesterServlet() {
myStaticResources = new HashMap<String, String>();
myStaticResources.put("jquery-2.1.0.min.js", "text/javascript");
myStaticResources.put("PublicTester.css", "text/css");
myStaticResources.put("hapi_fhir_banner.png", "image/png");
myStaticResources.put("hapi_fhir_banner_right.png", "image/png");
myCtx = new FhirContext();
}
@Override
protected void doGet(HttpServletRequest theReq, HttpServletResponse theResp) throws ServletException, IOException {
myTemplateEngine.getCacheManager().clearAllCaches();
ourLog.info("RequestURI: {}", theReq.getPathInfo());
String resName = theReq.getPathInfo().substring(1);
if (myStaticResources.containsKey(resName)) {
streamResponse(resName, myStaticResources.get(resName), theResp);
return;
}
ConformanceClient client = myCtx.newRestfulClient(ConformanceClient.class, myServerBase);
Conformance conformance = client.getConformance();
WebContext ctx = new WebContext(theReq, theResp, theReq.getServletContext(), theReq.getLocale());
ctx.setVariable("conf", conformance);
ctx.setVariable("base", myServerBase);
myTemplateEngine.process(theReq.getPathInfo(), ctx, theResp.getWriter());
}
private interface ConformanceClient extends IBasicClient
{
@Metadata
Conformance getConformance();
}
@Override
public void init(ServletConfig theConfig) throws ServletException {
myTemplateEngine = new TemplateEngine();
TemplateResolver resolver = new TemplateResolver();
resolver.setResourceResolver(new ProfileResourceResolver());
myTemplateEngine.setTemplateResolver(resolver);
StandardDialect dialect = new StandardDialect();
myTemplateEngine.setDialect(dialect);
myTemplateEngine.initialize();
}
private final class ProfileResourceResolver implements IResourceResolver {
@Override
public String getName() {
return getClass().getCanonicalName();
}
@Override
public InputStream getResourceAsStream(TemplateProcessingParameters theTemplateProcessingParameters, String theName) {
ourLog.info("Loading template: {}", theName);
if ("/".equals(theName)) {
return PublicTesterServlet.class.getResourceAsStream("/ca/uhn/fhir/rest/server/tester/PublicTester.html");
}
return null;
}
}
private void streamResponse(String theResourceName, String theContentType, HttpServletResponse theResp) throws IOException {
InputStream res = PublicTesterServlet.class.getResourceAsStream("/ca/uhn/fhir/rest/server/tester/" + theResourceName);
theResp.setContentType(theContentType);
IOUtils.copy(res, theResp.getOutputStream());
}
}

View File

@ -0,0 +1,16 @@
BODY {
font-family: sans-serif;
}
DIV.bodyHeaderBlock {
background-color: #E0E0E0;
margin-top: 5px;
border-radius: 5px;
padding: 5px;
}
TD.propertyKeyCell {
background-color: #E0E0FF;
border-radius: 3px;
padding: 3px;
}

View File

@ -0,0 +1,77 @@
<!DOCTYPE html>
<!--/*
************************************************************
This file is a Thymeleaf template for the
************************************************************
*/-->
<html>
<head>
<script type="text/javascript" src="jquery-2.1.0.min.js"></script>
<link rel="stylesheet" type="text/css" href="PublicTester.css"/>
</head>
<body>
<table border="0" width="100%">
<tr>
<td align="left"><img src="hapi_fhir_banner.png"/></td>
<td align="right"><img src="hapi_fhir_banner_right.png"/></td>
</tr>
</table>
<div class="bodyHeaderBlock">
This is a RESTful server tester, which can be used to send requests to, and receive responses
from the server at the following URL:<br/>
<a href="http://foo.com/fhir" th:href="${base}"><th:block th:text="${base}">http://foo.com/fhir</th:block></a>
</div>
<table border="0" width="100%" cellpadding="0" cellspacing="0" style="margin-top: 4px;">
<tr>
<td width="29%" valign="top">
<div class="bodyHeaderBlock">
Software Details
</div>
<table border="0">
<tr>
<td class="propertyKeyCell">Software</td>
<td th:text="${conf.software.name}">HAPI Restful Server</td>
</tr>
<tr>
<td class="propertyKeyCell">Version</td>
<td th:text="${conf.software.version}">1.1.1</td>
</tr>
</table>
</td>
<td width="1%"/>
<td width="70%" valign="top">
<th:block th:each="rest : ${conf.rest}">
<th:block th:each="resource : ${rest.resource}">
<div class="bodyHeaderBlock">
Resource: <th:block th:text="${resource.type.valueAsString}"/>
</div>
<table border="0">
<tr>
<td valign="top" class="propertyKeyCell">Supports operations:</td>
<td valign="top">
<th:block th:each="operation : ${resource.operation}">
<th:block th:text="${operation.code.value}"/>
</th:block>
</td>
</tr>
</table>
<div>
</div>
</th:block>
</th:block>
</td>
</tr>
</table>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

File diff suppressed because one or more lines are too long

View File

@ -45,9 +45,9 @@ import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.Since; import ca.uhn.fhir.rest.annotation.Since;
import ca.uhn.fhir.testutil.RandomServerPortProvider; import ca.uhn.fhir.testutil.RandomServerPortProvider;
public class NonResourceProviderServerTest { public class PlainProviderTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(NonResourceProviderServerTest.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PlainProviderTest.class);
private int myPort; private int myPort;
private Server myServer; private Server myServer;
private CloseableHttpClient myClient; private CloseableHttpClient myClient;

View File

@ -0,0 +1,199 @@
package ca.uhn.fhir.rest.server;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.dstu.composite.HumanNameDt;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu.resource.Organization;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.dstu.valueset.IdentifierUseEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.annotation.Count;
import ca.uhn.fhir.rest.annotation.History;
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.Search;
import ca.uhn.fhir.rest.annotation.Since;
import ca.uhn.fhir.rest.server.tester.PublicTesterServlet;
import ca.uhn.fhir.testutil.RandomServerPortProvider;
public class TesterTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TesterTest.class);
private int myPort;
private Server myServer;
private FhirContext myCtx;
private RestfulServer myRestfulServer;
@Before
public void before() throws Exception {
myPort = RandomServerPortProvider.findFreePort();
myPort = 8888;
myServer = new Server(myPort);
myCtx = new FhirContext(Patient.class);
myRestfulServer = new RestfulServer();
ServletContextHandler proxyHandler = new ServletContextHandler();
proxyHandler.setContextPath("/");
PublicTesterServlet testerServlet = new PublicTesterServlet();
testerServlet.setServerBase("http://localhost:" + myPort + "/fhir/context");
testerServlet.setServerBase("http://fhir.healthintersections.com.au/open");
ServletHolder handler = new ServletHolder();
handler.setServlet(testerServlet);
proxyHandler.addServlet(handler, "/fhir/tester/*");
ServletHolder servletHolder = new ServletHolder();
servletHolder.setServlet(myRestfulServer);
proxyHandler.addServlet(servletHolder, "/fhir/context/*");
myServer.setHandler(proxyHandler);
}
@After
public void after() throws Exception {
myServer.stop();
}
@Test
public void testTester() throws Exception {
if (true) return;
myRestfulServer.setProviders(new SearchProvider(), new GlobalHistoryProvider());
myServer.start();
Thread.sleep(9999999L);
}
/**
* Created by dsotnikov on 2/25/2014.
*/
public static class SearchProvider {
public Map<String, Patient> getIdToPatient() {
Map<String, Patient> idToPatient = new HashMap<String, Patient>();
{
Patient patient = createPatient();
idToPatient.put("1", patient);
}
{
Patient patient = new Patient();
patient.getIdentifier().add(new IdentifierDt());
patient.getIdentifier().get(0).setUse(IdentifierUseEnum.OFFICIAL);
patient.getIdentifier().get(0).setSystem(new UriDt("urn:hapitest:mrns"));
patient.getIdentifier().get(0).setValue("00002");
patient.getName().add(new HumanNameDt());
patient.getName().get(0).addFamily("Test");
patient.getName().get(0).addGiven("PatientTwo");
patient.getGender().setText("F");
idToPatient.put("2", patient);
}
return idToPatient;
}
@Search(type = Patient.class)
public Patient findPatient(@RequiredParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier) {
for (Patient next : getIdToPatient().values()) {
for (IdentifierDt nextId : next.getIdentifier()) {
if (nextId.matchesSystemAndValue(theIdentifier)) {
return next;
}
}
}
return null;
}
/**
* Retrieve the resource by its identifier
*
* @param theId
* The resource identity
* @return The resource
*/
@Read(type = Patient.class)
public Patient getPatientById(@IdParam IdDt theId) {
return getIdToPatient().get(theId.getValue());
}
}
public static class GlobalHistoryProvider {
private InstantDt myLastSince;
private IntegerDt myLastCount;
@History
public List<IResource> getGlobalHistory(@Since InstantDt theSince, @Count IntegerDt theCount) {
myLastSince = theSince;
myLastCount = theCount;
ArrayList<IResource> retVal = new ArrayList<IResource>();
IResource p = createPatient();
p.setId(new IdDt("1"));
p.getResourceMetadata().put(ResourceMetadataKeyEnum.VERSION_ID, new IdDt("A"));
p.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, new InstantDt("2012-01-01T00:00:01"));
p.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, new InstantDt("2012-01-01T01:00:01"));
retVal.add(p);
p = createPatient();
p.setId(new IdDt("1"));
p.getResourceMetadata().put(ResourceMetadataKeyEnum.VERSION_ID, new IdDt("B"));
p.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, new InstantDt("2012-01-01T00:00:01"));
p.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, new InstantDt("2012-01-01T01:00:03"));
retVal.add(p);
p = createOrganization();
p.setId(new IdDt("1"));
p.getResourceMetadata().put(ResourceMetadataKeyEnum.VERSION_ID, new IdDt("A"));
p.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, new InstantDt("2013-01-01T00:00:01"));
p.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, new InstantDt("2013-01-01T01:00:01"));
retVal.add(p);
return retVal;
}
}
private static Patient createPatient() {
Patient patient = new Patient();
patient.addIdentifier();
patient.getIdentifier().get(0).setUse(IdentifierUseEnum.OFFICIAL);
patient.getIdentifier().get(0).setSystem(new UriDt("urn:hapitest:mrns"));
patient.getIdentifier().get(0).setValue("00001");
patient.addName();
patient.getName().get(0).addFamily("Test");
patient.getName().get(0).addGiven("PatientOne");
patient.getGender().setText("M");
return patient;
}
private static Organization createOrganization() {
Organization retVal = new Organization();
retVal.addIdentifier();
retVal.getIdentifier().get(0).setUse(IdentifierUseEnum.OFFICIAL);
retVal.getIdentifier().get(0).setSystem(new UriDt("urn:hapitest:mrns"));
retVal.getIdentifier().get(0).setValue("00001");
retVal.getName().setValue("Test Org");
return retVal;
}
}

View File

@ -20,9 +20,11 @@ public class ExampleRestfulServlet extends RestfulServer {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* Constructor * This method is called automatically when the
* servlet is initializing.
*/ */
public ExampleRestfulServlet() { @Override
public void initialize() {
/* /*
* Two resource providers are defined * Two resource providers are defined

View File

@ -1,35 +1,49 @@
package ca.uhn.example.rest; package ca.uhn.example.rest;
import java.util.ArrayList; import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.dstu.composite.HumanNameDt; import ca.uhn.fhir.model.dstu.composite.HumanNameDt;
import ca.uhn.fhir.model.dstu.resource.OperationOutcome; import ca.uhn.fhir.model.dstu.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu.resource.Patient; import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.dstu.valueset.AdministrativeGenderCodesEnum; import ca.uhn.fhir.model.dstu.valueset.AdministrativeGenderCodesEnum;
import ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum; import ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum;
import ca.uhn.fhir.model.primitive.IdDt; 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.StringDt;
import ca.uhn.fhir.model.primitive.UriDt; 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.Create;
import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.VersionIdParam;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider; 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.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
/** /**
* All resource providers must implement IResourceProvider * 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 RestfulPatientResourceProvider implements IResourceProvider { public class RestfulPatientResourceProvider implements IResourceProvider {
private Map<Long, Patient> myIdToPatientMap = new HashMap<Long, Patient>(); /**
* 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; private long myNextId = 1;
/** /**
@ -43,94 +57,48 @@ public class RestfulPatientResourceProvider implements IResourceProvider {
patient.addName().addFamily("Test"); patient.addName().addFamily("Test");
patient.getName().get(0).addGiven("PatientOne"); patient.getName().get(0).addGiven("PatientOne");
patient.setGender(AdministrativeGenderCodesEnum.F); patient.setGender(AdministrativeGenderCodesEnum.F);
myIdToPatientMap.put(myNextId++, patient);
LinkedList<Patient> list = new LinkedList<Patient>();
list.add(patient);
myIdToPatientVersions.put(myNextId++, list);
} }
/** /**
* The getResourceType method comes from IResourceProvider, and must be overridden to indicate what type of resource this provider supplies. * Stores a new version of the patient in memory so that it
*/ * can be retrieved later.
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
/**
* The "@Read" annotation indicates that this method supports the read operation. Read operations should return a single resource instance.
* *
* @param theId * @param thePatient The patient resource to store
* The read operation takes one parameter, which must be of type IdDt and must be annotated with the "@Read.IdParam" annotation. * @param theId The ID of the patient to retrieve
* @return Returns a resource matching this identifier, or null if none exists.
*/ */
@Read() private void addNewVersion(Patient thePatient, Long theId) {
public Patient getPatientById(@IdParam IdDt theId) { InstantDt publishedDate;
Patient retVal; if (!myIdToPatientVersions.containsKey(theId)) {
try { myIdToPatientVersions.put(theId, new LinkedList<Patient>());
retVal = myIdToPatientMap.get(theId.asLong()); publishedDate = InstantDt.withCurrentTime();
} catch (NumberFormatException e) { } else {
/* Patient currentPatitne = myIdToPatientVersions.get(theId).getLast();
* If we can't parse the ID as a long, it's not valid so this is an unknown resource Map<ResourceMetadataKeyEnum, Object> resourceMetadata = currentPatitne.getResourceMetadata();
*/ publishedDate = (InstantDt) resourceMetadata.get(ResourceMetadataKeyEnum.PUBLISHED);
throw new ResourceNotFoundException(theId);
} }
return retVal;
}
/**
* 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) {
ArrayList<Patient> retVal = new ArrayList<Patient>();
/* /*
* Look for all patients matching the name * 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.
*/ */
for (Patient nextPatient : myIdToPatientMap.values()) { thePatient.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, publishedDate);
NAMELOOP: thePatient.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, InstantDt.withCurrentTime());
for (HumanNameDt nextName : nextPatient.getName()) {
for (StringDt nextFamily : nextName.getFamily()) {
if (theFamilyName.equals(nextFamily)) {
retVal.add(nextPatient);
break NAMELOOP;
}
}
}
}
return retVal; Deque<Patient> existingVersions = myIdToPatientVersions.get(theId);
}
/**
* The "@Create" annotation indicates that this method supports creating a new resource
*
* @param thePatient This is the actual resource to save
* @return This method returns a "MethodOutcome"
*/
@Create()
public MethodOutcome createPatient(@ResourceParam Patient thePatient) {
/* /*
* Our server will have a rule that patients must * We just use the current number of versions as the next version number
* have a family name or we will reject them
*/ */
if (thePatient.getNameFirstRep().getFamilyFirstRep().isEmpty()) { IdDt version = new IdDt(existingVersions.size());
OperationOutcome outcome = new OperationOutcome(); thePatient.getResourceMetadata().put(ResourceMetadataKeyEnum.VERSION_ID, version);
outcome.addIssue().setSeverity(IssueSeverityEnum.FATAL).setDetails("No last name provided");
throw new UnprocessableEntityException(outcome);
}
long id = myNextId++; existingVersions.add(thePatient);
myIdToPatientMap.put(id, thePatient);
// Let the caller know the ID of the newly created resource
return new MethodOutcome(new IdDt(id));
} }
/** /**
@ -144,23 +112,166 @@ public class RestfulPatientResourceProvider implements IResourceProvider {
*/ */
@Create() @Create()
public MethodOutcome createPatient(@ResourceParam Patient thePatient) { public MethodOutcome createPatient(@ResourceParam Patient thePatient) {
/* validateResource(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 last name provided");
throw new UnprocessableEntityException(outcome);
}
// Here we are just generating IDs sequentially
long id = myNextId++; long id = myNextId++;
myIdToPatientMap.put(id, thePatient);
addNewVersion(thePatient, id);
// Let the caller know the ID of the newly created resource // Let the caller know the ID of the newly created resource
return new MethodOutcome(new IdDt(id)); return new MethodOutcome(new IdDt(id));
} }
} /**
// END SNIPPET: provider * 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;
}
/**
* 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()
public Patient readPatient(@IdParam IdDt theId) {
Deque<Patient> retVal;
try {
retVal = myIdToPatientVersions.get(theId.asLong());
} 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);
}
return retVal.getLast();
}
/**
* 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"
*/
@Create()
public MethodOutcome updatePatient(@IdParam IdDt theId, @ResourceParam Patient thePatient) {
validateResource(thePatient);
Long id;
try {
id = theId.asLong();
} 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);
}
}
/**
* This is the "vread" operation.
* The "@Read" annotation indicates that this method supports the read and/or vread operation.
* <p>
* VRead operations take a parameter annotated with the {@link IdParam} paramater,
* and a paramater annotated with the {@link VersionIdParam} parmeter,
* 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()
public Patient vreadPatient(@IdParam IdDt theId, @VersionIdParam IdDt theVersionId) {
Deque<Patient> versions;
try {
versions = myIdToPatientVersions.get(theId.asLong());
} 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);
}
for (Patient nextVersion : versions) {
IdDt nextVersionId = (IdDt) nextVersion.getResourceMetadata().get(ResourceMetadataKeyEnum.VERSION_ID);
if (theVersionId.equals(nextVersionId)) {
return nextVersion;
}
}
throw new ResourceNotFoundException("Unknown version " + theVersionId);
}
}