Merge branch 'client_enhancements'

This commit is contained in:
James Agnew 2018-07-30 09:42:38 -04:00
commit d6293cf9b3
67 changed files with 4864 additions and 3004 deletions

View File

@ -19,11 +19,14 @@ cache:
install: /bin/true
# This seems to be required to get travis to set Xmx4g, per https://github.com/travis-ci/travis-ci/issues/3893
before_script:
# This seems to be required to get travis to set Xmx4g, per https://github.com/travis-ci/travis-ci/issues/3893
- export MAVEN_SKIP_RC=true
# Sometimes things get restored from the cache with bad permissions. See https://github.com/travis-ci/travis-ci/issues/9630
- sudo chmod -R 777 "$HOME/.m2/repository";
- sudo chown -R travis:travis "$HOME/.m2/repository";
script:
# - mvn -e -B clean install && cd hapi-fhir-ra && mvn -e -B -DTRAVIS_JOB_ID=$TRAVIS_JOB_ID clean test jacoco:report coveralls:report
# - mvn -Dci=true -e -B -P ALLMODULES,NOPARALLEL,ERRORPRONE clean install && cd hapi-fhir-jacoco && mvn -e -B -DTRAVIS_JOB_ID=$TRAVIS_JOB_ID jacoco:report coveralls:report
- mvn -Dci=true -e -B -P ALLMODULES,REDUCED_JPA_TESTS,ERRORPRONE,JACOCO clean install && cd hapi-fhir-jacoco && mvn -e -B -DTRAVIS_JOB_ID=$TRAVIS_JOB_ID jacoco:report coveralls:report
- mvn -Dci=true -e -B -P ALLMODULES,REDUCED_JPA_TESTS,ERRORPRONE,JACOCO clean install && cd hapi-fhir-jacoco && mvn -e -B -DTRAVIS_JOB_ID=$TRAVIS_JOB_ID jacoco:report coveralls:report;

View File

