Fluent interface is basically working

This commit is contained in:
jamesagnew 2014-05-09 08:32:42 -04:00
parent e66bd31171
commit 86e5948d60
17 changed files with 799 additions and 282 deletions

View File

@ -65,6 +65,7 @@ import ca.uhn.fhir.model.primitive.BooleanDt;
import ca.uhn.fhir.model.primitive.BoundCodeDt;
import ca.uhn.fhir.model.primitive.BoundCodeableConceptDt;
import ca.uhn.fhir.model.primitive.CodeDt;
import ca.uhn.fhir.rest.gclient.NumberParam;
/**
@ -90,6 +91,7 @@ import ca.uhn.fhir.model.primitive.CodeDt;
@ResourceDef(name="Encounter", profile="http://hl7.org/fhir/profiles/Encounter", id="encounter")
public class Encounter extends BaseResource implements IResource {
/**
* Search parameter constant for <b>identifier</b>
* <p>
@ -156,6 +158,8 @@ public class Encounter extends BaseResource implements IResource {
@SearchParamDefinition(name="length", path="Encounter.length", description="Length of encounter in days")
public static final String SP_LENGTH = "length";
public static final NumberParam LENGTH = new NumberParam(SP_LENGTH);
/**
* Search parameter constant for <b>indication</b>
* <p>

View File

@ -73,6 +73,7 @@ import ca.uhn.fhir.model.primitive.CodeDt;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.gclient.QuantityParam;
/**
@ -120,6 +121,8 @@ public class Observation extends BaseResource implements IResource {
@SearchParamDefinition(name="value-quantity", path="Observation.value[x]", description="The value of the observation, if the value is a Quantity, or a SampledData (just search on the bounds of the values in sampled data)")
public static final String SP_VALUE_QUANTITY = "value-quantity";
public static final QuantityParam VALUE_QUANTITY = new QuantityParam(SP_VALUE_QUANTITY);
/**
* Search parameter constant for <b>value-concept</b>
* <p>

View File

@ -69,7 +69,8 @@ public class IdDt extends BasePrimitive<String> {
}
/**
* Create a new ID using a string
* Create a new ID using a string. This String may contain a simple ID (e.g. "1234")
* or it may contain a complete URL (http://example.com/fhir/Patient/1234).
*
* <p>
* <b>Description</b>: A whole number in the range 0 to 2^64-1 (optionally represented in hex), a uuid, an oid, or any other combination of lowercase letters, numerals, "-" and ".", with a length
@ -84,6 +85,10 @@ public class IdDt extends BasePrimitive<String> {
setValue(theValue);
}
/**
* Returns the value of this ID as a big decimal, or <code>null</code> if the value is null
*

View File

@ -42,9 +42,9 @@ import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.gclient.ICriterion;
import ca.uhn.fhir.rest.gclient.ICriterionInternal;
import ca.uhn.fhir.rest.gclient.IFor;
import ca.uhn.fhir.rest.gclient.IParam;
import ca.uhn.fhir.rest.gclient.IQuery;
import ca.uhn.fhir.rest.gclient.IParam;
import ca.uhn.fhir.rest.gclient.IUntypedQuery;
import ca.uhn.fhir.rest.gclient.ISort;
import ca.uhn.fhir.rest.gclient.Include;
import ca.uhn.fhir.rest.method.BaseOutcomeReturningMethodBinding;
@ -139,6 +139,11 @@ public class GenericClient extends BaseClient implements IGenericClient {
return resp;
}
@Override
public MethodOutcome delete(Class<? extends IResource> theType, String theId) {
return delete(theType, new IdDt(theId));
}
public HttpRequestBase getLastRequest() {
return myLastRequest;
}
@ -164,6 +169,11 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public <T extends IResource> Bundle history(Class<T> theType, String theId) {
return history(theType, new IdDt(theId));
}
@Override
public <T extends IResource> T read(final Class<T> theType, IdDt theId) {
GetClientInvocation invocation = ReadMethodBinding.createReadInvocation(theId, toResourceName(theType));
@ -186,7 +196,12 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public IQuery search() {
public <T extends IResource> T read(Class<T> theType, String theId) {
return read(theType, new IdDt(theId));
}
@Override
public IUntypedQuery search() {
return new QueryInternal();
}
@ -249,6 +264,11 @@ public class GenericClient extends BaseClient implements IGenericClient {
return resp;
}
@Override
public MethodOutcome update(String theId, IResource theResource) {
return update(new IdDt(theId), theResource);
}
@Override
public MethodOutcome validate(IResource theResource) {
BaseClientInvocation invocation = ValidateMethodBinding.createValidateInvocation(theResource, null, myContext);
@ -292,20 +312,25 @@ public class GenericClient extends BaseClient implements IGenericClient {
return resp;
}
@Override
public <T extends IResource> T vread(Class<T> theType, String theId, String theVersionId) {
return vread(theType, new IdDt(theId), new IdDt(theVersionId));
}
private String toResourceName(Class<? extends IResource> theType) {
return myContext.getResourceDefinition(theType).getName();
}
private class ForInternal implements IFor {
private class ForInternal implements IQuery {
private List<ICriterionInternal> myCriterion = new ArrayList<ICriterionInternal>();
private List<Include> myInclude = new ArrayList<Include>();
private boolean myLogRequestAndResponse;
private EncodingEnum myParamEncoding;
private Integer myParamLimit;
private String myResourceName;
private Class<? extends IResource> myResourceType;
private List<SortInternal> mySort = new ArrayList<SortInternal>();
private boolean myLogRequestAndResponse;
public ForInternal(Class<? extends IResource> theResourceType) {
myResourceType = theResourceType;
@ -318,19 +343,25 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public IFor and(ICriterion theCriterion) {
public IQuery and(ICriterion theCriterion) {
myCriterion.add((ICriterionInternal) theCriterion);
return this;
}
@Override
public IFor encodedJson() {
public IQuery andLogRequestAndResponse(boolean theLogRequestAndResponse) {
myLogRequestAndResponse = theLogRequestAndResponse;
return this;
}
@Override
public IQuery encodedJson() {
myParamEncoding = EncodingEnum.JSON;
return this;
}
@Override
public IFor encodedXml() {
public IQuery encodedXml() {
myParamEncoding = EncodingEnum.XML;
return null;
}
@ -387,13 +418,13 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public IFor include(Include theInclude) {
public IQuery include(Include theInclude) {
myInclude.add(theInclude);
return this;
}
@Override
public IFor limitTo(int theLimitTo) {
public IQuery limitTo(int theLimitTo) {
if (theLimitTo > 0) {
myParamLimit = theLimitTo;
} else {
@ -410,7 +441,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public IFor where(ICriterion theCriterion) {
public IQuery where(ICriterion theCriterion) {
myCriterion.add((ICriterionInternal) theCriterion);
return this;
}
@ -422,24 +453,18 @@ public class GenericClient extends BaseClient implements IGenericClient {
params.get(parameterName).add(parameterValue);
}
@Override
public IFor andLogRequestAndResponse(boolean theLogRequestAndResponse) {
myLogRequestAndResponse =theLogRequestAndResponse;
return this;
}
}
private class QueryInternal implements IQuery {
private class QueryInternal implements IUntypedQuery {
@Override
public IFor forResource(String theResourceName) {
return new ForInternal(theResourceName);
public IQuery forResource(Class<? extends IResource> theResourceType) {
return new ForInternal(theResourceType);
}
@Override
public IFor forResource(Class<? extends IResource> theResourceType) {
return new ForInternal(theResourceType);
public IQuery forResource(String theResourceName) {
return new ForInternal(theResourceName);
}
}
@ -455,21 +480,21 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
@Override
public IFor ascending(IParam theParam) {
public IQuery ascending(IParam theParam) {
myParamName = Constants.PARAM_SORT_ASC;
myParamValue = theParam.getParamName();
return myFor;
}
@Override
public IFor defaultOrder(IParam theParam) {
public IQuery defaultOrder(IParam theParam) {
myParamName = Constants.PARAM_SORT;
myParamValue = theParam.getParamName();
return myFor;
}
@Override
public IFor descending(IParam theParam) {
public IQuery descending(IParam theParam) {
myParamName = Constants.PARAM_SORT_DESC;
myParamValue = theParam.getParamName();
return myFor;

View File

@ -29,71 +29,10 @@ import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.resource.Conformance;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.gclient.IQuery;
import ca.uhn.fhir.rest.gclient.IUntypedQuery;
public interface IGenericClient {
/**
* Implementation of the "instance read" method.
*
* @param theType The type of resource to load
* @param theId The ID to load
* @return The resource
*/
<T extends IResource> T read(Class<T> theType, IdDt theId);
/**
* Implementation of the "instance vread" method.
*
* @param theType The type of resource to load
* @param theId The ID to load
* @param theVersionId The version ID
* @return The resource
*/
<T extends IResource> T vread(Class<T> theType, IdDt theId, IdDt theVersionId);
/**
* Implementation of the "instance search" method.
* @param theType The type of resource to load
* @param theParams
* @return
*/
<T extends IResource> Bundle search(Class<T> theType, Map<String, List<IQueryParameterType>> theParams);
/**
* Implementation of the "instance update" method.
*
* @param theId The ID to update
* @param theResource The new resource body
* @return An outcome containing the results and possibly the new version ID
*/
MethodOutcome update(IdDt theIdDt, IResource theResource);
/**
* Implementation of the "type validate" method.
*
* @param theId The ID to validate
* @param theResource The resource to validate
* @return An outcome containing any validation issues
*/
MethodOutcome validate(IResource theResource);
/**
* Implementation of the "delete instance" method.
* @param theType The type of resource to delete
* @param theId the ID of the resource to delete
* @return An outcome
*/
MethodOutcome delete(Class<? extends IResource> theType, IdDt theId);
/**
* Implementation of the "history instance" method.
* @param theType The type of resource to return the history for
* @param theId the ID of the resource to return the history for
* @return An outcome
*/
<T extends IResource> Bundle history(Class<T> theType, IdDt theIdDt);
/**
* Retrieves and returns the server conformance statement
*/
@ -101,11 +40,148 @@ public interface IGenericClient {
/**
* Implementation of the "type create" method.
* @param theResource The resource to create
*
* @param theResource
* The resource to create
* @return An outcome
*/
MethodOutcome create(IResource theResource);
IQuery search();
/**
* Implementation of the "delete instance" method.
*
* @param theType
* The type of resource to delete
* @param theId
* the ID of the resource to delete
* @return An outcome
*/
MethodOutcome delete(Class<? extends IResource> theType, IdDt theId);
/**
* Implementation of the "delete instance" method.
*
* @param theType
* The type of resource to delete
* @param theId
* the ID of the resource to delete
* @return An outcome
*/
MethodOutcome delete(Class<? extends IResource> theType, String theId);
/**
* Implementation of the "history instance" method.
*
* @param theType
* The type of resource to return the history for
* @param theId
* the ID of the resource to return the history for
* @return An outcome
*/
<T extends IResource> Bundle history(Class<T> theType, IdDt theIdDt);
/**
* Implementation of the "history instance" method.
*
* @param theType
* The type of resource to return the history for
* @param theId
* the ID of the resource to return the history for
* @return An outcome
*/
<T extends IResource> Bundle history(Class<T> theType, String theIdDt);
/**
* Implementation of the "instance read" method.
*
* @param theType
* The type of resource to load
* @param theId
* The ID to load
* @return The resource
*/
<T extends IResource> T read(Class<T> theType, IdDt theId);
/**
* Implementation of the "instance read" method.
*
* @param theType
* The type of resource to load
* @param theId
* The ID to load
* @return The resource
*/
<T extends IResource> T read(Class<T> theType, String theId);
IUntypedQuery search();
/**
* Implementation of the "instance search" method.
*
* @param theType
* The type of resource to load
* @param theParams
* @return
*/
<T extends IResource> Bundle search(Class<T> theType, Map<String, List<IQueryParameterType>> theParams);
/**
* Implementation of the "instance update" method.
*
* @param theId
* The ID to update
* @param theResource
* The new resource body
* @return An outcome containing the results and possibly the new version ID
*/
MethodOutcome update(IdDt theIdDt, IResource theResource);
/**
* Implementation of the "instance update" method.
*
* @param theId
* The ID to update
* @param theResource
* The new resource body
* @return An outcome containing the results and possibly the new version ID
*/
MethodOutcome update(String theIdDt, IResource theResource);
/**
* Implementation of the "type validate" method.
*
* @param theId
* The ID to validate
* @param theResource
* The resource to validate
* @return An outcome containing any validation issues
*/
MethodOutcome validate(IResource theResource);
/**
* Implementation of the "instance vread" method.
*
* @param theType
* The type of resource to load
* @param theId
* The ID to load
* @param theVersionId
* The version ID
* @return The resource
*/
<T extends IResource> T vread(Class<T> theType, IdDt theId, IdDt theVersionId);
/**
* Implementation of the "instance vread" method.
*
* @param theType
* The type of resource to load
* @param theId
* The ID to load
* @param theVersionId
* The version ID
* @return The resource
*/
<T extends IResource> T vread(Class<T> theType, String theId, String theVersionId);
}

