Adding tester module - not nearly complete though
This commit is contained in:
parent
2287011601
commit
5615f9b7d7
|
@ -24,6 +24,12 @@ import ca.uhn.fhir.model.primitive.InstantDt;
|
|||
|
||||
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,
|
||||
|
||||
/**
|
||||
|
@ -31,7 +37,7 @@ public enum ResourceMetadataKeyEnum {
|
|||
* is defined by FHIR as "Time resource copied into the feed", which is generally
|
||||
* best left to the current time.
|
||||
* <p>
|
||||
* Values for this key are of type {@link InstantDt}
|
||||
* Values for this key are of type <b>{@link InstantDt}</b>
|
||||
* </p>
|
||||
* <p>
|
||||
* <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
|
||||
* that return a single resource (read, vread, etc.)
|
||||
* <p>
|
||||
* Values for this key are of type {@link InstantDt}
|
||||
* Values for this key are of type <b>{@link InstantDt}</b>
|
||||
* </p>
|
||||
*
|
||||
* @see InstantDt
|
||||
|
|
|
@ -29,7 +29,7 @@ import ca.uhn.fhir.model.api.annotation.DatatypeDef;
|
|||
import ca.uhn.fhir.model.api.annotation.SimpleSetter;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
|
||||
@DatatypeDef(name="instant")
|
||||
@DatatypeDef(name = "instant")
|
||||
public class InstantDt extends BaseDateTimeDt {
|
||||
|
||||
/**
|
||||
|
@ -38,12 +38,34 @@ public class InstantDt extends BaseDateTimeDt {
|
|||
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() {
|
||||
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:
|
||||
* <ul>
|
||||
|
@ -58,30 +80,11 @@ public class InstantDt extends BaseDateTimeDt {
|
|||
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
|
||||
*
|
||||
* @param theString The string representation of the string. Must be in
|
||||
* a valid format according to the FHIR specification
|
||||
* @param theString
|
||||
* The string representation of the string. Must be in a valid format according to the FHIR specification
|
||||
* @throws DataFormatException
|
||||
*/
|
||||
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)
|
||||
* and the local/default timezone (as retrieved using {@link TimeZone#getDefault()}. This
|
||||
* TimeZone is generally obtained from the underlying OS.
|
||||
* 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
|
||||
* from the underlying OS.
|
||||
*/
|
||||
public void setToCurrentTimeInLocalTimeZone() {
|
||||
setValue(new Date());
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.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)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface Read {
|
||||
|
||||
/**
|
||||
* 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 client implementations using {@link IBasicClient} or
|
||||
* {@link IRestfulClient}, or in plain providers on a server.
|
||||
* 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
|
||||
* client implementations using {@link IBasicClient} or {@link IRestfulClient}, or in plain providers on a server.
|
||||
* <p>
|
||||
* 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 <code>Patient</code> or <code>List<Patient></code>, the server/client
|
||||
* will automatically determine that the Patient resource is the return type, and this value
|
||||
* may be left blank.
|
||||
* 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
|
||||
* <code>Patient</code> or <code>List<Patient></code>, the server/client will automatically determine that the Patient resource is the return type, and this value may be left blank.
|
||||
* </p>
|
||||
*/
|
||||
// NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere
|
||||
|
|
|
@ -40,9 +40,11 @@ import ca.uhn.fhir.model.primitive.IdDt;
|
|||
import ca.uhn.fhir.model.primitive.StringDt;
|
||||
import ca.uhn.fhir.rest.annotation.Metadata;
|
||||
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.param.IParameter;
|
||||
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.RestfulServer;
|
||||
import ca.uhn.fhir.util.ExtensionConstants;
|
||||
|
@ -65,6 +67,8 @@ public class ServerConformanceProvider {
|
|||
Conformance retVal = new Conformance();
|
||||
retVal.getSoftware().setName(myRestfulServer.getServerName());
|
||||
retVal.getSoftware().setVersion(myRestfulServer.getServerVersion());
|
||||
retVal.addFormat(Constants.CT_FHIR_XML);
|
||||
retVal.addFormat(Constants.CT_FHIR_JSON);
|
||||
|
||||
Rest rest = retVal.addRest();
|
||||
rest.setMode(RestfulConformanceModeEnum.SERVER);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 |
4
hapi-fhir-base/src/main/resources/ca/uhn/fhir/rest/server/tester/jquery-2.1.0.min.js
vendored
Normal file
4
hapi-fhir-base/src/main/resources/ca/uhn/fhir/rest/server/tester/jquery-2.1.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -45,9 +45,9 @@ import ca.uhn.fhir.rest.annotation.Search;
|
|||
import ca.uhn.fhir.rest.annotation.Since;
|
||||
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 Server myServer;
|
||||
private CloseableHttpClient myClient;
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -20,9 +20,11 @@ public class ExampleRestfulServlet extends RestfulServer {
|
|||
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
|
||||
|
|
|
@ -1,35 +1,49 @@
|
|||
package ca.uhn.example.rest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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.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.AdministrativeGenderCodesEnum;
|
||||
import ca.uhn.fhir.model.dstu.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.VersionIdParam;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -43,94 +57,48 @@ public class RestfulPatientResourceProvider implements IResourceProvider {
|
|||
patient.addName().addFamily("Test");
|
||||
patient.getName().get(0).addGiven("PatientOne");
|
||||
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.
|
||||
*/
|
||||
@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.
|
||||
* Stores a new version of the patient in memory so that it
|
||||
* can be retrieved later.
|
||||
*
|
||||
* @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.
|
||||
* @param thePatient The patient resource to store
|
||||
* @param theId The ID of the patient to retrieve
|
||||
*/
|
||||
@Read()
|
||||
public Patient getPatientById(@IdParam IdDt theId) {
|
||||
Patient retVal;
|
||||
try {
|
||||
retVal = myIdToPatientMap.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);
|
||||
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);
|
||||
}
|
||||
|
||||
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()) {
|
||||
NAMELOOP:
|
||||
for (HumanNameDt nextName : nextPatient.getName()) {
|
||||
for (StringDt nextFamily : nextName.getFamily()) {
|
||||
if (theFamilyName.equals(nextFamily)) {
|
||||
retVal.add(nextPatient);
|
||||
break NAMELOOP;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
thePatient.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, publishedDate);
|
||||
thePatient.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, InstantDt.withCurrentTime());
|
||||
|
||||
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
|
||||
* have a family name or we will reject them
|
||||
* We just use the current number of versions as the next version number
|
||||
*/
|
||||
if (thePatient.getNameFirstRep().getFamilyFirstRep().isEmpty()) {
|
||||
OperationOutcome outcome = new OperationOutcome();
|
||||
outcome.addIssue().setSeverity(IssueSeverityEnum.FATAL).setDetails("No last name provided");
|
||||
throw new UnprocessableEntityException(outcome);
|
||||
}
|
||||
IdDt version = new IdDt(existingVersions.size());
|
||||
thePatient.getResourceMetadata().put(ResourceMetadataKeyEnum.VERSION_ID, version);
|
||||
|
||||
long id = myNextId++;
|
||||
myIdToPatientMap.put(id, thePatient);
|
||||
|
||||
// Let the caller know the ID of the newly created resource
|
||||
return new MethodOutcome(new IdDt(id));
|
||||
existingVersions.add(thePatient);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -144,23 +112,166 @@ public class RestfulPatientResourceProvider implements IResourceProvider {
|
|||
*/
|
||||
@Create()
|
||||
public MethodOutcome createPatient(@ResourceParam 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 last name provided");
|
||||
throw new UnprocessableEntityException(outcome);
|
||||
}
|
||||
validateResource(thePatient);
|
||||
|
||||
// Here we are just generating IDs sequentially
|
||||
long id = myNextId++;
|
||||
myIdToPatientMap.put(id, thePatient);
|
||||
|
||||
addNewVersion(thePatient, id);
|
||||
|
||||
// Let the caller know the ID of the newly created resource
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue