Lots of documentation updates

This commit is contained in:
James Agnew 2014-07-22 18:12:01 -04:00
parent efb14397df
commit 622e528f43
17 changed files with 483 additions and 173 deletions

View File

@ -8,9 +8,6 @@
<body>
<release version="0.5" date="TBD">
<action type="add">
<<<<<<< HEAD
=======
RESTful search method parameters have been overhauled to reduce confusing duplicate names and
having multiple ways of accomplishing the same thing. This means that a number of existing classes
have been deprocated in favour of new naming schemes.
<![CDATA[<br/><br/>]]>
@ -25,7 +22,6 @@
new parameters when possible.
</action>
<action type="add">
>>>>>>> af3c35cbc07df69c760e200b4a80f4bcc3d183e9
Allow server methods to return wildcard generic types (e.g. List&lt;? extends IResource&gt;)
</action>
<action type="add">

View File

@ -45,7 +45,13 @@ public @interface IncludeParam {
/**
* Optional parameter, if provided the server will only allow the values
* within the given set. If an _include parameter is passed to the server
* which does not match any allowed values the server will return an error.
* which does not match any allowed values the server will return an error.
* <p>
* You may also pass in a value of "*" which indicates to the server
* that any value is allowable and will be passed to this parameter. This is
* helpful if you want to explicitly declare support for some includes, but also
* allow others implicitly (e.g. imports from other resources)
* </p>
*/
String[] allow() default {};

View File

@ -61,7 +61,6 @@ import ca.uhn.fhir.rest.annotation.Validate;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException;
import ca.uhn.fhir.rest.param.IncludeParameter;
import ca.uhn.fhir.rest.server.BundleProviders;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.EncodingEnum;

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.rest.param;
package ca.uhn.fhir.rest.method;
/*
* #%L
@ -34,11 +34,11 @@ import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.PathSpecification;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
import ca.uhn.fhir.rest.annotation.IncludeParam;
import ca.uhn.fhir.rest.method.QualifiedParamList;
import ca.uhn.fhir.rest.param.BaseQueryParameter;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class IncludeParameter extends BaseQueryParameter {
class IncludeParameter extends BaseQueryParameter {
private Set<String> myAllow;
private Class<? extends Collection<Include>> myInstantiableCollectionType;
@ -133,7 +133,9 @@ public class IncludeParameter extends BaseQueryParameter {
String value = nextParamList.get(0);
if (myAllow != null) {
if (!myAllow.contains(value)) {
throw new InvalidRequestException("Invalid _include parameter value: '" + value + "'. Valid values are: " + new TreeSet<String>(myAllow));
if (!myAllow.contains("*")) {
throw new InvalidRequestException("Invalid _include parameter value: '" + value + "'. Valid values are: " + new TreeSet<String>(myAllow));
}
}
}
if (retValCollection == null) {

View File

@ -52,7 +52,6 @@ import ca.uhn.fhir.rest.annotation.VersionIdParam;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.param.CollectionBinder;
import ca.uhn.fhir.rest.param.IncludeParameter;
import ca.uhn.fhir.rest.param.ResourceParameter;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.EncodingEnum;

View File

@ -1167,4 +1167,6 @@ public class RestfulServer extends HttpServlet {
}
}
}

View File

@ -1,7 +1,12 @@
package example;
import java.util.ArrayList;
import java.util.List;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.resource.Conformance;
import ca.uhn.fhir.model.dstu.resource.Organization;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.rest.client.IGenericClient;
@ -27,21 +32,64 @@ Bundle versions = client.history(Patient.class, "1",null,null);
// END SNIPPET: simple
}
@SuppressWarnings("unused")
public static void fluentSearch() {
FhirContext ctx = new FhirContext();
IGenericClient client = ctx.newRestfulGenericClient("http://fhir.healthintersections.com.au/open");
{
//START SNIPPET: create
Patient patient = new Patient();
// ..populate the patient object..
patient.addIdentifier("urn:system", "12345");
patient.addName().addFamily("Smith").addGiven("John");
//START SNIPPET: fluentExample
// Invoke the server create method (and send pretty-printed JSON encoding to the server
// instead of the default which is non-pretty printed XML)
client.create()
.resource(patient)
.prettyPrint()
.encodedJson()
.execute();
//END SNIPPET: create
}
{
//START SNIPPET: conformance
// Retrieve the server's conformance statement and print its description
Conformance conf = client.conformance();
System.out.println(conf.getDescription().getValue());
//END SNIPPET: conformance
}
{
//START SNIPPET: search
Bundle response = client.search()
.forResource(Patient.class)
.where(Patient.BIRTHDATE.beforeOrEquals().day("2011-01-01"))
.and(Patient.PROVIDER.hasChainedProperty(Organization.NAME.matches().value("Health")))
.andLogRequestAndResponse(true)
.execute();
//END SNIPPET: fluentExample
//END SNIPPET: search
//START SNIPPET: searchPaging
if (response.getLinkNext().isEmpty() == false) {
// load next page
Bundle nextPage = client.loadPage()
.next(response)
.execute();
}
//END SNIPPET: searchPaging
}
{
//START SNIPPET: transaction
List<IResource> resources = new ArrayList<IResource>();
// .. populate this list - note that you can also pass in a populated Bundle if you want to create one manually ..
List<IResource> response = client.transaction()
.withResources(resources)
.execute();
//END SNIPPET: transaction
}
System.out.println(ctx.newXmlParser().setPrettyPrint(true).encodeBundleToString(response));
}
public static void main(String[] args) {

View File

@ -64,6 +64,8 @@ import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringAndListParam;
import ca.uhn.fhir.rest.param.StringOrListParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
@ -180,6 +182,10 @@ public Patient getResourceById(@IdParam IdDt theId) {
retVal.addName().addFamily("Smith").addGiven("Tester").addGiven("Q");
// ...etc...
// if you know the version ID of the resource, you should set it and HAPI will
// include it in a Content-Location header
retVal.setId(new IdDt("Patient", "123", "2"));
return retVal;
}
//END SNIPPET: read
@ -251,10 +257,8 @@ public List<Patient> searchByLastName(@RequiredParam(name=Patient.SP_FAMILY) Str
patient.addName().addFamily("Smith").addGiven("Tester").addGiven("Q");
// ...etc...
/*
* Every returned resource must have its logical ID set. If the server
* supports versioning, that should be set too
*/
// Every returned resource must have its logical ID set. If the server
// supports versioning, that should be set too
String logicalId = "4325";
String versionId = "2"; // optional
patient.setId(new IdDt("Patient", logicalId, versionId));
@ -325,7 +329,9 @@ public List<Patient> searchWithDocs(
//START SNIPPET: searchMultiple
@Search()
public List<Observation> searchByObservationNames( @RequiredParam(name=Observation.SP_NAME) TokenOrListParam theCodings ) {
public List<Observation> searchByObservationNames(
@RequiredParam(name=Observation.SP_NAME) TokenOrListParam theCodings ) {
// The list here will contain 0..* codings, and any observations which match any of the
// given codings should be returned
List<CodingDt> wantedCodings = theCodings.getListAsCodings();
@ -336,6 +342,32 @@ public List<Observation> searchByObservationNames( @RequiredParam(name=Observati
}
//END SNIPPET: searchMultiple
//START SNIPPET: searchMultipleAnd
@Search()
public List<Patient> searchByPatientAddress(
@RequiredParam(name=Patient.SP_ADDRESS) StringAndListParam theAddressParts ) {
// StringAndListParam is a container for 0..* StringOrListParam, which is in turn a
// container for 0..* strings. It is a little bit weird to understand at first, but think of the
// StringAndListParam to be an AND list with multiple OR lists inside it. So you will need
// to return results which match at least one string within every OR list.
List<StringOrListParam> wantedCodings = theAddressParts.getValuesAsQueryTokens();
for (StringOrListParam nextOrList : wantedCodings) {
List<StringParam> queryTokens = nextOrList.getValuesAsQueryTokens();
// Only return results that match at least one of the tokens in the list below
for (StringParam nextString : queryTokens) {
// ....check for match...
}
}
List<Patient> retVal = new ArrayList<Patient>();
// ...populate...
return retVal;
}
//END SNIPPET: searchMultipleAnd
//START SNIPPET: dates
@Search()
public List<Patient> searchByObservationNames( @RequiredParam(name=Patient.SP_BIRTHDATE) DateParam theDate ) {

View File

@ -57,9 +57,8 @@
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>
<subsection name="Search/Query">
<subsection name="Fluent Operations">
<p>
The generic client supports queries using a fluent interface
which is inspired by the fantastic
@ -69,23 +68,77 @@
you to take advantage of intellisense/code completion in your favourite
IDE.
</p>
<p>
The following example shows how to query using this interface:
Note that most fluent operations end with an <code>execute()</code>
statement which actually performs the invocation. You may also invoke
several configuration operations just prior to the execute() statement,
such as <code>encodedJson()</code> or <code>encodedXml()</code>.
</p>
</subsection>
<subsection name="Type - Create">
<p>
The following example shows how to perform a create
operation using the generic client:
</p>
<macro name="snippet">
<param name="id" value="fluentExample" />
<param name="id" value="create" />
<param name="file"
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
</subsection>
<subsection name="Type - Search/Query">
<p>
The following example shows how to query using the generic client:
</p>
<macro name="snippet">
<param name="id" value="search" />
<param name="file"
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
<p>
If the server supports paging results, the client has a page method
which can be used to load subsequent pages.
</p>
<macro name="snippet">
<param name="id" value="searchPaging" />
<param name="file"
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
</subsection>
<subsection name="Server - Conformance">
<p>
To retrieve the server's conformance statement, simply call the <code>conformance()</code>
method as shown below.
</p>
<macro name="snippet">
<param name="id" value="conformance" />
<param name="file"
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
</subsection>
<subsection name="Server - Transaction">
<p>
The following example shows how to execute a transaction using the generic client:
</p>
<macro name="snippet">
<param name="id" value="transaction" />
<param name="file"
value="src/site/example/java/example/GenericClientExample.java" />
</macro>
</subsection>
</section>
<section name="The Annotation-Driven Client">
<p>
HAPI also provides a second style of client caled the annotation-driven client.
</p>
<p>
The design of the annotation-driven client
is intended to be similar to that of
@ -95,10 +148,24 @@
annotated methods which HAPI binds to calls against a server.
</p>
<p>
The annotation-driven client is particularly useful if you have a server that
exposes a set of specific operations (search parameter combinations, named queries, etc.)
and you want to let developers have a stongly/statically typed interface to that
server.
</p>
<p>
There is no difference in terms of capability between the two styles of
client. There is simply a difference in programming style and complexity. It
is probably safe to say that the generic client is easier to use and leads to
more readable code, at the expense of not giving any visibility into the
specific capabilities of the server you are interacting with.
</p>
<subsection name="Defining A Restful Client Interface">
<p>
The first step in creating a FHIR RESTful Client is to define a
The first step in creating an annotation-driven client is to define a
restful client interface.
</p>
@ -134,7 +201,7 @@
<macro name="snippet">
<param name="id" value="provider" />
<param name="file"
value="src/site/example/java/example/RestfulClientImpl.java" />
value="src/site/example/java/example/IRestfulClient.java" />
</macro>
<p>

View File

@ -466,14 +466,14 @@
</p>
</subsection>
<subsection name="Search Parameters: Introduction (String)">
<subsection name="Search Parameters: String Introduction">
<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/RequiredParam.html">@RequiredParam</a>
<a href="./apidocs/ca/uhn/fhir/rest/annotation/RequiredParam.html">@RequiredParam</a>
or
<a href="./apidocs/ca/uhn/fhir/rest/server/parameters/OptionalParam.html">@OptionalParam</a>
<a href="./apidocs/ca/uhn/fhir/rest/annotation/OptionalParam.html">@OptionalParam</a>
.
</p>
@ -734,13 +734,13 @@
may (or may not) be optional.
To add a second required parameter, annotate the
parameter with
<a href="./apidocs/ca/uhn/fhir/rest/server/parameters/RequiredParam.html">@RequiredParam</a>
<a href="./apidocs/ca/uhn/fhir/rest/annotation/RequiredParam.html">@RequiredParam</a>
.
To add an optional parameter (which will be passed in as
<code>null</code>
if no value
is supplied), annotate the method with
<a href="./apidocs/ca/uhn/fhir/rest/server/parameters/OptionalParam.html">@OptionalParam</a>
<a href="./apidocs/ca/uhn/fhir/rest/annotation/OptionalParam.html">@OptionalParam</a>
.
</p>
@ -821,13 +821,31 @@
</li>
</ul>
<p>
It is worth noting that according to the FHIR specification, you can have an
AND relationship combining multiple OR relationships, but not vice-versa. In
other words, it's possible to support a search like
<code>("name" = ("joe" or "john")) AND ("age" = (11 or 12))</code> but not
a search like
<code>("language" = ("en" AND "fr") OR ("address" = ("Canada" AND "Quebec"))</code>
</p>
<h4>OR Relationship Query Parameters</h4>
<p>
To accept a composite parameter, use a parameter type which implements the
<a href="./apidocs/ca/uhn/fhir/model/api/IQueryParameterOr.html">IQueryParameterOr</a>
interface. For example, to accept searches for
Observations matching any of a collection of names:
interface.
</p>
<p>
Each parameter type (StringParam, TokenParam, etc.) has a corresponding parameter
which accepts an OR list of parameters. These types are called "[type]OrListParam", for example:
StringOrListParam and TokenOrListParam.
</p>
<p>
The following example shows a search for Observation by name, where a list of
names may be passed in (and the expectation is that the server will return Observations
that match any of these names):
</p>
<macro name="snippet">
@ -848,12 +866,32 @@
<a href="./apidocs/ca/uhn/fhir/model/api/IQueryParameterAnd.html">IQueryParameterAnd</a>
interface.
</p>
<p>
See the section below on
<a href="#DATE_RANGES">date ranges</a>
for
one example of an AND parameter.
An example follows which shows a search for Patients by address, where multiple string
lists may be supplied by the client. For example, the client might request that the
address match <code>("Montreal" OR "Sherbrooke") AND ("Quebec" OR "QC")</code> using
the following query:
<br/>
<code>http://fhir.example.com/Patient?address=Montreal,Sherbrooke&amp;address=Quebec,QC</code>
</p>
<p>
The following code shows how to receive this parameter using a
<a href="./apidocs/ca/uhn/fhir/rest/param/StringAndListParam.html">StringAndListParameter</a>,
which can handle an AND list of multiple OR lists of strings.
</p>
<macro name="snippet">
<param name="id" value="searchMultipleAnd" />
<param name="file" value="src/site/example/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<h4>AND Relationship Query Parameters for Dates</h4>
<p>
Dates are a bit of a special case, since it is a common scenario to want to match
a date range (which is really just an AND query on two qualified date parameters).
See the section below on <a href="#DATE_RANGES">date ranges</a>
for an example of a DateRangeParameter.
</p>
</subsection>

View File

@ -910,7 +910,7 @@ public abstract class BaseFhirDao implements IDao {
};
}
protected void loadResourcesById(Set<IdDt> theIncludePids, List<IResource> theResourceListToPopulate) {
protected List<IResource> loadResourcesById(Set<IdDt> theIncludePids) {
Set<Long> pids = new HashSet<Long>();
for (IdDt next : theIncludePids) {
pids.add(next.getIdPartAsLong());
@ -926,10 +926,13 @@ public abstract class BaseFhirDao implements IDao {
// }
TypedQuery<ResourceTable> q = myEntityManager.createQuery(cq);
ArrayList<IResource> retVal = new ArrayList<IResource>();
for (ResourceTable next : q.getResultList()) {
IResource resource = toResource(next);
theResourceListToPopulate.add(resource);
retVal.add(resource);
}
return retVal;
}
protected String normalizeString(String theString) {

View File

@ -7,7 +7,21 @@ public class DaoConfig {
private int myHardSearchLimit = 1000;
private int myHardTagListLimit = 1000;
private ResourceEncodingEnum myResourceEncoding=ResourceEncodingEnum.JSONC;
private int myIncludeLimit = 2000;
/**
* This is the maximum number of resources that will be added to a single page of
* returned resources. Because of includes with wildcards and other possibilities it is possible for a client to make
* requests that include very large amounts of data, so this hard limit can be imposed to prevent runaway
* requests.
*/
public void setIncludeLimit(int theIncludeLimit) {
myIncludeLimit = theIncludeLimit;
}
/**
* See {@link #setIncludeLimit(int)}
*/
public int getHardSearchLimit() {
return myHardSearchLimit;
}
@ -32,4 +46,8 @@ public class DaoConfig {
myResourceEncoding = theResourceEncoding;
}
public int getIncludeLimit() {
return myIncludeLimit;
}
}

View File

@ -63,6 +63,8 @@ import ca.uhn.fhir.model.dstu.composite.CodingDt;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu.composite.QuantityDt;
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu.valueset.IssueSeverityEnum;
import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
import ca.uhn.fhir.model.primitive.IdDt;
@ -83,6 +85,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.FhirTerser;
import example.QuickUsage.MyClientInterface;
@Transactional(propagation = Propagation.REQUIRED)
public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements IFhirResourceDao<T> {
@ -153,10 +156,10 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
Predicate lt = builder.greaterThanOrEqualTo(from.<Date> get("myValueHigh"), lowerBound);
lb = builder.or(gt, lt);
// Predicate gin = builder.isNull(from.get("myValueLow"));
// Predicate lbo = builder.or(gt, gin);
// Predicate lin = builder.isNull(from.get("myValueHigh"));
// Predicate hbo = builder.or(lt, lin);
// Predicate gin = builder.isNull(from.get("myValueLow"));
// Predicate lbo = builder.or(gt, gin);
// Predicate lin = builder.isNull(from.get("myValueHigh"));
// Predicate hbo = builder.or(lt, lin);
// lb = builder.and(lbo, hbo);
}
@ -165,11 +168,11 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
Predicate gt = builder.lessThanOrEqualTo(from.<Date> get("myValueLow"), upperBound);
Predicate lt = builder.lessThanOrEqualTo(from.<Date> get("myValueHigh"), upperBound);
ub = builder.or(gt, lt);
// Predicate gin = builder.isNull(from.get("myValueLow"));
// Predicate lbo = builder.or(gt, gin);
// Predicate lin = builder.isNull(from.get("myValueHigh"));
// Predicate ubo = builder.or(lt, lin);
// Predicate gin = builder.isNull(from.get("myValueLow"));
// Predicate lbo = builder.or(gt, gin);
// Predicate lin = builder.isNull(from.get("myValueHigh"));
// Predicate ubo = builder.or(lt, lin);
// ub = builder.and(ubo, lbo);
}
@ -207,9 +210,9 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
return found;
}
// private Set<Long> addPredicateComposite(String theParamName, Set<Long> thePids, List<? extends IQueryParameterType> theList) {
// }
// private Set<Long> addPredicateComposite(String theParamName, Set<Long> thePids, List<? extends IQueryParameterType> theList) {
// }
private Set<Long> addPredicateQuantity(String theParamName, Set<Long> thePids, List<? extends IQueryParameterType> theList) {
if (theList == null || theList.isEmpty()) {
return thePids;
@ -228,7 +231,7 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
String unitsValue;
QuantityCompararatorEnum cmpValue;
BigDecimal valueValue;
boolean approx=false;
boolean approx = false;
if (params instanceof QuantityDt) {
QuantityDt param = (QuantityDt) params;
@ -441,7 +444,8 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
}
if (rawSearchTerm.length() > ResourceIndexedSearchParamString.MAX_LENGTH) {
throw new InvalidRequestException("Parameter[" + theParamName + "] has length (" + rawSearchTerm.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm);
throw new InvalidRequestException("Parameter[" + theParamName + "] has length (" + rawSearchTerm.length() + ") that is longer than maximum allowed ("
+ ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm);
}
String likeExpression = normalizeString(rawSearchTerm);
@ -573,10 +577,12 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
}
if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
throw new InvalidRequestException("Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system);
throw new InvalidRequestException("Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed ("
+ ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system);
}
if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
throw new InvalidRequestException("Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code);
throw new InvalidRequestException("Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH
+ "): " + code);
}
ArrayList<Predicate> singleCodePredicates = (new ArrayList<Predicate>());
@ -645,7 +651,8 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
if (theResource.getId().isEmpty() == false) {
if (isValidPid(theResource.getId())) {
throw new UnprocessableEntityException("This server cannot create an entity with a user-specified numeric ID - Client should not specify an ID when creating a new resource, or should include at least one letter in the ID to force a client-defined ID");
throw new UnprocessableEntityException(
"This server cannot create an entity with a user-specified numeric ID - Client should not specify an ID when creating a new resource, or should include at least one letter in the ID to force a client-defined ID");
}
createForcedIdIfNeeded(entity, theResource.getId());
@ -729,7 +736,8 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
final T current = currentTmp;
String querySring = "SELECT count(h) FROM ResourceHistoryTable h " + "WHERE h.myResourceId = :PID AND h.myResourceType = :RESTYPE" + " AND h.myUpdated < :END" + (theSince != null ? " AND h.myUpdated >= :SINCE" : "");
String querySring = "SELECT count(h) FROM ResourceHistoryTable h " + "WHERE h.myResourceId = :PID AND h.myResourceType = :RESTYPE" + " AND h.myUpdated < :END"
+ (theSince != null ? " AND h.myUpdated >= :SINCE" : "");
TypedQuery<Long> countQuery = myEntityManager.createQuery(querySring, Long.class);
countQuery.setParameter("PID", theId.getIdPartAsLong());
countQuery.setParameter("RESTYPE", resourceType);
@ -767,8 +775,9 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
retVal.add(current);
}
TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery("SELECT h FROM ResourceHistoryTable h WHERE h.myResourceId = :PID AND h.myResourceType = :RESTYPE AND h.myUpdated < :END " + (theSince != null ? " AND h.myUpdated >= :SINCE" : "")
+ " ORDER BY h.myUpdated ASC", ResourceHistoryTable.class);
TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery(
"SELECT h FROM ResourceHistoryTable h WHERE h.myResourceId = :PID AND h.myResourceType = :RESTYPE AND h.myUpdated < :END "
+ (theSince != null ? " AND h.myUpdated >= :SINCE" : "") + " ORDER BY h.myUpdated ASC", ResourceHistoryTable.class);
q.setParameter("PID", theId.getIdPartAsLong());
q.setParameter("RESTYPE", resourceType);
q.setParameter("END", end.getValue(), TemporalType.TIMESTAMP);
@ -837,7 +846,8 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
throw new ConfigurationException("Unknown search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName + "]");
}
if (sp.getParamType() != SearchParamTypeEnum.TOKEN) {
throw new ConfigurationException("Search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName + "] is not a token type, only token is supported");
throw new ConfigurationException("Search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName
+ "] is not a token type, only token is supported");
}
}
@ -864,7 +874,8 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
private void validateResourceType(BaseHasResource entity) {
if (!myResourceName.equals(entity.getResourceType())) {
throw new ResourceNotFoundException("Resource with ID " + entity.getIdDt().getIdPart() + " exists but it is not of type " + myResourceName + ", found resource of type " + entity.getResourceType());
throw new ResourceNotFoundException("Resource with ID " + entity.getIdDt().getIdPart() + " exists but it is not of type " + myResourceName + ", found resource of type "
+ entity.getResourceType());
}
}
@ -888,7 +899,8 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
if (entity == null) {
if (theId.hasVersionIdPart()) {
TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class);
TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery(
"SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class);
q.setParameter("RID", theId.getIdPartAsLong());
q.setParameter("RTYP", myResourceName);
q.setParameter("RVER", theId.getVersionIdPartAsLong());
@ -993,38 +1005,61 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
// Load _include resources
if (theParams.getIncludes() != null && theParams.getIncludes().isEmpty() == false) {
Set<IdDt> includePids = new HashSet<IdDt>();
FhirTerser t = getContext().newTerser();
for (Include next : theParams.getIncludes()) {
for (IResource nextResource : retVal) {
assert myResourceType.isAssignableFrom(nextResource.getClass());
Set<IdDt> previouslyLoadedPids = new HashSet<IdDt>();
List<Object> values = t.getValues(nextResource, next.getValue());
for (Object object : values) {
if (object == null) {
Set<IdDt> includePids = new HashSet<IdDt>();
List<IResource> resources = retVal;
do {
includePids.clear();
FhirTerser t = getContext().newTerser();
for (Include next : theParams.getIncludes()) {
for (IResource nextResource : resources) {
RuntimeResourceDefinition def = getContext().getResourceDefinition(nextResource);
if (!next.getValue().startsWith(def.getName() + ".")) {
continue;
}
if (!(object instanceof ResourceReferenceDt)) {
throw new InvalidRequestException("Path '" + next.getValue() + "' produced non ResourceReferenceDt value: " + object.getClass());
List<Object> values = t.getValues(nextResource, next.getValue());
for (Object object : values) {
if (object == null) {
continue;
}
if (!(object instanceof ResourceReferenceDt)) {
throw new InvalidRequestException("Path '" + next.getValue() + "' produced non ResourceReferenceDt value: " + object.getClass());
}
ResourceReferenceDt rr = (ResourceReferenceDt) object;
if (rr.getReference().isEmpty()) {
continue;
}
if (rr.getReference().isLocal()) {
continue;
}
IdDt nextId = rr.getReference().toUnqualified();
if (!previouslyLoadedPids.contains(nextId)) {
includePids.add(nextId);
previouslyLoadedPids.add(nextId);
}
}
ResourceReferenceDt rr = (ResourceReferenceDt) object;
if (rr.getReference().isEmpty()) {
continue;
}
if (rr.getReference().isLocal()) {
continue;
}
includePids.add(rr.getReference().toUnqualified());
}
}
}
if (!includePids.isEmpty()) {
ourLog.info("Loading {} included resources", includePids.size());
loadResourcesById(includePids, retVal);
if (!includePids.isEmpty()) {
ourLog.info("Loading {} included resources", includePids.size());
resources = loadResourcesById(includePids);
retVal.addAll(resources);
}
} while (includePids.size() > 0 && previouslyLoadedPids.size() < getConfig().getIncludeLimit());
if (previouslyLoadedPids.size() >= getConfig().getIncludeLimit()) {
OperationOutcome oo = new OperationOutcome();
oo.addIssue().setSeverity(IssueSeverityEnum.WARNING).setDetails("Not all _include resources were actually included as the request surpassed the limit of " + getConfig().getIncludeLimit() + " resources");
retVal.add(0, oo);
}
}
return retVal;
}
});
@ -1181,8 +1216,7 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
}
/**
* If set, the given param will be treated as a secondary primary key, and multiple resources will not be able to
* share the same value.
* If set, the given param will be treated as a secondary primary key, and multiple resources will not be able to share the same value.
*/
public void setSecondaryPrimaryKeyParamName(String theSecondaryPrimaryKeyParamName) {
mySecondaryPrimaryKeyParamName = theSecondaryPrimaryKeyParamName;

View File

@ -138,6 +138,7 @@
<packageBase>ca.uhn.test.jpasrv</packageBase>
<baseResourceNames>
<baseResourceName>device</baseResourceName>
<baseResourceName>encounter</baseResourceName>
<baseResourceName>diagnosticreport</baseResourceName>
<baseResourceName>location</baseResourceName>
<baseResourceName>observation</baseResourceName>

View File

@ -24,6 +24,9 @@
<bean id="myDiagnosticReportDao" class="ca.uhn.fhir.jpa.dao.FhirResourceDao">
<property name="resourceType" value="ca.uhn.fhir.model.dstu.resource.DiagnosticReport"/>
</bean>
<bean id="myLocationDao" class="ca.uhn.fhir.jpa.dao.FhirResourceDao">
<property name="resourceType" value="ca.uhn.fhir.model.dstu.resource.Location"/>
</bean>
<bean id="myPatientDao" class="ca.uhn.fhir.jpa.dao.FhirResourceDao">
<property name="resourceType" value="ca.uhn.fhir.model.dstu.resource.Patient"/>
</bean>
@ -33,12 +36,12 @@
<bean id="myOrganizationDao" class="ca.uhn.fhir.jpa.dao.FhirResourceDao">
<property name="resourceType" value="ca.uhn.fhir.model.dstu.resource.Organization"/>
</bean>
<bean id="myLocationDao" class="ca.uhn.fhir.jpa.dao.FhirResourceDao">
<property name="resourceType" value="ca.uhn.fhir.model.dstu.resource.Location"/>
</bean>
<bean id="myQuestionnaireDao" class="ca.uhn.fhir.jpa.dao.FhirResourceDao">
<property name="resourceType" value="ca.uhn.fhir.model.dstu.resource.Questionnaire"/>
</bean>
<bean id="myEncounterDao" class="ca.uhn.fhir.jpa.dao.FhirResourceDao">
<property name="resourceType" value="ca.uhn.fhir.model.dstu.resource.Encounter"/>
</bean>
<bean id="myPersistenceDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" lazy-init="true">
<property name="url" value="jdbc:derby:memory:myUnitTestDB;create=true" />

View File

@ -1,7 +1,12 @@
package ca.uhn.fhir.jpa.test;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.util.Date;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
@ -17,31 +22,39 @@ import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.provider.JpaSystemProvider;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.dstu.composite.PeriodDt;
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu.resource.Encounter;
import ca.uhn.fhir.model.dstu.resource.Location;
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.model.dstu.resource.Questionnaire;
import ca.uhn.fhir.model.dstu.valueset.EncounterClassEnum;
import ca.uhn.fhir.model.dstu.valueset.EncounterStateEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.test.jpasrv.EncounterResourceProvider;
import ca.uhn.test.jpasrv.LocationResourceProvider;
import ca.uhn.test.jpasrv.ObservationResourceProvider;
import ca.uhn.test.jpasrv.OrganizationResourceProvider;
import ca.uhn.test.jpasrv.PatientResourceProvider;
public class CompleteResourceProviderTest {
private static IFhirResourceDao<Observation> observationDao;
private static ClassPathXmlApplicationContext ourAppCtx;
private static FhirContext ourCtx;
private static IGenericClient ourClient;
private static FhirContext ourCtx;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CompleteResourceProviderTest.class);
private static Server ourServer;
private static IFhirResourceDao<Patient> patientDao;
private static IFhirResourceDao<Questionnaire> questionnaireDao;
private static IGenericClient ourClient;
private static IFhirResourceDao<Observation> observationDao;
// private static JpaConformanceProvider ourConfProvider;
@ -66,23 +79,7 @@ public class CompleteResourceProviderTest {
//
// }
@Test
public void testSearchByIdentifier() {
Patient p1 = new Patient();
p1.addIdentifier().setSystem("urn:system").setValue("testSearchByIdentifier01");
p1.addName().addFamily("testSearchByIdentifierFamily01").addGiven("testSearchByIdentifierGiven01");
IdDt p1Id = ourClient.create(p1).getId();
Patient p2 = new Patient();
p2.addIdentifier().setSystem("urn:system").setValue("testSearchByIdentifier02");
p2.addName().addFamily("testSearchByIdentifierFamily01").addGiven("testSearchByIdentifierGiven02");
ourClient.create(p2).getId();
Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode("urn:system", "testSearchByIdentifier01")).encodedJson().prettyPrint().execute();
assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getId().getIdPart());
}
private static IFhirResourceDao<Questionnaire> questionnaireDao;
@Test
public void testCreateWithId() {
@ -112,52 +109,21 @@ public class CompleteResourceProviderTest {
}
@Test
public void testSearchByIdentifierWithoutSystem() {
Patient p1 = new Patient();
p1.addIdentifier().setValue("testSearchByIdentifierWithoutSystem01");
IdDt p1Id = ourClient.create(p1).getId();
Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode(null, "testSearchByIdentifierWithoutSystem01")).encodedJson().prettyPrint()
.execute();
assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getId().getIdPart());
}
@Test
public void testSearchByResourceChain() {
Organization o1 = new Organization();
o1.setName("testSearchByResourceChainName01");
IdDt o1id = ourClient.create(o1).getId();
public void testInsertBadReference() {
Patient p1 = new Patient();
p1.addIdentifier().setSystem("urn:system").setValue("testSearchByResourceChain01");
p1.addName().addFamily("testSearchByResourceChainFamily01").addGiven("testSearchByResourceChainGiven01");
p1.setManagingOrganization(new ResourceReferenceDt(o1id));
IdDt p1Id = ourClient.create(p1).getId();
p1.setManagingOrganization(new ResourceReferenceDt("Organization/132312323"));
//@formatter:off
Bundle actual = ourClient.search()
.forResource(Patient.class)
.where(Patient.PROVIDER.hasId(o1id.getIdPart()))
.encodedJson().andLogRequestAndResponse(true).prettyPrint().execute();
//@formatter:on
assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getId().getIdPart());
//@formatter:off
actual = ourClient.search()
.forResource(Patient.class)
.where(Patient.PROVIDER.hasId(o1id.getValue()))
.encodedJson().andLogRequestAndResponse(true).prettyPrint().execute();
//@formatter:on
assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getId().getIdPart());
try {
ourClient.create(p1).getId();
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("Organization/132312323"));
}
}
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CompleteResourceProviderTest.class);
@Test
public void testInsertUpdatesConformance() {
// Conformance conf = ourConfProvider.getServerConformance();
@ -197,22 +163,6 @@ public class CompleteResourceProviderTest {
// assertEquals(initial+1, number.getValueAsInteger());
}
@Test
public void testInsertBadReference() {
Patient p1 = new Patient();
p1.addIdentifier().setSystem("urn:system").setValue("testSearchByResourceChain01");
p1.addName().addFamily("testSearchByResourceChainFamily01").addGiven("testSearchByResourceChainGiven01");
p1.setManagingOrganization(new ResourceReferenceDt("Organization/132312323"));
try {
ourClient.create(p1).getId();
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("Organization/132312323"));
}
}
@Test
public void testSaveAndRetrieveExistingNarrative() {
Patient p1 = new Patient();
@ -236,6 +186,108 @@ public class CompleteResourceProviderTest {
assertThat(actual.getText().getDiv().getValueAsString(), containsString("<td>Identifier</td><td>testSearchByResourceChain01</td>"));
}
@Test
public void testSearchByIdentifier() {
Patient p1 = new Patient();
p1.addIdentifier().setSystem("urn:system").setValue("testSearchByIdentifier01");
p1.addName().addFamily("testSearchByIdentifierFamily01").addGiven("testSearchByIdentifierGiven01");
IdDt p1Id = ourClient.create(p1).getId();
Patient p2 = new Patient();
p2.addIdentifier().setSystem("urn:system").setValue("testSearchByIdentifier02");
p2.addName().addFamily("testSearchByIdentifierFamily01").addGiven("testSearchByIdentifierGiven02");
ourClient.create(p2).getId();
Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode("urn:system", "testSearchByIdentifier01")).encodedJson().prettyPrint().execute();
assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getId().getIdPart());
}
@Test
public void testSearchByIdentifierWithoutSystem() {
Patient p1 = new Patient();
p1.addIdentifier().setValue("testSearchByIdentifierWithoutSystem01");
IdDt p1Id = ourClient.create(p1).getId();
Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode(null, "testSearchByIdentifierWithoutSystem01")).encodedJson().prettyPrint()
.execute();
assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getId().getIdPart());
}
@Test
public void testDeepChaining() {
// ourClient = ourCtx.newRestfulGenericClient("http://fhir.healthintersections.com.au/open");
// ourClient = ourCtx.newRestfulGenericClient("https://fhir.orionhealth.com/blaze/fhir");
// ourClient = ourCtx.newRestfulGenericClient("http://spark.furore.com/fhir");
// ourClient.registerInterceptor(new LoggingInterceptor(true));
Location l1 = new Location();
l1.getName().setValue("testDeepChainingL1");
IdDt l1id = ourClient.create().resource(l1).execute().getId();
Location l2 = new Location();
l2.getName().setValue("testDeepChainingL2");
l2.getPartOf().setReference(l1id.toVersionless().toUnqualified());
IdDt l2id = ourClient.create().resource(l2).execute().getId();
Encounter e1 = new Encounter();
e1.addIdentifier().setSystem("urn:foo").setValue("testDeepChainingE1");
e1.getStatus().setValueAsEnum(EncounterStateEnum.IN_PROGRESS);
e1.getClassElement().setValueAsEnum(EncounterClassEnum.HOME);
ca.uhn.fhir.model.dstu.resource.Encounter.Location location = e1.addLocation();
location.getLocation().setReference(l2id.toUnqualifiedVersionless());
location.setPeriod(new PeriodDt().setStartWithSecondsPrecision(new Date()).setEndWithSecondsPrecision(new Date()));
IdDt e1id = ourClient.create().resource(e1).execute().getId();
//@formatter:off
Bundle res = ourClient.search()
.forResource(Encounter.class)
.where(Encounter.IDENTIFIER.exactly().systemAndCode("urn:foo", "testDeepChainingE1"))
.include(Encounter.INCLUDE_LOCATION_LOCATION)
.include(Location.INCLUDE_PARTOF)
.execute();
//@formatter:on
assertEquals(3, res.size());
assertEquals(1, res.getResources(Encounter.class).size());
assertEquals(e1id.toUnqualifiedVersionless(), res.getResources(Encounter.class).get(0).getId().toUnqualifiedVersionless());
}
@Test
public void testSearchByResourceChain() {
Organization o1 = new Organization();
o1.setName("testSearchByResourceChainName01");
IdDt o1id = ourClient.create(o1).getId();
Patient p1 = new Patient();
p1.addIdentifier().setSystem("urn:system").setValue("testSearchByResourceChain01");
p1.addName().addFamily("testSearchByResourceChainFamily01").addGiven("testSearchByResourceChainGiven01");
p1.setManagingOrganization(new ResourceReferenceDt(o1id));
IdDt p1Id = ourClient.create(p1).getId();
//@formatter:off
Bundle actual = ourClient.search()
.forResource(Patient.class)
.where(Patient.PROVIDER.hasId(o1id.getIdPart()))
.encodedJson().andLogRequestAndResponse(true).prettyPrint().execute();
//@formatter:on
assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getId().getIdPart());
//@formatter:off
actual = ourClient.search()
.forResource(Patient.class)
.where(Patient.PROVIDER.hasId(o1id.getValue()))
.encodedJson().andLogRequestAndResponse(true).prettyPrint().execute();
//@formatter:on
assertEquals(1, actual.size());
assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getId().getIdPart());
}
@AfterClass
public static void afterClass() throws Exception {
ourServer.stop();
@ -259,12 +311,21 @@ public class CompleteResourceProviderTest {
ObservationResourceProvider observationRp = new ObservationResourceProvider();
observationRp.setDao(observationDao);
IFhirResourceDao<Location> locationDao = (IFhirResourceDao<Location>) ourAppCtx.getBean("myLocationDao", IFhirResourceDao.class);
LocationResourceProvider locationRp = new LocationResourceProvider();
locationRp.setDao(locationDao);
IFhirResourceDao<Encounter> encounterDao = (IFhirResourceDao<Encounter>) ourAppCtx.getBean("myEncounterDao", IFhirResourceDao.class);
EncounterResourceProvider encounterRp = new EncounterResourceProvider();
encounterRp.setDao(encounterDao);
IFhirResourceDao<Organization> organizationDao = (IFhirResourceDao<Organization>) ourAppCtx.getBean("myOrganizationDao", IFhirResourceDao.class);
OrganizationResourceProvider organizationRp = new OrganizationResourceProvider();
organizationRp.setDao(organizationDao);
RestfulServer restServer = new RestfulServer();
restServer.setResourceProviders(patientRp, questionnaireRp, observationRp, organizationRp);
restServer.setResourceProviders(encounterRp, locationRp, patientRp, questionnaireRp, observationRp, organizationRp);
restServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
IFhirSystemDao systemDao = (IFhirSystemDao) ourAppCtx.getBean("mySystemDao", IFhirSystemDao.class);
@ -292,7 +353,7 @@ public class CompleteResourceProviderTest {
ourCtx = restServer.getFhirContext();
ourClient = ourCtx.newRestfulGenericClient(serverBase);
ourClient.setLogRequestAndResponse(true);
ourClient.registerInterceptor(new LoggingInterceptor(true));
}
}

View File

@ -63,6 +63,7 @@ public class ${className}ResourceProvider extends JpaResourceProvider<${classNam
"${include.path}" #{if}($foreach.hasNext || $haveMore), #{end}
#end
#end
#{if}($searchParamsReference.empty == false), #{end}"*"
})
Set<Include> theIncludes
) {