View File

@ -1,29 +0,0 @@
package ca.uhn.fhir.rest.gclient;
import ca.uhn.fhir.model.api.Bundle;
public interface IFor {
IFor where(ICriterion theCriterion);
IFor and(ICriterion theCriterion);
Bundle execute();
IFor include(Include theIncludeManagingorganization);
IFor encodedJson();
IFor encodedXml();
ISort sort();
IFor limitTo(int theLimitTo);
/**
* If set to true, the client will log the request and response to the SLF4J logger. This
* can be useful for debugging, but is generally not desirable in a production situation.
*/
IFor andLogRequestAndResponse(boolean theLogRequestAndResponse);
}

View File

@ -1,12 +1,29 @@
package ca.uhn.fhir.rest.gclient;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Bundle;
public interface IQuery {
IFor forResource(String theResourceName);
IQuery where(ICriterion theCriterion);
IQuery and(ICriterion theCriterion);
Bundle execute();
IFor forResource(Class<? extends IResource> theClass);
IQuery include(Include theIncludeManagingorganization);
IQuery encodedJson();
IQuery encodedXml();
ISort sort();
IQuery limitTo(int theLimitTo);
/**
* If set to true, the client will log the request and response to the SLF4J logger. This
* can be useful for debugging, but is generally not desirable in a production situation.
*/
IQuery andLogRequestAndResponse(boolean theLogRequestAndResponse);
}