@ -130,7 +130,7 @@ public class Constants {
/**
* Used in paging links
*/
public static final Object PARAM_BUNDLETYPE = "_bundletype";
public static final String PARAM_BUNDLETYPE = "_bundletype";
public static final String PARAM_CONTENT = "_content";
public static final String PARAM_COUNT = "_count";
public static final String PARAM_DELETE = "_delete";
@ -140,7 +140,7 @@ public class Constants {
public static final String PARAM_HISTORY = "_history";
public static final String PARAM_INCLUDE = "_include";
public static final String PARAM_INCLUDE_QUALIFIER_RECURSE = ":recurse";
public static final String PARAM_INCLUDE_RECURSE = "_include"+PARAM_INCLUDE_QUALIFIER_RECURSE;
public static final String PARAM_INCLUDE_RECURSE = "_include" + PARAM_INCLUDE_QUALIFIER_RECURSE;
public static final String PARAM_LASTUPDATED = "_lastUpdated";
public static final String PARAM_NARRATIVE = "_narrative";
public static final String PARAM_PAGINGACTION = "_getpages";
@ -152,7 +152,7 @@ public class Constants {
public static final String PARAM_QUERY = "_query";
public static final String PARAM_RESPONSE_URL = "response-url"; //Used in messaging
public static final String PARAM_REVINCLUDE = "_revinclude";
public static final String PARAM_REVINCLUDE_RECURSE = PARAM_REVINCLUDE+PARAM_INCLUDE_QUALIFIER_RECURSE;
public static final String PARAM_REVINCLUDE_RECURSE = PARAM_REVINCLUDE + PARAM_INCLUDE_QUALIFIER_RECURSE;
public static final String PARAM_SEARCH = "_search";
public static final String PARAM_SECURITY = "_security";
public static final String PARAM_SINCE = "_since";
@ -160,9 +160,9 @@ public class Constants {
public static final String PARAM_SORT_ASC = "_sort:asc";
public static final String PARAM_SORT_DESC = "_sort:desc";
public static final String PARAM_SUMMARY = "_summary";
public static final String PARAM_TAG = "_tag";
public static final String PARAM_TAGS = "_tags";
public static final String PARAM_TEXT = "_text";
public static final String PARAM_TAG = "_tag";
public static final String PARAM_TAGS = "_tags";
public static final String PARAM_TEXT = "_text";
public static final String PARAM_VALIDATE = "_validate";
public static final String PARAMQUALIFIER_MISSING = ":missing";
public static final String PARAMQUALIFIER_MISSING_FALSE = "false";
@ -177,7 +177,7 @@ public class Constants {
public static final int STATUS_HTTP_400_BAD_REQUEST = 400;
public static final int STATUS_HTTP_401_CLIENT_UNAUTHORIZED = 401;
public static final int STATUS_HTTP_403_FORBIDDEN = 403;
public static final int STATUS_HTTP_404_NOT_FOUND = 404;
public static final int STATUS_HTTP_405_METHOD_NOT_ALLOWED = 405;
public static final int STATUS_HTTP_409_CONFLICT = 409;
@ -195,9 +195,12 @@ public class Constants {
public static final String HEADER_X_CACHE = "X-Cache";
public static final String HEADER_X_SECURITY_CONTEXT = "X-Security-Context";
public static final String POWERED_BY_HEADER = "X-Powered-By";
public static final Charset CHARSET_US_ASCII;
public static final String PARAM_PAGEID = "_pageId";
static {
CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8);
CHARSET_US_ASCII = Charset.forName("ISO-8859-1");
HashMap<Integer, String> statusNames = new HashMap<>();
statusNames.put(200, "OK");
@ -263,7 +266,7 @@ public class Constants {
statusNames.put(510, "Not Extended");
statusNames.put(511, "Network Authentication Required");
HTTP_STATUS_NAMES = Collections.unmodifiableMap(statusNames);
Set<String> formatsHtml = new HashSet<>();
formatsHtml.add(CT_HTML);
formatsHtml.add(FORMAT_HTML);

View File

@ -34,36 +34,35 @@ public interface IHttpRequest {
* @param theName the header name
* @param theValue the header value
*/
public void addHeader(String theName, String theValue);
void addHeader(String theName, String theValue);
/**
* Execute the request
* @return the response
* @throws IOException
*/
public IHttpResponse execute() throws IOException;
IHttpResponse execute() throws IOException;
/**
* @return all request headers in lower case
* @return all request headers in lower case. Note that this method
* returns an <b>immutable</b> Map
*/
public Map<String, List<String>> getAllHeaders();
Map<String, List<String>> getAllHeaders();
/**
* Return the requestbody as a string.
* Return the request body as a string.
* If this is not supported by the underlying technology, null is returned
* @return a string representation of the request or null if not supported or empty.
* @throws IOException
*/
public String getRequestBodyFromStream() throws IOException;
String getRequestBodyFromStream() throws IOException;
/**
* Return the request URI, or null
*/
public String getUri();
String getUri();
/**
* Return the HTTP verb (e.g. "GET")
*/
public String getHttpVerbName();
String getHttpVerbName();
}

View File

@ -1,9 +1,10 @@
package ca.uhn.fhir.rest.gclient;
import java.util.*;
import ca.uhn.fhir.model.api.IQueryParameterType;
import java.util.List;
import java.util.Map;
/*
* #%L
* HAPI FHIR - Core Library
@ -26,10 +27,32 @@ import ca.uhn.fhir.model.api.IQueryParameterType;
public interface IBaseQuery<T extends IBaseQuery<?>> {
T where(ICriterion<?> theCriterion);
/**
* Add a search parameter to the query.
* <p>
* Note that this method is a synonym for {@link #where(ICriterion)}, and is only
* here to make fluent queries read more naturally.
* </p>
*/
T and(ICriterion<?> theCriterion);
T where(Map<String, List<IQueryParameterType>> theCriterion);
/**
* Add a set of search parameters to the query.
*/
T where(Map<String, List<IQueryParameterType>> theCriterion);
T and(ICriterion<?> theCriterion);
/**
* Add a search parameter to the query.
*/
T where(ICriterion<?> theCriterion);
/**
* Add a set of search parameters to the query.
* <p>
* Values will be treated semi-literally. No FHIR escaping will be performed
* on the values, but regular URL escaping will be.
* </p>
*/
T whereMap(Map<String, List<String>> theRawMap);
}

View File

@ -20,16 +20,23 @@ package ca.uhn.fhir.rest.gclient;
* #L%
*/
import java.util.Date;
import ca.uhn.fhir.rest.param.DateRangeParam;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
public interface IHistoryTyped<T> extends IClientExecutable<IHistoryTyped<T>, T> {
import java.util.Date;
public interface IHistoryTyped<T> extends IClientExecutable<IHistoryTyped<T>, T> {
/**
* Request that the server return only resource versions that were created at or after the given time (inclusive)
* Request that the server return only the history elements between the
* specific range
*/
IHistoryTyped<T> since(Date theCutoff);
IHistoryTyped<T> at(DateRangeParam theDateRangeParam);
/**
* Request that the server return only up to <code>theCount</code> number of resources
*/
IHistoryTyped<T> count(Integer theCount);
/**
* Request that the server return only resource versions that were created at or after the given time (inclusive)
@ -41,9 +48,9 @@ public interface IHistoryTyped<T> extends IClientExecutable<IHistoryTyped<T>, T>
IHistoryTyped<T> since(IPrimitiveType<Date> theCutoff);
/**
* Request that the server return only up to <code>theCount</code> number of resources
* Request that the server return only resource versions that were created at or after the given time (inclusive)
*/
IHistoryTyped<T> count(Integer theCount);
IHistoryTyped<T> since(Date theCutoff);
}

View File

@ -1,6 +1,14 @@
package ca.uhn.fhir.rest.gclient;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.SearchStyleEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.param.DateRangeParam;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/*
* #%L
@ -22,14 +30,23 @@ import java.util.Collection;
* #L%
*/
import org.hl7.fhir.instance.model.api.IBaseBundle;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.SearchStyleEnum;
import ca.uhn.fhir.rest.param.DateRangeParam;
public interface IQuery<Y> extends IBaseQuery<IQuery<Y>>, IClientExecutable<IQuery<Y>, Y> {
/**
* {@inheritDoc}
*/
// This is here as an overridden method to allow mocking clients with Mockito to work
@Override
IQuery<Y> and(ICriterion<?> theCriterion);
/**
* Specifies the <code>_count</code> parameter, which indicates to the server how many resources should be returned
* on a single page.
*
* @since 1.4
*/
IQuery<Y> count(int theCount);
/**
* Add an "_include" specification or an "_include:recurse" specification. If you are using
* a constant from one of the built-in structures you can select whether you want recursive
@ -41,88 +58,60 @@ public interface IQuery<Y> extends IBaseQuery<IQuery<Y>>, IClientExecutable<IQue
*/
IQuery<Y> include(Include theInclude);
ISort<Y> sort();
/**
* Add a "_lastUpdated" specification
*
* @since HAPI FHIR 1.1 - Note that option was added to FHIR itself in DSTU2
*/
IQuery<Y> lastUpdated(DateRangeParam theLastUpdated);
/**
* Specifies the <code>_count</code> parameter, which indicates to the server how many resources should be returned
* on a single page.
*
*
* @deprecated This parameter is badly named, since FHIR calls this parameter "_count" and not "_limit". Use {@link #count(int)} instead (it also sets the _count parameter)
*/
@Deprecated
IQuery<Y> limitTo(int theLimitTo);
/**
* Specifies the <code>_count</code> parameter, which indicates to the server how many resources should be returned
* on a single page.
*
* @since 1.4
* Request that the client return the specified bundle type, e.g. <code>org.hl7.fhir.instance.model.Bundle.class</code>
* or <code>ca.uhn.fhir.model.dstu2.resource.Bundle.class</code>
*/
IQuery<Y> count(int theCount);
/**
* Match only resources where the resource has the given tag. This parameter corresponds to
* the <code>_tag</code> URL parameter.
* @param theSystem The tag code system, or <code>null</code> to match any code system (this may not be supported on all servers)
* @param theCode The tag code. Must not be <code>null</code> or empty.
*/
IQuery<Y> withTag(String theSystem, String theCode);
/**
* Match only resources where the resource has the given security tag. This parameter corresponds to
* the <code>_security</code> URL parameter.
* @param theSystem The tag code system, or <code>null</code> to match any code system (this may not be supported on all servers)
* @param theCode The tag code. Must not be <code>null</code> or empty.
*/
IQuery<Y> withSecurity(String theSystem, String theCode);
/**
* Match only resources where the resource has the given profile declaration. This parameter corresponds to
* the <code>_profile</code> URL parameter.
* @param theProfileUri The URI of a given profile to search for resources which match
*/
IQuery<Y> withProfile(String theProfileUri);
/**
* Matches any of the profiles given as argument. This would result in an OR search for resources matching one or more profiles.
* To do an AND search, make multiple calls to {@link #withProfile(String)}.
* @param theProfileUris The URIs of a given profile to search for resources which match.
*/
IQuery<Y> withAnyProfile(Collection<String> theProfileUris);
/**
* Forces the query to perform the search using the given method (allowable methods are described in the
* <a href="http://www.hl7.org/fhir/search.html">FHIR Search Specification</a>)
* <p>
* This can be used to force the use of an HTTP POST instead of an HTTP GET
* </p>
*
* @see SearchStyleEnum
* @since 0.6
*/
IQuery<Y> usingStyle(SearchStyleEnum theStyle);
IQuery<Y> withIdAndCompartment(String theResourceId, String theCompartmentName);
<B extends IBaseBundle> IQuery<B> returnBundle(Class<B> theClass);
/**
* Add a "_revinclude" specification
*
*
* @since HAPI FHIR 1.0 - Note that option was added to FHIR itself in DSTU2
*/
IQuery<Y> revInclude(Include theIncludeTarget);
/**
* Add a "_lastUpdated" specification
*
* @since HAPI FHIR 1.1 - Note that option was added to FHIR itself in DSTU2
* Adds a sort criteria
*
* @see #sort(SortSpec) for an alternate way of speciyfing sorts
*/
IQuery<Y> lastUpdated(DateRangeParam theLastUpdated);
ISort<Y> sort();
/**
* Request that the client return the specified bundle type, e.g. <code>org.hl7.fhir.instance.model.Bundle.class</code>
* or <code>ca.uhn.fhir.model.dstu2.resource.Bundle.class</code>
* Adds a sort using a {@link SortSpec} object
*
* @see #sort() for an alternate way of speciyfing sorts
*/
<B extends IBaseBundle> IQuery<B> returnBundle(Class<B> theClass);
IQuery<Y> sort(SortSpec theSortSpec);
/**
* Forces the query to perform the search using the given method (allowable methods are described in the
* <a href="http://www.hl7.org/fhir/search.html">FHIR Search Specification</a>)
* <p>
* This can be used to force the use of an HTTP POST instead of an HTTP GET
* </p>
*
* @see SearchStyleEnum
* @since 0.6
*/
IQuery<Y> usingStyle(SearchStyleEnum theStyle);
/**
* {@inheritDoc}
@ -132,11 +121,40 @@ public interface IQuery<Y> extends IBaseQuery<IQuery<Y>>, IClientExecutable<IQue
IQuery<Y> where(ICriterion<?> theCriterion);
/**
* {@inheritDoc}
* Matches any of the profiles given as argument. This would result in an OR search for resources matching one or more profiles.
* To do an AND search, make multiple calls to {@link #withProfile(String)}.
*
* @param theProfileUris The URIs of a given profile to search for resources which match.
*/
// This is here as an overridden method to allow mocking clients with Mockito to work
@Override
IQuery<Y> and(ICriterion<?> theCriterion);
IQuery<Y> withAnyProfile(Collection<String> theProfileUris);
IQuery<Y> withIdAndCompartment(String theResourceId, String theCompartmentName);
/**
* Match only resources where the resource has the given profile declaration. This parameter corresponds to
* the <code>_profile</code> URL parameter.
*
* @param theProfileUri The URI of a given profile to search for resources which match
*/
IQuery<Y> withProfile(String theProfileUri);
/**
* Match only resources where the resource has the given security tag. This parameter corresponds to
* the <code>_security</code> URL parameter.
*
* @param theSystem The tag code system, or <code>null</code> to match any code system (this may not be supported on all servers)
* @param theCode The tag code. Must not be <code>null</code> or empty.
*/
IQuery<Y> withSecurity(String theSystem, String theCode);
/**
* Match only resources where the resource has the given tag. This parameter corresponds to
* the <code>_tag</code> URL parameter.
*
* @param theSystem The tag code system, or <code>null</code> to match any code system (this may not be supported on all servers)
* @param theCode The tag code. Must not be <code>null</code> or empty.
*/
IQuery<Y> withTag(String theSystem, String theCode);
// Y execute();

View File

@ -23,13 +23,13 @@ package ca.uhn.fhir.rest.gclient;
public interface ISort<T> {
/**
* Sort ascending
* Sort ascending
*/
IQuery<T> ascending(IParam theParam);
/**
* Sort ascending
*
* Sort ascending
*
* @param theParam The param name, e.g. "address"
*/
IQuery<T> ascending(String theParam);
@ -37,22 +37,30 @@ public interface ISort<T> {
/**
* Sort by the default order. Note that as of STU3, there is no longer
* a concept of default order, only ascending and descending. This method
* technically implies "ascending" but it makes more sense to use
* technically implies "ascending" but it makes more sense to use
* {@link #ascending(IParam)}
*/
IQuery<T> defaultOrder(IParam theParam);
/**
* Sort by the default order. Note that as of STU3, there is no longer
* a concept of default order, only ascending and descending. This method
* technically implies "ascending" but it makes more sense to use
* {@link #ascending(IParam)}
*/
IQuery<T> defaultOrder(String theParam);
/**
* Sort descending
*
* @param theParam A query param - Could be a constant such as <code>Patient.ADDRESS</code> or a custom
* param such as <code>new StringClientParam("foo")</code>
*
* @param theParam A query param - Could be a constant such as <code>Patient.ADDRESS</code> or a custom
* param such as <code>new StringClientParam("foo")</code>
*/
IQuery<T> descending(IParam theParam);
/**
* Sort ascending
*
* Sort ascending
*
* @param theParam The param name, e.g. "address"
*/
IQuery<T> descending(String theParam);

View File

@ -1,8 +1,18 @@
package ca.uhn.fhir.rest.param;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.EQUAL;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.*;
import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -25,20 +35,11 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* limitations under the License.
* #L%
*/
import java.util.*;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class DateRangeParam implements IQueryParameterAnd<DateParam> {
private static final long serialVersionUID = 1L;
private DateParam myLowerBound;
private DateParam myUpperBound;
@ -52,15 +53,13 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
/**
* Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public DateRangeParam(Date theLowerBound, Date theUpperBound) {
this();
@ -84,37 +83,35 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
} else {
switch (theDateParam.getPrefix()) {
case EQUAL:
setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
break;
case STARTS_AFTER:
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
validateAndSet(theDateParam, null);
break;
case ENDS_BEFORE:
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
validateAndSet(null, theDateParam);
break;
default:
// Should not happen
throw new InvalidRequestException("Invalid comparator for date range parameter:" + theDateParam.getPrefix() + ". This is a bug.");
case EQUAL:
setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
break;
case STARTS_AFTER:
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
validateAndSet(theDateParam, null);
break;
case ENDS_BEFORE:
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
validateAndSet(null, theDateParam);
break;
default:
// Should not happen
throw new InvalidRequestException("Invalid comparator for date range parameter:" + theDateParam.getPrefix() + ". This is a bug.");
}
}
}
/**
* Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public DateRangeParam(DateParam theLowerBound, DateParam theUpperBound) {
this();
@ -123,15 +120,13 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
/**
* Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public DateRangeParam(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
this();
@ -140,15 +135,13 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
/**
* Constructor which takes two strings representing the lower and upper bounds of the range (inclusive on both ends)
*
* @param theLowerBound
* An unqualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
* one may be null, but it is not valid for both to be null.
* @param theUpperBound
* An unqualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
* one may be null, but it is not valid for both to be null.
*
* @param theLowerBound An unqualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
* one may be null, but it is not valid for both to be null.
* @param theUpperBound An unqualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
* one may be null, but it is not valid for both to be null.
*/
public DateRangeParam(String theLowerBound, String theUpperBound) {
this();
@ -168,35 +161,53 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
myLowerBound = new DateParam(EQUAL, theParsed.getValueAsString());
myUpperBound = new DateParam(EQUAL, theParsed.getValueAsString());
}
} else {
switch (theParsed.getPrefix()) {
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
if (myLowerBound != null) {
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify a lower bound");
}
myLowerBound = theParsed;
break;
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
if (myUpperBound != null) {
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify an upper bound");
}
myUpperBound = theParsed;
break;
default:
throw new InvalidRequestException("Unknown comparator: " + theParsed.getPrefix());
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
if (myLowerBound != null) {
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify a lower bound");
}
myLowerBound = theParsed;
break;
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
if (myUpperBound != null) {
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify an upper bound");
}
myUpperBound = theParsed;
break;
default:
throw new InvalidRequestException("Unknown comparator: " + theParsed.getPrefix());
}
}
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof DateRangeParam)) {
return false;
}
DateRangeParam other = (DateRangeParam) obj;
return Objects.equals(myLowerBound, other.myLowerBound) &&
Objects.equals(myUpperBound, other.myUpperBound);
}
public DateParam getLowerBound() {
return myLowerBound;
}
public DateRangeParam setLowerBound(DateParam theLowerBound) {
validateAndSet(theLowerBound, myUpperBound);
return this;
}
public Date getLowerBoundAsInstant() {
if (myLowerBound == null) {
return null;
@ -204,19 +215,19 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
Date retVal = myLowerBound.getValue();
if (myLowerBound.getPrefix() != null) {
switch (myLowerBound.getPrefix()) {
case GREATERTHAN:
case STARTS_AFTER:
retVal = myLowerBound.getPrecision().add(retVal, 1);
break;
case EQUAL:
case GREATERTHAN_OR_EQUALS:
break;
case LESSTHAN:
case APPROXIMATE:
case LESSTHAN_OR_EQUALS:
case ENDS_BEFORE:
case NOT_EQUAL:
throw new IllegalStateException("Unvalid lower bound comparator: " + myLowerBound.getPrefix());
case GREATERTHAN:
case STARTS_AFTER:
retVal = myLowerBound.getPrecision().add(retVal, 1);
break;
case EQUAL:
case GREATERTHAN_OR_EQUALS:
break;
case LESSTHAN:
case APPROXIMATE:
case LESSTHAN_OR_EQUALS:
case ENDS_BEFORE:
case NOT_EQUAL:
throw new IllegalStateException("Unvalid lower bound comparator: " + myLowerBound.getPrefix());
}
}
return retVal;
@ -226,6 +237,11 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
return myUpperBound;
}
public DateRangeParam setUpperBound(DateParam theUpperBound) {
validateAndSet(myLowerBound, theUpperBound);
return this;
}
public Date getUpperBoundAsInstant() {
if (myUpperBound == null) {
return null;
@ -233,21 +249,21 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
Date retVal = myUpperBound.getValue();
if (myUpperBound.getPrefix() != null) {
switch (myUpperBound.getPrefix()) {
case LESSTHAN:
case ENDS_BEFORE:
retVal = new Date(retVal.getTime() - 1L);
break;
case EQUAL:
case LESSTHAN_OR_EQUALS:
retVal = myUpperBound.getPrecision().add(retVal, 1);
retVal = new Date(retVal.getTime() - 1L);
break;
case GREATERTHAN_OR_EQUALS:
case GREATERTHAN:
case APPROXIMATE:
case NOT_EQUAL:
case STARTS_AFTER:
throw new IllegalStateException("Unvalid upper bound comparator: " + myUpperBound.getPrefix());
case LESSTHAN:
case ENDS_BEFORE:
retVal = new Date(retVal.getTime() - 1L);
break;
case EQUAL:
case LESSTHAN_OR_EQUALS:
retVal = myUpperBound.getPrecision().add(retVal, 1);
retVal = new Date(retVal.getTime() - 1L);
break;
case GREATERTHAN_OR_EQUALS:
case GREATERTHAN:
case APPROXIMATE:
case NOT_EQUAL:
case STARTS_AFTER:
throw new IllegalStateException("Unvalid upper bound comparator: " + myUpperBound.getPrefix());
}
}
return retVal;
@ -273,46 +289,55 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
return bound != null && !bound.isEmpty();
}
@Override
public int hashCode() {
return Objects.hash(myLowerBound, myUpperBound);
}
public boolean isEmpty() {
return (getLowerBoundAsInstant() == null) && (getUpperBoundAsInstant() == null);
}
public DateRangeParam setLowerBound(DateParam theLowerBound) {
validateAndSet(theLowerBound, myUpperBound);
/**
* Sets the lower bound using a string that is compliant with
* FHIR dateTime format (ISO-8601).
* <p>
* This lower bound is assumed to have a <code>ge</code>
* (greater than or equals) modifier.
* </p>
*/
public DateRangeParam setLowerBound(String theLowerBound) {
setLowerBound(new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound));
return this;
}
/**
* Sets the range from a pair of dates, inclusive on both ends
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public void setRangeFromDatesInclusive(Date theLowerBound, Date theUpperBound) {
DateParam lowerBound = theLowerBound != null
? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null;
? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null;
DateParam upperBound = theUpperBound != null
? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null;
? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null;
validateAndSet(lowerBound, upperBound);
}
/**
* Sets the range from a pair of dates, inclusive on both ends
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public void setRangeFromDatesInclusive(DateParam theLowerBound, DateParam theUpperBound) {
validateAndSet(theLowerBound, theUpperBound);
@ -322,15 +347,13 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
* Sets the range from a pair of dates, inclusive on both ends. Note that if
* theLowerBound is after theUpperBound, thie method will automatically reverse
* the order of the arguments in order to create an inclusive range.
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public void setRangeFromDatesInclusive(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
IPrimitiveType<Date> lowerBound = theLowerBound;
@ -349,23 +372,21 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
/**
* Sets the range from a pair of dates, inclusive on both ends
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public void setRangeFromDatesInclusive(String theLowerBound, String theUpperBound) {
DateParam lowerBound = theLowerBound != null
? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound)
: null;
? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound)
: null;
DateParam upperBound = theUpperBound != null
? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound)
: null;
? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound)
: null;
if (isNotBlank(theLowerBound) && isNotBlank(theUpperBound) && theLowerBound.equals(theUpperBound)) {
lowerBound.setPrefix(EQUAL);
upperBound.setPrefix(EQUAL);
@ -373,14 +394,22 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
validateAndSet(lowerBound, upperBound);
}
public DateRangeParam setUpperBound(DateParam theUpperBound) {
validateAndSet(myLowerBound, theUpperBound);
/**
* Sets the upper bound using a string that is compliant with
* FHIR dateTime format (ISO-8601).
* <p>
* This upper bound is assumed to have a <code>le</code>
* (less than or equals) modifier.
* </p>
*/
public DateRangeParam setUpperBound(String theUpperBound) {
setUpperBound(new DateParam(LESSTHAN_OR_EQUALS, theUpperBound));
return this;
}
@Override
public void setValuesAsQueryTokens(FhirContext theContext, String theParamName, List<QualifiedParamList> theParameters)
throws InvalidRequestException {
throws InvalidRequestException {
boolean haveHadUnqualifiedParameter = false;
for (QualifiedParamList paramList : theParameters) {
@ -391,13 +420,13 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
throw new InvalidRequestException("DateRange parameter does not suppport OR queries");
}
String param = paramList.get(0);
/*
* Since ' ' is escaped as '+' we'll be nice to anyone might have accidentally not
* escaped theirs
*/
param = param.replace(' ', '+');
DateParam parsed = new DateParam();
parsed.setValueAsQueryToken(theContext, theParamName, paramList.getQualifier(), param);
addParam(parsed);
@ -413,24 +442,6 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof DateRangeParam)) {
return false;
}
DateRangeParam other = (DateRangeParam) obj;
return Objects.equals(myLowerBound, other.myLowerBound) &&
Objects.equals(myUpperBound, other.myUpperBound);
}
@Override
public int hashCode() {
return Objects.hash(myLowerBound, myUpperBound);
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
@ -463,8 +474,8 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
if (hasBound(lowerBound) && hasBound(upperBound)) {
if (lowerBound.getValue().getTime() > upperBound.getValue().getTime()) {
throw new DataFormatException(format(
"Lower bound of %s is after upper bound of %s",
lowerBound.getValueAsString(), upperBound.getValueAsString()));
"Lower bound of %s is after upper bound of %s",
lowerBound.getValueAsString(), upperBound.getValueAsString()));
}
}
@ -473,13 +484,13 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
lowerBound.setPrefix(GREATERTHAN_OR_EQUALS);
}
switch (lowerBound.getPrefix()) {
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
default:
break;
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
throw new DataFormatException("Lower bound comparator must be > or >=, can not be " + lowerBound.getPrefix().getValue());
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
default:
break;
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
throw new DataFormatException("Lower bound comparator must be > or >=, can not be " + lowerBound.getPrefix().getValue());
}
}
@ -488,13 +499,13 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
upperBound.setPrefix(LESSTHAN_OR_EQUALS);
}
switch (upperBound.getPrefix()) {
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
default:
break;
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
throw new DataFormatException("Upper bound comparator must be < or <=, can not be " + upperBound.getPrefix().getValue());
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
default:
break;
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
throw new DataFormatException("Upper bound comparator must be < or <=, can not be " + upperBound.getPrefix().getValue());
}
}

View File

@ -20,44 +20,42 @@ package ca.uhn.fhir.rest.server.exceptions;
* #L%
*/
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.util.CoverageIgnore;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
/**
* Represents an <b>HTTP 422 Unprocessable Entity</b> response, which means that a resource was rejected by the server because it "violated applicable FHIR profiles or server business rules".
*
*
* <p>
* This exception will generally contain an {@link IBaseOperationOutcome} instance which details the failure.
* </p>
*
*
* @see InvalidRequestException Which corresponds to an <b>HTTP 400 Bad Request</b> failure
*/
@CoverageIgnore
public class UnprocessableEntityException extends BaseServerResponseException {
public static final int STATUS_CODE = Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY;
private static final String DEFAULT_MESSAGE = "Unprocessable Entity";
private static final long serialVersionUID = 1L;
public static final int STATUS_CODE = Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY;
/**
* Constructor
*
* @param theMessage
* The message to add to the status line
* @param theOperationOutcome The {@link IBaseOperationOutcome} resource to return to the client
*
* @param theMessage The message to add to the status line
* @param theOperationOutcome The {@link IBaseOperationOutcome} resource to return to the client
*/
public UnprocessableEntityException(String theMessage, IBaseOperationOutcome theOperationOutcome) {
super(STATUS_CODE, theMessage, theOperationOutcome);
}
/**
* Constructor which accepts an {@link IBaseOperationOutcome} resource which will be supplied in the response
*
*
* @deprecated Use constructor with FhirContext argument
*/
@Deprecated
@ -79,6 +77,13 @@ public class UnprocessableEntityException extends BaseServerResponseException {
super(STATUS_CODE, theMessage);
}
/**
* Constructor which accepts a String describing the issue. This string will be translated into an {@link IBaseOperationOutcome} resource which will be supplied in the response.
*/
public UnprocessableEntityException(String theMessage, Throwable theCause) {
super(STATUS_CODE, theMessage, theCause);
}
/**
* Constructor which accepts an array of Strings describing the issue. This strings will be translated into an {@link IBaseOperationOutcome} resource which will be supplied in the response.
*/

View File

@ -86,7 +86,7 @@ public class ReflectionUtil {
public static Class<?> getGenericCollectionTypeOfMethodParameter(Method theMethod, int theParamIndex) {
Class<?> type;
Type genericParameterType = theMethod.getGenericParameterTypes()[theParamIndex];
if (Class.class.equals(genericParameterType)) {
if (Class.class.equals(genericParameterType) || Class.class.equals(genericParameterType.getClass())) {
return null;
}
ParameterizedType collectionType = (ParameterizedType) genericParameterType;

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.util;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@ -25,9 +25,9 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@ -57,6 +57,7 @@ ca.uhn.fhir.validation.ValidationResult.noIssuesDetected=No issues detected duri
# JPA Messages
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceVersionConstraintFailure=The operation has failed with a version constraint failure. This generally means that two clients/threads were trying to update the same resource at the same time, and this request was chosen as the failing request.
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceIndexedCompositeStringUniqueConstraintFailure=The operation has failed with a unique index constraint failure. This probably means that the operation was trying to create/update a resource that would have resulted in a duplicate value for a unique index.
@ -90,9 +91,13 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulUpdate=Successfully update
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulDeletes=Successfully deleted {0} resource(s) in {1}ms
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidSearchParameter=Unknown search parameter "{0}". Value search parameters for this search are: {1}
ca.uhn.fhir.jpa.dao.BaseSearchParamExtractor.failedToExtractPaths=Failed to extract values from resource using FHIRPath "{0}": {1}
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidQuantityPrefix=Unable to handle quantity prefix "{0}" for value: {1}
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidNumberPrefix=Unable to handle number prefix "{0}" for value: {1}
ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoSearchParameterR4.invalidSearchParamExpression=The expression "{0}" can not be evaluated and may be invalid: {1}
ca.uhn.fhir.jpa.provider.BaseJpaProvider.cantCombintAtAndSince=Unable to combine _at and _since parameters for history operation
ca.uhn.fhir.jpa.term.BaseHapiTerminologySvcImpl.cannotCreateDuplicateConceptMapUrl=Can not create multiple ConceptMap resources with ConceptMap.url "{0}", already have one with resource ID: {1}

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.okhttp.client;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -74,11 +75,11 @@ public class OkHttpRestfulRequest implements IHttpRequest {
@Override
public Map<String, List<String>> getAllHeaders() {
return myRequestBuilder.build().headers().toMultimap();
return Collections.unmodifiableMap(myRequestBuilder.build().headers().toMultimap());
}
@Override
public String getRequestBodyFromStream() throws IOException {
public String getRequestBodyFromStream() {
// returning null to indicate this is not supported, as documented in IHttpRequest's contract
return null;
}

View File

@ -22,10 +22,7 @@ package ca.uhn.fhir.rest.client.apache;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.*;
import ca.uhn.fhir.util.StopWatch;
import org.apache.commons.io.IOUtils;
@ -70,14 +67,14 @@ public class ApacheHttpRequest implements IHttpRequest {
@Override
public Map<String, List<String>> getAllHeaders() {
Map<String, List<String>> result = new HashMap<String, List<String>>();
Map<String, List<String>> result = new HashMap<>();
for (Header header : myRequest.getAllHeaders()) {
if (!result.containsKey(header.getName())) {
result.put(header.getName(), new LinkedList<String>());
result.put(header.getName(), new LinkedList<>());
}
result.get(header.getName()).add(header.getValue());
}
return result;
return Collections.unmodifiableMap(result);
}
/**

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.client.impl;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -316,7 +316,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private static void addParam(Map<String, List<String>> params, String parameterName, String parameterValue) {
if (!params.containsKey(parameterName)) {
params.put(parameterName, new ArrayList<String>());
params.put(parameterName, new ArrayList<>());
}
params.get(parameterName).add(parameterValue);
}
@ -516,6 +516,19 @@ public class GenericClient extends BaseClient implements IGenericClient {
return (QUERY) this;
}
@Override
public QUERY whereMap(Map<String, List<String>> theRawMap) {
if (theRawMap != null) {
for (String nextKey : theRawMap.keySet()) {
for (String nextValue : theRawMap.get(nextKey)) {
addParam(myParams, nextKey, nextValue);
}
}
}
return (QUERY) this;
}
@SuppressWarnings("unchecked")
@Override
public QUERY where(Map<String, List<IQueryParameterType>> theCriterion) {
@ -743,6 +756,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private Class<? extends IBaseBundle> myReturnType;
private IPrimitiveType mySince;
private Class<? extends IBaseResource> myType;
private DateRangeParam myAt;
@SuppressWarnings("unchecked")
@Override
@ -752,6 +766,12 @@ public class GenericClient extends BaseClient implements IGenericClient {
return this;
}
@Override
public IHistoryTyped at(DateRangeParam theDateRangeParam) {
myAt = theDateRangeParam;
return this;
}
@Override
public IHistoryTyped count(Integer theCount) {
myCount = theCount;
@ -774,7 +794,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
id = null;
}
HttpGetClientInvocation invocation = HistoryMethodBinding.createHistoryInvocation(myContext, resourceName, id, mySince, myCount);
HttpGetClientInvocation invocation = HistoryMethodBinding.createHistoryInvocation(myContext, resourceName, id, mySince, myCount, myAt);
IClientResponseHandler handler;
handler = new ResourceResponseHandler(myReturnType, getPreferResponseTypes(myType));
@ -1847,6 +1867,16 @@ public class GenericClient extends BaseClient implements IGenericClient {
return retVal;
}
@Override
public IQuery sort(SortSpec theSortSpec) {
SortSpec sortSpec = theSortSpec;
while (sortSpec != null) {
mySort.add(new SortInternal(sortSpec));
sortSpec = sortSpec.getChain();
}
return this;
}
@Override
public IQuery usingStyle(SearchStyleEnum theStyle) {
mySearchStyle = theStyle;
@ -2133,6 +2163,18 @@ public class GenericClient extends BaseClient implements IGenericClient {
myFor = theFor;
}
public SortInternal(SortSpec theSortSpec) {
if (theSortSpec.getOrder() == null) {
myParamName = Constants.PARAM_SORT;
} else if (theSortSpec.getOrder() == SortOrderEnum.ASC) {
myParamName = Constants.PARAM_SORT_ASC;
} else if (theSortSpec.getOrder() == SortOrderEnum.DESC) {
myParamName = Constants.PARAM_SORT_DESC;
}
myDirection = theSortSpec.getOrder();
myParamValue = theSortSpec.getParamName();
}
@Override
public IQuery ascending(IParam theParam) {
myParamName = Constants.PARAM_SORT_ASC;
@ -2157,6 +2199,14 @@ public class GenericClient extends BaseClient implements IGenericClient {
return myFor;
}
@Override
public IQuery defaultOrder(String theParam) {
myParamName = Constants.PARAM_SORT;
myDirection = null;
myParamValue = theParam;
return myFor;
}
@Override
public IQuery descending(IParam theParam) {
myParamName = Constants.PARAM_SORT_DESC;

View File

@ -29,6 +29,7 @@ import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.client.api.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.apache.commons.lang3.Validate;
/**
* HTTP interceptor to be used for adding HTTP basic auth username/password tokens
@ -42,23 +43,29 @@ public class BasicAuthInterceptor implements IClientInterceptor {
private String myUsername;
private String myPassword;
private String myHeaderValue;
public BasicAuthInterceptor(String theUsername, String thePassword) {
super();
myUsername = theUsername;
myPassword = thePassword;
/**
* @param theUsername The username
* @param thePassword The password
*/
public BasicAuthInterceptor(String theUsername, String thePassword) {
this(StringUtils.defaultString(theUsername) + ":" + StringUtils.defaultString(thePassword));
}
/**
* @param theCredentialString A credential string in the format <code>username:password</code>
*/
public BasicAuthInterceptor(String theCredentialString) {
Validate.notBlank(theCredentialString, "theCredentialString must not be null or blank");
Validate.isTrue(theCredentialString.contains(":"), "theCredentialString must be in the format 'username:password'");
String encoded = Base64.encodeBase64String(theCredentialString.getBytes(Constants.CHARSET_US_ASCII));
myHeaderValue = "Basic " + encoded;
}
@Override
public void interceptRequest(IHttpRequest theRequest) {
String authorizationUnescaped = StringUtils.defaultString(myUsername) + ":" + StringUtils.defaultString(myPassword);
String encoded;
try {
encoded = Base64.encodeBase64String(authorizationUnescaped.getBytes("ISO-8859-1"));
} catch (UnsupportedEncodingException e) {
throw new InternalErrorException("Could not find US-ASCII encoding. This shouldn't happen!");
}
theRequest.addHeader(Constants.HEADER_AUTHORIZATION, ("Basic " + encoded));
theRequest.addHeader(Constants.HEADER_AUTHORIZATION, myHeaderValue);
}
@Override
@ -66,6 +73,4 @@ public class BasicAuthInterceptor implements IClientInterceptor {
// nothing
}
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.client.interceptor;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,19 +20,20 @@ package ca.uhn.fhir.rest.client.interceptor;
* #L%
*/
import java.io.IOException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import java.io.IOException;
/**
* Client interceptor which simply captures request and response objects and stores them so that they can be inspected after a client
* call has returned
*
* @see ThreadLocalCapturingInterceptor for an interceptor that uses a ThreadLocal in order to work in multithreaded environments
*/
public class CapturingInterceptor implements IClientInterceptor {
@ -63,10 +64,16 @@ public class CapturingInterceptor implements IClientInterceptor {
@Override
public void interceptResponse(IHttpResponse theResponse) {
//Buffer the reponse to avoid errors when content has already been read and the entity is not repeatable
bufferResponse(theResponse);
myLastResponse = theResponse;
}
static void bufferResponse(IHttpResponse theResponse) {
try {
if(theResponse.getResponse() instanceof HttpResponse) {
if (theResponse.getResponse() instanceof HttpResponse) {
HttpEntity entity = ((HttpResponse) theResponse.getResponse()).getEntity();
if( entity != null && !entity.isRepeatable()){
if (entity != null && !entity.isRepeatable()) {
theResponse.bufferEntity();
}
} else {
@ -75,9 +82,6 @@ public class CapturingInterceptor implements IClientInterceptor {
} catch (IOException e) {
throw new InternalErrorException("Unable to buffer the entity for capturing", e);
}
myLastResponse = theResponse;
}
}

View File

@ -0,0 +1,79 @@
package ca.uhn.fhir.rest.client.interceptor;
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import java.io.IOException;
/**
* This is a client interceptor that captures the current request and response
* in a ThreadLocal variable, meaning that it can work in multithreaded
* environments without mixing up requests.
* <p>
* Use this with caution, since <b>this interceptor does not automatically clean up</b>
* the ThreadLocal after setting it. You must make sure to call
* {@link #clearThreadLocals()} after a given request has been completed,
* or you will end up leaving stale request/response objects associated
* with threads that no longer need them.
* </p>
*
* @see CapturingInterceptor for an equivalent interceptor that does not use a ThreadLocal
* @since 3.5.0
*/
public class ThreadLocalCapturingInterceptor implements IClientInterceptor {
private final ThreadLocal<IHttpRequest> myRequestThreadLocal = new ThreadLocal<>();
private final ThreadLocal<IHttpResponse> myResponseThreadLocal = new ThreadLocal<>();
private boolean myBufferResponse;
/**
* This method should be called at the end of any request process, in
* order to clear the last request and response from the current thread.
*/
public void clearThreadLocals() {
myRequestThreadLocal.remove();
myResponseThreadLocal.remove();
}
public IHttpRequest getRequestForCurrentThread() {
return myRequestThreadLocal.get();
}
public IHttpResponse getResponseForCurrentThread() {
return myResponseThreadLocal.get();
}
@Override
public void interceptRequest(IHttpRequest theRequest) {
myRequestThreadLocal.set(theRequest);
}
@Override
public void interceptResponse(IHttpResponse theResponse) {
if (isBufferResponse()) {
CapturingInterceptor.bufferResponse(theResponse);
}
myResponseThreadLocal.set(theResponse);
}
/**
* Should we buffer (capture) the response body? This defaults to
* <code>false</code>. Set to <code>true</code> if you are planning on
* examining response bodies after the response processing is complete.
*/
public boolean isBufferResponse() {
return myBufferResponse;
}
/**
* Should we buffer (capture) the response body? This defaults to
* <code>false</code>. Set to <code>true</code> if you are planning on
* examining response bodies after the response processing is complete.
*/
public ThreadLocalCapturingInterceptor setBufferResponse(boolean theBufferResponse) {
myBufferResponse = theBufferResponse;
return this;
}
}

View File

@ -26,6 +26,8 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Date;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.FhirContext;
@ -96,7 +98,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
}
String historyId = id != null ? id.getIdPart() : null;
HttpGetClientInvocation retVal = createHistoryInvocation(getContext(), resourceName, historyId, null, null);
HttpGetClientInvocation retVal = createHistoryInvocation(getContext(), resourceName, historyId, null, null, null);
if (theArgs != null) {
for (int idx = 0; idx < theArgs.length; idx++) {
@ -108,7 +110,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
return retVal;
}
public static HttpGetClientInvocation createHistoryInvocation(FhirContext theContext, String theResourceName, String theId, IPrimitiveType<Date> theSince, Integer theLimit) {
public static HttpGetClientInvocation createHistoryInvocation(FhirContext theContext, String theResourceName, String theId, IPrimitiveType<Date> theSince, Integer theLimit, DateRangeParam theAt) {
StringBuilder b = new StringBuilder();
if (theResourceName != null) {
b.append(theResourceName);
@ -129,8 +131,18 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
}
if (theLimit != null) {
b.append(haveParam ? '&' : '?');
haveParam = true;
b.append(Constants.PARAM_COUNT).append('=').append(theLimit);
}
if (theAt != null) {
for (DateParam next : theAt.getValuesAsQueryTokens()) {
b.append(haveParam ? '&' : '?');
haveParam = true;
b.append(Constants.PARAM_AT);
b.append("=");
b.append(next.getValueAsQueryToken(theContext));
}
}
HttpGetClientInvocation retVal = new HttpGetClientInvocation(theContext, b.toString());
return retVal;

View File

@ -28,10 +28,7 @@ import ca.uhn.fhir.util.StopWatch;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.*;
/**
* A Http Request based on JaxRs. This is an adapter around the class
@ -41,7 +38,7 @@ import java.util.Map;
*/
public class JaxRsHttpRequest implements IHttpRequest {
private final Map<String, List<String>> myHeaders = new HashMap<String, List<String>>();
private final Map<String, List<String>> myHeaders = new HashMap<>();
private Invocation.Builder myRequest;
private RequestTypeEnum myRequestType;
private Entity<?> myEntity;
@ -55,7 +52,7 @@ public class JaxRsHttpRequest implements IHttpRequest {
@Override
public void addHeader(String theName, String theValue) {
if (!myHeaders.containsKey(theName)) {
myHeaders.put(theName, new LinkedList<String>());
myHeaders.put(theName, new LinkedList<>());
}
myHeaders.get(theName).add(theValue);
getRequest().header(theName, theValue);
@ -71,7 +68,7 @@ public class JaxRsHttpRequest implements IHttpRequest {
@Override
public Map<String, List<String>> getAllHeaders() {
return this.myHeaders;
return Collections.unmodifiableMap(this.myHeaders);
}
/**

View File

@ -108,7 +108,7 @@ public class BaseR4Config extends BaseConfig {
@Bean(name = "myResourceCountsCache")
public ResourceCountCache resourceCountsCache() {
ResourceCountCache retVal = new ResourceCountCache(() -> systemDaoR4().getResourceCounts());
retVal.setCacheMillis(60 * DateUtils.MILLIS_PER_SECOND);
retVal.setCacheMillis(10 * DateUtils.MILLIS_PER_MINUTE);
return retVal;
}

View File

@ -74,7 +74,6 @@ import javax.persistence.criteria.Root;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.XMLEvent;
import java.io.CharArrayWriter;
import java.io.UnsupportedEncodingException;
import java.text.Normalizer;
import java.util.*;
import java.util.Map.Entry;
@ -92,9 +91,9 @@ import static org.apache.commons.lang3.StringUtils.*;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -1250,25 +1249,10 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
if (theEntity.getDeleted() == null) {
encoding = myConfig.getResourceEncoding();
IParser parser = encoding.newParser(myContext);
parser.setDontEncodeElements(EXCLUDE_ELEMENTS_IN_ENCODED);
String encoded = parser.encodeResourceToString(theResource);
Set<String> excludeElements = EXCLUDE_ELEMENTS_IN_ENCODED;
theEntity.setFhirVersion(myContext.getVersion().getVersion());
switch (encoding) {
case JSON:
bytes = encoded.getBytes(Charsets.UTF_8);
break;
case JSONC:
bytes = GZipUtil.compress(encoded);
break;
default:
case DEL:
bytes = new byte[0];
break;
}
ourLog.debug("Encoded {} chars of resource body as {} bytes", encoded.length(), bytes.length);
bytes = encodeResource(theResource, encoding, excludeElements, myContext);
if (theUpdateHash) {
HashFunction sha256 = Hashing.sha256();
@ -1664,22 +1648,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return null;
}
// 2. get The text
String resourceText = null;
switch (resourceEncoding) {
case JSON:
try {
resourceText = new String(resourceBytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new Error("Should not happen", e);
}
break;
case JSONC:
resourceText = GZipUtil.decompress(resourceBytes);
break;
case DEL:
break;
}
// 2. get The text
String resourceText = decodeResource(resourceBytes, resourceEncoding);
// 3. Use the appropriate custom type if one is specified in the context
Class<R> resourceType = theResourceType;
@ -2393,6 +2363,45 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
public static String decodeResource(byte[] theResourceBytes, ResourceEncodingEnum theResourceEncoding) {
String resourceText = null;
switch (theResourceEncoding) {
case JSON:
resourceText = new String(theResourceBytes, Charsets.UTF_8);
break;
case JSONC:
resourceText = GZipUtil.decompress(theResourceBytes);
break;
case DEL:
break;
}
return resourceText;
}
public static byte[] encodeResource(IBaseResource theResource, ResourceEncodingEnum theEncoding, Set<String> theExcludeElements, FhirContext theContext) {
byte[] bytes;
IParser parser = theEncoding.newParser(theContext);
parser.setDontEncodeElements(theExcludeElements);
String encoded = parser.encodeResourceToString(theResource);
switch (theEncoding) {
case JSON:
bytes = encoded.getBytes(Charsets.UTF_8);
break;
case JSONC:
bytes = GZipUtil.compress(encoded);
break;
default:
case DEL:
bytes = new byte[0];
break;
}
ourLog.debug("Encoded {} chars of resource body as {} bytes", encoded.length(), bytes.length);
return bytes;
}
/**
* This method is used to create a set of all possible combinations of
* parameters across a set of search parameters. An example of why

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.FhirTerser;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.ObjectUtils;
@ -47,6 +48,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
private DaoConfig myDaoConfig;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
public BaseSearchParamExtractor() {
super();
}
@ -73,31 +75,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
return refs;
}
protected List<Object> extractValues(String thePaths, IBaseResource theResource) {
List<Object> values = new ArrayList<Object>();
String[] nextPathsSplit = SPLIT.split(thePaths);
FhirTerser t = myContext.newTerser();
for (String nextPath : nextPathsSplit) {
String nextPathTrimmed = nextPath.trim();
try {
List<Object> allValues = t.getValues(theResource, nextPathTrimmed);
for (Object next : allValues) {
if (next instanceof IBaseExtension) {
IBaseDatatype value = ((IBaseExtension) next).getValue();
if (value != null) {
values.add(value);
}
} else {
values.add(next);
}
}
} catch (Exception e) {
RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource);
ourLog.warn("Failed to index values from path[{}] in resource type[{}]: {}", new Object[] {nextPathTrimmed, def.getName(), e.toString(), e});
}
}
return values;
}
protected abstract List<Object> extractValues(String thePaths, IBaseResource theResource);
protected FhirContext getContext() {
return myContext;

View File

@ -20,9 +20,9 @@ import java.util.*;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -144,6 +144,7 @@ public class DaoConfig {
private boolean myExpungeEnabled;
private int myReindexThreadCount;
private Set<String> myBundleTypesAllowedForStorage;
private boolean myValidateSearchParameterExpressionsOnSave = true;
/**
* Constructor
@ -786,7 +787,6 @@ public class DaoConfig {
this.myAllowContainsSearches = theAllowContainsSearches;
}
/**
* If set to <code>true</code> (default is <code>false</code>) the server will allow
* resources to have references to external servers. For example if this server is
@ -1188,6 +1188,34 @@ public class DaoConfig {
myUniqueIndexesEnabled = theUniqueIndexesEnabled;
}
/**
* If <code>true</code> (default is <code>true</code>), before allowing a
* SearchParameter resource to be stored (create, update, etc.) the
* expression will be performed against an empty resource to ensure that
* the FHIRPath executor is able to process it.
* <p>
* This should proabably always be set to true, but is configurable
* in order to support some unit tests.
* </p>
*/
public boolean isValidateSearchParameterExpressionsOnSave() {
return myValidateSearchParameterExpressionsOnSave;
}
/**
* If <code>true</code> (default is <code>true</code>), before allowing a
* SearchParameter resource to be stored (create, update, etc.) the
* expression will be performed against an empty resource to ensure that
* the FHIRPath executor is able to process it.
* <p>
* This should proabably always be set to true, but is configurable
* in order to support some unit tests.
* </p>
*/
public void setValidateSearchParameterExpressionsOnSave(boolean theValidateSearchParameterExpressionsOnSave) {
myValidateSearchParameterExpressionsOnSave = theValidateSearchParameterExpressionsOnSave;
}
/**
* Do not call this method, it exists only for legacy reasons. It
* will be removed in a future version. Configure the page size on your

View File

@ -29,14 +29,8 @@ import ca.uhn.fhir.model.dstu2.resource.SearchParameter;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.SearchParamTypeEnum;
import ca.uhn.fhir.model.primitive.BoundCodeDt;
import ca.uhn.fhir.model.primitive.CodeDt;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -83,7 +77,7 @@ public class FhirResourceDaoSearchParameterDstu2 extends FhirResourceDaoDstu2<Se
FhirContext context = getContext();
SearchParamTypeEnum type = theResource.getTypeElement().getValueAsEnum();
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context);
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context, getConfig());
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -19,33 +19,40 @@ package ca.uhn.fhir.jpa.dao;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.math.BigDecimal;
import java.util.*;
import javax.measure.quantity.Quantity;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.model.api.*;
import ca.uhn.fhir.model.api.IDatatype;
import ca.uhn.fhir.model.api.IPrimitiveDatatype;
import ca.uhn.fhir.model.api.IValueSetEnumBinder;
import ca.uhn.fhir.model.base.composite.BaseHumanNameDt;
import ca.uhn.fhir.model.dstu2.composite.*;
import ca.uhn.fhir.model.dstu2.composite.BoundCodeableConceptDt;
import ca.uhn.fhir.model.dstu2.resource.*;
import ca.uhn.fhir.model.dstu2.resource.Conformance.RestSecurity;
import ca.uhn.fhir.model.dstu2.resource.Location;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.resource.Patient.Communication;
import ca.uhn.fhir.model.dstu2.resource.Questionnaire;
import ca.uhn.fhir.model.dstu2.resource.ValueSet;
import ca.uhn.fhir.model.dstu2.valueset.RestfulSecurityServiceEnum;
import ca.uhn.fhir.model.primitive.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.FhirTerser;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.measure.quantity.Quantity;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import java.math.BigDecimal;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implements ISearchParamExtractor {
@ -81,13 +88,13 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamDates(ca.uhn.fhir.jpa.entity.ResourceTable,
* ca.uhn.fhir.model.api.IResource)
*/
@Override
public Set<ResourceIndexedSearchParamDate> extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) {
HashSet<ResourceIndexedSearchParamDate> retVal = new HashSet<ResourceIndexedSearchParamDate>();
HashSet<ResourceIndexedSearchParamDate> retVal = new HashSet<>();
Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
for (RuntimeSearchParam nextSpDef : searchParams) {
@ -142,7 +149,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamNumber(ca.uhn.fhir.jpa.entity.ResourceTable,
* ca.uhn.fhir.model.api.IResource)
*/
@ -196,7 +203,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
* org.unitsofmeasurement.quantity.Quantity<?>>)
* UCUMFormat.getCaseInsensitiveInstance().parse(nextValue.getCode().getValue(), null); if
* (unit.isCompatible(UCUM.DAY)) {
*
*
* @SuppressWarnings("unchecked") PhysicsUnit<org.unitsofmeasurement.quantity.Time> timeUnit =
* (PhysicsUnit<Time>) unit; UnitConverter conv = timeUnit.getConverterTo(UCUM.DAY); double
* dayValue = conv.convert(nextValue.getValue().getValue().doubleValue()); DurationDt newValue =
@ -251,7 +258,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamQuantity(ca.uhn.fhir.jpa.entity.ResourceTable,
* ca.uhn.fhir.model.api.IResource)
*/
@ -305,7 +312,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamStrings(ca.uhn.fhir.jpa.entity.ResourceTable,
* ca.uhn.fhir.model.api.IResource)
*/
@ -314,7 +321,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
HashSet<ResourceIndexedSearchParamString> retVal = new HashSet<ResourceIndexedSearchParamString>();
String resourceName = getContext().getResourceDefinition(theResource).getName();
Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
for (RuntimeSearchParam nextSpDef : searchParams) {
if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.STRING) {
@ -389,7 +396,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamTokens(ca.uhn.fhir.jpa.entity.ResourceTable,
* ca.uhn.fhir.model.api.IResource)
*/
@ -626,6 +633,35 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
}
}
@Override
protected List<Object> extractValues(String thePaths, IBaseResource theResource) {
List<Object> values = new ArrayList<>();
String[] nextPathsSplit = SPLIT.split(thePaths);
FhirTerser t = getContext().newTerser();
for (String nextPath : nextPathsSplit) {
String nextPathTrimmed = nextPath.trim();
List<Object> allValues;
try {
allValues = t.getValues(theResource, nextPathTrimmed);
} catch (Exception e) {
String msg = getContext().getLocalizer().getMessage(BaseSearchParamExtractor.class, "failedToExtractPaths", nextPath, e.toString());
throw new InternalErrorException(msg, e);
}
for (Object next : allValues) {
if (next instanceof IBaseExtension) {
IBaseDatatype value = ((IBaseExtension) next).getValue();
if (value != null) {
values.add(value);
}
} else {
values.add(next);
}
}
}
return values;
}
private static <T extends Enum<?>> String extractSystem(BoundCodeDt<T> theBoundCode) {
if (theBoundCode.getValueAsEnum() != null) {
IValueSetEnumBinder<T> binder = theBoundCode.getBinder();

View File

@ -74,7 +74,7 @@ public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3<Se
FhirContext context = getContext();
Enumerations.SearchParamType type = theResource.getType();
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context);
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context, getConfig());
}
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao.dstu3;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -19,17 +19,15 @@ package ca.uhn.fhir.jpa.dao.dstu3;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.trim;
import java.math.BigDecimal;
import java.util.*;
import javax.annotation.PostConstruct;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
@ -41,16 +39,18 @@ import org.hl7.fhir.dstu3.model.Location.LocationPositionComponent;
import org.hl7.fhir.dstu3.model.Patient.PatientCommunicationComponent;
import org.hl7.fhir.dstu3.utils.FHIRPathEngine;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.springframework.beans.factory.annotation.Autowired;
import com.google.common.annotations.VisibleForTesting;
import javax.annotation.PostConstruct;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import java.math.BigDecimal;
import java.util.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import static org.apache.commons.lang3.StringUtils.*;
public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implements ISearchParamExtractor {
@ -61,11 +61,6 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
private HapiWorkerContext myWorkerContext;
@PostConstruct
public void start() {
myWorkerContext = new HapiWorkerContext(getContext(), myValidationSupport);
}
/**
* Constructor
*/
@ -78,6 +73,17 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
myValidationSupport = theValidationSupport;
}
private void addQuantity(ResourceTable theEntity, HashSet<ResourceIndexedSearchParamQuantity> retVal, String resourceName, Quantity nextValue) {
if (!nextValue.getValueElement().isEmpty()) {
BigDecimal nextValueValue = nextValue.getValueElement().getValue();
String nextValueString = nextValue.getSystemElement().getValueAsString();
String nextValueCode = nextValue.getCode();
ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(resourceName, nextValueValue, nextValueString, nextValueCode);
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
}
}
private void addSearchTerm(ResourceTable theEntity, Set<ResourceIndexedSearchParamString> retVal, String resourceName, String searchTerm) {
if (isBlank(searchTerm)) {
return;
@ -100,6 +106,23 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
retVal.add(nextEntity);
}
@Override
public List<PathAndRef> extractResourceLinks(IBaseResource theResource, RuntimeSearchParam theNextSpDef) {
ArrayList<PathAndRef> retVal = new ArrayList<PathAndRef>();
String[] nextPathsSplit = SPLIT.split(theNextSpDef.getPath());
for (String path : nextPathsSplit) {
path = path.trim();
if (isNotBlank(path)) {
for (Object next : extractValues(path, theResource)) {
retVal.add(new PathAndRef(path, next));
}
}
}
return retVal;
}
@Override
public Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
// TODO: implement
@ -108,7 +131,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamDates(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/
@Override
@ -195,12 +218,12 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamNumber(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/
@Override
public HashSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) {
HashSet<ResourceIndexedSearchParamNumber> retVal = new HashSet<ResourceIndexedSearchParamNumber>();
HashSet<ResourceIndexedSearchParamNumber> retVal = new HashSet<>();
Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
for (RuntimeSearchParam nextSpDef : searchParams) {
@ -245,7 +268,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
/*
* @SuppressWarnings("unchecked") PhysicsUnit<? extends org.unitsofmeasurement.quantity.Quantity<?>> unit = (PhysicsUnit<? extends org.unitsofmeasurement.quantity.Quantity<?>>)
* UCUMFormat.getCaseInsensitiveInstance().parse(nextValue.getCode().getValue(), null); if (unit.isCompatible(UCUM.DAY)) {
*
*
* @SuppressWarnings("unchecked") PhysicsUnit<org.unitsofmeasurement.quantity.Time> timeUnit = (PhysicsUnit<Time>) unit; UnitConverter conv = timeUnit.getConverterTo(UCUM.DAY);
* double dayValue = conv.convert(nextValue.getValue().getValue().doubleValue()); Duration newValue = new Duration(); newValue.setSystem(UCUM_NS);
* newValue.setCode(UCUM.DAY.getSymbol()); newValue.setValue(dayValue); nextValue=newValue; }
@ -298,7 +321,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamQuantity(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/
@Override
@ -349,20 +372,9 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
return retVal;
}
private void addQuantity(ResourceTable theEntity, HashSet<ResourceIndexedSearchParamQuantity> retVal, String resourceName, Quantity nextValue) {
if (!nextValue.getValueElement().isEmpty()) {
BigDecimal nextValueValue = nextValue.getValueElement().getValue();
String nextValueString = nextValue.getSystemElement().getValueAsString();
String nextValueCode = nextValue.getCode();
ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(resourceName, nextValueValue, nextValueString, nextValueCode);
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
}
}
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamStrings(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/
@Override
@ -462,7 +474,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamTokens(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/
@Override
@ -674,14 +686,14 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
}
private void extractTokensFromCodeableConcept(List<String> theSystems, List<String> theCodes, CodeableConcept theCodeableConcept, ResourceTable theEntity,
Set<BaseResourceIndexedSearchParam> theListToPopulate, RuntimeSearchParam theParameterDef) {
Set<BaseResourceIndexedSearchParam> theListToPopulate, RuntimeSearchParam theParameterDef) {
for (Coding nextCoding : theCodeableConcept.getCoding()) {
extractTokensFromCoding(theSystems, theCodes, theEntity, theListToPopulate, theParameterDef, nextCoding);
}
}
private void extractTokensFromCoding(List<String> theSystems, List<String> theCodes, ResourceTable theEntity, Set<BaseResourceIndexedSearchParam> theListToPopulate,
RuntimeSearchParam theParameterDef, Coding nextCoding) {
RuntimeSearchParam theParameterDef, Coding nextCoding) {
if (nextCoding != null && !nextCoding.isEmpty()) {
String nextSystem = nextCoding.getSystemElement().getValueAsString();
@ -706,16 +718,18 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
FHIRPathEngine fp = new FHIRPathEngine(myWorkerContext);
List<Object> values = new ArrayList<>();
try {
String[] nextPathsSplit = SPLIT.split(thePaths);
for (String nextPath : nextPathsSplit) {
List<Base> allValues = fp.evaluate((Base) theResource, trim(nextPath));
if (allValues.isEmpty() == false) {
values.addAll(allValues);
}
String[] nextPathsSplit = SPLIT.split(thePaths);
for (String nextPath : nextPathsSplit) {
List<Base> allValues;
try {
allValues = fp.evaluate((Base) theResource, trim(nextPath));
} catch (FHIRException e) {
String msg = getContext().getLocalizer().getMessage(BaseSearchParamExtractor.class, "failedToExtractPaths", nextPath, e.toString());
throw new InternalErrorException(msg, e);
}
if (allValues.isEmpty() == false) {
values.addAll(allValues);
}
} catch (FHIRException e) {
throw new InternalErrorException(e);
}
for (int i = 0; i < values.size(); i++) {
@ -730,28 +744,16 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
return values;
}
@Override
public List<PathAndRef> extractResourceLinks(IBaseResource theResource, RuntimeSearchParam theNextSpDef) {
ArrayList<PathAndRef> retVal = new ArrayList<PathAndRef>();
String[] nextPathsSplit = SPLIT.split(theNextSpDef.getPath());
for (String path : nextPathsSplit) {
path = path.trim();
if (isNotBlank(path)) {
for (Object next : extractValues(path, theResource)) {
retVal.add(new PathAndRef(path, next));
}
}
}
return retVal;
}
@VisibleForTesting
void setValidationSupportForTesting(org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport theValidationSupport) {
myValidationSupport = theValidationSupport;
}
@PostConstruct
public void start() {
myWorkerContext = new HapiWorkerContext(getContext(), myValidationSupport);
}
private static <T extends Enum<?>> String extractSystem(Enumeration<T> theBoundCode) {
if (theBoundCode.getValue() != null) {
return theBoundCode.getEnumFactory().toSystem(theBoundCode.getValue());

View File

@ -1,13 +1,17 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.dao.BaseSearchParamExtractor;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSearchParameter;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.ElementUtil;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.*;
import org.springframework.beans.factory.annotation.Autowired;
@ -76,10 +80,10 @@ public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4<SearchPa
FhirContext context = getContext();
Enum<?> type = theResource.getType();
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context);
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context, getConfig());
}
public static void validateSearchParam(Enum<?> theType, Enum<?> theStatus, List<? extends IPrimitiveType> theBase, String theExpression, FhirContext theContext) {
public static void validateSearchParam(Enum<?> theType, Enum<?> theStatus, List<? extends IPrimitiveType> theBase, String theExpression, FhirContext theContext, DaoConfig theDaoConfig) {
if (theStatus == null) {
throw new UnprocessableEntityException("SearchParameter.status is missing or invalid");
}
@ -116,6 +120,17 @@ public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4<SearchPa
throw new UnprocessableEntityException("Invalid SearchParameter.expression value \"" + nextPath + "\": " + e.getMessage());
}
if (theContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
if (theDaoConfig.isValidateSearchParameterExpressionsOnSave()) {
IBaseResource temporaryInstance = theContext.getResourceDefinition(resourceName).newInstance();
try {
theContext.newFluentPath().evaluate(temporaryInstance, nextPath, IBase.class);
} catch (Exception e) {
String msg = theContext.getLocalizer().getMessage(FhirResourceDaoSearchParameterR4.class, "invalidSearchParamExpression", nextPath, e.getMessage());
throw new UnprocessableEntityException(msg, e);
}
}
}
}
} // if have expression

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao.r4;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -19,17 +19,21 @@ package ca.uhn.fhir.jpa.dao.r4;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.math.BigDecimal;
import java.util.*;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.context.IWorkerContext;
import org.hl7.fhir.r4.hapi.ctx.IValidationSupport;
import org.hl7.fhir.r4.model.*;
@ -38,17 +42,15 @@ import org.hl7.fhir.r4.model.Enumeration;
import org.hl7.fhir.r4.model.Location.LocationPositionComponent;
import org.hl7.fhir.r4.model.Patient.PatientCommunicationComponent;
import org.hl7.fhir.r4.utils.FHIRPathEngine;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import com.google.common.annotations.VisibleForTesting;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import java.math.BigDecimal;
import java.util.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements ISearchParamExtractor {
@ -69,6 +71,17 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
myValidationSupport = theValidationSupport;
}
private void addQuantity(ResourceTable theEntity, HashSet<ResourceIndexedSearchParamQuantity> retVal, String resourceName, Quantity nextValue) {
if (!nextValue.getValueElement().isEmpty()) {
BigDecimal nextValueValue = nextValue.getValueElement().getValue();
String nextValueString = nextValue.getSystemElement().getValueAsString();
String nextValueCode = nextValue.getCode();
ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(resourceName, nextValueValue, nextValueString, nextValueCode);
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
}
}
private void addSearchTerm(ResourceTable theEntity, Set<ResourceIndexedSearchParamString> retVal, String resourceName, String searchTerm) {
if (isBlank(searchTerm)) {
return;
@ -91,6 +104,23 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
retVal.add(nextEntity);
}
@Override
public List<PathAndRef> extractResourceLinks(IBaseResource theResource, RuntimeSearchParam theNextSpDef) {
ArrayList<PathAndRef> retVal = new ArrayList<>();
String[] nextPathsSplit = SPLIT.split(theNextSpDef.getPath());
for (String path : nextPathsSplit) {
path = path.trim();
if (isNotBlank(path)) {
for (Object next : extractValues(path, theResource)) {
retVal.add(new PathAndRef(path, next));
}
}
}
return retVal;
}
@Override
public Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
// TODO: implement
@ -99,7 +129,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamDates(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/
@Override
@ -182,7 +212,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamNumber(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/
@Override
@ -232,7 +262,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
/*
* @SuppressWarnings("unchecked") PhysicsUnit<? extends org.unitsofmeasurement.quantity.Quantity<?>> unit = (PhysicsUnit<? extends org.unitsofmeasurement.quantity.Quantity<?>>)
* UCUMFormat.getCaseInsensitiveInstance().parse(nextValue.getCode().getValue(), null); if (unit.isCompatible(UCUM.DAY)) {
*
*
* @SuppressWarnings("unchecked") PhysicsUnit<org.unitsofmeasurement.quantity.Time> timeUnit = (PhysicsUnit<Time>) unit; UnitConverter conv = timeUnit.getConverterTo(UCUM.DAY);
* double dayValue = conv.convert(nextValue.getValue().getValue().doubleValue()); Duration newValue = new Duration(); newValue.setSystem(UCUM_NS);
* newValue.setCode(UCUM.DAY.getSymbol()); newValue.setValue(dayValue); nextValue=newValue; }
@ -285,7 +315,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamQuantity(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/
@Override
@ -336,20 +366,9 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
return retVal;
}
private void addQuantity(ResourceTable theEntity, HashSet<ResourceIndexedSearchParamQuantity> retVal, String resourceName, Quantity nextValue) {
if (!nextValue.getValueElement().isEmpty()) {
BigDecimal nextValueValue = nextValue.getValueElement().getValue();
String nextValueString = nextValue.getSystemElement().getValueAsString();
String nextValueCode = nextValue.getCode();
ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(resourceName, nextValueValue, nextValueString, nextValueCode);
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
}
}
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamStrings(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/
@Override
@ -449,7 +468,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
/*
* (non-Javadoc)
*
*
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamTokens(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/
@Override
@ -658,14 +677,14 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
}
private void extractTokensFromCodeableConcept(List<String> theSystems, List<String> theCodes, CodeableConcept theCodeableConcept, ResourceTable theEntity,
Set<BaseResourceIndexedSearchParam> theListToPopulate, RuntimeSearchParam theParameterDef) {
Set<BaseResourceIndexedSearchParam> theListToPopulate, RuntimeSearchParam theParameterDef) {
for (Coding nextCoding : theCodeableConcept.getCoding()) {
extractTokensFromCoding(theSystems, theCodes, theEntity, theListToPopulate, theParameterDef, nextCoding);
}
}
private void extractTokensFromCoding(List<String> theSystems, List<String> theCodes, ResourceTable theEntity, Set<BaseResourceIndexedSearchParam> theListToPopulate,
RuntimeSearchParam theParameterDef, Coding nextCoding) {
RuntimeSearchParam theParameterDef, Coding nextCoding) {
if (nextCoding != null && !nextCoding.isEmpty()) {
String nextSystem = nextCoding.getSystemElement().getValueAsString();
@ -691,16 +710,18 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
FHIRPathEngine fp = new FHIRPathEngine(worker);
List<Object> values = new ArrayList<>();
try {
String[] nextPathsSplit = SPLIT.split(thePaths);
for (String nextPath : nextPathsSplit) {
List<Base> allValues = fp.evaluate((Base) theResource, nextPath);
if (allValues.isEmpty() == false) {
values.addAll(allValues);
}
String[] nextPathsSplit = SPLIT.split(thePaths);
for (String nextPath : nextPathsSplit) {
List<Base> allValues;
try {
allValues = fp.evaluate((Base) theResource, nextPath);
} catch (FHIRException e) {
String msg = getContext().getLocalizer().getMessage(BaseSearchParamExtractor.class, "failedToExtractPaths", nextPath, e.toString());
throw new InternalErrorException(msg, e);
}
if (allValues.isEmpty() == false) {
values.addAll(allValues);
}
} catch (FHIRException e) {
throw new InternalErrorException(e);
}
for (int i = 0; i < values.size(); i++) {
@ -715,23 +736,6 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
return values;
}
@Override
public List<PathAndRef> extractResourceLinks(IBaseResource theResource, RuntimeSearchParam theNextSpDef) {
ArrayList<PathAndRef> retVal = new ArrayList<>();
String[] nextPathsSplit = SPLIT.split(theNextSpDef.getPath());
for (String path : nextPathsSplit) {
path = path.trim();
if (isNotBlank(path)) {
for (Object next : extractValues(path, theResource)) {
retVal.add(new PathAndRef(path, next));
}
}
}
return retVal;
}
@VisibleForTesting
void setValidationSupportForTesting(org.hl7.fhir.r4.hapi.ctx.IValidationSupport theValidationSupport) {
myValidationSupport = theValidationSupport;

View File

@ -23,6 +23,9 @@ package ca.uhn.fhir.jpa.entity;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
/**
* @see ResourceHistoryTable#ENCODING_COL_LENGTH
*/
public enum ResourceEncodingEnum {
/*

View File

@ -43,6 +43,10 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl
private static final long serialVersionUID = 1L;
public static final String IDX_RESVER_ID_VER = "IDX_RESVER_ID_VER";
/**
* @see ResourceEncodingEnum
*/
public static final int ENCODING_COL_LENGTH = 5;
@Id
@SequenceGenerator(name = "SEQ_RESOURCE_HISTORY_ID", sequenceName = "SEQ_RESOURCE_HISTORY_ID")
@ -67,7 +71,7 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl
@OptimisticLock(excluded = true)
private byte[] myResource;
@Column(name = "RES_ENCODING", nullable = false, length = 5)
@Column(name = "RES_ENCODING", nullable = false, length = ENCODING_COL_LENGTH)
@Enumerated(EnumType.STRING)
@OptimisticLock(excluded = true)
private ResourceEncodingEnum myEncoding;

View File

@ -51,6 +51,7 @@ public class Search implements Serializable {
private static final long serialVersionUID = 1L;
public static final int MAX_SEARCH_QUERY_STRING = 10000;
public static final int UUID_COLUMN_LENGTH = 36;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="CREATED", nullable=false, updatable=false)
@ -118,7 +119,7 @@ public class Search implements Serializable {
@Column(name="TOTAL_COUNT", nullable=true)
private Integer myTotalCount;
@Column(name="SEARCH_UUID", length=40, nullable=false, updatable=false)
@Column(name="SEARCH_UUID", length= UUID_COLUMN_LENGTH, nullable=false, updatable=false)
private String myUuid;
/**

View File

@ -61,6 +61,7 @@ import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Nullable;
import javax.persistence.EntityManager;
import java.util.*;
import java.util.concurrent.*;
@ -408,7 +409,11 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
myManagedTxManager = theTxManager;
}
static Pageable toPage(final int theFromIndex, int theToIndex) {
/**
* Creates a {@link Pageable} using a start and end index
*/
@SuppressWarnings("WeakerAccess")
public static @Nullable Pageable toPage(final int theFromIndex, int theToIndex) {
int pageSize = theToIndex - theFromIndex;
if (pageSize < 1) {
return null;

View File

@ -62,6 +62,9 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest {
@Qualifier("mySearchParameterDaoDstu2")
protected IFhirResourceDao<SearchParameter> mySearchParameterDao;
@Autowired
@Qualifier("myCommunicationDaoDstu2")
protected IFhirResourceDao<Communication> myCommunicationDao;
@Autowired
@Qualifier("myBundleDaoDstu2")
protected IFhirResourceDao<Bundle> myBundleDao;
@Autowired

View File

@ -13,14 +13,12 @@ import ca.uhn.fhir.model.dstu2.valueset.*;
import ca.uhn.fhir.model.primitive.*;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.*;
import org.mockito.internal.util.collections.ListUtil;
import java.util.List;
@ -37,6 +35,12 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu
myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden());
}
@After
public void after() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(new DaoConfig().isValidateSearchParameterExpressionsOnSave());
}
@Test
public void testCreateInvalidNoBase() {
SearchParameter fooSp = new SearchParameter();
@ -53,6 +57,31 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu
}
}
@Test
public void testIndexFailsIfInvalidSearchParameterExists() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(false);
SearchParameter threadIdSp = new SearchParameter();
threadIdSp.setBase(ResourceTypeEnum.COMMUNICATION);
threadIdSp.setCode("has-attachments");
threadIdSp.setType(SearchParamTypeEnum.REFERENCE);
threadIdSp.setXpath("Communication.payload[1].contentAttachment is not null");
threadIdSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
threadIdSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
mySearchParameterDao.create(threadIdSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Communication com = new Communication();
com.setStatus(CommunicationStatusEnum.IN_PROGRESS);
try {
myCommunicationDao.create(com, mySrd);
fail();
} catch (InternalErrorException e) {
assertThat(e.getMessage(), startsWith("Failed to extract values from resource using FHIRPath \"Communication.payload[1].contentAttachment is not null\": ca.uhn"));
}
}
@Test
public void testCreateInvalidParamInvalidResourceName() {
SearchParameter fooSp = new SearchParameter();

View File

@ -168,6 +168,9 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
@Qualifier("myPatientDaoDstu3")
protected IFhirResourceDaoPatient<Patient> myPatientDao;
@Autowired
@Qualifier("myCommunicationDaoDstu3")
protected IFhirResourceDao<Communication> myCommunicationDao;
@Autowired
@Qualifier("myPractitionerDaoDstu3")
protected IFhirResourceDao<Practitioner> myPractitionerDao;
@Autowired

View File

@ -1,9 +1,11 @@
package ca.uhn.fhir.jpa.dao.dstu3;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
@ -12,6 +14,7 @@ import org.hl7.fhir.dstu3.model.Appointment.AppointmentStatus;
import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
@ -29,6 +32,11 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
}
@After
public void after() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(new DaoConfig().isValidateSearchParameterExpressionsOnSave());
}
@Test
public void testCreateInvalidNoBase() {
SearchParameter fooSp = new SearchParameter();
@ -211,6 +219,47 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
}
@Test
public void testIndexFailsIfInvalidSearchParameterExists() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(false);
SearchParameter threadIdSp = new SearchParameter();
threadIdSp.addBase("Communication");
threadIdSp.setCode("has-attachments");
threadIdSp.setType(Enumerations.SearchParamType.REFERENCE);
threadIdSp.setExpression("Communication.payload[1].contentAttachment is not null");
threadIdSp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
threadIdSp.setStatus(Enumerations.PublicationStatus.ACTIVE);
mySearchParameterDao.create(threadIdSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Communication com = new Communication();
com.setStatus(Communication.CommunicationStatus.INPROGRESS);
try {
myCommunicationDao.create(com, mySrd);
fail();
} catch (InternalErrorException e) {
assertThat(e.getMessage(), startsWith("Failed to extract values from resource using FHIRPath \"Communication.payload[1].contentAttachment is not null\": org.hl7.fhir"));
}
}
@Test
public void testRejectSearchParamWithInvalidExpression() {
SearchParameter threadIdSp = new SearchParameter();
threadIdSp.addBase("Communication");
threadIdSp.setCode("has-attachments");
threadIdSp.setType(Enumerations.SearchParamType.REFERENCE);
threadIdSp.setExpression("Communication.payload[1].contentAttachment is not null");
threadIdSp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
threadIdSp.setStatus(Enumerations.PublicationStatus.ACTIVE);
try {
mySearchParameterDao.create(threadIdSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), startsWith("The expression \"Communication.payload[1].contentAttachment is not null\" can not be evaluated and may be invalid: "));
}
}
/**
* See #863
*/

View File

@ -16,6 +16,7 @@ import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Bundle.*;
@ -152,36 +153,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
return input;
}
@Test
public void testTransactionUpdateTwoResourcesWithSameId() {
Bundle request = new Bundle();
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue("DDD");
p.setId("Patient/ABC");
request.addEntry()
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Patient/ABC");
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue("DDD");
p.setId("Patient/ABC");
request.addEntry()
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Patient/ABC");
try {
mySystemDao.transaction(mySrd, request);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("Transaction bundle contains multiple resources with ID: Patient/ABC"));
}
}
@SuppressWarnings("unchecked")
private <T extends org.hl7.fhir.dstu3.model.Resource> T find(Bundle theBundle, Class<T> theType, int theIndex) {
int count = 0;
@ -470,6 +441,17 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
}
}
/**
* See #1044
*/
@Test
public void testStructureDefinitionInBundle() throws IOException {
String input = IOUtils.toString(FhirSystemDaoDstu3Test.class.getResourceAsStream("/bug1044-bundle.xml"), Charsets.UTF_8);
Bundle inputBundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, input);
mySystemDao.transaction(mySrd, inputBundle);
}
@Test
public void testSystemMetaOperation() {
@ -2256,6 +2238,36 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
}
@Test
public void testTransactionUpdateTwoResourcesWithSameId() {
Bundle request = new Bundle();
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue("DDD");
p.setId("Patient/ABC");
request.addEntry()
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Patient/ABC");
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue("DDD");
p.setId("Patient/ABC");
request.addEntry()
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Patient/ABC");
try {
mySystemDao.transaction(mySrd, request);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("Transaction bundle contains multiple resources with ID: Patient/ABC"));
}
}
@Test
public void testTransactionWIthInvalidPlaceholder() throws Exception {
Bundle res = new Bundle();

View File

@ -92,6 +92,9 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
@Qualifier("myBundleDaoR4")
protected IFhirResourceDao<Bundle> myBundleDao;
@Autowired
@Qualifier("myCommunicationDaoR4")
protected IFhirResourceDao<Communication> myCommunicationDao;
@Autowired
@Qualifier("myCarePlanDaoR4")
protected IFhirResourceDao<CarePlan> myCarePlanDao;
@Autowired

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
@ -13,6 +14,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Appointment.AppointmentStatus;
import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
@ -30,6 +32,11 @@ import static org.junit.Assert.*;
public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4SearchCustomSearchParamTest.class);
@After
public void after() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(new DaoConfig().isValidateSearchParameterExpressionsOnSave());
}
@Before
public void beforeDisableResultReuse() {
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
@ -71,7 +78,6 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
}
}
@Test
public void testCreateInvalidParamNoPath() {
SearchParameter fooSp = new SearchParameter();
@ -237,6 +243,30 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
}
@Test
public void testIndexFailsIfInvalidSearchParameterExists() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(false);
SearchParameter threadIdSp = new SearchParameter();
threadIdSp.addBase("Communication");
threadIdSp.setCode("has-attachments");
threadIdSp.setType(Enumerations.SearchParamType.REFERENCE);
threadIdSp.setExpression("Communication.payload[1].contentAttachment is not null");
threadIdSp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
threadIdSp.setStatus(Enumerations.PublicationStatus.ACTIVE);
mySearchParameterDao.create(threadIdSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Communication com = new Communication();
com.setStatus(Communication.CommunicationStatus.INPROGRESS);
try {
myCommunicationDao.create(com, mySrd);
fail();
} catch (InternalErrorException e) {
assertThat(e.getMessage(), startsWith("Failed to extract values from resource using FHIRPath \"Communication.payload[1].contentAttachment is not null\": org.hl7.fhir"));
}
}
@Test
public void testOverrideAndDisableBuiltInSearchParametersWithOverridingDisabled() {
myDaoConfig.setDefaultSearchParamsCanBeOverridden(false);
@ -387,6 +417,23 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
assertThat(results, contains(mrId));
}
@Test
public void testRejectSearchParamWithInvalidExpression() {
SearchParameter threadIdSp = new SearchParameter();
threadIdSp.addBase("Communication");
threadIdSp.setCode("has-attachments");
threadIdSp.setType(Enumerations.SearchParamType.REFERENCE);
threadIdSp.setExpression("Communication.payload[1].contentAttachment is not null");
threadIdSp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
threadIdSp.setStatus(Enumerations.PublicationStatus.ACTIVE);
try {
mySearchParameterDao.create(threadIdSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), startsWith("The expression \"Communication.payload[1].contentAttachment is not null\" can not be evaluated and may be invalid: "));
}
}
@Test
public void testSearchForExtensionReferenceWithNonMatchingTarget() {
SearchParameter siblingSp = new SearchParameter();

View File

@ -0,0 +1,214 @@
<Bundle>
<type value="transaction"/>
<entry>
<resource>
<StructureDefinition xmlns="http://hl7.org/fhir">
<meta>
<lastUpdated value="2017-10-20T11:01:15.167+02:00"/>
</meta>
<url value="http://fhir.de/StructureDefinition/organization-de-basis/0.2"/>
<version value="0.2-WORK"/>
<name value="organization-de-basis-0.2"/>
<title value="Organisation, deutsches Basisprofil (Version 0.2)"/>
<status value="draft"/>
<date value="2018-06-28"/>
<publisher value="HL7 Deutschland e.V. (Technisches Komitee FHIR)"/>
<contact>
<telecom>
<system value="other"/>
<value value="http://hl7.de/technische-komitees/fhir/"/>
</telecom>
</contact>
<description value="Basisprofil für die Verwendung der Organization Ressource in Deutschland."/>
<copyright value="HL7 Deutschland e.V."/>
<fhirVersion value="3.0.1"/>
<kind value="resource"/>
<abstract value="false"/>
<type value="Organization"/>
<baseDefinition value="http://hl7.org/fhir/StructureDefinition/Organization"/>
<differential>
<element id="Organization">
<path value="Organization"/>
<short value="Organisationen im deutschen Gesundheitswesen."/>
<definition
value="Basisprofil für die Repräsentation verschiedener Organisationen mit in Deutschland üblichen Identifiern."/>
</element>
<element id="Organization.extension">
<path value="Organization.extension"/>
<slicing>
<discriminator>
<type value="value"/>
<path value="url"/>
</discriminator>
<rules value="open"/>
</slicing>
</element>
<element id="Organization.extension:betriebsstaetten-hierarchie">
<path value="Organization.extension"/>
<sliceName value="betriebsstaetten-hierarchie"/>
<max value="1"/>
<type>
<code value="Extension"/>
<profile value="http://fhir.de/StructureDefinition/betriebsstaetten-hierarchie/0.2"/>
</type>
</element>
<element id="Organization.identifier">
<path value="Organization.identifier"/>
<slicing>
<discriminator>
<type value="value"/>
<path value="system"/>
</discriminator>
<discriminator>
<type value="value"/>
<path value="value"/>
</discriminator>
<rules value="open"/>
</slicing>
<short value="Identifiziert eine Organisation"/>
<definition
value="Identifikator für die Organisation, mit dem die Organisation über mehrere verschiedene Systeme hinweg identifiziert wird."/>
</element>
<element id="Organization.identifier.system">
<path value="Organization.identifier.system"/>
<min value="1"/>
</element>
<element id="Organization.identifier.value">
<path value="Organization.identifier.value"/>
<min value="1"/>
</element>
<element id="Organization.identifier:Betriebsstaettennummer">
<path value="Organization.identifier"/>
<sliceName value="Betriebsstaettennummer"/>
<short value="Betriebstättennummer (BSNR) vergeben durch die KBV."/>
<definition
value="Die Betriebsstättennummer (BSNR) entspricht der bis zum 30. Juni 2008 gültigen siebenstelligen KV-Abrechnungsnummer, ergänzt um zwei angehängte Nullen. Sie identifiziert die Arztpraxis als abrechnende Einheit und ermöglicht die Zuordnung ärztlicher Leistungen zum Ort der Leistungserbringung. Dabei umfasst der Begriff Arztpraxis auch Medizinische Versorgungszentren (MVZ), Institute, Notfallambulanzen sowie Ermächtigungen an Krankenhäusern. Stellen 12: KV-Landes- oder Bezirksstellenschlüssel[1] Stellen 37: eindeutige Identifikationsnummer der KV, in deren Bereich die Betriebsstätte liegt Stellen 8, 9: „00“"/>
<max value="1"/>
</element>
<element id="Organization.identifier:Betriebsstaettennummer.system">
<path value="Organization.identifier.system"/>
<short value="Namespace für Betriebsstättennnummern der KBV"/>
<definition value="Die URL dient als eindeutiger Name des BSNR-Nummernkreises."/>
<min value="1"/>
<fixedUri value="http://fhir.de/NamingSystem/kbv/bsnr"/>
</element>
<element id="Organization.identifier:Betriebsstaettennummer.value">
<path value="Organization.identifier.value"/>
<short value="Betriebsstättennummer der Organisation"/>
<definition value="Betriebsstättennummer der Organisation"/>
<min value="1"/>
</element>
<element id="Organization.identifier:Betriebsstaettennummer.period">
<path value="Organization.identifier.period"/>
<short value="Zeitraum in welchem der Identifikator gültig ist oder war."/>
<definition value="Zeitraum in welchem der Identifikator gültig ist oder war."/>
</element>
<element id="Organization.identifier:Betriebsstaettennummer.period.start">
<path value="Organization.identifier.period.start"/>
<short value="Beginn der BSNR Gültigkeit"/>
<definition value="Beginn der BSNR Gültigkeit"/>
</element>
<element id="Organization.identifier:Betriebsstaettennummer.period.end">
<path value="Organization.identifier.period.end"/>
<short value="Ende der BSNR Gültigkeit"/>
<definition value="Ende der BSNR Gültigkeit. leer, falls aktuell gültig."/>
</element>
<element id="Organization.identifier:Betriebsstaettennummer.assigner">
<path value="Organization.identifier.assigner"/>
<short value="Organisation welche den Identifikator vergeben hat."/>
<definition value="Organisation welche den Identifikator vergeben hat."/>
</element>
<element id="Organization.identifier:Betriebsstaettennummer.assigner.display">
<extension url="http://hl7.org/fhir/StructureDefinition/elementdefinition-translatable">
<valueBoolean value="true"/>
</extension>
<path value="Organization.identifier.assigner.display"/>
<short value="Name der zuständigen Kassenärztlichen Vereinigung (KV)"/>
<definition
value="Name der zuständigen Kassenärztlichen Vereinigung (KV). z.B.: &quot;KV Baden-Württemberg&quot;"/>
</element>
<element id="Organization.identifier:Institutionskennzeichen">
<path value="Organization.identifier"/>
<sliceName value="Institutionskennzeichen"/>
<short value="IK Nummer vergeben durch die Arbeitsgemeinschaft Institutionskennzeichen."/>
<definition
value="Die Institutionskennzeichen (kurz: IK) sind bundesweit eindeutige, neunstellige Zahlen vergeben durch die Arbeitsgemeinschaft Institutionskennzeichen, mit deren Hilfe Abrechnungen und Qualitätssicherungsmaßnahmen im Bereich der deutschen Sozialversicherung einrichtungsübergreifend abgewickelt werden können. "/>
<max value="1"/>
</element>
<element id="Organization.identifier:Institutionskennzeichen.system">
<path value="Organization.identifier.system"/>
<short value="Namespace für Instituskennzeichen."/>
<min value="1"/>
<fixedUri value=" http://fhir.de/NamingSystem/arge-ik/iknr"/>
</element>
<element id="Organization.identifier:Institutionskennzeichen.value">
<path value="Organization.identifier.value"/>
<short value="Institutskennzeichen der Organisation"/>
<definition value="Institutskennzeichen der Organisation"/>
<min value="1"/>
</element>
<element id="Organization.identifier:ASV-Teamnummer">
<path value="Organization.identifier"/>
<sliceName value="ASV-Teamnummer"/>
<short value="Die ASV-Teamnummer"/>
<definition value="ASV-Teamnummer. Wird nur für Organizations vom Typ ASV-Team vergeben."/>
<max value="1"/>
</element>
<element id="Organization.identifier:ASV-Teamnummer.system">
<path value="Organization.identifier.system"/>
<min value="1"/>
<fixedUri value="http://fhir.de/NamingSystem/asv/teamnummer"/>
</element>
<element id="Organization.identifier:ASV-Teamnummer.value">
<path value="Organization.identifier.value"/>
<min value="1"/>
</element>
<element id="Organization.type">
<path value="Organization.type"/>
<short value="Art(en) der Organisation"/>
<definition value="Art(en) der Organisation"/>
<binding>
<extension url="http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName">
<valueString value="OrganizationType"/>
</extension>
<strength value="extensible"/>
<valueSetUri value="http://fhir.de/ValueSet/arge-ik/klassifikation"/>
</binding>
</element>
<element id="Organization.type.coding">
<path value="Organization.type.coding"/>
<short value="IK Klassifikation der Organisation"/>
<definition value="IK Klassifikation der Organisation"/>
</element>
<element id="Organization.name">
<path value="Organization.name"/>
<short value="Name der Betriebsstätte"/>
<definition
value="Menschenlesbarer Name der Betriebsstätte, z.B.: &quot;Gemeinschaftspraxis Dr. Soundso&quot;"/>
</element>
<element id="Organization.address">
<path value="Organization.address"/>
<type>
<code value="Address"/>
<profile value="http://fhir.de/StructureDefinition/address-de-basis/0.2"/>
</type>
</element>
<element id="Organization.address.state">
<path value="Organization.address.state"/>
<definition value="Name (oder Kürzel) des Bundeslandes."/>
</element>
<element id="Organization.partOf">
<path value="Organization.partOf"/>
<type>
<code value="Reference"/>
<targetProfile value="http://fhir.de/StructureDefinition/organization-de-basis/0.2"/>
</type>
</element>
</differential>
</StructureDefinition>
</resource>
<request>
<method value="POST"/>
</request>
</entry>
</Bundle>

View File

@ -0,0 +1,46 @@
{
"resourceType": "Communication",
"meta": {
"lastUpdated": "2018-07-20T19:34:56.236+05:30",
"tag": [
{
"system": "systemDefined",
"code": "read"
}
]
},
"text": {
"status": "generated"
},
"extension": [
{
"url": "http://telus.com/fhir/StructureDefinition/ext-communication-msgOwner",
"valueString": "17427"
},
{
"url": "http://telus.com/fhir/StructureDefinition/ext-communication-priority",
"valueCode": "normal"
},
{
"url": "http://telus.com/fhir/StructureDefinition/ext-communication-topic",
"valueString": "dsads"
},
{
"url": "http://telus.com/fhir/StructureDefinition/ext-communication-state",
"valueCode": "draft"
},
{
"url": "http://telus.com/fhir/StructureDefinition/ext-communication-thread-id",
"valueString": "bd7bc833-953b-4379-b0b0-be3d898dee40"
}
],
"status": "in-progress",
"recipient": [
{
"reference": "RelatedPerson/17852"
}
],
"sender": {
"reference": "RelatedPerson/17427"
}
}

View File

@ -1,6 +1,10 @@
package ca.uhn.fhir.rest.api.server;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.Date;
import java.util.List;
/*
* #%L
@ -22,51 +26,89 @@ import java.util.Date;
* #L%
*/
import java.util.List;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
public interface IBundleProvider {
/**
* If this method is implemented, provides an ID for the current
* page of results. This ID should be unique (at least within
* the current search as identified by {@link #getUuid()})
* so that it can be used to look up a specific page of results.
* <p>
* This can be used in order to allow the
* server paging mechanism to work using completely
* opaque links (links that do not encode any index/offset
* information), which can be useful on some servers.
* </p>
*
* @since 3.5.0
*/
default String getCurrentPageId() {
return null;
}
/**
* If this method is implemented, provides an ID for the next
* page of results. This ID should be unique (at least within
* the current search as identified by {@link #getUuid()})
* so that it can be used to look up a specific page of results.
* <p>
* This can be used in order to allow the
* server paging mechanism to work using completely
* opaque links (links that do not encode any index/offset
* information), which can be useful on some servers.
* </p>
*
* @since 3.5.0
*/
default String getNextPageId() {
return null;
}
/**
* If this method is implemented, provides an ID for the previous
* page of results. This ID should be unique (at least within
* the current search as identified by {@link #getUuid()})
* so that it can be used to look up a specific page of results.
* <p>
* This can be used in order to allow the
* server paging mechanism to work using completely
* opaque links (links that do not encode any index/offset
* information), which can be useful on some servers.
* </p>
*
* @since 3.5.0
*/
default String getPreviousPageId() {
return null;
}
/**
* Returns the instant as of which this result was created. The
* result of this value is used to populate the <code>lastUpdated</code>
* value on search result/history result bundles.
*/
IPrimitiveType<Date> getPublished();
/**
* Load the given collection of resources by index, plus any additional resources per the
* server's processing rules (e.g. _include'd resources, OperationOutcome, etc.). For example,
* if the method is invoked with index 0,10 the method might return 10 search results, plus an
* additional 20 resources which matched a client's _include specification.
*
* @param theFromIndex
* The low index (inclusive) to return
* @param theToIndex
* The high index (exclusive) to return
* <p>
* Note that if this bundle provider was loaded using a
* page ID (i.e. via {@link ca.uhn.fhir.rest.server.IPagingProvider#retrieveResultList(String, String)}
* because {@link #getNextPageId()} provided a value on the
* previous page, then the indexes should be ignored and the
* whole page returned.
* </p>
*
* @param theFromIndex The low index (inclusive) to return
* @param theToIndex The high index (exclusive) to return
* @return A list of resources. The size of this list must be at least <code>theToIndex - theFromIndex</code>.
*/
List<IBaseResource> getResources(int theFromIndex, int theToIndex);
/**
* Optionally may be used to signal a preferred page size to the server, e.g. because
* the implementing code recognizes that the resources which will be returned by this
* implementation are expensive to load so a smaller page size should be used. The value
* returned by this method will only be used if the client has not explicitly requested
* a page size.
*
* @return Returns the preferred page size or <code>null</code>
*/
Integer preferredPageSize();
/**
* Returns the total number of results which match the given query (exclusive of any
* _include's or OperationOutcome). May return {@literal null} if the total size is not
* known or would be too expensive to calculate.
*/
Integer size();
/**
* Returns the instant as of which this result was valid
*/
IPrimitiveType<Date> getPublished();
/**
* Returns the UUID associated with this search. Note that this
* does not need to return a non-null value unless it a
@ -79,7 +121,29 @@ public interface IBundleProvider {
* IPagingProvider implementation you might use this method to communicate
* the search ID back to the provider.
* </p>
* <p>
* Note that the UUID returned by this method corresponds to
* the search, and not to the individual page.
* </p>
*/
public String getUuid();
String getUuid();
/**
* Optionally may be used to signal a preferred page size to the server, e.g. because
* the implementing code recognizes that the resources which will be returned by this
* implementation are expensive to load so a smaller page size should be used. The value
* returned by this method will only be used if the client has not explicitly requested
* a page size.
*
* @return Returns the preferred page size or <code>null</code>
*/
Integer preferredPageSize();
/**
* Returns the total number of results which match the given query (exclusive of any
* _include's or OperationOutcome). May return {@literal null} if the total size is not
* known or would be too expensive to calculate.
*/
Integer size();
}

View File

@ -0,0 +1,98 @@
package ca.uhn.fhir.rest.server;
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.List;
/**
* Bundle provider that uses named pages instead of counts
*/
public class BundleProviderWithNamedPages extends SimpleBundleProvider {
private String myNextPageId;
private String myCurrentPageId;
private String myPreviousPageId;
/**
* Constructor
*
* @param theResultsInThisPage The complete list of results in the current page. Must not be null.
* @param theSearchId The ID for the search. Note that you should also populate {@link #setNextPageId(String)} and {@link #setPreviousPageId(String)} if these are known. Must not be <code>null</code> or blank.
* @param thePageId The ID for the current page. Note that you should also populate {@link #setNextPageId(String)} and {@link #setPreviousPageId(String)} if these are known. Must not be <code>null</code> or blank.
* @param theTotalResults The total number of result (if this is known), or <code>null</code>
* @see #setNextPageId(String)
* @see #setPreviousPageId(String)
*/
public BundleProviderWithNamedPages(List<IBaseResource> theResultsInThisPage, String theSearchId, String thePageId, Integer theTotalResults) {
super(theResultsInThisPage, theSearchId);
Validate.notNull(theResultsInThisPage, "theResultsInThisPage must not be null");
Validate.notBlank(thePageId, "thePageId must not be null or blank");
setCurrentPageId(thePageId);
setSize(theTotalResults);
}
@Override
public String getCurrentPageId() {
return myCurrentPageId;
}
public BundleProviderWithNamedPages setCurrentPageId(String theCurrentPageId) {
myCurrentPageId = theCurrentPageId;
return this;
}
@Override
public String getNextPageId() {
return myNextPageId;
}
public BundleProviderWithNamedPages setNextPageId(String theNextPageId) {
myNextPageId = theNextPageId;
return this;
}
@Override
public String getPreviousPageId() {
return myPreviousPageId;
}
public BundleProviderWithNamedPages setPreviousPageId(String thePreviousPageId) {
myPreviousPageId = thePreviousPageId;
return this;
}
@Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
return getList(); // indexes are ignored for this provider type
}
@Override
public BundleProviderWithNamedPages setSize(Integer theSize) {
super.setSize(theSize);
return this;
}
}

View File

@ -25,17 +25,24 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
public interface IPagingProvider {
int getDefaultPageSize();
int getMaximumPageSize();
/**
* Stores a result list and returns an ID with which that list can be returned
*/
public String storeResultList(IBundleProvider theList);
/**
* Retrieve a result list by ID
*/
public IBundleProvider retrieveResultList(String theId);
IBundleProvider retrieveResultList(String theSearchId);
/**
* Retrieve a result list by ID
*/
default IBundleProvider retrieveResultList(String theSearchId, String thePageId) {
return null;
}
/**
* Stores a result list and returns an ID with which that list can be returned
*/
String storeResultList(IBundleProvider theList);
}

View File

@ -122,7 +122,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
*/
private String myServerVersion = createPoweredByHeaderProductVersion();
private boolean myStarted;
private Map<String, IResourceProvider> myTypeToProvider = new HashMap<>();
private boolean myUncompressIncomingContents = true;
private boolean myUseBrowserFriendlyContentTypes;
private ITenantIdentificationStrategy myTenantIdentificationStrategy;
@ -376,7 +375,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
try {
count += findResourceMethods(theProvider, clazz);
} catch (ConfigurationException e) {
throw new ConfigurationException("Failure scanning class " + clazz.getSimpleName() + ": " + e.getMessage());
throw new ConfigurationException("Failure scanning class " + clazz.getSimpleName() + ": " + e.getMessage(), e);
}
if (count == 0) {
throw new ConfigurationException("Did not find any annotated RESTful methods on provider class " + theProvider.getClass().getCanonicalName());
@ -1365,14 +1364,9 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
throw new NullPointerException("getResourceType() on class '" + rsrcProvider.getClass().getCanonicalName() + "' returned null");
}
String resourceName = getFhirContext().getResourceDefinition(resourceType).getName();
if (myTypeToProvider.containsKey(resourceName)) {
throw new ConfigurationException("Multiple resource providers return resource type[" + resourceName + "]: First[" + myTypeToProvider.get(resourceName).getClass().getCanonicalName()
+ "] and Second[" + rsrcProvider.getClass().getCanonicalName() + "]");
}
if (!inInit) {
myResourceProviders.add(rsrcProvider);
}
myTypeToProvider.put(resourceName, rsrcProvider);
providedResourceScanner.scanForProvidedResources(rsrcProvider);
newResourceProviders.add(rsrcProvider);
} else {
@ -1384,7 +1378,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
}
if (!newResourceProviders.isEmpty()) {
ourLog.info("Added {} resource provider(s). Total {}", newResourceProviders.size(), myTypeToProvider.size());
ourLog.info("Added {} resource provider(s). Total {}", newResourceProviders.size(), myResourceProviders.size());
for (IResourceProvider provider : newResourceProviders) {
assertProviderIsValid(provider);
findResourceMethods(provider);
@ -1594,7 +1588,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
IResourceProvider rsrcProvider = (IResourceProvider) provider;
Class<? extends IBaseResource> resourceType = rsrcProvider.getResourceType();
String resourceName = getFhirContext().getResourceDefinition(resourceType).getName();
myTypeToProvider.remove(resourceName);
providedResourceScanner.removeProvidedResources(rsrcProvider);
} else {
myPlainProviders.remove(provider);

View File

@ -130,6 +130,18 @@ public class RestfulServerUtils {
public static String createPagingLink(Set<Include> theIncludes, String theServerBase, String theSearchId, int theOffset, int theCount, Map<String, String[]> theRequestParameters, boolean thePrettyPrint,
BundleTypeEnum theBundleType) {
return createPagingLink(theIncludes, theServerBase, theSearchId, theOffset, theCount, theRequestParameters, thePrettyPrint,
theBundleType, null);
}
public static String createPagingLink(Set<Include> theIncludes, String theServerBase, String theSearchId, String thePageId, Map<String, String[]> theRequestParameters, boolean thePrettyPrint,
BundleTypeEnum theBundleType) {
return createPagingLink(theIncludes, theServerBase, theSearchId, null, null, theRequestParameters, thePrettyPrint,
theBundleType, thePageId);
}
private static String createPagingLink(Set<Include> theIncludes, String theServerBase, String theSearchId, Integer theOffset, Integer theCount, Map<String, String[]> theRequestParameters, boolean thePrettyPrint,
BundleTypeEnum theBundleType, String thePageId) {
StringBuilder b = new StringBuilder();
b.append(theServerBase);
b.append('?');
@ -137,14 +149,24 @@ public class RestfulServerUtils {
b.append('=');
b.append(UrlUtil.escapeUrlParam(theSearchId));
b.append('&');
b.append(Constants.PARAM_PAGINGOFFSET);
b.append('=');
b.append(theOffset);
b.append('&');
b.append(Constants.PARAM_COUNT);
b.append('=');
b.append(theCount);
if (theOffset != null) {
b.append('&');
b.append(Constants.PARAM_PAGINGOFFSET);
b.append('=');
b.append(theOffset);
}
if (theCount != null) {
b.append('&');
b.append(Constants.PARAM_COUNT);
b.append('=');
b.append(theCount);
}
if (isNotBlank(thePageId)) {
b.append('&');
b.append(Constants.PARAM_PAGEID);
b.append('=');
b.append(UrlUtil.escapeUrlParam(thePageId));
}
String[] strings = theRequestParameters.get(Constants.PARAM_FORMAT);
if (strings != null && strings.length > 0) {
b.append('&');
@ -442,6 +464,18 @@ public class RestfulServerUtils {
return retVal;
}
private static FhirContext getContextForVersion(FhirContext theContext, FhirVersionEnum theForVersion) {
FhirContext context = theContext;
if (context.getVersion().getVersion() != theForVersion) {
context = myFhirContextMap.get(theForVersion);
if (context == null) {
context = theForVersion.newContext();
myFhirContextMap.put(theForVersion, context);
}
}
return context;
}
private static ResponseEncoding getEncodingForContentType(FhirContext theFhirContext, boolean theStrict, String theContentType) {
EncodingEnum encoding;
if (theStrict) {
@ -476,18 +510,6 @@ public class RestfulServerUtils {
return parser;
}
private static FhirContext getContextForVersion(FhirContext theContext, FhirVersionEnum theForVersion) {
FhirContext context = theContext;
if (context.getVersion().getVersion() != theForVersion) {
context = myFhirContextMap.get(theForVersion);
if (context == null) {
context = theForVersion.newContext();
myFhirContextMap.put(theForVersion, context);
}
}
return context;
}
public static Set<String> parseAcceptHeaderAndReturnHighestRankedOptions(HttpServletRequest theRequest) {
Set<String> retVal = new HashSet<String>();
@ -725,7 +747,7 @@ public class RestfulServerUtils {
try {
return Integer.parseInt(retVal[0]);
} catch (NumberFormatException e) {
ourLog.debug("Failed to parse {} value '{}': {}", new Object[]{theParamName, retVal[0], e});
ourLog.debug("Failed to parse {} value '{}': {}", new Object[] {theParamName, retVal[0], e});
return null;
}
}

View File

@ -20,31 +20,62 @@ package ca.uhn.fhir.rest.server;
* #L%
*/
import java.util.Collections;
import java.util.List;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.Collections;
import java.util.Date;
import java.util.List;
public class SimpleBundleProvider implements IBundleProvider {
private List<IBaseResource> myList;
private final List<IBaseResource> myList;
private final String myUuid;
private Integer myPreferredPageSize;
private Integer mySize;
private IPrimitiveType<Date> myPublished = InstantDt.withCurrentTime();
public SimpleBundleProvider(List<IBaseResource> theList) {
myList = theList;
this(theList, null);
}
public SimpleBundleProvider(IBaseResource theResource) {
myList = Collections.singletonList(theResource);
this(Collections.singletonList(theResource));
}
/**
* Create an empty bundle
*/
public SimpleBundleProvider() {
myList = Collections.emptyList();
this(Collections.emptyList());
}
public SimpleBundleProvider(List<IBaseResource> theList, String theUuid) {
myList = theList;
myUuid = theUuid;
setSize(theList.size());
}
/**
* Returns the results stored in this provider
*/
protected List<IBaseResource> getList() {
return myList;
}
@Override
public IPrimitiveType<Date> getPublished() {
return myPublished;
}
/**
* By default this class uses the object creation date/time (for this object)
* to determine {@link #getPublished() the published date} but this
* method may be used to specify an alternate date/time
*/
public void setPublished(IPrimitiveType<Date> thePublished) {
myPublished = thePublished;
}
@Override
@ -53,23 +84,38 @@ public class SimpleBundleProvider implements IBundleProvider {
}
@Override
public Integer size() {
return myList.size();
}
@Override
public InstantDt getPublished() {
return InstantDt.withCurrentTime();
public String getUuid() {
return myUuid;
}
/**
* Defaults to null
*/
@Override
public Integer preferredPageSize() {
return null;
return myPreferredPageSize;
}
/**
* Sets the preferred page size to be returned by {@link #preferredPageSize()}.
* Default is <code>null</code>.
*/
public void setPreferredPageSize(Integer thePreferredPageSize) {
myPreferredPageSize = thePreferredPageSize;
}
/**
* Sets the total number of results, if this provider
* corresponds to a single page within a larger search result
*/
public SimpleBundleProvider setSize(Integer theSize) {
mySize = theSize;
return this;
}
@Override
public String getUuid() {
return null;
public Integer size() {
return mySize;
}
}

View File

@ -123,8 +123,8 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
}
protected IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes,
IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) {
IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes,
IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) {
IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
int numToReturn;
@ -152,7 +152,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
numToReturn = Math.min(numToReturn, numTotalResults - theOffset);
}
if (numToReturn > 0) {
if (numToReturn > 0 || theResult.getCurrentPageId() != null) {
resourceList = theResult.getResources(theOffset, numToReturn + theOffset);
} else {
resourceList = Collections.emptyList();
@ -166,6 +166,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
searchId = pagingProvider.storeResultList(theResult);
if (isBlank(searchId)) {
ourLog.info("Found {} results but paging provider did not provide an ID to use for paging", numTotalResults);
searchId = null;
}
}
}
@ -183,11 +184,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
}
}
if (hasNull) {
for (Iterator<IBaseResource> iter = resourceList.iterator(); iter.hasNext(); ) {
if (iter.next() == null) {
iter.remove();
}
}
resourceList.removeIf(Objects::isNull);
}
/*
@ -207,7 +204,18 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
String linkPrev = null;
String linkNext = null;
if (searchId != null) {
if (isNotBlank(theResult.getCurrentPageId())) {
// We're doing named pages
searchId = theResult.getUuid();
if (isNotBlank(theResult.getNextPageId())) {
linkNext = RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, theResult.getNextPageId(), theRequest.getParameters(), prettyPrint, theBundleType);
}
if (isNotBlank(theResult.getPreviousPageId())) {
linkPrev = RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, theResult.getPreviousPageId(), theRequest.getParameters(), prettyPrint, theBundleType);
}
} else if (searchId != null) {
// We're doing offset pages
if (numTotalResults == null || theOffset + numToReturn < numTotalResults) {
linkNext = (RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, theOffset + numToReturn, numToReturn, theRequest.getParameters(), prettyPrint, theBundleType));
}

View File

@ -19,16 +19,6 @@ package ca.uhn.fhir.rest.server.method;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Date;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
@ -45,12 +35,22 @@ import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Date;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isBlank;
public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
private final Integer myIdParamIndex;
private String myResourceName;
private final RestOperationTypeEnum myResourceOperationType;
private String myResourceName;
public HistoryMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
super(toReturnType(theMethod, theProvider), theMethod, theContext, theProvider);
@ -87,13 +87,13 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
}
@Override
public RestOperationTypeEnum getRestOperationType() {
return myResourceOperationType;
protected BundleTypeEnum getResponseBundleType() {
return BundleTypeEnum.HISTORY;
}
@Override
protected BundleTypeEnum getResponseBundleType() {
return BundleTypeEnum.HISTORY;
public RestOperationTypeEnum getRestOperationType() {
return myResourceOperationType;
}
@Override
@ -128,7 +128,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
return true;
}
@Override
public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException {
@ -139,18 +139,33 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
Object response = invokeServerMethod(theServer, theRequest, theMethodParams);
final IBundleProvider resources = toResourceList(response);
/*
* We wrap the response so we can verify that it has the ID and version set,
* as is the contract for history
*/
return new IBundleProvider() {
@Override
public String getCurrentPageId() {
return resources.getCurrentPageId();
}
@Override
public String getNextPageId() {
return resources.getNextPageId();
}
@Override
public String getPreviousPageId() {
return resources.getPreviousPageId();
}
@Override
public IPrimitiveType<Date> getPublished() {
return resources.getPublished();
}
@Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
List<IBaseResource> retVal = resources.getResources(theFromIndex, theToIndex);
@ -170,10 +185,10 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
}
return retVal;
}
@Override
public Integer size() {
return resources.size();
public String getUuid() {
return resources.getUuid();
}
@Override
@ -182,8 +197,8 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
}
@Override
public String getUuid() {
return resources.getUuid();
public Integer size() {
return resources.size();
}
};
}

View File

@ -20,18 +20,7 @@ package ca.uhn.fhir.rest.server.method;
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Include;
@ -44,11 +33,25 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.binder.CollectionBinder;
import ca.uhn.fhir.rest.server.method.OperationParameter.IOperationParamConverter;
import ca.uhn.fhir.rest.server.method.ResourceParameter.Mode;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.ReflectionUtil;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class MethodUtil {
public static void extractDescription(SearchParameter theParameter, Annotation[] theAnnotations) {
for (Annotation annotation : theAnnotations) {
if (annotation instanceof Description) {
@ -62,7 +65,7 @@ public class MethodUtil {
}
}
@SuppressWarnings("unchecked")
public static List<IParameter> getResourceParameters(final FhirContext theContext, Method theMethod, Object theProvider, RestOperationTypeEnum theRestfulOperationTypeEnum) {
List<IParameter> parameters = new ArrayList<IParameter>();
@ -90,7 +93,26 @@ public class MethodUtil {
}
if (Collection.class.isAssignableFrom(parameterType)) {
throw new ConfigurationException("Argument #" + paramIndex + " of Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName()
+ "' is of an invalid generic type (can not be a collection of a collection of a collection)");
+ "' is of an invalid generic type (can not be a collection of a collection of a collection)");
}
/*
* If the user is trying to bind IPrimitiveType they are probably
* trying to write code that is compatible across versions of FHIR.
* We'll try and come up with an appropriate subtype to give
* them.
*
* This gets tested in HistoryR4Test
*/
if (IPrimitiveType.class.equals(parameterType)) {
Class<?> genericType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
if (Date.class.equals(genericType)) {
BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("dateTime");
parameterType = dateTimeDef.getImplementingClass();
} else if (String.class.equals(genericType) || genericType == null) {
BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("string");
parameterType = dateTimeDef.getImplementingClass();
}
}
}
@ -141,7 +163,7 @@ public class MethodUtil {
specType = String.class;
} else if ((parameterType != Include.class) || innerCollectionType == null || outerCollectionType != null) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<"
+ Include.class.getSimpleName() + ">");
+ Include.class.getSimpleName() + ">");
} else {
instantiableCollectionType = (Class<? extends Collection<Include>>) CollectionBinder.getInstantiableCollectionType(innerCollectionType, "Method '" + theMethod.getName() + "'");
specType = parameterType;
@ -198,7 +220,7 @@ public class MethodUtil {
} else if (nextAnnotation instanceof Validate.Mode) {
if (parameterType.equals(ValidationModeEnum.class) == false) {
throw new ConfigurationException(
"Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName());
"Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName());
}
param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1).setConverter(new IOperationParamConverter() {
@Override
@ -221,7 +243,7 @@ public class MethodUtil {
} else if (nextAnnotation instanceof Validate.Profile) {
if (parameterType.equals(String.class) == false) {
throw new ConfigurationException(
"Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName());
"Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName());
}
param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1).setConverter(new IOperationParamConverter() {
@Override
@ -244,8 +266,8 @@ public class MethodUtil {
if (param == null) {
throw new ConfigurationException(
"Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName()
+ "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter");
"Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName()
+ "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter");
}
param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType);
@ -256,5 +278,5 @@ public class MethodUtil {
return parameters;
}
}

View File

@ -20,23 +20,30 @@ package ca.uhn.fhir.rest.server.method;
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.*;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
import ca.uhn.fhir.rest.server.exceptions.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class PageMethodBinding extends BaseResourceReturningMethodBinding {
@ -75,34 +82,51 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding {
if (pagingProvider == null) {
throw new InvalidRequestException("This server does not support paging");
}
IBundleProvider resultList = pagingProvider.retrieveResultList(thePagingAction);
Integer offsetI;
int start = 0;
IBundleProvider resultList;
String pageId = null;
String[] pageIdParams = theRequest.getParameters().get(Constants.PARAM_PAGEID);
if (pageIdParams != null) {
if (pageIdParams.length > 0) {
if (isNotBlank(pageIdParams[0])) {
pageId = pageIdParams[0];
}
}
}
if (pageId != null) {
resultList = pagingProvider.retrieveResultList(thePagingAction, pageId);
} else {
resultList = pagingProvider.retrieveResultList(thePagingAction);
offsetI = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_PAGINGOFFSET);
if (offsetI == null || offsetI < 0) {
offsetI = 0;
}
Integer totalNum = resultList.size();
start = offsetI;
if (totalNum != null) {
start = Math.min(start, totalNum - 1);
}
}
// Return an HTTP 409 if the search is not known
if (resultList == null) {
ourLog.info("Client requested unknown paging ID[{}]", thePagingAction);
String msg = getContext().getLocalizer().getMessage(PageMethodBinding.class, "unknownSearchId", thePagingAction);
throw new ResourceGoneException(msg);
}
Integer count = RestfulServerUtils.extractCountParameter(theRequest);
if (count == null) {
count = pagingProvider.getDefaultPageSize();
} else if (count > pagingProvider.getMaximumPageSize()) {
count = pagingProvider.getMaximumPageSize();
}
Integer offsetI = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_PAGINGOFFSET);
if (offsetI == null || offsetI < 0) {
offsetI = 0;
}
Integer totalNum = resultList.size();
int start = offsetI;
if (totalNum != null) {
start = Math.min(start, totalNum - 1);
}
ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequest, theServer.getDefaultResponseEncoding());
Set<Include> includes = new HashSet<Include>();
Set<Include> includes = new HashSet<>();
String[] reqIncludes = theRequest.getParameters().get(Constants.PARAM_INCLUDE);
if (reqIncludes != null) {
for (String nextInclude : reqIncludes) {
@ -125,7 +149,14 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding {
if (responseEncoding != null) {
encodingEnum = responseEncoding.getEncoding();
}
Integer count = RestfulServerUtils.extractCountParameter(theRequest);
if (count == null) {
count = pagingProvider.getDefaultPageSize();
} else if (count > pagingProvider.getMaximumPageSize()) {
count = pagingProvider.getMaximumPageSize();
}
return createBundleFromBundleProvider(theServer, theRequest, count, linkSelf, includes, resultList, start, bundleType, encodingEnum, thePagingAction);
}
@ -140,10 +171,7 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding {
if (pageId == null || pageId.length == 0 || isBlank(pageId[0])) {
return false;
}
if (theRequest.getRequestType() != RequestTypeEnum.GET) {
return false;
}
return true;
return theRequest.getRequestType() == RequestTypeEnum.GET;
}

View File

@ -47,8 +47,8 @@ public class SearchParameter extends BaseQueryParameter {
static final String QUALIFIER_ANY_TYPE = ":*";
static {
ourParamTypes = new HashMap<Class<?>, RestSearchParameterTypeEnum>();
ourParamQualifiers = new HashMap<RestSearchParameterTypeEnum, Set<String>>();
ourParamTypes = new HashMap<>();
ourParamQualifiers = new HashMap<>();
ourParamTypes.put(StringParam.class, RestSearchParameterTypeEnum.STRING);
ourParamTypes.put(StringOrListParam.class, RestSearchParameterTypeEnum.STRING);
@ -124,7 +124,7 @@ public class SearchParameter extends BaseQueryParameter {
*/
@Override
public List<QualifiedParamList> encode(FhirContext theContext, Object theObject) throws InternalErrorException {
ArrayList<QualifiedParamList> retVal = new ArrayList<QualifiedParamList>();
ArrayList<QualifiedParamList> retVal = new ArrayList<>();
// TODO: declaring method should probably have a generic type..
@SuppressWarnings("rawtypes")
@ -197,7 +197,7 @@ public class SearchParameter extends BaseQueryParameter {
}
public void setChainlists(String[] theChainWhitelist, String[] theChainBlacklist) {
myQualifierWhitelist = new HashSet<String>(theChainWhitelist.length);
myQualifierWhitelist = new HashSet<>(theChainWhitelist.length);
myQualifierWhitelist.add(QUALIFIER_ANY_TYPE);
for (int i = 0; i < theChainWhitelist.length; i++) {
@ -211,7 +211,7 @@ public class SearchParameter extends BaseQueryParameter {
}
if (theChainBlacklist.length > 0) {
myQualifierBlacklist = new HashSet<String>(theChainBlacklist.length);
myQualifierBlacklist = new HashSet<>(theChainBlacklist.length);
for (String next : theChainBlacklist) {
if (next.equals(EMPTY_STRING)) {
myQualifierBlacklist.add(EMPTY_STRING);
@ -282,7 +282,7 @@ public class SearchParameter extends BaseQueryParameter {
Set<String> builtInQualifiers = ourParamQualifiers.get(typeEnum);
if (builtInQualifiers != null) {
if (myQualifierWhitelist != null) {
HashSet<String> qualifierWhitelist = new HashSet<String>();
HashSet<String> qualifierWhitelist = new HashSet<>();
qualifierWhitelist.addAll(myQualifierWhitelist);
qualifierWhitelist.addAll(builtInQualifiers);
myQualifierWhitelist = qualifierWhitelist;

View File

@ -20,7 +20,12 @@ package ca.uhn.fhir.rest.server.provider;
* #L%
*/
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.param.TokenAndListParam;
@ -29,8 +34,10 @@ import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -58,7 +65,9 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
private final Class<T> myResourceType;
private final FhirContext myFhirContext;
private final String myResourceName;
protected Map<String, TreeMap<Long, T>> myIdToVersionToResourceMap = new HashMap<>();
protected Map<String, TreeMap<Long, T>> myIdToVersionToResourceMap = new LinkedHashMap<>();
protected Map<String, LinkedList<T>> myIdToHistory = new LinkedHashMap<>();
protected LinkedList<T> myTypeHistory = new LinkedList<>();
private long myNextId;
private AtomicLong myDeleteCount = new AtomicLong(0);
private AtomicLong mySearchCount = new AtomicLong(0);
@ -86,6 +95,8 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
public void clear() {
myNextId = 1;
myIdToVersionToResourceMap.clear();
myIdToHistory.clear();
myTypeHistory.clear();
}
/**
@ -183,6 +194,21 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
return myIdToVersionToResourceMap.get(theIdPart);
}
@History
public List<T> historyInstance(@IdParam IIdType theId) {
LinkedList<T> retVal = myIdToHistory.get(theId.getIdPart());
if (retVal == null) {
throw new ResourceNotFoundException(theId);
}
return retVal;
}
@History
public List<T> historyType() {
return myTypeHistory;
}
@Read(version = true)
public IBaseResource read(@IdParam IIdType theId) {
TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart());
@ -213,8 +239,23 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
}
@Search
public List<IBaseResource> search(
@OptionalParam(name = "_id") TokenAndListParam theIds) {
public List<IBaseResource> searchAll() {
List<IBaseResource> retVal = new ArrayList<>();
for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) {
if (next.isEmpty() == false) {
T nextResource = next.lastEntry().getValue();
retVal.add(nextResource);
}
}
mySearchCount.incrementAndGet();
return retVal;
}
@Search
public List<IBaseResource> searchById(
@RequiredParam(name = "_id") TokenAndListParam theIds) {
List<IBaseResource> retVal = new ArrayList<>();
@ -252,16 +293,52 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
private IIdType store(@ResourceParam T theResource, String theIdPart, Long theVersionIdPart) {
IIdType id = myFhirContext.getVersion().newIdType();
id.setParts(null, myResourceName, theIdPart, Long.toString(theVersionIdPart));
String versionIdPart = Long.toString(theVersionIdPart);
id.setParts(null, myResourceName, theIdPart, versionIdPart);
if (theResource != null) {
theResource.setId(id);
}
TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart);
versionToResource.put(theVersionIdPart, theResource);
/*
* This is a bit of magic to make sure that the versionId attribute
* in the resource being stored accurately represents the version
* that was assigned by this provider
*/
if (theResource != null) {
if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) {
ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, versionIdPart);
} else {
BaseRuntimeChildDefinition metaChild = myFhirContext.getResourceDefinition(myResourceType).getChildByName("meta");
List<IBase> metaValues = metaChild.getAccessor().getValues(theResource);
if (metaValues.size() > 0) {
IBase meta = metaValues.get(0);
BaseRuntimeElementCompositeDefinition<?> metaDef = (BaseRuntimeElementCompositeDefinition<?>) myFhirContext.getElementDefinition(meta.getClass());
BaseRuntimeChildDefinition versionIdDef = metaDef.getChildByName("versionId");
List<IBase> versionIdValues = versionIdDef.getAccessor().getValues(meta);
if (versionIdValues.size() > 0) {
IPrimitiveType<?> versionId = (IPrimitiveType<?>) versionIdValues.get(0);
versionId.setValueAsString(versionIdPart);
}
}
}
}
ourLog.info("Storing resource with ID: {}", id.getValue());
// Store to ID->version->resource map
TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart);
versionToResource.put(theVersionIdPart, theResource);
// Store to type history map
myTypeHistory.addFirst(theResource);
// Store to ID history map
if (!myIdToHistory.containsKey(theIdPart)) {
myIdToHistory.put(theIdPart, new LinkedList<>());
}
myIdToHistory.get(theIdPart).addFirst(theResource);
// Return the newly assigned ID including the version ID
return id;
}

View File

@ -0,0 +1,18 @@
package ca.uhn.fhir.rest.server;
import org.junit.Test;
import static org.junit.Assert.*;
public class SimpleBundleProviderTest {
@Test
public void testPreferredPageSize() {
SimpleBundleProvider p = new SimpleBundleProvider();
assertEquals(null, p.preferredPageSize());
p.setPreferredPageSize(100);
assertEquals(100, p.preferredPageSize().intValue());
}
}

View File

@ -49,7 +49,7 @@ public class FhirR4 implements IFhirVersion {
@Override
public IContextValidationSupport<?, ?, ?, ?, ?, ?> createValidationSupport() {
return ReflectionUtil.newInstanceOfFhirProfileValidationSupport("org.hl7.fhir.r4.hapi.validation.DefaultProfileValidationSupport");
return ReflectionUtil.newInstanceOfFhirProfileValidationSupport("org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport");
}
@Override

View File

@ -9,9 +9,9 @@ package org.hl7.fhir.r4.hapi.rest.server;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -123,9 +123,9 @@ public class R4BundleFactory implements IVersionSpecificBundleFactory {
}
}
/*
* Actually add the resources to the bundle
*/
/*
* Actually add the resources to the bundle
*/
for (IBaseResource next : includedResources) {
BundleEntryComponent entry = myBundle.addEntry();
entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE);
@ -195,7 +195,7 @@ public class R4BundleFactory implements IVersionSpecificBundleFactory {
includedResources.addAll(addedResourcesThisPass);
// Linked resources may themselves have linked resources
references = new ArrayList<ResourceReferenceInfo>();
references = new ArrayList<>();
for (IAnyResource iResource : addedResourcesThisPass) {
List<ResourceReferenceInfo> newReferences = myContext.newTerser().getAllResourceReferences(iResource);
references.addAll(newReferences);
@ -219,9 +219,9 @@ public class R4BundleFactory implements IVersionSpecificBundleFactory {
}
}
/*
* Actually add the resources to the bundle
*/
/*
* Actually add the resources to the bundle
*/
for (IAnyResource next : includedResources) {
BundleEntryComponent entry = myBundle.addEntry();
entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE);

View File

@ -0,0 +1,174 @@
package ca.uhn.fhir.rest.client;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.impl.BaseClient;
import ca.uhn.fhir.rest.client.interceptor.ThreadLocalCapturingInterceptor;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.VersionUtil;
import javassist.tools.web.BadHttpRequest;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.ReaderInputStream;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicStatusLine;
import org.hl7.fhir.r4.model.*;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.Charset;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ThreadLocalCapturingInterceptorR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ThreadLocalCapturingInterceptorR4Test.class);
private static FhirContext ourCtx;
private int myAnswerCount;
private HttpClient myHttpClient;
private HttpResponse myHttpResponse;
@Before
public void before() {
myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs());
ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient);
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
myAnswerCount = 0;
System.setProperty(BaseClient.HAPI_CLIENT_KEEPRESPONSES, "true");
}
private String expectedUserAgent() {
return "HAPI-FHIR/" + VersionUtil.getVersion() + " (FHIR Client; FHIR " + FhirVersionEnum.R4.getFhirVersionString() + "/R4; apache)";
}
private byte[] extractBodyAsByteArray(ArgumentCaptor<HttpUriRequest> capt) throws IOException {
byte[] body = IOUtils.toByteArray(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(0)).getEntity().getContent());
return body;
}
private String extractBodyAsString(ArgumentCaptor<HttpUriRequest> capt) throws IOException {
String body = IOUtils.toString(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(0)).getEntity().getContent(), "UTF-8");
return body;
}
private ArgumentCaptor<HttpUriRequest> prepareClientForSearchResponse() throws IOException {
final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}";
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_JSON + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).then(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
return capt;
}
@Test
public void testSuccessfulSearch() throws Exception {
final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}";
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_JSON + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).then(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
ThreadLocalCapturingInterceptor interceptor = new ThreadLocalCapturingInterceptor();
interceptor.setBufferResponse(true);
client.registerInterceptor(interceptor);
client.setEncoding(EncodingEnum.JSON);
client.search()
.forResource("Device")
.returnBundle(Bundle.class)
.execute();
assertEquals("http://example.com/fhir/Device?_format=json", interceptor.getRequestForCurrentThread().getUri());
assertEquals(200, interceptor.getResponseForCurrentThread().getStatus());
assertEquals(msg, IOUtils.toString(interceptor.getResponseForCurrentThread().createReader()));
}
@Test
public void testFailingSearch() throws Exception {
final String msg = "BAD REQUEST";
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), 400, "Bad Request"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT_WITH_UTF8));
when(myHttpResponse.getEntity().getContent()).then(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) {
return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"));
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
int idx = 0;
ThreadLocalCapturingInterceptor interceptor = new ThreadLocalCapturingInterceptor();
interceptor.setBufferResponse(true);
client.registerInterceptor(interceptor);
client.setEncoding(EncodingEnum.JSON);
try {
client.search()
.forResource("Device")
.returnBundle(Bundle.class)
.execute();
fail();
} catch (InvalidRequestException e) {
// good
}
assertEquals("http://example.com/fhir/Device?_format=json", interceptor.getRequestForCurrentThread().getUri());
assertEquals(400, interceptor.getResponseForCurrentThread().getStatus());
assertEquals(msg, IOUtils.toString(interceptor.getResponseForCurrentThread().createReader()));
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() {
ourCtx = FhirContext.forR4();
}
}

View File

@ -259,12 +259,14 @@ public class CreateR4Test {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
PatientProvider patientProvider = new PatientProvider();
PatientProviderCreate patientProviderCreate = new PatientProviderCreate();
PatientProviderRead patientProviderRead = new PatientProviderRead();
PatientProviderSearch patientProviderSearch = new PatientProviderSearch();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setResourceProviders(patientProvider);
servlet.setResourceProviders(patientProviderCreate, patientProviderRead, patientProviderSearch);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
@ -276,21 +278,7 @@ public class CreateR4Test {
ourClient = builder.build();
}
public static class PatientProvider implements IResourceProvider {
@Create()
public MethodOutcome create(@ResourceParam Patient theIdParam) {
assertNull(theIdParam.getIdElement().getIdPart());
theIdParam.setId("1");
theIdParam.getMeta().setVersionId("1");
return new MethodOutcome(new IdType("Patient", "1"), true).setOperationOutcome(ourReturnOo).setResource(theIdParam);
}
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
public static class PatientProviderRead implements IResourceProvider {
@Read()
public MyPatientWithExtensions read(@IdParam IdType theIdParam) {
@ -300,6 +288,35 @@ public class CreateR4Test {
return p0;
}
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
}
public static class PatientProviderCreate implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Create()
public MethodOutcome create(@ResourceParam Patient theIdParam) {
assertNull(theIdParam.getIdElement().getIdPart());
theIdParam.setId("1");
theIdParam.getMeta().setVersionId("1");
return new MethodOutcome(new IdType("Patient", "1"), true).setOperationOutcome(ourReturnOo).setResource(theIdParam);
}
}
public static class PatientProviderSearch implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Search
public List<IBaseResource> search() {
ArrayList<IBaseResource> retVal = new ArrayList<IBaseResource>();

View File

@ -1,13 +1,14 @@
package ca.uhn.fhir.rest.server;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
@ -15,65 +16,53 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.*;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.annotation.At;
import ca.uhn.fhir.rest.annotation.History;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.Since;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class HistoryDstu2Test {
import static org.junit.Assert.*;
public class HistoryR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(HistoryR4Test.class);
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forDstu2();
private static FhirContext ourCtx = FhirContext.forR4();
private static DateRangeParam ourLastAt;
private static InstantDt ourLastSince;
private static InstantType ourLastSince;
private static IPrimitiveType<Date> ourLastSince2;
private static IPrimitiveType<String> ourLastSince3;
private static IPrimitiveType<?> ourLastSince4;
private static int ourPort;
private static Server ourServer;
@Before
public void before() {
ourLastAt = null;
ourLastSince = null;
}
@Test
public void testSince() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/_history?_since=2005");
HttpResponse status = ourClient.execute(httpGet);
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals(null, ourLastAt);
assertEquals("2005", ourLastSince.getValueAsString());
}
ourLastSince2 = null;
ourLastSince3 = null;
ourLastSince4 = null;
}
@Test
public void testAt() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/_history?_at=gt2001&_at=lt2005");
HttpResponse status = ourClient.execute(httpGet);
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
}
assertEquals(ParamPrefixEnum.GREATERTHAN, ourLastAt.getLowerBound().getPrefix());
assertEquals("2001", ourLastAt.getLowerBound().getValueAsString());
@ -86,56 +75,79 @@ public class HistoryDstu2Test {
public void testInstanceHistory() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/_history?_pretty=true");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
}
Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent);
assertEquals(2, bundle.getEntry().size());
assertEquals("http://localhost:" + ourPort + "/Patient/ih1/_history/1", bundle.getEntry().get(0).getResource().getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/ih1/_history/2", bundle.getEntry().get(1).getResource().getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/ih1/_history/1", bundle.getEntry().get(0).getResource().getId());
assertEquals("http://localhost:" + ourPort + "/Patient/ih1/_history/2", bundle.getEntry().get(1).getResource().getId());
}
}private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HistoryDstu2Test.class);
}
@Test
public void testServerHistory() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/_history");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
}
Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent);
assertEquals(2, bundle.getEntry().size());
assertEquals("http://localhost:" + ourPort + "/Patient/h1/_history/1", bundle.getEntry().get(0).getResource().getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/h1/_history/2", bundle.getEntry().get(1).getResource().getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/h1/_history/1", bundle.getEntry().get(0).getResource().getId());
assertEquals("http://localhost:" + ourPort + "/Patient/h1/_history/2", bundle.getEntry().get(1).getResource().getId());
}
}
@Test
public void testSince() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/_history?_since=2005");
String responseContent;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
}
assertEquals(null, ourLastAt);
assertEquals("2005", ourLastSince.getValueAsString());
assertEquals("2005", ourLastSince2.getValueAsString());
assertTrue(DateTimeType.class.equals(ourLastSince2.getClass()));
assertEquals("2005", ourLastSince3.getValueAsString());
assertTrue(StringType.class.equals(ourLastSince3.getClass()));
assertEquals("2005", ourLastSince4.getValueAsString());
assertTrue(StringType.class.equals(ourLastSince4.getClass()));
}
}
@Test
public void testTypeHistory() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/_history");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
}
assertNull(ourLastAt);
Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent);
assertEquals(2, bundle.getEntry().size());
assertEquals("http://localhost:" + ourPort + "/Patient/th1/_history/1", bundle.getEntry().get(0).getResource().getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/th1/_history/2", bundle.getEntry().get(1).getResource().getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/th1/_history/1", bundle.getEntry().get(0).getResource().getId());
assertEquals("http://localhost:" + ourPort + "/Patient/th1/_history/2", bundle.getEntry().get(1).getResource().getId());
}
}
@ -147,14 +159,15 @@ public class HistoryDstu2Test {
public void testVread() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/_history/456");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
}
Patient bundle = ourCtx.newXmlParser().parseResource(Patient.class, responseContent);
assertEquals("vread", bundle.getNameFirstRep().getFamilyFirstRep().getValue());
assertEquals("vread", bundle.getNameFirstRep().getFamily());
}
}
@ -191,20 +204,27 @@ public class HistoryDstu2Test {
public static class DummyPlainProvider {
@History
public List<Patient> history(@Since InstantDt theSince, @At DateRangeParam theAt) {
public List<Patient> history(@Since InstantType theSince,
@Since IPrimitiveType<Date> theSince2,
@Since IPrimitiveType<String> theSince3,
@Since IPrimitiveType theSince4,
@At DateRangeParam theAt) {
ourLastAt = theAt;
ourLastSince = theSince;
ourLastSince2 = theSince2;
ourLastSince3 = theSince3;
ourLastSince4 = theSince4;
ArrayList<Patient> retVal = new ArrayList<Patient>();
ArrayList<Patient> retVal = new ArrayList<>();
Patient patient = new Patient();
patient.setId("Patient/h1/_history/1");
patient.addName().addFamily("history");
patient.addName().setFamily("history");
retVal.add(patient);
Patient patient2 = new Patient();
patient2.setId("Patient/h1/_history/2");
patient2.addName().addFamily("history");
patient2.addName().setFamily("history");
retVal.add(patient2);
return retVal;
@ -220,17 +240,17 @@ public class HistoryDstu2Test {
}
@History
public List<Patient> instanceHistory(@IdParam IdDt theId) {
ArrayList<Patient> retVal = new ArrayList<Patient>();
public List<Patient> instanceHistory(@IdParam IdType theId) {
ArrayList<Patient> retVal = new ArrayList<>();
Patient patient = new Patient();
patient.setId("Patient/ih1/_history/1");
patient.addName().addFamily("history");
patient.addName().setFamily("history");
retVal.add(patient);
Patient patient2 = new Patient();
patient2.setId("Patient/ih1/_history/2");
patient2.addName().addFamily("history");
patient2.addName().setFamily("history");
retVal.add(patient2);
return retVal;
@ -238,25 +258,25 @@ public class HistoryDstu2Test {
@History
public List<Patient> typeHistory() {
ArrayList<Patient> retVal = new ArrayList<Patient>();
ArrayList<Patient> retVal = new ArrayList<>();
Patient patient = new Patient();
patient.setId("Patient/th1/_history/1");
patient.addName().addFamily("history");
patient.addName().setFamily("history");
retVal.add(patient);
Patient patient2 = new Patient();
patient2.setId("Patient/th1/_history/2");
patient2.addName().addFamily("history");
patient2.addName().setFamily("history");
retVal.add(patient2);
return retVal;
}
@Read(version = true)
public Patient vread(@IdParam IdDt theId) {
public Patient vread(@IdParam IdType theId) {
Patient retVal = new Patient();
retVal.addName().addFamily("vread");
retVal.addName().setFamily("vread");
retVal.setId(theId);
return retVal;
}

View File

@ -0,0 +1,226 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.config.SocketConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Patient;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class PagingUsingNamedPagesR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PagingUsingNamedPagesR4Test.class);
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forR4();
private static int ourPort;
private static Server ourServer;
private static RestfulServer servlet;
private IPagingProvider myPagingProvider;
@Before
public void before() {
myPagingProvider = mock(IPagingProvider.class);
servlet.setPagingProvider(myPagingProvider);
ourNextBundleProvider = null;
}
private Bundle executeAndReturnBundle(HttpGet httpGet, EncodingEnum theExpectEncoding) throws IOException {
Bundle bundle;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
EncodingEnum ct = EncodingEnum.forContentType(status.getEntity().getContentType().getValue().replaceAll(";.*", "").trim());
assertEquals(theExpectEncoding, ct);
assert ct != null;
bundle = ct.newParser(ourCtx).parseResource(Bundle.class, responseContent);
assertEquals(10, bundle.getEntry().size());
}
return bundle;
}
@Test
public void testPagingLinksSanitizeBundleType() throws Exception {
List<IBaseResource> patients0 = createPatients(0, 9);
BundleProviderWithNamedPages provider0 = new BundleProviderWithNamedPages(patients0, "SEARCHID0", "PAGEID0", 1000);
provider0.setNextPageId("PAGEID1");
when(myPagingProvider.retrieveResultList(eq("SEARCHID0"), eq("PAGEID0"))).thenReturn(provider0);
// Initial search
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "?_getpages=SEARCHID0&pageId=PAGEID0&_format=xml&_bundletype=FOO" + UrlUtil.escapeUrlParam("\""));
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertThat(responseContent, not(containsString("FOO\"")));
assertEquals(200, status.getStatusLine().getStatusCode());
EncodingEnum ct = EncodingEnum.forContentType(status.getEntity().getContentType().getValue().replaceAll(";.*", "").trim());
assert ct != null;
Bundle bundle = EncodingEnum.XML.newParser(ourCtx).parseResource(Bundle.class, responseContent);
assertEquals(10, bundle.getEntry().size());
}
}
@Test
public void testPaging() throws Exception {
List<IBaseResource> patients0 = createPatients(0, 9);
BundleProviderWithNamedPages provider0 = new BundleProviderWithNamedPages(patients0, "SEARCHID0", "PAGEID0", 1000);
provider0.setNextPageId("PAGEID1");
when(myPagingProvider.retrieveResultList(eq("SEARCHID0"), eq("PAGEID0"))).thenReturn(provider0);
List<IBaseResource> patients1 = createPatients(10, 19);
BundleProviderWithNamedPages provider1 = new BundleProviderWithNamedPages(patients1, "SEARCHID0", "PAGEID1", 1000);
provider1.setPreviousPageId("PAGEID0");
provider1.setNextPageId("PAGEID2");
when(myPagingProvider.retrieveResultList(eq("SEARCHID0"), eq("PAGEID1"))).thenReturn(provider1);
List<IBaseResource> patients2 = createPatients(20, 29);
BundleProviderWithNamedPages provider2 = new BundleProviderWithNamedPages(patients2, "SEARCHID0", "PAGEID2", 1000);
provider2.setPreviousPageId("PAGEID1");
when(myPagingProvider.retrieveResultList(eq("SEARCHID0"), eq("PAGEID2"))).thenReturn(provider2);
ourNextBundleProvider = provider0;
HttpGet httpGet;
String linkSelf;
String linkNext;
String linkPrev;
Bundle bundle;
// Initial search
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_format=xml");
bundle = executeAndReturnBundle(httpGet, EncodingEnum.XML);
linkSelf = bundle.getLink(Constants.LINK_SELF).getUrl();
assertEquals("http://localhost:"+ourPort+"/Patient?_format=xml", linkSelf);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertEquals("http://localhost:"+ourPort+"?_getpages=SEARCHID0&pageId=PAGEID1&_format=xml&_bundletype=searchset", linkNext);
assertNull(bundle.getLink(Constants.LINK_PREVIOUS));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnBundle(httpGet, EncodingEnum.XML);
linkSelf = bundle.getLink(Constants.LINK_SELF).getUrl();
assertEquals("http://localhost:"+ourPort+"?_getpages=SEARCHID0&pageId=PAGEID1&_format=xml&_bundletype=searchset", linkSelf);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertEquals("http://localhost:"+ourPort+"?_getpages=SEARCHID0&pageId=PAGEID2&_format=xml&_bundletype=searchset", linkNext);
linkPrev = bundle.getLink(Constants.LINK_PREVIOUS).getUrl();
assertEquals("http://localhost:"+ourPort+"?_getpages=SEARCHID0&pageId=PAGEID0&_format=xml&_bundletype=searchset", linkPrev);
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnBundle(httpGet, EncodingEnum.XML);
linkSelf = bundle.getLink(Constants.LINK_SELF).getUrl();
assertEquals("http://localhost:"+ourPort+"?_getpages=SEARCHID0&pageId=PAGEID2&_format=xml&_bundletype=searchset", linkSelf);
assertNull(bundle.getLink(Constants.LINK_NEXT));
linkPrev = bundle.getLink(Constants.LINK_PREVIOUS).getUrl();
assertEquals("http://localhost:"+ourPort+"?_getpages=SEARCHID0&pageId=PAGEID1&_format=xml&_bundletype=searchset", linkPrev);
}
private List<IBaseResource> createPatients(int theLow, int theHigh) {
List<IBaseResource> patients = new ArrayList<>();
for (int id = theLow; id <= theHigh; id++) {
Patient pt = new Patient();
pt.setId("Patient/" + id);
pt.addName().setFamily("FAM" + id);
patients.add(pt);
}
return patients;
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
ServletHandler proxyHandler = new ServletHandler();
servlet = new RestfulServer(ourCtx);
servlet.setDefaultResponseEncoding(EncodingEnum.JSON);
servlet.setPagingProvider(new FifoMemoryPagingProvider(10));
servlet.setResourceProviders(patientProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
builder.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(600000).build());
ourClient = builder.build();
}
private static IBundleProvider ourNextBundleProvider;
public static class DummyPatientResourceProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
@SuppressWarnings("rawtypes")
@Search()
public IBundleProvider search() {
IBundleProvider retVal = ourNextBundleProvider;
Validate.notNull(retVal);
ourNextBundleProvider = null;
return retVal;
}
}
}

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.rest.server.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
@ -19,17 +20,19 @@ import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletException;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
public class HashMapResourceProviderTest {
private static final Logger ourLog = LoggerFactory.getLogger(HashMapResourceProviderTest.class);
private static MyRestfulServer ourRestServer;
private static Server ourListenerServer;
private static IGenericClient ourClient;
@ -100,6 +103,93 @@ public class HashMapResourceProviderTest {
}
}
@Test
public void testHistoryInstance() {
// Create Res 1
Patient p = new Patient();
p.setActive(true);
IIdType id1 = ourClient.create().resource(p).execute().getId();
assertThat(id1.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("1", id1.getVersionIdPart());
// Create Res 2
p = new Patient();
p.setActive(true);
IIdType id2 = ourClient.create().resource(p).execute().getId();
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("1", id2.getVersionIdPart());
// Update Res 2
p = new Patient();
p.setId(id2);
p.setActive(false);
id2 = ourClient.update().resource(p).execute().getId();
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("2", id2.getVersionIdPart());
Bundle history = ourClient
.history()
.onInstance(id2.toUnqualifiedVersionless())
.andReturnBundle(Bundle.class)
.encodedJson()
.prettyPrint()
.execute();
ourLog.debug(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(history));
List<String> ids = history
.getEntry()
.stream()
.map(t -> t.getResource().getIdElement().toUnqualified().getValue())
.collect(Collectors.toList());
assertThat(ids, contains(
id2.toUnqualified().withVersion("2").getValue(),
id2.toUnqualified().withVersion("1").getValue()
));
}
@Test
public void testHistoryType() {
// Create Res 1
Patient p = new Patient();
p.setActive(true);
IIdType id1 = ourClient.create().resource(p).execute().getId();
assertThat(id1.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("1", id1.getVersionIdPart());
// Create Res 2
p = new Patient();
p.setActive(true);
IIdType id2 = ourClient.create().resource(p).execute().getId();
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("1", id2.getVersionIdPart());
// Update Res 2
p = new Patient();
p.setId(id2);
p.setActive(false);
id2 = ourClient.update().resource(p).execute().getId();
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("2", id2.getVersionIdPart());
Bundle history = ourClient
.history()
.onType(Patient.class)
.andReturnBundle(Bundle.class)
.execute();
List<String> ids = history
.getEntry()
.stream()
.map(t -> t.getResource().getIdElement().toUnqualified().getValue())
.collect(Collectors.toList());
ourLog.info("Received IDs: {}", ids);
assertThat(ids, contains(
id2.toUnqualified().withVersion("2").getValue(),
id2.toUnqualified().withVersion("1").getValue(),
id1.toUnqualified().withVersion("1").getValue()
));
}
@Test
public void testSearchAll() {
// Create
@ -112,7 +202,11 @@ public class HashMapResourceProviderTest {
}
// Search
Bundle resp = ourClient.search().forResource("Patient").returnBundle(Bundle.class).execute();
Bundle resp = ourClient
.search()
.forResource("Patient")
.returnBundle(Bundle.class)
.execute();
assertEquals(100, resp.getTotal());
assertEquals(100, resp.getEntry().size());

View File

@ -86,7 +86,7 @@
Resource loading logic for the JPA server has been optimized to
reduce the number of database round trips required when loading
search results where many of the entries have a "forced ID" (an alphanumeric
client-assigned reosurce ID). Thanks to Frank Tao for the pull
client-assigned resource ID). Thanks to Frank Tao for the pull
request!
</action>
<action type="add">
@ -163,6 +163,82 @@
"Repreentation: OperationOutcome" value.
Thanks to Ana Maria Radu for the pul request!
</action>
<action type="add">
The REST Server module now allows more than one Resource Provider
(i.e. more than one implementation of IResourceProvider) to be registered
to the RestfulServer for the same resource type. Previous versions of
HAPI FHIR have always limited support to a single resource provider, but
this limitation did not serve any purpose so it has been removed.
</action>
<action type="add">
The HashMapResourceProvider now supports the type and
instance history operations. In addition, the search method
for the
<![CDATA[<code>_id</code>]]> search parameter now has the
search parameter marked as "required". This means that additional
search methods can be added in subclasses without their intended
searches being routed to the searchById method. Also, the resource
map now uses a LinkedHashMap, so searches return a predictable
order for unit tests.
</action>
<action type="fix">
Fixed a bug when creating a custom search parameter in the JPA
server: if the SearchParameter resource contained an invalid
expression, create/update operations for the given resource would
fail with a cryptic error. SearchParameter expressions are now
validated upon storage, and the SearchParameter will be rejected
if the expression can not be processed.
</action>
<action type="add">
The generic client history operations (history-instance, history-type,
and history-server) now support the
<![CDATA[<code>_at</code>]]> parameter.
</action>
<action type="add">
In the plain server, many resource provider method parameters may now
use a generic
<![CDATA[<code>IPrimitiveType&lt;String&gt;</code>]]>
or
<![CDATA[<code>IPrimitiveType&lt;Date&gt;</code>]]> at the
parameter type. This is handy if you are trying to write code
that works across versions of FHIR.
</action>
<action type="add">
Several convenience methods have been added to the fluent/generic
client interfaces. These methods allow the adding of a sort via a
SortSpec object, as well as specifying search parameters via a plain
Map of Strings.
</action>
<action type="add">
A new client interceptor called ThreadLocalCapturingInterceptor has been
added. This interceptor works the same way as CapturingInterceptor in that
it captures requests and responses for later processing, but it uses
a ThreadLocal object to store them in order to facilitate
use in multithreaded environments.
</action>
<action type="add">
A new constructor has been added to the client BasicAuthInterceptor
allowing credentials to be specified in the form
"username:password" as an alternate to specifying them as two
discrete strings.
</action>
<action type="add">
SimpleBundleProvider has been modified to optionally allow calling
code to specify a search UUID, and a field to allow the preferred
page size to be configured.
</action>
<action type="add">
The JPA server search UUID column has been reduced in length from
40 chars to 36, in order to align with the actual length of the
generated UUIDs.
</action>
<action type="add">
Plain servers using paging may now specify an ID/name for
individual pages being returned, avoiding the need to
respond to arbitrary offset/index requests from the server.
In this mode, page links in search result bundles simply
include the ID to the next page.
</action>
</release>
<release version="3.4.0" date="2018-05-28">
<action type="add">

View File

@ -336,7 +336,34 @@
</macro>
</subsection>
<subsection name="Using Named Pages">
<p>
By default, the paging system uses parameters that are embedded into the
page links for the start index and the page size. This is useful for servers that
can retrieve arbitrary offsets within a search result. For example,
if a given search can easily retrieve "items 5-10 from the given search", then
the mechanism above works well.
</p>
<p>
Another option is to use "named pages", meaning that each
page is simply assigned an ID by the server, and the next/previous
page is requested using this ID.
</p>
<p>
In order to support named pages, the IPagingProvider must
implement the
<code>retrieveResultList(String theSearchId, String thePageId)</code>
method.
</p>
<p>
Then, individual search/history methods may return a
<code>BundleProviderWithNamedPages</code> instead of a simple
<code>IBundleProvider</code>.
</p>
</subsection>
</section>