More documentation

This commit is contained in:
jamesagnew 2014-03-11 17:39:41 -04:00
parent fefd0e9a57
commit df2675867a
21 changed files with 711 additions and 115 deletions

View File

@ -152,7 +152,6 @@
</plugin>
<!--
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
@ -178,6 +177,7 @@
<artifactId>maven-jxr-plugin</artifactId>
<version>2.3</version>
</plugin>
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>

View File

@ -391,6 +391,7 @@ public class XmlParser implements IParser {
eventWriter = decorateStreamWriter(eventWriter);
encodeResourceToXmlStreamWriter(theResource, eventWriter);
eventWriter.flush();
} catch (XMLStreamException e) {
throw new ConfigurationException("Failed to initialize STaX event factory", e);
}
@ -410,7 +411,6 @@ public class XmlParser implements IParser {
encodeCompositeElementToStreamWriter(theResource, eventWriter, resDef);
eventWriter.writeEndElement();
eventWriter.close();
}
private void encodeXhtml(XhtmlDt theDt, XMLStreamWriter theEventWriter) throws XMLStreamException {

View File

@ -90,7 +90,7 @@ public class ClientInvocationHandler implements InvocationHandler {
if (list.size() == 0) {
return null;
} else if (list.size() == 1) {
return list.get(1);
return list.get(0);
} else {
throw new InvalidResponseException("FHIR server call returned a bundle with multiple resources, but this method is only able to returns one.");
}

View File

@ -1,12 +1,18 @@
package ca.uhn.fhir.rest.client;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URLEncodedUtils;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.rest.common.BaseMethodBinding;
public class GetClientInvocation extends BaseClientInvocation {
@ -24,7 +30,6 @@ public class GetClientInvocation extends BaseClientInvocation {
myUrlPath = StringUtils.join(theUrlFragments, '/');
}
public Map<String, String> getParameters() {
return myParameters;
}
@ -35,7 +40,28 @@ public class GetClientInvocation extends BaseClientInvocation {
@Override
public HttpRequestBase asHttpRequest(String theUrlBase) {
return new HttpGet(theUrlBase + myUrlPath);
StringBuilder b = new StringBuilder();
b.append(theUrlBase);
b.append(myUrlPath);
boolean first = true;
for (Entry<String, String> next : myParameters.entrySet()) {
if (first) {
b.append('?');
first = false;
} else {
b.append('&');
}
try {
b.append(URLEncoder.encode(next.getKey(), "UTF-8"));
b.append('=');
b.append(URLEncoder.encode(next.getValue(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new ConfigurationException("Could not find UTF-8 encoding. This shouldn't happen.", e);
}
}
return new HttpGet(b.toString());
}
}

View File

@ -41,7 +41,7 @@ public abstract class BaseMethodBinding {
public abstract List<IResource> invokeServer(IResourceProvider theResourceProvider, IdDt theId, IdDt theVersionId, Map<String, String[]> theParameterValues) throws InvalidRequestException, InternalErrorException;
public abstract boolean matches(String theResourceName, IdDt theId, IdDt theVersion, Set<String> theParameterNames);
public abstract boolean matches(Request theRequest);
public String getResourceName() {
return myResourceName;
@ -56,7 +56,7 @@ public abstract class BaseMethodBinding {
Class<?> methodReturnType = theMethod.getReturnType();
MethodReturnTypeEnum methodReturnTypeEnum;
if (methodReturnType.equals(List.class)) {
if (Collection.class.isAssignableFrom(methodReturnType)) {
methodReturnTypeEnum = MethodReturnTypeEnum.LIST_OF_RESOURCES;
} else if (IResource.class.isAssignableFrom(methodReturnType)) {
methodReturnTypeEnum = MethodReturnTypeEnum.RESOURCE;

View File

@ -3,7 +3,6 @@ package ca.uhn.fhir.rest.common;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.Validate;
@ -11,6 +10,7 @@ import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.client.GetClientInvocation;
import ca.uhn.fhir.rest.common.SearchMethodBinding.RequestType;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.Util;
@ -18,7 +18,8 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class ReadMethodBinding extends BaseMethodBinding {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReadMethodBinding.class);
private Method myMethod;
private Integer myIdIndex;
private Integer myVersionIdIndex;
@ -48,17 +49,21 @@ public class ReadMethodBinding extends BaseMethodBinding {
}
@Override
public boolean matches(String theResourceName, IdDt theId, IdDt theVersion, Set<String> theParameterNames) {
if (!theResourceName.equals(getResourceName())) {
public boolean matches(Request theRequest) {
if (!theRequest.getResourceName().equals(getResourceName())) {
return false;
}
if (theParameterNames.isEmpty() == false) {
if (theRequest.getParameterNames().isEmpty() == false) {
return false;
}
if ((theVersion == null) != (myVersionIdIndex == null)) {
if ((theRequest.getVersion() == null) != (myVersionIdIndex == null)) {
return false;
}
if (theId == null) {
if (theRequest.getId() == null) {
return false;
}
if (theRequest.getRequestType() != RequestType.GET) {
ourLog.trace("Method {} doesn't match because request type is not GET: {}", theRequest.getId(), theRequest.getRequestType());
return false;
}
return true;

View File

@ -0,0 +1,74 @@
package ca.uhn.fhir.rest.common;
import java.util.Set;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.common.SearchMethodBinding.RequestType;
public class Request {
private IdDt myId;
private String myOperation;
private Set<String> myParameterNames;
private RequestType myRequestType;
private String myResourceName;
private IdDt myVersion;
public IdDt getId() {
return myId;
}
public String getOperation() {
return myOperation;
}
public Set<String> getParameterNames() {
return myParameterNames;
}
public RequestType getRequestType() {
return myRequestType;
}
public String getResourceName() {
return myResourceName;
}
public IdDt getVersion() {
return myVersion;
}
public void setId(IdDt theId) {
myId = theId;
}
public void setOperation(String theOperation) {
myOperation = theOperation;
}
public void setParameterNames(Set<String> theParameterNames) {
myParameterNames = theParameterNames;
}
public void setRequestType(RequestType theRequestType) {
myRequestType = theRequestType;
}
public void setResourceName(String theResourceName) {
myResourceName = theResourceName;
}
public void setVersion(IdDt theVersion) {
myVersion = theVersion;
}
public static Request withResourceAndParams(String theResourceName, RequestType theRequestType, Set<String> theParamNamess) {
Request retVal = new Request();
retVal.setResourceName(theResourceName);
retVal.setParameterNames(theParamNamess);
retVal.setRequestType(theRequestType);
return retVal;
}
}

View File

@ -27,9 +27,8 @@ public class SearchMethodBinding extends BaseMethodBinding {
private Method method;
private List<Parameter> myParameters;
private RequestType requestType;
private Class<?> myDeclaredResourceType;
private List<Parameter> myParameters;
public SearchMethodBinding(MethodReturnTypeEnum theMethodReturnTypeEnum, Class<? extends IResource> theReturnResourceType, Method theMethod) {
super(theMethodReturnTypeEnum, theReturnResourceType);
@ -39,6 +38,10 @@ public class SearchMethodBinding extends BaseMethodBinding {
this.myDeclaredResourceType = theMethod.getReturnType();
}
public Class<?> getDeclaredResourceType() {
return myDeclaredResourceType.getClass();
}
public Method getMethod() {
return method;
}
@ -47,19 +50,27 @@ public class SearchMethodBinding extends BaseMethodBinding {
return myParameters;
}
public RequestType getRequestType() {
return requestType;
}
public Class getDeclaredResourceType() {
return myDeclaredResourceType.getClass();
}
@Override
public ReturnTypeEnum getReturnType() {
return ReturnTypeEnum.BUNDLE;
}
@Override
public GetClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
assert theArgs.length == myParameters.size() : "Wrong number of arguments: " + theArgs.length;
Map<String, String> args = new HashMap<String, String>();
for (int idx = 0; idx < theArgs.length; idx++) {
Object object = theArgs[idx];
Parameter nextParam = myParameters.get(idx);
String value = nextParam.encode(object);
args.put(nextParam.getName(), value);
}
return new GetClientInvocation(args, getResourceName());
}
@Override
public List<IResource> invokeServer(IResourceProvider theResourceProvider, IdDt theId, IdDt theVersionId, Map<String, String[]> parameterValues) throws InvalidRequestException, InternalErrorException {
assert theId == null;
@ -94,26 +105,34 @@ public class SearchMethodBinding extends BaseMethodBinding {
}
@Override
public boolean matches(String theResourceName, IdDt theId, IdDt theVersion, Set<String> theParameterNames) {
if (!theResourceName.equals(getResourceName())) {
ourLog.trace("Method {} doesn't match because resource name {} != {}", method.getName(), theResourceName, getResourceName());
public boolean matches(Request theRequest) {
if (!theRequest.getResourceName().equals(getResourceName())) {
ourLog.trace("Method {} doesn't match because resource name {} != {}", method.getName(), theRequest.getResourceName(), getResourceName());
return false;
}
if (theId != null || theVersion != null) {
ourLog.trace("Method {} doesn't match because ID or Version are not null: {} - {}", theId, theVersion);
if (theRequest.getId() != null || theRequest.getVersion() != null) {
ourLog.trace("Method {} doesn't match because ID or Version are not null: {} - {}", theRequest.getId(), theRequest.getVersion());
return false;
}
if (theRequest.getRequestType() == RequestType.GET && theRequest.getOperation() != null) {
ourLog.trace("Method {} doesn't match because request type is GET but operation is not null: {}", theRequest.getId(), theRequest.getOperation());
return false;
}
if (theRequest.getRequestType() == RequestType.POST && !"_search".equals(theRequest.getOperation())) {
ourLog.trace("Method {} doesn't match because request type is POST but operation is not _search: {}", theRequest.getId(), theRequest.getOperation());
return false;
}
Set<String> methodParamsTemp = new HashSet<String>();
for (int i = 0; i < this.myParameters.size(); i++) {
Parameter temp = this.myParameters.get(i);
methodParamsTemp.add(temp.getName());
if (temp.isRequired() && !theParameterNames.contains(temp.getName())) {
if (temp.isRequired() && !theRequest.getParameterNames().contains(temp.getName())) {
ourLog.trace("Method {} doesn't match param '{}' is not present", method.getName(), temp.getName());
return false;
}
}
boolean retVal = methodParamsTemp.containsAll(theParameterNames);
boolean retVal = methodParamsTemp.containsAll(theRequest.getParameterNames());
ourLog.trace("Method {} matches: {}", method.getName(), retVal);
@ -128,10 +147,6 @@ public class SearchMethodBinding extends BaseMethodBinding {
this.myParameters = parameters;
}
public void setRequestType(RequestType requestType) {
this.requestType = requestType;
}
public void setResourceType(Class<?> resourceType) {
this.myDeclaredResourceType = resourceType;
}
@ -140,20 +155,4 @@ public class SearchMethodBinding extends BaseMethodBinding {
DELETE, GET, POST, PUT
}
@Override
public GetClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
assert theArgs.length == myParameters.size() : "Wrong number of arguments: " + theArgs.length;
Map<String, String> args = new HashMap<String, String>();
for (int idx = 0; idx < theArgs.length; idx++) {
Object object = theArgs[idx];
Parameter nextParam = myParameters.get(idx);
String value = nextParam.encode(object);
args.put(nextParam.getName(), value);
}
return new GetClientInvocation(args, getResourceName());
}
}

View File

@ -6,6 +6,7 @@ import java.util.Set;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.common.BaseMethodBinding;
import ca.uhn.fhir.rest.common.Request;
/**
* Created by dsotnikov on 2/25/2014.
@ -26,15 +27,15 @@ public class Resource {
this.methods = methods;
}
public BaseMethodBinding getMethod(String theResourceName, IdDt theId, IdDt theVersionId, Set<String> theParameters) throws Exception {
public BaseMethodBinding getMethod(Request theRequest) throws Exception {
if (null == methods) {
ourLog.warn("No methods exist for resource provider: {}", resourceProvider.getClass());
return null;
}
ourLog.info("Looking for a handler for {} / {} / {} / {}", new Object[] {theResourceName,theId, theVersionId, theParameters});
ourLog.debug("Looking for a handler for {}", theRequest);
for (BaseMethodBinding rm : methods) {
if (rm.matches(theResourceName, theId, theVersionId, theParameters)) {
if (rm.matches(theRequest)) {
ourLog.info("Handler {} matches", rm);
return rm;
} else {

View File

@ -27,6 +27,7 @@ import ca.uhn.fhir.model.api.BundleEntry;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.common.BaseMethodBinding;
import ca.uhn.fhir.rest.common.Request;
import ca.uhn.fhir.rest.common.SearchMethodBinding;
public abstract class RestfulServer extends HttpServlet {
@ -120,8 +121,6 @@ public abstract class RestfulServer extends HttpServlet {
}
String resourceName = null;
Long identity = null;
String requestPath = StringUtils.defaultString(request.getRequestURI());
String contextPath = StringUtils.defaultString(request.getContextPath());
requestPath = requestPath.substring(contextPath.length());
@ -147,11 +146,27 @@ public abstract class RestfulServer extends HttpServlet {
IdDt id = null;
IdDt versionId = null;
String operation = null;
if (tok.hasMoreTokens()) {
String identityString = tok.nextToken();
id = new IdDt(identityString);
String nextString = tok.nextToken();
if (nextString.startsWith("_")) {
operation = nextString;
}else {
id = new IdDt(nextString);
}
}
if (tok.hasMoreTokens()) {
String nextString = tok.nextToken();
if (nextString.startsWith("_history")) {
if (tok.hasMoreTokens()) {
versionId = new IdDt(tok.nextToken());
}else {
throw new InvalidRequestException("_history search specified but no version requested in URL");
}
}
}
// TODO: look for more tokens for version, compartments, etc...
//
@ -169,7 +184,15 @@ public abstract class RestfulServer extends HttpServlet {
// }
// }
BaseMethodBinding resourceMethod = resourceBinding.getMethod(resourceName, id, versionId, params.keySet());
Request r = new Request();
r.setResourceName(resourceName);
r.setId(id);
r.setVersion(versionId);
r.setOperation(operation);
r.setParameterNames(params.keySet());
r.setRequestType(requestType);
BaseMethodBinding resourceMethod = resourceBinding.getMethod(r);
if (null == resourceMethod) {
throw new MethodNotFoundException("No resource method available for the supplied parameters " + params);
}

View File

@ -15,7 +15,6 @@ public class PrettyPrintWriterWrapper implements XMLStreamWriter {
private XMLStreamWriter myTarget;
private int depth = 0;
private Map<Integer, Boolean> hasChildElement = new HashMap<Integer, Boolean>();
private XMLEventFactory myXmlEventFactory;
private static final String INDENT_CHAR = " ";
private static final String LINEFEED_CHAR = "\n";
@ -135,7 +134,8 @@ public class PrettyPrintWriterWrapper implements XMLStreamWriter {
@Override
public void writeEndDocument() throws XMLStreamException {
decrementAndIndent();myTarget.writeEndDocument();
decrementAndIndent();
myTarget.writeEndDocument();
}
@Override
@ -215,7 +215,7 @@ public class PrettyPrintWriterWrapper implements XMLStreamWriter {
@Override
public void writeCharacters(char[] theText, int theStart, int theLen) throws XMLStreamException {
myTarget.writeCharacters(theText,theStart,theLen);
myTarget.writeCharacters(theText, theStart, theLen);
}
@Override

View File

@ -4,10 +4,18 @@ import java.util.ArrayList;
import java.util.List;
import ca.uhn.fhir.model.api.IResource;
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.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.operations.Search;
import ca.uhn.fhir.rest.server.parameters.Optional;
import ca.uhn.fhir.rest.server.parameters.Required;
@SuppressWarnings("unused")
public class RestfulPatientResourceProviderMore implements IResourceProvider {
//START SNIPPET: searchAll
@ -18,6 +26,59 @@ public List<Organization> getAllOrganizations() {
}
//END SNIPPET: searchAll
//START SNIPPET: read
@Read()
public Patient getResourceById(@Read.IdParam IdDt theId) {
Patient retVal = new Patient();
// ...populate...
return retVal;
}
//END SNIPPET: read
//START SNIPPET: vread
@Read()
public Patient getResourceById(@Read.IdParam IdDt theId,
@Read.VersionIdParam IdDt theVersionId) {
Patient retVal = new Patient();
// ...populate...
return retVal;
}
//END SNIPPET: vread
//START SNIPPET: searchStringParam
@Search()
public List<Patient> searchByLastName(@Required(name=Patient.SP_FAMILY) StringDt theId) {
List<Patient> retVal = new ArrayList<Patient>();
// ...populate...
return retVal;
}
//END SNIPPET: searchStringParam
//START SNIPPET: searchIdentifierParam
@Search()
public List<Patient> searchByIdentifier(@Required(name=Patient.SP_IDENTIFIER) IdentifierDt theId) {
String identifierSystem = theId.getSystem().getValueAsString();
String identifier = theId.getValue().getValue();
List<Patient> retVal = new ArrayList<Patient>();
// ...populate...
return retVal;
}
//END SNIPPET: searchIdentifierParam
//START SNIPPET: searchOptionalParam
@Search()
public List<Patient> searchByNames( @Required(name=Patient.SP_FAMILY) StringDt theFamilyName,
@Optional(name=Patient.SP_GIVEN) StringDt theGivenName ) {
String familyName = theFamilyName.getValue();
String givenName = theGivenName != null ? theGivenName.getValue() : null;
List<Patient> retVal = new ArrayList<Patient>();
// ...populate...
return retVal;
}
//END SNIPPET: searchOptionalParam
@Override
public Class<? extends IResource> getResourceType() {
return null;

View File

@ -56,6 +56,14 @@
padding-left: 25px;
}
tt {
margin-left: 20px;
white-space: pre;
color: #448;
padding: 4px;
margin-bottom: 5px;
margin-top: 10px;
}
h1,h2,h3,h4,h5 {
color: #E3701A;

View File

@ -30,6 +30,7 @@
<item name="Introduction" href="./doc_intro.html"/>
<item name="The Data Model" href="./doc_fhirobjects.html"/>
<item name="RESTful Server" href="./doc_rest_server.html"/>
<item name="RESTful Operations" href="./doc_rest_operations.html"/>
<item name="JavaDocs" href="./apidocs/index.html"/>
</menu>

View File

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
<properties>
<title>RESTful Operations - HAPI FHIR</title>
<author email="jamesagnew@users.sourceforge.net">James Agnew</author>
</properties>
<body>
<section name="Implementing Resource Provider Operations">
<p>
RESTful Clients and Servers both share the same
method pattern, with one key difference: A client
is defined using annotated methods on an interface
which are used to retrieve resources,
whereas a server requires concrete method implementations
to actually provide those resources.
</p>
<p>
Unless otherwise specified, the examples below show <b>server</b>
implementations, but client methods will follow the same patterns.
</p>
<macro name="toc">
</macro>
</section>
<section name="Instance Level - read">
<p>
The
<a href="http://latest.fhir.me/http.html#read"><b>read</b></a>
operation retrieves a resource by ID. It is annotated with the
<a href="./apidocs/ca/uhn/fhir/rest/annotation/Read.html">@Read</a>
annotation, and has a single parameter annotated with the
<a href="./apidocs/ca/uhn/fhir/rest/annotation/Read.IdParam.html">@Read.IdParam</a>
annotation.
</p>
<macro name="snippet">
<param name="id" value="read" />
<param name="file" value="src/site/example/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
Example URL to invoke this method:<br/>
<code>http://fhir.example.com/Patient/111</code>
</p>
</section>
<section name="Instance Level - vread">
<p>
The
<a href="http://latest.fhir.me/http.html#vread"><b>vread</b></a>
operation retrieves a specific version of a resource with a given ID. It looks exactly
like a "read" operation, but with a second
parameter annotated with the
<a href="./apidocs/ca/uhn/fhir/rest/annotation/Read.VersionIdParam.html">@VersionIdParam</a>
annotation.
</p>
<macro name="snippet">
<param name="id" value="vread" />
<param name="file" value="src/site/example/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
Example URL to invoke this method:<br/>
<code>http://fhir.example.com/Patient/111/_history/2</code>
</p>
</section>
<section name="Type Level - search">
<p>
The
<a href="http://latest.fhir.me/http.html#search"><b>search</b></a> operation returns a bundle
with zero-to-many resources of a given type, matching a given set of parameters.
</p>
<subsection name="Search with No Parameters">
<p>
The following example shows a search with no parameters. This operation
should return all resources of a given type (this obviously doesn't make
sense in all contexts, but does for some resource types).
</p>
<macro name="snippet">
<param name="id" value="searchAll" />
<param name="file" value="src/site/example/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
Example URL to invoke this method:<br/>
<code>http://fhir.example.com/Patient</code>
</p>
</subsection>
<subsection name="Search with a String Parameter">
<p>
To allow a search using given search parameters, add one or more parameters
to your search method and tag these parameters as either
<a href="./apidocs/ca/uhn/fhir/rest/server/parameters/Required.html">@Required</a>
or
<a href="./apidocs/ca/uhn/fhir/rest/server/parameters/Optional.html">@Optional</a>.
</p>
<p>
This annotation takes a "name" parameter which specifies the parameter's
name (as it will appear in the search URL). FHIR defines standardized parameter
names for each resource, and these are available as constants on the
individual HAPI resource classes.
</p>
<macro name="snippet">
<param name="id" value="searchStringParam" />
<param name="file" value="src/site/example/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
Example URL to invoke this method:<br/>
<code>http://fhir.example.com/Patient?family=SMITH</code>
</p>
</subsection>
<subsection name="Search By Identifier">
<p>
Searches method parameters may be of any type that implements the
<a href="./apidocs/ca/uhn/fhir/model/api/IQueryParameterType.html">IQueryParameterType</a>
interface. For example, the search below can be used to search by
identifier (e.g. search for an MRN).
</p>
<macro name="snippet">
<param name="id" value="searchIdentifierParam" />
<param name="file" value="src/site/example/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
Example URL to invoke this method:<br/>
<code>http://fhir.example.com/Patient?identifier=urn:foo|7000135</code>
</p>
</subsection>
<subsection name="Multiple and Optional Parameters">
<p>
Search methods may take multiple parameters, and these parameters
may be optional. To add a second required parameter, annotate the
parameter with
<a href="./apidocs/ca/uhn/fhir/rest/server/parameters/Required.html">@Required</a>.
To add an optional parameter (which will be pased in as <code>null</code> if no value
is supplied), annotate the method with
<a href="./apidocs/ca/uhn/fhir/rest/server/parameters/Required.html">@Optional</a>.
</p>
<macro name="snippet">
<param name="id" value="searchOptionalParam" />
<param name="file" value="src/site/example/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
Example URLs to invoke this method:<br/>
<code>http://fhir.example.com/Patient?family=SMITH</code><br/>
<code>http://fhir.example.com/Patient?family=SMITH&amp;given=JOHN</code>
</p>
</subsection>
</section>
</body>
</document>

View File

@ -59,6 +59,13 @@
<param name="file" value="src/site/example/java/example/RestfulPatientResourceProvider.java" />
</macro>
<p>
You will probable wish to add more methods
to your resource provider. See
<a href="./doc_rest_operations.html">RESTful Operations</a> for
lots more examples of how to add methods for various operations.
</p>
</subsection>
<subsection name="Create a Server">
@ -94,25 +101,6 @@
</section>
<section name="Resource Providers">
<p>
Resource providers can support a wide variety of operations.
</p>
<p>
The following example shows a search with no parameters. This operation
should return all resources of a given type (this obviously doesn't make
sense in all contexts, but does for some resource types).
</p>
<macro name="snippet">
<param name="id" value="searchAll" />
<param name="file" value="src/site/example/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
</section>
</body>
</document>

View File

@ -20,6 +20,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.Constants;
@ -73,5 +74,77 @@ public class ClientTest {
assertEquals("PRP1660", response.getIdentifier().get(0).getValue().getValue());
}
@Test
public void testVRead() throws Exception {
//@formatter:off
String msg = "<Patient xmlns=\"http://hl7.org/fhir\">"
+ "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>"
+ "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>"
+ "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>"
+ "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>"
+ "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>"
+ "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>"
+ "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />"
+ "</Patient>";
//@formatter:on
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(httpClient.execute(capt.capture())).thenReturn(httpResponse);
when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP",1,1), 200, "OK"));
when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
ITestClient client = clientFactory.newClient(ITestClient.class, "http://foo");
// Patient response = client.findPatientByMrn(new IdentifierDt("urn:foo", "123"));
Patient response = client.getPatientByVersionId(new IdDt("111"), new IdDt("999"));
assertEquals("http://foo/Patient/111/_history/999", capt.getValue().getURI().toString());
assertEquals("PRP1660", response.getIdentifier().get(0).getValue().getValue());
}
@Test
public void testSearch() throws Exception {
//@formatter:off
String msg = "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" +
"<title/>\n" +
"<id>d039f91a-cc3c-4013-988e-af4d8d0614bd</id>\n" +
"<os:totalResults xmlns:os=\"http://a9.com/-/spec/opensearch/1.1/\">1</os:totalResults>\n" +
"<published>2014-03-11T16:35:07-04:00</published>\n" +
"<author>\n" +
"<name>ca.uhn.fhir.rest.server.DummyRestfulServer</name>\n" +
"</author>\n" +
"<entry>\n" +
"<content type=\"text/xml\">"
+ "<Patient xmlns=\"http://hl7.org/fhir\">"
+ "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>"
+ "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>"
+ "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>"
+ "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>"
+ "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>"
+ "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>"
+ "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />"
+ "</Patient>"
+ "</content>\n"
+ " </entry>\n"
+ "</feed>";
//@formatter:on
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(httpClient.execute(capt.capture())).thenReturn(httpResponse);
when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP",1,1), 200, "OK"));
when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
ITestClient client = clientFactory.newClient(ITestClient.class, "http://foo");
Patient response = client.findPatientByMrn(new IdentifierDt("urn:foo", "123"));
assertEquals("http://foo/Patient?identifier=urn%3Afoo%7C123", capt.getValue().getURI().toString());
assertEquals("PRP1660", response.getIdentifier().get(0).getValue().getValue());
}
}

View File

@ -14,6 +14,9 @@ public interface ITestClient extends IRestfulClient {
@Read(type=Patient.class)
Patient getPatientById(@Read.IdParam IdDt theId);
@Read(type=Patient.class)
Patient getPatientByVersionId(@Read.IdParam IdDt theId, @Read.VersionIdParam IdDt theVersionId);
@Search(type=Patient.class)
Patient findPatientByMrn(@Required(name = Patient.SP_IDENTIFIER) IdentifierDt theId);

View File

@ -1,18 +1,21 @@
package ca.uhn.fhir.rest.server;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ca.uhn.fhir.model.dstu.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu.composite.HumanNameDt;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
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.StringDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.operations.Search;
import ca.uhn.fhir.rest.server.parameters.Optional;
import ca.uhn.fhir.rest.server.parameters.Required;
/**
@ -20,9 +23,8 @@ import ca.uhn.fhir.rest.server.parameters.Required;
*/
public class DummyPatientResourceProvider implements IResourceProvider {
private Map<String, Patient> myIdToPatient = new HashMap<String, Patient>();
public DummyPatientResourceProvider() {
public Map<String, Patient> getIdToPatient() {
Map<String, Patient> idToPatient = new HashMap<String, Patient>();
{
Patient patient = new Patient();
patient.addIdentifier();
@ -33,7 +35,7 @@ public class DummyPatientResourceProvider implements IResourceProvider {
patient.getName().get(0).addFamily("Test");
patient.getName().get(0).addGiven("PatientOne");
patient.getGender().setText("M");
myIdToPatient.put("1", patient);
idToPatient.put("1", patient);
}
{
Patient patient = new Patient();
@ -45,13 +47,14 @@ public class DummyPatientResourceProvider implements IResourceProvider {
patient.getName().get(0).addFamily("Test");
patient.getName().get(0).addGiven("PatientTwo");
patient.getGender().setText("F");
myIdToPatient.put("2", patient);
idToPatient.put("2", patient);
}
return idToPatient;
}
@Search()
public Patient getPatient(@Required(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier) {
for (Patient next : myIdToPatient.values()) {
for (Patient next : getIdToPatient().values()) {
for (IdentifierDt nextId : next.getIdentifier()) {
if (nextId.matchesSystemAndValue(theIdentifier)) {
return next;
@ -61,9 +64,17 @@ public class DummyPatientResourceProvider implements IResourceProvider {
return null;
}
@Override
public Class<Patient> getResourceType() {
return Patient.class;
@Search()
public List<Patient> getPatientWithOptionalName(@Required(name = "name1") StringDt theName1, @Optional(name = "name2") StringDt theName2) {
List<Patient> retVal = new ArrayList<Patient>();
Patient next = getIdToPatient().get("1");
next.getName().get(0).getFamily().set(0, theName1);
if (theName2 != null) {
next.getName().get(0).getGiven().set(0, theName2);
}
retVal.add(next);
return retVal;
}
/**
@ -75,6 +86,31 @@ public class DummyPatientResourceProvider implements IResourceProvider {
*/
@Read()
public Patient getResourceById(@Read.IdParam IdDt theId) {
return myIdToPatient.get(theId.getValue());
return getIdToPatient().get(theId.getValue());
}
/**
* Retrieve the resource by its identifier
*
* @param theId
* The resource identity
* @return The resource
*/
@Read()
public Patient getResourceById(@Read.IdParam IdDt theId, @Read.VersionIdParam IdDt theVersionId) {
Patient retVal = getIdToPatient().get(theId.getValue());
retVal.getName().get(0).setText(theVersionId.getValue());
return retVal;
}
@Search()
public Collection<Patient> getResources() {
return getIdToPatient().values();
}
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
}

View File

@ -7,6 +7,7 @@ import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
@ -52,14 +53,53 @@ public class ResfulServerTest {
ourClient = builder.build();
ourCtx = new FhirContext(Patient.class);
}
@AfterClass
public static void afterClass() throws Exception {
ourServer.stop();
}
@Test
public void testSearchWithOptionalParam() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?name1=AAA");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.getEntries().size());
Patient patient = (Patient) bundle.getEntries().get(0).getResource();
assertEquals("AAA", patient.getName().get(0).getFamily().get(0).getValue());
assertEquals("PatientOne", patient.getName().get(0).getGiven().get(0).getValue());
/*
* Now with optional value populated
*/
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?name1=AAA&name2=BBB");
status = ourClient.execute(httpGet);
responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.getEntries().size());
patient = (Patient) bundle.getEntries().get(0).getResource();
assertEquals("AAA", patient.getName().get(0).getFamily().get(0).getValue());
assertEquals("BBB", patient.getName().get(0).getGiven().get(0).getValue());
}
@Test
public void testSearchByParamIdentifier() throws Exception {
@ -68,30 +108,84 @@ public class ResfulServerTest {
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.getEntries().size());
Patient patient = (Patient)bundle.getEntries().get(0).getResource();
Patient patient = (Patient) bundle.getEntries().get(0).getResource();
assertEquals("PatientOne", patient.getName().get(0).getGiven().get(0).getValue());
/**
* Alternate form
*/
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/_search?identifier=urn:hapitest:mrns%7C00001");
status = ourClient.execute(httpPost);
responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.getEntries().size());
patient = (Patient) bundle.getEntries().get(0).getResource();
assertEquals("PatientOne", patient.getName().get(0).getGiven().get(0).getValue());
/**
* failing form
*/
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/_search?identifier=urn:hapitest:mrns%7C00001");
status = ourClient.execute(httpGet);
responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(404, status.getStatusLine().getStatusCode());
}
@Test
public void testSearchAll() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(2, bundle.getEntries().size());
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/_search");
status = ourClient.execute(httpPost);
responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(2, bundle.getEntries().size());
}
@Test
public void testGetById() throws Exception {
// HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/1");
// httpPost.setEntity(new StringEntity("test", ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
// HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/1");
// httpPost.setEntity(new StringEntity("test", ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
Patient patient = (Patient) ourCtx.newXmlParser().parseResource(responseContent);
assertEquals("PatientOne", patient.getName().get(0).getGiven().get(0).getValue());
@ -99,13 +193,13 @@ public class ResfulServerTest {
/*
* Different ID
*/
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2");
status = ourClient.execute(httpGet);
responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.debug("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
patient = (Patient) ourCtx.newXmlParser().parseResource(responseContent);
assertEquals("PatientTwo", patient.getName().get(0).getGiven().get(0).getValue());
@ -113,17 +207,34 @@ public class ResfulServerTest {
/*
* Bad ID
*/
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/9999999");
status = ourClient.execute(httpGet);
responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.debug("Response was:\n{}", responseContent);
assertEquals(404, status.getStatusLine().getStatusCode());
}
@Test
public void testGetByVersionId() throws Exception {
// HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/1");
// httpPost.setEntity(new StringEntity("test", ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/_history/999");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
Patient patient = (Patient) ourCtx.newXmlParser().parseResource(responseContent);
assertEquals("PatientOne", patient.getName().get(0).getGiven().get(0).getValue());
assertEquals("999", patient.getName().get(0).getText().getValue());
}
}

View File

@ -11,8 +11,10 @@ import org.junit.Before;
import org.junit.Test;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.rest.common.Request;
import ca.uhn.fhir.rest.common.SearchMethodBinding;
import ca.uhn.fhir.rest.common.BaseMethodBinding.MethodReturnTypeEnum;
import ca.uhn.fhir.rest.common.SearchMethodBinding.RequestType;
import ca.uhn.fhir.rest.server.Parameter;
public class ResourceMethodTest {
@ -38,7 +40,7 @@ public class ResourceMethodTest {
inputParams.add("firstName");
inputParams.add("lastName");
assertEquals(false, rm.matches("Patient", null, null, inputParams)); // False
assertEquals(false, rm.matches(Request.withResourceAndParams("Patient", RequestType.GET, inputParams))); // False
}
@Test
@ -53,7 +55,7 @@ public class ResourceMethodTest {
Set<String> inputParams = new HashSet<String>();
inputParams.add("mrn");
assertEquals(true, rm.matches("Patient", null, null, inputParams)); // True
assertEquals(true, rm.matches(Request.withResourceAndParams("Patient", RequestType.GET, inputParams))); // True
}
@Test
@ -70,7 +72,7 @@ public class ResourceMethodTest {
inputParams.add("firstName");
inputParams.add("mrn");
assertEquals(true, rm.matches("Patient", null, null, inputParams)); // True
assertEquals(true, rm.matches(Request.withResourceAndParams("Patient", RequestType.GET, inputParams))); // True
}
@Test
@ -88,7 +90,7 @@ public class ResourceMethodTest {
inputParams.add("lastName");
inputParams.add("mrn");
assertEquals(true, rm.matches("Patient", null, null, inputParams)); // True
assertEquals(true, rm.matches(Request.withResourceAndParams("Patient", RequestType.GET, inputParams))); // True
}
@Test
@ -107,6 +109,6 @@ public class ResourceMethodTest {
inputParams.add("mrn");
inputParams.add("foo");
assertEquals(false, rm.matches("Patient", null, null, inputParams)); // False
assertEquals(false, rm.matches(Request.withResourceAndParams("Patient", RequestType.GET, inputParams))); // False
}
}