View File

@ -2,10 +2,10 @@ package ca.uhn.fhir.rest.gclient;
public interface ISort {
IFor ascending(IParam theParam);
IQuery ascending(IParam theParam);
IFor defaultOrder(IParam theParam);
IQuery defaultOrder(IParam theParam);
IFor descending(IParam theParam);
IQuery descending(IParam theParam);
}

View File

@ -0,0 +1,12 @@
package ca.uhn.fhir.rest.gclient;
import ca.uhn.fhir.model.api.IResource;
public interface IUntypedQuery {
IQuery forResource(String theResourceName);
IQuery forResource(Class<? extends IResource> theClass);
}

View File

@ -0,0 +1,109 @@
package ca.uhn.fhir.rest.gclient;
/**
* Token parameter type for use in fluent client interfaces
*/
public class NumberParam implements IParam {
private String myParamName;
public NumberParam(String theParamName) {
myParamName = theParamName;
}
public IMatches<ICriterion> exactly() {
return new IMatches<ICriterion>() {
@Override
public ICriterion number(long theNumber) {
return new StringCriterion(getParamName(), Long.toString(theNumber));
}
@Override
public ICriterion number(String theNumber) {
return new StringCriterion(getParamName(), (theNumber));
}
};
}
@Override
public String getParamName() {
return myParamName;
}
public IMatches<ICriterion> greaterThan() {
return new IMatches<ICriterion>() {
@Override
public ICriterion number(long theNumber) {
return new StringCriterion(getParamName(), ">" + Long.toString(theNumber));
}
@Override
public ICriterion number(String theNumber) {
return new StringCriterion(getParamName(), ">" + (theNumber));
}
};
}
public IMatches<ICriterion> greaterThanOrEqual() {
return new IMatches<ICriterion>() {
@Override
public ICriterion number(long theNumber) {
return new StringCriterion(getParamName(), ">=" + Long.toString(theNumber));
}
@Override
public ICriterion number(String theNumber) {
return new StringCriterion(getParamName(), ">=" + (theNumber));
}
};
}
public IMatches<ICriterion> lessThan() {
return new IMatches<ICriterion>() {
@Override
public ICriterion number(long theNumber) {
return new StringCriterion(getParamName(), "<" + Long.toString(theNumber));
}
@Override
public ICriterion number(String theNumber) {
return new StringCriterion(getParamName(), "<" + (theNumber));
}
};
}
public IMatches<ICriterion> lessThanOrEqual() {
return new IMatches<ICriterion>() {
@Override
public ICriterion number(long theNumber) {
return new StringCriterion(getParamName(), "<=" + Long.toString(theNumber));
}
@Override
public ICriterion number(String theNumber) {
return new StringCriterion(getParamName(), "<=" + (theNumber));
}
};
}
public interface IMatches<T> {
/**
* Creates a search criterion that matches against the given number
*
* @param theNumber
* The number
* @return A criterion
*/
T number(long theNumber);
/**
* Creates a search criterion that matches against the given number
*
* @param theNumber
* The number
* @return A criterion
*/
T number(String theNumber);
}
}

View File

@ -0,0 +1,159 @@
package ca.uhn.fhir.rest.gclient;
import static org.apache.commons.lang3.StringUtils.*;
import org.apache.commons.lang.StringUtils;
import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum;
import ca.uhn.fhir.rest.gclient.NumberParam.IMatches;
/**
* Token parameter type for use in fluent client interfaces
*/
public class QuantityParam implements IParam {
private String myParamName;
@Override
public String getParamName() {
return myParamName;
}
public QuantityParam(String theParamName) {
myParamName = theParamName;
}
public IMatches<IAndUnits> exactly() {
return new NumberParam.IMatches<IAndUnits>() {
@Override
public IAndUnits number(long theNumber) {
return new AndUnits("", Long.toString(theNumber));
}
@Override
public IAndUnits number(String theNumber) {
return new AndUnits("", theNumber);
}
};
}
public IMatches<IAndUnits> greaterThan() {
return new NumberParam.IMatches<IAndUnits>() {
@Override
public IAndUnits number(long theNumber) {
return new AndUnits(">", Long.toString(theNumber));
}
@Override
public IAndUnits number(String theNumber) {
return new AndUnits(">", theNumber);
}
};
}
public IMatches<IAndUnits> greaterThanOrEquals() {
return new NumberParam.IMatches<IAndUnits>() {
@Override
public IAndUnits number(long theNumber) {
return new AndUnits(">=", Long.toString(theNumber));
}
@Override
public IAndUnits number(String theNumber) {
return new AndUnits(">=", theNumber);
}
};
}
public IMatches<IAndUnits> approximately() {
return new NumberParam.IMatches<IAndUnits>() {
@Override
public IAndUnits number(long theNumber) {
return new AndUnits("~", Long.toString(theNumber));
}
@Override
public IAndUnits number(String theNumber) {
return new AndUnits("~", theNumber);
}
};
}
public IMatches<IAndUnits> withComparator(QuantityCompararatorEnum theComparator) {
final String cmp = theComparator != null ? theComparator.getCode() : "";
return new NumberParam.IMatches<IAndUnits>() {
@Override
public IAndUnits number(long theNumber) {
return new AndUnits(cmp, Long.toString(theNumber));
}
@Override
public IAndUnits number(String theNumber) {
return new AndUnits(cmp, theNumber);
}
};
}
public IMatches<IAndUnits> lessThan() {
return new NumberParam.IMatches<IAndUnits>() {
@Override
public IAndUnits number(long theNumber) {
return new AndUnits("<", Long.toString(theNumber));
}
@Override
public IAndUnits number(String theNumber) {
return new AndUnits("<", theNumber);
}
};
}
public IMatches<IAndUnits> lessThanOrEquals() {
return new NumberParam.IMatches<IAndUnits>() {
@Override
public IAndUnits number(long theNumber) {
return new AndUnits("<=", Long.toString(theNumber));
}
@Override
public IAndUnits number(String theNumber) {
return new AndUnits("<=", theNumber);
}
};
}
private class AndUnits implements IAndUnits {
private String myToken1;
public AndUnits(String theComparator, String theNumber) {
myToken1 = StringUtils.defaultString(theComparator) + StringUtils.defaultString(theNumber);
}
@Override
public ICriterion andUnits(String theUnits) {
return andUnits(theUnits, null);
}
@Override
public ICriterion andUnits(String theSystem, String theUnits) {
return new StringCriterion(getParamName(), myToken1 + "|" + defaultString(theSystem) + "|" + defaultString(theUnits));
}
@Override
public ICriterion andNoUnits() {
return andUnits(null, null);
}
}
public interface IAndUnits {
ICriterion andNoUnits();
ICriterion andUnits(String theUnits);
ICriterion andUnits(String theSystem, String theUnits);
}
}

View File

@ -554,6 +554,8 @@ public class RestfulServer extends HttpServlet {
if (tok.hasMoreTokens()) {
String versionString = tok.nextToken();
versionId = new IdDt(versionString);
} else {
operation = Constants.PARAM_HISTORY;
}
} else if (nextString.startsWith("_")) {
if (operation != null) {

View File

@ -1,6 +1,5 @@
package example;
import static org.junit.Assert.assertEquals;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.dstu.resource.Organization;
@ -9,21 +8,44 @@ import ca.uhn.fhir.rest.client.IGenericClient;
public class GenericClientExample {
@SuppressWarnings("unused")
public static void simpleExample() {
// START SNIPPET: simple
FhirContext ctx = new FhirContext();
String serverBase = "http://fhir.healthintersections.com.au/open";
IGenericClient client = ctx.newRestfulGenericClient(serverBase);
// Read a patient
Patient patient = client.read(Patient.class, "1");
// Change the patient and update it to the server
patient.getNameFirstRep().getFamilyFirstRep().setValue("Jones");
client.update("1", patient);
// Return the version history for that patient
Bundle versions = client.history(Patient.class, "1");
// END SNIPPET: simple
}
public static void fluentSearch() {
FhirContext ctx = new FhirContext();
IGenericClient client = ctx.newRestfulGenericClient("http://fhir.healthintersections.com.au/open");
//START SNIPPET: fluentExample
Bundle response = client.search()
.forResource(Patient.class)
.where(Patient.PARAM_BIRTHDATE.beforeOrEquals().day("2011-01-01"))
.and(Patient.PARAM_PROVIDER.hasChainedProperty(Organization.NAME.matches().value("Health")))
.andLogRequestAndResponse(true)
.execute();
//END SNIPPET: fluentExample
System.out.println(ctx.newXmlParser().setPrettyPrint(true).encodeBundleToString(response));
}
public static void main(String[] args) {
FhirContext ctx = new FhirContext();
IGenericClient client = ctx.newRestfulGenericClient("http://fhir.healthintersections.com.au/open");
Bundle response = client.search()
.forResource(Patient.class)
.where(Patient.PARAM_BIRTHDATE.beforeOrEquals().day("2011-01-01"))
.and(Patient.PARAM_PROVIDER.hasChainedProperty(Organization.NAME.matches().value("Health")))
.andLogRequestAndResponse(true)
.execute();
System.out.println(ctx.newXmlParser().setPrettyPrint(true).encodeBundleToString(response));
// nothing
}
}

View File

@ -24,14 +24,73 @@
</p>
<p>
Setup is mostly done using simple annotations, which means that it
should
be possible to create a FHIR compliant server quickly and
easily. Once again,
this design is intended to be similar to that of
There are two types of clients provided by HAPI: Generic and Annotation-driven.
The generic client (introduced in HAPI-FHIR 0.3) is much simpler to create
and generally provides the faster way to get started. The annotation-driven
client is more powerful and can rely on code generation to give better
compile-time checking.
</p>
</section>
<section name="The Generic Client">
<p>
Creating a generic client simply requires you to create an instance of
<code>FhirContext</code> and use that to instantiate a client.
<b>Performance Tip: </b> Note that FhirContext is an expensive object to create,
so you should try to keep an instance around for the lifetime of your application. It
is thread-safe so it can be passed as needed. Client instances, on the other hand,
are very inexpensive to create so you can create a new one for each request if needed
(although there is no requirement to do so, clients are reusable and thread-safe as well).
</p>
<p>
The following example shows how to create a client, and a few operations which
can be performed.
</p>
<macro name="snippet">
<param name="id" value="simple" />
<param name="file"
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
<subsection name="Search/Query">
<p>
The generic client supports queries using a fluent interface
which is inspired by the fantastic
<a href="http://ewoutkramer.github.io/fhir-net-api/client-search.html">.NET FHIR API</a>.
The fluent interface allows you to construct powerful queries by chaining
method calls together, leading to highly readable code. It also allows
you to take advantage of intellisense/code completion in your favourite
IDE.
</p>
<p>
The following example shows how to query using this interface:
</p>
<macro name="snippet">
<param name="id" value="fluentExample" />
<param name="file"
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
</subsection>
</section>
<section name="The Annotation-Driven Client">
<p>
The design of the annotation-driven client
is intended to be similar to that of
JAX-WS, so users of that
specification should be comfortable with
this one.
this one. It uses a user-defined interface containing special
annotated methods which HAPI binds to calls against a server.
</p>
<subsection name="Defining A Restful Client Interface">

View File

@ -669,7 +669,7 @@ public class XmlParserTest {
assertEquals("http://spark.furore.com/fhir/_snapshot?id=327d6bb9-83b0-4929-aa91-6dd9c41e587b&start=0&_count=20", bundle.getLinkSelf().getValue());
assertEquals("Patient resource with id 3216379", bundle.getEntries().get(0).getTitle().getValue());
assertEquals("3216379", bundle.getEntries().get(0).getId().getValue());
assertEquals("http://spark.furore.com/fhir/Patient/3216379", bundle.getEntries().get(0).getId().getValue());
assertEquals("3216379", bundle.getEntries().get(0).getResource().getId().getValue());
}

View File

@ -20,6 +20,8 @@ import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.dstu.resource.Encounter;
import ca.uhn.fhir.model.dstu.resource.Observation;
import ca.uhn.fhir.model.dstu.resource.Organization;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.rest.server.Constants;
@ -93,7 +95,6 @@ public class GenericClientTest {
}
@SuppressWarnings("unused")
@Test
public void testSearchByStringExact() throws Exception {
@ -115,9 +116,62 @@ public class GenericClientTest {
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Patient?name:exact=james", capt.getValue().getURI().toString());
assertEquals("http://example.com/fhir/Patient?name%3Aexact=james", capt.getValue().getURI().toString());
}
@SuppressWarnings("unused")
@Test
public void testSearchByNumberExact() throws Exception {
String msg = new FhirContext().newXmlParser().encodeBundleToString(new Bundle());
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
IGenericClient client = myCtx.newRestfulGenericClient("http://example.com/fhir");
//@formatter:off
Bundle response = client.search()
.forResource(Observation.class)
.where(Observation.VALUE_QUANTITY.greaterThan().number(123).andUnits("foo", "bar"))
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Observation?value-quantity=%3E123%7Cfoo%7Cbar", capt.getValue().getURI().toString());
}
@SuppressWarnings("unused")
@Test
public void testSearchByQuantity() throws Exception {
String msg = getPatientFeedWithOneResult();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
IGenericClient client = myCtx.newRestfulGenericClient("http://example.com/fhir");
//@formatter:off
Bundle response = client.search()
.forResource(Patient.class)
.where(Encounter.LENGTH.exactly().number(123))
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Patient?length=123", capt.getValue().getURI().toString());
}
@SuppressWarnings("unused")
@Test
public void testSearchByToken() throws Exception {
@ -139,7 +193,7 @@ public class GenericClientTest {
.execute();
//@formatter:on
assertEquals("http://example.com/fhir/Patient?identifier=http%3A%2F%2Ffoo%7CZZZ", capt.getValue().getURI().toString());
assertEquals("http://example.com/fhir/Patient?identifier=http%3A%2F%2Fexample.com%2Ffhir%7CZZZ", capt.getValue().getURI().toString());
}
@ -193,7 +247,6 @@ public class GenericClientTest {
}
@SuppressWarnings("unused")
@Test
public void testSearchByDate() throws Exception {

View File

@ -93,8 +93,8 @@ public class ResfulServerMethodTest {
private static FhirContext ourCtx;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResfulServerMethodTest.class);
private static int ourPort;
private static Server ourServer;
private static DummyDiagnosticReportResourceProvider ourReportProvider;
private static Server ourServer;
@Test
public void test404IsPropagatedCorrectly() throws Exception {
@ -188,22 +188,6 @@ public class ResfulServerMethodTest {
}
@Test
public void testWithAdditionalParams() throws Exception {
HttpDelete httpGet = new HttpDelete("http://localhost:" + ourPort + "/Patient/1234?_pretty=true");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
OperationOutcome patient = ourCtx.newXmlParser().parseResource(OperationOutcome.class, responseContent);
assertEquals("1234", patient.getIssueFirstRep().getDetails().getValue());
}
@Test
public void testDeleteNoResponse() throws Exception {
@ -383,6 +367,28 @@ public class ResfulServerMethodTest {
// }
}
@Test
public void testHistoryFailsIfResourcesAreIncorrectlyPopulated() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/999/_history");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(500, status.getStatusLine().getStatusCode());
}
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/998/_history");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(500, status.getStatusLine().getStatusCode());
}
}
// @Test
// public void testSearchByComplex() throws Exception {
//
@ -446,28 +452,6 @@ public class ResfulServerMethodTest {
}
@Test
public void testHistoryFailsIfResourcesAreIncorrectlyPopulated() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/999/_history");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(500, status.getStatusLine().getStatusCode());
}
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/998/_history");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(500, status.getStatusLine().getStatusCode());
}
}
@Test
public void testHistoryResourceType() throws Exception {
@ -542,6 +526,19 @@ public class ResfulServerMethodTest {
}
@Test
public void testReadOnTypeThatDoesntSupportRead() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/AdverseReaction/223");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(Constants.STATUS_HTTP_404_NOT_FOUND, status.getStatusLine().getStatusCode());
}
@Test
public void testSearchAll() throws Exception {
@ -569,19 +566,6 @@ public class ResfulServerMethodTest {
}
@Test
public void testReadOnTypeThatDoesntSupportRead() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/AdverseReaction/223");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(Constants.STATUS_HTTP_404_NOT_FOUND, status.getStatusLine().getStatusCode());
}
@Test
public void testSearchAllProfiles() throws Exception {
@ -909,22 +893,6 @@ public class ResfulServerMethodTest {
}
public void testUpdateWrongResourceType() throws Exception {
// TODO: this method sends in the wrong resource type vs. the URL so it
// should
// give a useful error message (and then make this unit test actually
// run)
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/DiagnosticReport/001");
httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
ourClient.execute(httpPost);
fail();
}
@Test
public void testUpdateNoResponse() throws Exception {
@ -941,6 +909,30 @@ public class ResfulServerMethodTest {
}
@Test
public void testUpdateWithTagMultiple() throws Exception {
DiagnosticReport dr = new DiagnosticReport();
dr.addCodedDiagnosis().addCoding().setCode("AAA");
HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/DiagnosticReport/001");
httpPost.addHeader("Category", "Dog, Cat");
httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(dr), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
ourClient.execute(httpPost);
assertEquals(2, ourReportProvider.getLastTags().size());
assertEquals(new Tag("Dog"), ourReportProvider.getLastTags().get(0));
assertEquals(new Tag("Cat"), ourReportProvider.getLastTags().get(1));
httpPost = new HttpPut("http://localhost:" + ourPort + "/DiagnosticReport/001");
httpPost.addHeader("Category", "Dog; label=\"aa\", Cat; label=\"bb\"");
httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(dr), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
ourClient.execute(httpPost);
assertEquals(2, ourReportProvider.getLastTags().size());
assertEquals(new Tag("Dog", "aa", (String)null), ourReportProvider.getLastTags().get(0));
assertEquals(new Tag("Cat", "bb", (String)null), ourReportProvider.getLastTags().get(1));
}
@Test
public void testUpdateWithTagSimple() throws Exception {
@ -978,7 +970,6 @@ public class ResfulServerMethodTest {
}
@Test
public void testUpdateWithTagWithSchemeAndLabel() throws Exception {
@ -1001,30 +992,7 @@ public class ResfulServerMethodTest {
}
@Test
public void testUpdateWithTagMultiple() throws Exception {
DiagnosticReport dr = new DiagnosticReport();
dr.addCodedDiagnosis().addCoding().setCode("AAA");
HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/DiagnosticReport/001");
httpPost.addHeader("Category", "Dog, Cat");
httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(dr), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
ourClient.execute(httpPost);
assertEquals(2, ourReportProvider.getLastTags().size());
assertEquals(new Tag("Dog"), ourReportProvider.getLastTags().get(0));
assertEquals(new Tag("Cat"), ourReportProvider.getLastTags().get(1));
httpPost = new HttpPut("http://localhost:" + ourPort + "/DiagnosticReport/001");
httpPost.addHeader("Category", "Dog; label=\"aa\", Cat; label=\"bb\"");
httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(dr), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
ourClient.execute(httpPost);
assertEquals(2, ourReportProvider.getLastTags().size());
assertEquals(new Tag("Dog", "aa", (String)null), ourReportProvider.getLastTags().get(0));
assertEquals(new Tag("Cat", "bb", (String)null), ourReportProvider.getLastTags().get(1));
}
@Test
public void testUpdateWithVersion() throws Exception {
@ -1064,20 +1032,20 @@ public class ResfulServerMethodTest {
assertEquals(400, results.getStatusLine().getStatusCode());
}
@Test
public void testValidateWithPrettyPrintResponse() throws Exception {
public void testUpdateWrongResourceType() throws Exception {
// TODO: this method sends in the wrong resource type vs. the URL so it
// should
// give a useful error message (and then make this unit test actually
// run)
Patient patient = new Patient();
patient.addName().addFamily("FOO");
patient.addIdentifier().setValue("002");
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/_validate?_pretty=true");
HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/DiagnosticReport/001");
httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertThat(responseContent, containsString("\n "));
ourClient.execute(httpPost);
fail();
}
@Test
@ -1135,6 +1103,38 @@ public class ResfulServerMethodTest {
}
@Test
public void testValidateWithPrettyPrintResponse() throws Exception {
Patient patient = new Patient();
patient.addName().addFamily("FOO");
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/_validate?_pretty=true");
httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertThat(responseContent, containsString("\n "));
}
@Test
public void testWithAdditionalParams() throws Exception {
HttpDelete httpGet = new HttpDelete("http://localhost:" + ourPort + "/Patient/1234?_pretty=true");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
OperationOutcome patient = ourCtx.newXmlParser().parseResource(OperationOutcome.class, responseContent);
assertEquals("1234", patient.getIssueFirstRep().getDetails().getValue());
}
@AfterClass
public static void afterClass() throws Exception {
ourServer.stop();
@ -1165,6 +1165,30 @@ public class ResfulServerMethodTest {
}
/**
* Created by dsotnikov on 2/25/2014.
*/
public static class DummyAdverseReactionResourceProvider implements IResourceProvider {
/*
* *********************
* NO NEW METHODS *********************
*/
@Create()
public MethodOutcome create(@ResourceParam AdverseReaction thePatient) {
IdDt id = new IdDt(thePatient.getIdentifier().get(0).getValue().getValue());
IdDt version = new IdDt(thePatient.getIdentifier().get(1).getValue().getValue());
return new MethodOutcome(id, version);
}
@Override
public Class<? extends IResource> getResourceType() {
return AdverseReaction.class;
}
}
public static class DummyDiagnosticReportResourceProvider implements IResourceProvider {
private TagList myLastTags;
@ -1191,6 +1215,10 @@ public class ResfulServerMethodTest {
// do nothing
}
public TagList getLastTags() {
return myLastTags;
}
@Override
public Class<? extends IResource> getResourceType() {
return DiagnosticReport.class;
@ -1204,10 +1232,6 @@ public class ResfulServerMethodTest {
return new MethodOutcome(id, version);
}
public TagList getLastTags() {
return myLastTags;
}
@Update()
public MethodOutcome updateDiagnosticReportWithVersionAndNoResponse(@IdParam IdDt theId, @ResourceParam DiagnosticReport theDr) {
IdDt id = theId;
@ -1218,30 +1242,6 @@ public class ResfulServerMethodTest {
}
/**
* Created by dsotnikov on 2/25/2014.
*/
public static class DummyAdverseReactionResourceProvider implements IResourceProvider {
/*
* *********************
* NO NEW METHODS *********************
*/
@Override
public Class<? extends IResource> getResourceType() {
return AdverseReaction.class;
}
@Create()
public MethodOutcome create(@ResourceParam AdverseReaction thePatient) {
IdDt id = new IdDt(thePatient.getIdentifier().get(0).getValue().getValue());
IdDt version = new IdDt(thePatient.getIdentifier().get(1).getValue().getValue());
return new MethodOutcome(id, version);
}
}
/**
* Created by dsotnikov on 2/25/2014.
*/