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 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: 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 - 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: 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 -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,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 * 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_CONTENT = "_content";
public static final String PARAM_COUNT = "_count"; public static final String PARAM_COUNT = "_count";
public static final String PARAM_DELETE = "_delete"; 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_HISTORY = "_history";
public static final String PARAM_INCLUDE = "_include"; public static final String PARAM_INCLUDE = "_include";
public static final String PARAM_INCLUDE_QUALIFIER_RECURSE = ":recurse"; 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_LASTUPDATED = "_lastUpdated";
public static final String PARAM_NARRATIVE = "_narrative"; public static final String PARAM_NARRATIVE = "_narrative";
public static final String PARAM_PAGINGACTION = "_getpages"; 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_QUERY = "_query";
public static final String PARAM_RESPONSE_URL = "response-url"; //Used in messaging 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 = "_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_SEARCH = "_search";
public static final String PARAM_SECURITY = "_security"; public static final String PARAM_SECURITY = "_security";
public static final String PARAM_SINCE = "_since"; 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_ASC = "_sort:asc";
public static final String PARAM_SORT_DESC = "_sort:desc"; public static final String PARAM_SORT_DESC = "_sort:desc";
public static final String PARAM_SUMMARY = "_summary"; public static final String PARAM_SUMMARY = "_summary";
public static final String PARAM_TAG = "_tag"; public static final String PARAM_TAG = "_tag";
public static final String PARAM_TAGS = "_tags"; public static final String PARAM_TAGS = "_tags";
public static final String PARAM_TEXT = "_text"; public static final String PARAM_TEXT = "_text";
public static final String PARAM_VALIDATE = "_validate"; public static final String PARAM_VALIDATE = "_validate";
public static final String PARAMQUALIFIER_MISSING = ":missing"; public static final String PARAMQUALIFIER_MISSING = ":missing";
public static final String PARAMQUALIFIER_MISSING_FALSE = "false"; 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_400_BAD_REQUEST = 400;
public static final int STATUS_HTTP_401_CLIENT_UNAUTHORIZED = 401; 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_403_FORBIDDEN = 403;
public static final int STATUS_HTTP_404_NOT_FOUND = 404; 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_405_METHOD_NOT_ALLOWED = 405;
public static final int STATUS_HTTP_409_CONFLICT = 409; 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_CACHE = "X-Cache";
public static final String HEADER_X_SECURITY_CONTEXT = "X-Security-Context"; public static final String HEADER_X_SECURITY_CONTEXT = "X-Security-Context";
public static final String POWERED_BY_HEADER = "X-Powered-By"; 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 { static {
CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8); CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8);
CHARSET_US_ASCII = Charset.forName("ISO-8859-1");
HashMap<Integer, String> statusNames = new HashMap<>(); HashMap<Integer, String> statusNames = new HashMap<>();
statusNames.put(200, "OK"); statusNames.put(200, "OK");
@ -263,7 +266,7 @@ public class Constants {
statusNames.put(510, "Not Extended"); statusNames.put(510, "Not Extended");
statusNames.put(511, "Network Authentication Required"); statusNames.put(511, "Network Authentication Required");
HTTP_STATUS_NAMES = Collections.unmodifiableMap(statusNames); HTTP_STATUS_NAMES = Collections.unmodifiableMap(statusNames);
Set<String> formatsHtml = new HashSet<>(); Set<String> formatsHtml = new HashSet<>();
formatsHtml.add(CT_HTML); formatsHtml.add(CT_HTML);
formatsHtml.add(FORMAT_HTML); formatsHtml.add(FORMAT_HTML);

View File

@ -34,36 +34,35 @@ public interface IHttpRequest {
* @param theName the header name * @param theName the header name
* @param theValue the header value * @param theValue the header value
*/ */
public void addHeader(String theName, String theValue); void addHeader(String theName, String theValue);
/** /**
* Execute the request * Execute the request
* @return the response * @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 * 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. * @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 * Return the request URI, or null
*/ */
public String getUri(); String getUri();
/** /**
* Return the HTTP verb (e.g. "GET") * Return the HTTP verb (e.g. "GET")
*/ */
public String getHttpVerbName(); String getHttpVerbName();
} }

View File

@ -1,9 +1,10 @@
package ca.uhn.fhir.rest.gclient; package ca.uhn.fhir.rest.gclient;
import java.util.*;
import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.IQueryParameterType;
import java.util.List;
import java.util.Map;
/* /*
* #%L * #%L
* HAPI FHIR - Core Library * HAPI FHIR - Core Library
@ -26,10 +27,32 @@ import ca.uhn.fhir.model.api.IQueryParameterType;
public interface IBaseQuery<T extends IBaseQuery<?>> { 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% * #L%
*/ */
import java.util.Date; import ca.uhn.fhir.rest.param.DateRangeParam;
import org.hl7.fhir.instance.model.api.IPrimitiveType; 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) * 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); 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; 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.Collection;
import java.util.List;
import java.util.Map;
/* /*
* #%L * #%L
@ -22,14 +30,23 @@ import java.util.Collection;
* #L% * #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> { 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 * 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 * 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); 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 * Specifies the <code>_count</code> parameter, which indicates to the server how many resources should be returned
* on a single page. * 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 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 @Deprecated
IQuery<Y> limitTo(int theLimitTo); IQuery<Y> limitTo(int theLimitTo);
/** /**
* Specifies the <code>_count</code> parameter, which indicates to the server how many resources should be returned * Request that the client return the specified bundle type, e.g. <code>org.hl7.fhir.instance.model.Bundle.class</code>
* on a single page. * or <code>ca.uhn.fhir.model.dstu2.resource.Bundle.class</code>
*
* @since 1.4
*/ */
IQuery<Y> count(int theCount); <B extends IBaseBundle> IQuery<B> returnBundle(Class<B> theClass);
/**
* 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);
/** /**
* Add a "_revinclude" specification * Add a "_revinclude" specification
* *
* @since HAPI FHIR 1.0 - Note that option was added to FHIR itself in DSTU2 * @since HAPI FHIR 1.0 - Note that option was added to FHIR itself in DSTU2
*/ */
IQuery<Y> revInclude(Include theIncludeTarget); IQuery<Y> revInclude(Include theIncludeTarget);
/** /**
* Add a "_lastUpdated" specification * Adds a sort criteria
* *
* @since HAPI FHIR 1.1 - Note that option was added to FHIR itself in DSTU2 * @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> * Adds a sort using a {@link SortSpec} object
* or <code>ca.uhn.fhir.model.dstu2.resource.Bundle.class</code> *
* @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} * {@inheritDoc}
@ -132,11 +121,40 @@ public interface IQuery<Y> extends IBaseQuery<IQuery<Y>>, IClientExecutable<IQue
IQuery<Y> where(ICriterion<?> theCriterion); 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 IQuery<Y> withAnyProfile(Collection<String> theProfileUris);
@Override
IQuery<Y> and(ICriterion<?> theCriterion); 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(); // Y execute();

View File

@ -23,13 +23,13 @@ package ca.uhn.fhir.rest.gclient;
public interface ISort<T> { public interface ISort<T> {
/** /**
* Sort ascending * Sort ascending
*/ */
IQuery<T> ascending(IParam theParam); IQuery<T> ascending(IParam theParam);
/** /**
* Sort ascending * Sort ascending
* *
* @param theParam The param name, e.g. "address" * @param theParam The param name, e.g. "address"
*/ */
IQuery<T> ascending(String theParam); 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 * 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 * 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)} * {@link #ascending(IParam)}
*/ */
IQuery<T> defaultOrder(IParam theParam); 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 * Sort descending
* *
* @param theParam A query param - Could be a constant such as <code>Patient.ADDRESS</code> or a custom * @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 such as <code>new StringClientParam("foo")</code>
*/ */
IQuery<T> descending(IParam theParam); IQuery<T> descending(IParam theParam);
/** /**
* Sort ascending * Sort ascending
* *
* @param theParam The param name, e.g. "address" * @param theParam The param name, e.g. "address"
*/ */
IQuery<T> descending(String theParam); IQuery<T> descending(String theParam);

View File

@ -1,8 +1,18 @@
package ca.uhn.fhir.rest.param; package ca.uhn.fhir.rest.param;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.EQUAL; import ca.uhn.fhir.context.FhirContext;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS; import ca.uhn.fhir.model.api.IQueryParameterAnd;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS; 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 java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -25,20 +35,11 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* limitations under the License. * limitations under the License.
* #L% * #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> { public class DateRangeParam implements IQueryParameterAnd<DateParam> {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private DateParam myLowerBound; private DateParam myLowerBound;
private DateParam myUpperBound; 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) * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
* *
* @param theLowerBound * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* 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
* "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.
* 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.
* @param theUpperBound * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* A qualified date param representing the upper date bound (optionally may include time), e.g. * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* "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) { public DateRangeParam(Date theLowerBound, Date theUpperBound) {
this(); this();
@ -84,37 +83,35 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString()); setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
} else { } else {
switch (theDateParam.getPrefix()) { switch (theDateParam.getPrefix()) {
case EQUAL: case EQUAL:
setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString()); setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
break; break;
case STARTS_AFTER: case STARTS_AFTER:
case GREATERTHAN: case GREATERTHAN:
case GREATERTHAN_OR_EQUALS: case GREATERTHAN_OR_EQUALS:
validateAndSet(theDateParam, null); validateAndSet(theDateParam, null);
break; break;
case ENDS_BEFORE: case ENDS_BEFORE:
case LESSTHAN: case LESSTHAN:
case LESSTHAN_OR_EQUALS: case LESSTHAN_OR_EQUALS:
validateAndSet(null, theDateParam); validateAndSet(null, theDateParam);
break; break;
default: default:
// Should not happen // Should not happen
throw new InvalidRequestException("Invalid comparator for date range parameter:" + theDateParam.getPrefix() + ". This is a bug."); 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) * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
* *
* @param theLowerBound * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* 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
* "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.
* 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.
* @param theUpperBound * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* A qualified date param representing the upper date bound (optionally may include time), e.g. * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* "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) { public DateRangeParam(DateParam theLowerBound, DateParam theUpperBound) {
this(); 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) * Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
* *
* @param theLowerBound * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* 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
* "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.
* 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.
* @param theUpperBound * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* A qualified date param representing the upper date bound (optionally may include time), e.g. * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* "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) { public DateRangeParam(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
this(); 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) * Constructor which takes two strings representing the lower and upper bounds of the range (inclusive on both ends)
* *
* @param theLowerBound * @param theLowerBound An unqualified date param representing the lower date bound (optionally may include time), e.g.
* 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
* "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.
* 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.
* @param theUpperBound * "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
* An unqualified date param representing the upper date bound (optionally may include time), e.g. * one may be null, but it is not valid for both to be null.
* "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) { public DateRangeParam(String theLowerBound, String theUpperBound) {
this(); this();
@ -168,35 +161,53 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
myLowerBound = new DateParam(EQUAL, theParsed.getValueAsString()); myLowerBound = new DateParam(EQUAL, theParsed.getValueAsString());
myUpperBound = new DateParam(EQUAL, theParsed.getValueAsString()); myUpperBound = new DateParam(EQUAL, theParsed.getValueAsString());
} }
} else { } else {
switch (theParsed.getPrefix()) { switch (theParsed.getPrefix()) {
case GREATERTHAN: case GREATERTHAN:
case GREATERTHAN_OR_EQUALS: case GREATERTHAN_OR_EQUALS:
if (myLowerBound != null) { if (myLowerBound != null) {
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify a lower bound"); throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify a lower bound");
} }
myLowerBound = theParsed; myLowerBound = theParsed;
break; break;
case LESSTHAN: case LESSTHAN:
case LESSTHAN_OR_EQUALS: case LESSTHAN_OR_EQUALS:
if (myUpperBound != null) { if (myUpperBound != null) {
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify an upper bound"); throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify an upper bound");
} }
myUpperBound = theParsed; myUpperBound = theParsed;
break; break;
default: default:
throw new InvalidRequestException("Unknown comparator: " + theParsed.getPrefix()); 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() { public DateParam getLowerBound() {
return myLowerBound; return myLowerBound;
} }
public DateRangeParam setLowerBound(DateParam theLowerBound) {
validateAndSet(theLowerBound, myUpperBound);
return this;
}
public Date getLowerBoundAsInstant() { public Date getLowerBoundAsInstant() {
if (myLowerBound == null) { if (myLowerBound == null) {
return null; return null;
@ -204,19 +215,19 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
Date retVal = myLowerBound.getValue(); Date retVal = myLowerBound.getValue();
if (myLowerBound.getPrefix() != null) { if (myLowerBound.getPrefix() != null) {
switch (myLowerBound.getPrefix()) { switch (myLowerBound.getPrefix()) {
case GREATERTHAN: case GREATERTHAN:
case STARTS_AFTER: case STARTS_AFTER:
retVal = myLowerBound.getPrecision().add(retVal, 1); retVal = myLowerBound.getPrecision().add(retVal, 1);
break; break;
case EQUAL: case EQUAL:
case GREATERTHAN_OR_EQUALS: case GREATERTHAN_OR_EQUALS:
break; break;
case LESSTHAN: case LESSTHAN:
case APPROXIMATE: case APPROXIMATE:
case LESSTHAN_OR_EQUALS: case LESSTHAN_OR_EQUALS:
case ENDS_BEFORE: case ENDS_BEFORE:
case NOT_EQUAL: case NOT_EQUAL:
throw new IllegalStateException("Unvalid lower bound comparator: " + myLowerBound.getPrefix()); throw new IllegalStateException("Unvalid lower bound comparator: " + myLowerBound.getPrefix());
} }
} }
return retVal; return retVal;
@ -226,6 +237,11 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
return myUpperBound; return myUpperBound;
} }
public DateRangeParam setUpperBound(DateParam theUpperBound) {
validateAndSet(myLowerBound, theUpperBound);
return this;
}
public Date getUpperBoundAsInstant() { public Date getUpperBoundAsInstant() {
if (myUpperBound == null) { if (myUpperBound == null) {
return null; return null;
@ -233,21 +249,21 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
Date retVal = myUpperBound.getValue(); Date retVal = myUpperBound.getValue();
if (myUpperBound.getPrefix() != null) { if (myUpperBound.getPrefix() != null) {
switch (myUpperBound.getPrefix()) { switch (myUpperBound.getPrefix()) {
case LESSTHAN: case LESSTHAN:
case ENDS_BEFORE: case ENDS_BEFORE:
retVal = new Date(retVal.getTime() - 1L); retVal = new Date(retVal.getTime() - 1L);
break; break;
case EQUAL: case EQUAL:
case LESSTHAN_OR_EQUALS: case LESSTHAN_OR_EQUALS:
retVal = myUpperBound.getPrecision().add(retVal, 1); retVal = myUpperBound.getPrecision().add(retVal, 1);
retVal = new Date(retVal.getTime() - 1L); retVal = new Date(retVal.getTime() - 1L);
break; break;
case GREATERTHAN_OR_EQUALS: case GREATERTHAN_OR_EQUALS:
case GREATERTHAN: case GREATERTHAN:
case APPROXIMATE: case APPROXIMATE:
case NOT_EQUAL: case NOT_EQUAL:
case STARTS_AFTER: case STARTS_AFTER:
throw new IllegalStateException("Unvalid upper bound comparator: " + myUpperBound.getPrefix()); throw new IllegalStateException("Unvalid upper bound comparator: " + myUpperBound.getPrefix());
} }
} }
return retVal; return retVal;
@ -273,46 +289,55 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
return bound != null && !bound.isEmpty(); return bound != null && !bound.isEmpty();
} }
@Override
public int hashCode() {
return Objects.hash(myLowerBound, myUpperBound);
}
public boolean isEmpty() { public boolean isEmpty() {
return (getLowerBoundAsInstant() == null) && (getUpperBoundAsInstant() == null); 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; return this;
} }
/** /**
* Sets the range from a pair of dates, inclusive on both ends * Sets the range from a pair of dates, inclusive on both ends
* *
* @param theLowerBound * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* 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
* "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.
* 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.
* @param theUpperBound * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* A qualified date param representing the upper date bound (optionally may include time), e.g. * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* "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) { public void setRangeFromDatesInclusive(Date theLowerBound, Date theUpperBound) {
DateParam lowerBound = theLowerBound != null DateParam lowerBound = theLowerBound != null
? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null; ? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null;
DateParam upperBound = theUpperBound != null DateParam upperBound = theUpperBound != null
? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null; ? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null;
validateAndSet(lowerBound, upperBound); validateAndSet(lowerBound, upperBound);
} }
/** /**
* Sets the range from a pair of dates, inclusive on both ends * Sets the range from a pair of dates, inclusive on both ends
* *
* @param theLowerBound * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* 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
* "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.
* 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.
* @param theUpperBound * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* A qualified date param representing the upper date bound (optionally may include time), e.g. * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* "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) { public void setRangeFromDatesInclusive(DateParam theLowerBound, DateParam theUpperBound) {
validateAndSet(theLowerBound, 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 * Sets the range from a pair of dates, inclusive on both ends. Note that if
* theLowerBound is after theUpperBound, thie method will automatically reverse * theLowerBound is after theUpperBound, thie method will automatically reverse
* the order of the arguments in order to create an inclusive range. * the order of the arguments in order to create an inclusive range.
* *
* @param theLowerBound * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* 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
* "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.
* 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.
* @param theUpperBound * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* A qualified date param representing the upper date bound (optionally may include time), e.g. * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* "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) { public void setRangeFromDatesInclusive(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
IPrimitiveType<Date> lowerBound = theLowerBound; 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 * Sets the range from a pair of dates, inclusive on both ends
* *
* @param theLowerBound * @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* 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
* "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.
* 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.
* @param theUpperBound * "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* A qualified date param representing the upper date bound (optionally may include time), e.g. * theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* "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) { public void setRangeFromDatesInclusive(String theLowerBound, String theUpperBound) {
DateParam lowerBound = theLowerBound != null DateParam lowerBound = theLowerBound != null
? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) ? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound)
: null; : null;
DateParam upperBound = theUpperBound != null DateParam upperBound = theUpperBound != null
? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) ? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound)
: null; : null;
if (isNotBlank(theLowerBound) && isNotBlank(theUpperBound) && theLowerBound.equals(theUpperBound)) { if (isNotBlank(theLowerBound) && isNotBlank(theUpperBound) && theLowerBound.equals(theUpperBound)) {
lowerBound.setPrefix(EQUAL); lowerBound.setPrefix(EQUAL);
upperBound.setPrefix(EQUAL); upperBound.setPrefix(EQUAL);
@ -373,14 +394,22 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
validateAndSet(lowerBound, upperBound); 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; return this;
} }
@Override @Override
public void setValuesAsQueryTokens(FhirContext theContext, String theParamName, List<QualifiedParamList> theParameters) public void setValuesAsQueryTokens(FhirContext theContext, String theParamName, List<QualifiedParamList> theParameters)
throws InvalidRequestException { throws InvalidRequestException {
boolean haveHadUnqualifiedParameter = false; boolean haveHadUnqualifiedParameter = false;
for (QualifiedParamList paramList : theParameters) { for (QualifiedParamList paramList : theParameters) {
@ -391,13 +420,13 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
throw new InvalidRequestException("DateRange parameter does not suppport OR queries"); throw new InvalidRequestException("DateRange parameter does not suppport OR queries");
} }
String param = paramList.get(0); String param = paramList.get(0);
/* /*
* Since ' ' is escaped as '+' we'll be nice to anyone might have accidentally not * Since ' ' is escaped as '+' we'll be nice to anyone might have accidentally not
* escaped theirs * escaped theirs
*/ */
param = param.replace(' ', '+'); param = param.replace(' ', '+');
DateParam parsed = new DateParam(); DateParam parsed = new DateParam();
parsed.setValueAsQueryToken(theContext, theParamName, paramList.getQualifier(), param); parsed.setValueAsQueryToken(theContext, theParamName, paramList.getQualifier(), param);
addParam(parsed); 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 @Override
public String toString() { public String toString() {
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
@ -463,8 +474,8 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
if (hasBound(lowerBound) && hasBound(upperBound)) { if (hasBound(lowerBound) && hasBound(upperBound)) {
if (lowerBound.getValue().getTime() > upperBound.getValue().getTime()) { if (lowerBound.getValue().getTime() > upperBound.getValue().getTime()) {
throw new DataFormatException(format( throw new DataFormatException(format(
"Lower bound of %s is after upper bound of %s", "Lower bound of %s is after upper bound of %s",
lowerBound.getValueAsString(), upperBound.getValueAsString())); lowerBound.getValueAsString(), upperBound.getValueAsString()));
} }
} }
@ -473,13 +484,13 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
lowerBound.setPrefix(GREATERTHAN_OR_EQUALS); lowerBound.setPrefix(GREATERTHAN_OR_EQUALS);
} }
switch (lowerBound.getPrefix()) { switch (lowerBound.getPrefix()) {
case GREATERTHAN: case GREATERTHAN:
case GREATERTHAN_OR_EQUALS: case GREATERTHAN_OR_EQUALS:
default: default:
break; break;
case LESSTHAN: case LESSTHAN:
case LESSTHAN_OR_EQUALS: case LESSTHAN_OR_EQUALS:
throw new DataFormatException("Lower bound comparator must be > or >=, can not be " + lowerBound.getPrefix().getValue()); 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); upperBound.setPrefix(LESSTHAN_OR_EQUALS);
} }
switch (upperBound.getPrefix()) { switch (upperBound.getPrefix()) {
case LESSTHAN: case LESSTHAN:
case LESSTHAN_OR_EQUALS: case LESSTHAN_OR_EQUALS:
default: default:
break; break;
case GREATERTHAN: case GREATERTHAN:
case GREATERTHAN_OR_EQUALS: case GREATERTHAN_OR_EQUALS:
throw new DataFormatException("Upper bound comparator must be < or <=, can not be " + upperBound.getPrefix().getValue()); 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% * #L%
*/ */
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.util.CoverageIgnore; import ca.uhn.fhir.util.CoverageIgnore;
import ca.uhn.fhir.util.OperationOutcomeUtil; 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". * 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> * <p>
* This exception will generally contain an {@link IBaseOperationOutcome} instance which details the failure. * This exception will generally contain an {@link IBaseOperationOutcome} instance which details the failure.
* </p> * </p>
* *
* @see InvalidRequestException Which corresponds to an <b>HTTP 400 Bad Request</b> failure * @see InvalidRequestException Which corresponds to an <b>HTTP 400 Bad Request</b> failure
*/ */
@CoverageIgnore @CoverageIgnore
public class UnprocessableEntityException extends BaseServerResponseException { 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 String DEFAULT_MESSAGE = "Unprocessable Entity";
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
public static final int STATUS_CODE = Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY;
/** /**
* Constructor * Constructor
* *
* @param theMessage * @param theMessage The message to add to the status line
* The message to add to the status line * @param theOperationOutcome The {@link IBaseOperationOutcome} resource to return to the client
* @param theOperationOutcome The {@link IBaseOperationOutcome} resource to return to the client
*/ */
public UnprocessableEntityException(String theMessage, IBaseOperationOutcome theOperationOutcome) { public UnprocessableEntityException(String theMessage, IBaseOperationOutcome theOperationOutcome) {
super(STATUS_CODE, theMessage, theOperationOutcome); super(STATUS_CODE, theMessage, theOperationOutcome);
} }
/** /**
* Constructor which accepts an {@link IBaseOperationOutcome} resource which will be supplied in the response * Constructor which accepts an {@link IBaseOperationOutcome} resource which will be supplied in the response
* *
* @deprecated Use constructor with FhirContext argument * @deprecated Use constructor with FhirContext argument
*/ */
@Deprecated @Deprecated
@ -79,6 +77,13 @@ public class UnprocessableEntityException extends BaseServerResponseException {
super(STATUS_CODE, theMessage); 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. * 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) { public static Class<?> getGenericCollectionTypeOfMethodParameter(Method theMethod, int theParamIndex) {
Class<?> type; Class<?> type;
Type genericParameterType = theMethod.getGenericParameterTypes()[theParamIndex]; Type genericParameterType = theMethod.getGenericParameterTypes()[theParamIndex];
if (Class.class.equals(genericParameterType)) { if (Class.class.equals(genericParameterType) || Class.class.equals(genericParameterType.getClass())) {
return null; return null;
} }
ParameterizedType collectionType = (ParameterizedType) genericParameterType; 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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 # 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.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. 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.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.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.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.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.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} 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; package ca.uhn.fhir.okhttp.client;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -74,11 +75,11 @@ public class OkHttpRestfulRequest implements IHttpRequest {
@Override @Override
public Map<String, List<String>> getAllHeaders() { public Map<String, List<String>> getAllHeaders() {
return myRequestBuilder.build().headers().toMultimap(); return Collections.unmodifiableMap(myRequestBuilder.build().headers().toMultimap());
} }
@Override @Override
public String getRequestBodyFromStream() throws IOException { public String getRequestBodyFromStream() {
// returning null to indicate this is not supported, as documented in IHttpRequest's contract // returning null to indicate this is not supported, as documented in IHttpRequest's contract
return null; return null;
} }

View File

@ -22,10 +22,7 @@ package ca.uhn.fhir.rest.client.apache;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.HashMap; import java.util.*;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.StopWatch;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
@ -70,14 +67,14 @@ public class ApacheHttpRequest implements IHttpRequest {
@Override @Override
public Map<String, List<String>> getAllHeaders() { 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()) { for (Header header : myRequest.getAllHeaders()) {
if (!result.containsKey(header.getName())) { 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()); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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) { private static void addParam(Map<String, List<String>> params, String parameterName, String parameterValue) {
if (!params.containsKey(parameterName)) { if (!params.containsKey(parameterName)) {
params.put(parameterName, new ArrayList<String>()); params.put(parameterName, new ArrayList<>());
} }
params.get(parameterName).add(parameterValue); params.get(parameterName).add(parameterValue);
} }
@ -516,6 +516,19 @@ public class GenericClient extends BaseClient implements IGenericClient {
return (QUERY) this; 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") @SuppressWarnings("unchecked")
@Override @Override
public QUERY where(Map<String, List<IQueryParameterType>> theCriterion) { 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 Class<? extends IBaseBundle> myReturnType;
private IPrimitiveType mySince; private IPrimitiveType mySince;
private Class<? extends IBaseResource> myType; private Class<? extends IBaseResource> myType;
private DateRangeParam myAt;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
@ -752,6 +766,12 @@ public class GenericClient extends BaseClient implements IGenericClient {
return this; return this;
} }
@Override
public IHistoryTyped at(DateRangeParam theDateRangeParam) {
myAt = theDateRangeParam;
return this;
}
@Override @Override
public IHistoryTyped count(Integer theCount) { public IHistoryTyped count(Integer theCount) {
myCount = theCount; myCount = theCount;
@ -774,7 +794,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
id = null; id = null;
} }
HttpGetClientInvocation invocation = HistoryMethodBinding.createHistoryInvocation(myContext, resourceName, id, mySince, myCount); HttpGetClientInvocation invocation = HistoryMethodBinding.createHistoryInvocation(myContext, resourceName, id, mySince, myCount, myAt);
IClientResponseHandler handler; IClientResponseHandler handler;
handler = new ResourceResponseHandler(myReturnType, getPreferResponseTypes(myType)); handler = new ResourceResponseHandler(myReturnType, getPreferResponseTypes(myType));
@ -1847,6 +1867,16 @@ public class GenericClient extends BaseClient implements IGenericClient {
return retVal; 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 @Override
public IQuery usingStyle(SearchStyleEnum theStyle) { public IQuery usingStyle(SearchStyleEnum theStyle) {
mySearchStyle = theStyle; mySearchStyle = theStyle;
@ -2133,6 +2163,18 @@ public class GenericClient extends BaseClient implements IGenericClient {
myFor = theFor; 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 @Override
public IQuery ascending(IParam theParam) { public IQuery ascending(IParam theParam) {
myParamName = Constants.PARAM_SORT_ASC; myParamName = Constants.PARAM_SORT_ASC;
@ -2157,6 +2199,14 @@ public class GenericClient extends BaseClient implements IGenericClient {
return myFor; return myFor;
} }
@Override
public IQuery defaultOrder(String theParam) {
myParamName = Constants.PARAM_SORT;
myDirection = null;
myParamValue = theParam;
return myFor;
}
@Override @Override
public IQuery descending(IParam theParam) { public IQuery descending(IParam theParam) {
myParamName = Constants.PARAM_SORT_DESC; 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.api.Constants;
import ca.uhn.fhir.rest.client.api.*; import ca.uhn.fhir.rest.client.api.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 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 * 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 myUsername;
private String myPassword; private String myPassword;
private String myHeaderValue;
public BasicAuthInterceptor(String theUsername, String thePassword) { /**
super(); * @param theUsername The username
myUsername = theUsername; * @param thePassword The password
myPassword = thePassword; */
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 @Override
public void interceptRequest(IHttpRequest theRequest) { public void interceptRequest(IHttpRequest theRequest) {
String authorizationUnescaped = StringUtils.defaultString(myUsername) + ":" + StringUtils.defaultString(myPassword); theRequest.addHeader(Constants.HEADER_AUTHORIZATION, myHeaderValue);
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));
} }
@Override @Override
@ -66,6 +73,4 @@ public class BasicAuthInterceptor implements IClientInterceptor {
// nothing // nothing
} }
} }

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.client.interceptor;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,19 +20,20 @@ package ca.uhn.fhir.rest.client.interceptor;
* #L% * #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.IClientInterceptor;
import ca.uhn.fhir.rest.client.api.IHttpRequest; import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse; import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 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 * Client interceptor which simply captures request and response objects and stores them so that they can be inspected after a client
* call has returned * call has returned
*
* @see ThreadLocalCapturingInterceptor for an interceptor that uses a ThreadLocal in order to work in multithreaded environments
*/ */
public class CapturingInterceptor implements IClientInterceptor { public class CapturingInterceptor implements IClientInterceptor {
@ -63,10 +64,16 @@ public class CapturingInterceptor implements IClientInterceptor {
@Override @Override
public void interceptResponse(IHttpResponse theResponse) { public void interceptResponse(IHttpResponse theResponse) {
//Buffer the reponse to avoid errors when content has already been read and the entity is not repeatable //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 { try {
if(theResponse.getResponse() instanceof HttpResponse) { if (theResponse.getResponse() instanceof HttpResponse) {
HttpEntity entity = ((HttpResponse) theResponse.getResponse()).getEntity(); HttpEntity entity = ((HttpResponse) theResponse.getResponse()).getEntity();
if( entity != null && !entity.isRepeatable()){ if (entity != null && !entity.isRepeatable()) {
theResponse.bufferEntity(); theResponse.bufferEntity();
} }
} else { } else {
@ -75,9 +82,6 @@ public class CapturingInterceptor implements IClientInterceptor {
} catch (IOException e) { } catch (IOException e) {
throw new InternalErrorException("Unable to buffer the entity for capturing", 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.lang.reflect.Modifier;
import java.util.Date; 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 org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
@ -96,7 +98,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
} }
String historyId = id != null ? id.getIdPart() : null; 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) { if (theArgs != null) {
for (int idx = 0; idx < theArgs.length; idx++) { for (int idx = 0; idx < theArgs.length; idx++) {
@ -108,7 +110,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
return retVal; 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(); StringBuilder b = new StringBuilder();
if (theResourceName != null) { if (theResourceName != null) {
b.append(theResourceName); b.append(theResourceName);
@ -129,8 +131,18 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
} }
if (theLimit != null) { if (theLimit != null) {
b.append(haveParam ? '&' : '?'); b.append(haveParam ? '&' : '?');
haveParam = true;
b.append(Constants.PARAM_COUNT).append('=').append(theLimit); 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()); HttpGetClientInvocation retVal = new HttpGetClientInvocation(theContext, b.toString());
return retVal; 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.Entity;
import javax.ws.rs.client.Invocation; import javax.ws.rs.client.Invocation;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.HashMap; import java.util.*;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/** /**
* A Http Request based on JaxRs. This is an adapter around the class * 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 { 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 Invocation.Builder myRequest;
private RequestTypeEnum myRequestType; private RequestTypeEnum myRequestType;
private Entity<?> myEntity; private Entity<?> myEntity;
@ -55,7 +52,7 @@ public class JaxRsHttpRequest implements IHttpRequest {
@Override @Override
public void addHeader(String theName, String theValue) { public void addHeader(String theName, String theValue) {
if (!myHeaders.containsKey(theName)) { if (!myHeaders.containsKey(theName)) {
myHeaders.put(theName, new LinkedList<String>()); myHeaders.put(theName, new LinkedList<>());
} }
myHeaders.get(theName).add(theValue); myHeaders.get(theName).add(theValue);
getRequest().header(theName, theValue); getRequest().header(theName, theValue);
@ -71,7 +68,7 @@ public class JaxRsHttpRequest implements IHttpRequest {
@Override @Override
public Map<String, List<String>> getAllHeaders() { 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") @Bean(name = "myResourceCountsCache")
public ResourceCountCache resourceCountsCache() { public ResourceCountCache resourceCountsCache() {
ResourceCountCache retVal = new ResourceCountCache(() -> systemDaoR4().getResourceCounts()); ResourceCountCache retVal = new ResourceCountCache(() -> systemDaoR4().getResourceCounts());
retVal.setCacheMillis(60 * DateUtils.MILLIS_PER_SECOND); retVal.setCacheMillis(10 * DateUtils.MILLIS_PER_MINUTE);
return retVal; return retVal;
} }

View File

@ -74,7 +74,6 @@ import javax.persistence.criteria.Root;
import javax.xml.stream.events.Characters; import javax.xml.stream.events.Characters;
import javax.xml.stream.events.XMLEvent; import javax.xml.stream.events.XMLEvent;
import java.io.CharArrayWriter; import java.io.CharArrayWriter;
import java.io.UnsupportedEncodingException;
import java.text.Normalizer; import java.text.Normalizer;
import java.util.*; import java.util.*;
import java.util.Map.Entry; 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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) { if (theEntity.getDeleted() == null) {
encoding = myConfig.getResourceEncoding(); encoding = myConfig.getResourceEncoding();
IParser parser = encoding.newParser(myContext); Set<String> excludeElements = EXCLUDE_ELEMENTS_IN_ENCODED;
parser.setDontEncodeElements(EXCLUDE_ELEMENTS_IN_ENCODED);
String encoded = parser.encodeResourceToString(theResource);
theEntity.setFhirVersion(myContext.getVersion().getVersion()); 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) { if (theUpdateHash) {
HashFunction sha256 = Hashing.sha256(); HashFunction sha256 = Hashing.sha256();
@ -1664,22 +1648,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return null; return null;
} }
// 2. get The text // 2. get The text
String resourceText = null; String resourceText = decodeResource(resourceBytes, resourceEncoding);
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;
}
// 3. Use the appropriate custom type if one is specified in the context // 3. Use the appropriate custom type if one is specified in the context
Class<R> resourceType = theResourceType; 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 * This method is used to create a set of all possible combinations of
* parameters across a set of search parameters. An example of why * 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.FhirTerser;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
@ -47,6 +48,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
private DaoConfig myDaoConfig; private DaoConfig myDaoConfig;
@Autowired @Autowired
private ISearchParamRegistry mySearchParamRegistry; private ISearchParamRegistry mySearchParamRegistry;
public BaseSearchParamExtractor() { public BaseSearchParamExtractor() {
super(); super();
} }
@ -73,31 +75,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
return refs; return refs;
} }
protected List<Object> extractValues(String thePaths, IBaseResource theResource) { protected abstract 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 FhirContext getContext() { protected FhirContext getContext() {
return myContext; return myContext;

View File

@ -20,9 +20,9 @@ import java.util.*;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -144,6 +144,7 @@ public class DaoConfig {
private boolean myExpungeEnabled; private boolean myExpungeEnabled;
private int myReindexThreadCount; private int myReindexThreadCount;
private Set<String> myBundleTypesAllowedForStorage; private Set<String> myBundleTypesAllowedForStorage;
private boolean myValidateSearchParameterExpressionsOnSave = true;
/** /**
* Constructor * Constructor
@ -786,7 +787,6 @@ public class DaoConfig {
this.myAllowContainsSearches = theAllowContainsSearches; this.myAllowContainsSearches = theAllowContainsSearches;
} }
/** /**
* If set to <code>true</code> (default is <code>false</code>) the server will allow * 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 * resources to have references to external servers. For example if this server is
@ -1188,6 +1188,34 @@ public class DaoConfig {
myUniqueIndexesEnabled = theUniqueIndexesEnabled; 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 * 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 * 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.ResourceTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.SearchParamTypeEnum; import ca.uhn.fhir.model.dstu2.valueset.SearchParamTypeEnum;
import ca.uhn.fhir.model.primitive.BoundCodeDt; 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.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.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -83,7 +77,7 @@ public class FhirResourceDaoSearchParameterDstu2 extends FhirResourceDaoDstu2<Se
FhirContext context = getContext(); FhirContext context = getContext();
SearchParamTypeEnum type = theResource.getTypeElement().getValueAsEnum(); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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. * limitations under the License.
* #L% * #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.ConfigurationException;
import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.entity.*; 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.base.composite.BaseHumanNameDt;
import ca.uhn.fhir.model.dstu2.composite.*; 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.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.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.dstu2.valueset.RestfulSecurityServiceEnum;
import ca.uhn.fhir.model.primitive.*; import ca.uhn.fhir.model.primitive.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 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 { public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implements ISearchParamExtractor {
@ -81,13 +88,13 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamDates(ca.uhn.fhir.jpa.entity.ResourceTable, * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamDates(ca.uhn.fhir.jpa.entity.ResourceTable,
* ca.uhn.fhir.model.api.IResource) * ca.uhn.fhir.model.api.IResource)
*/ */
@Override @Override
public Set<ResourceIndexedSearchParamDate> extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) { public Set<ResourceIndexedSearchParamDate> extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) {
HashSet<ResourceIndexedSearchParamDate> retVal = new HashSet<ResourceIndexedSearchParamDate>(); HashSet<ResourceIndexedSearchParamDate> retVal = new HashSet<>();
Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource); Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
for (RuntimeSearchParam nextSpDef : searchParams) { for (RuntimeSearchParam nextSpDef : searchParams) {
@ -142,7 +149,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamNumber(ca.uhn.fhir.jpa.entity.ResourceTable, * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamNumber(ca.uhn.fhir.jpa.entity.ResourceTable,
* ca.uhn.fhir.model.api.IResource) * ca.uhn.fhir.model.api.IResource)
*/ */
@ -196,7 +203,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
* org.unitsofmeasurement.quantity.Quantity<?>>) * org.unitsofmeasurement.quantity.Quantity<?>>)
* UCUMFormat.getCaseInsensitiveInstance().parse(nextValue.getCode().getValue(), null); if * UCUMFormat.getCaseInsensitiveInstance().parse(nextValue.getCode().getValue(), null); if
* (unit.isCompatible(UCUM.DAY)) { * (unit.isCompatible(UCUM.DAY)) {
* *
* @SuppressWarnings("unchecked") PhysicsUnit<org.unitsofmeasurement.quantity.Time> timeUnit = * @SuppressWarnings("unchecked") PhysicsUnit<org.unitsofmeasurement.quantity.Time> timeUnit =
* (PhysicsUnit<Time>) unit; UnitConverter conv = timeUnit.getConverterTo(UCUM.DAY); double * (PhysicsUnit<Time>) unit; UnitConverter conv = timeUnit.getConverterTo(UCUM.DAY); double
* dayValue = conv.convert(nextValue.getValue().getValue().doubleValue()); DurationDt newValue = * dayValue = conv.convert(nextValue.getValue().getValue().doubleValue()); DurationDt newValue =
@ -251,7 +258,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamQuantity(ca.uhn.fhir.jpa.entity.ResourceTable, * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamQuantity(ca.uhn.fhir.jpa.entity.ResourceTable,
* ca.uhn.fhir.model.api.IResource) * ca.uhn.fhir.model.api.IResource)
*/ */
@ -305,7 +312,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamStrings(ca.uhn.fhir.jpa.entity.ResourceTable, * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamStrings(ca.uhn.fhir.jpa.entity.ResourceTable,
* ca.uhn.fhir.model.api.IResource) * ca.uhn.fhir.model.api.IResource)
*/ */
@ -314,7 +321,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
HashSet<ResourceIndexedSearchParamString> retVal = new HashSet<ResourceIndexedSearchParamString>(); HashSet<ResourceIndexedSearchParamString> retVal = new HashSet<ResourceIndexedSearchParamString>();
String resourceName = getContext().getResourceDefinition(theResource).getName(); String resourceName = getContext().getResourceDefinition(theResource).getName();
Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource); Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
for (RuntimeSearchParam nextSpDef : searchParams) { for (RuntimeSearchParam nextSpDef : searchParams) {
if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.STRING) { if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.STRING) {
@ -389,7 +396,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamTokens(ca.uhn.fhir.jpa.entity.ResourceTable, * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamTokens(ca.uhn.fhir.jpa.entity.ResourceTable,
* ca.uhn.fhir.model.api.IResource) * 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) { private static <T extends Enum<?>> String extractSystem(BoundCodeDt<T> theBoundCode) {
if (theBoundCode.getValueAsEnum() != null) { if (theBoundCode.getValueAsEnum() != null) {
IValueSetEnumBinder<T> binder = theBoundCode.getBinder(); IValueSetEnumBinder<T> binder = theBoundCode.getBinder();

View File

@ -74,7 +74,7 @@ public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3<Se
FhirContext context = getContext(); FhirContext context = getContext();
Enumerations.SearchParamType type = theResource.getType(); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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. * limitations under the License.
* #L% * #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.StringUtils;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext; 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.model.Patient.PatientCommunicationComponent;
import org.hl7.fhir.dstu3.utils.FHIRPathEngine; import org.hl7.fhir.dstu3.utils.FHIRPathEngine;
import org.hl7.fhir.exceptions.FHIRException; 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 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 static org.apache.commons.lang3.StringUtils.*;
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;
public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implements ISearchParamExtractor { public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implements ISearchParamExtractor {
@ -61,11 +61,6 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
private HapiWorkerContext myWorkerContext; private HapiWorkerContext myWorkerContext;
@PostConstruct
public void start() {
myWorkerContext = new HapiWorkerContext(getContext(), myValidationSupport);
}
/** /**
* Constructor * Constructor
*/ */
@ -78,6 +73,17 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
myValidationSupport = theValidationSupport; 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) { private void addSearchTerm(ResourceTable theEntity, Set<ResourceIndexedSearchParamString> retVal, String resourceName, String searchTerm) {
if (isBlank(searchTerm)) { if (isBlank(searchTerm)) {
return; return;
@ -100,6 +106,23 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
retVal.add(nextEntity); 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 @Override
public Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) { public Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
// TODO: implement // TODO: implement
@ -108,7 +131,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamDates(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource) * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamDates(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/ */
@Override @Override
@ -195,12 +218,12 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamNumber(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource) * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamNumber(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/ */
@Override @Override
public HashSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) { public HashSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) {
HashSet<ResourceIndexedSearchParamNumber> retVal = new HashSet<ResourceIndexedSearchParamNumber>(); HashSet<ResourceIndexedSearchParamNumber> retVal = new HashSet<>();
Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource); Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
for (RuntimeSearchParam nextSpDef : searchParams) { 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<?>>) * @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)) { * 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); * @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); * 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; } * newValue.setCode(UCUM.DAY.getSymbol()); newValue.setValue(dayValue); nextValue=newValue; }
@ -298,7 +321,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamQuantity(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource) * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamQuantity(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/ */
@Override @Override
@ -349,20 +372,9 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
return retVal; 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) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamStrings(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource) * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamStrings(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/ */
@Override @Override
@ -462,7 +474,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamTokens(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource) * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamTokens(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/ */
@Override @Override
@ -674,14 +686,14 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
} }
private void extractTokensFromCodeableConcept(List<String> theSystems, List<String> theCodes, CodeableConcept theCodeableConcept, ResourceTable theEntity, 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()) { for (Coding nextCoding : theCodeableConcept.getCoding()) {
extractTokensFromCoding(theSystems, theCodes, theEntity, theListToPopulate, theParameterDef, nextCoding); extractTokensFromCoding(theSystems, theCodes, theEntity, theListToPopulate, theParameterDef, nextCoding);
} }
} }
private void extractTokensFromCoding(List<String> theSystems, List<String> theCodes, ResourceTable theEntity, Set<BaseResourceIndexedSearchParam> theListToPopulate, 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()) { if (nextCoding != null && !nextCoding.isEmpty()) {
String nextSystem = nextCoding.getSystemElement().getValueAsString(); String nextSystem = nextCoding.getSystemElement().getValueAsString();
@ -706,16 +718,18 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
FHIRPathEngine fp = new FHIRPathEngine(myWorkerContext); FHIRPathEngine fp = new FHIRPathEngine(myWorkerContext);
List<Object> values = new ArrayList<>(); List<Object> values = new ArrayList<>();
try { String[] nextPathsSplit = SPLIT.split(thePaths);
String[] nextPathsSplit = SPLIT.split(thePaths); for (String nextPath : nextPathsSplit) {
for (String nextPath : nextPathsSplit) { List<Base> allValues;
List<Base> allValues = fp.evaluate((Base) theResource, trim(nextPath)); try {
if (allValues.isEmpty() == false) { allValues = fp.evaluate((Base) theResource, trim(nextPath));
values.addAll(allValues); } 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++) { for (int i = 0; i < values.size(); i++) {
@ -730,28 +744,16 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
return values; 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 @VisibleForTesting
void setValidationSupportForTesting(org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport theValidationSupport) { void setValidationSupportForTesting(org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport theValidationSupport) {
myValidationSupport = theValidationSupport; myValidationSupport = theValidationSupport;
} }
@PostConstruct
public void start() {
myWorkerContext = new HapiWorkerContext(getContext(), myValidationSupport);
}
private static <T extends Enum<?>> String extractSystem(Enumeration<T> theBoundCode) { private static <T extends Enum<?>> String extractSystem(Enumeration<T> theBoundCode) {
if (theBoundCode.getValue() != null) { if (theBoundCode.getValue() != null) {
return theBoundCode.getEnumFactory().toSystem(theBoundCode.getValue()); return theBoundCode.getEnumFactory().toSystem(theBoundCode.getValue());

View File

@ -1,13 +1,17 @@
package ca.uhn.fhir.jpa.dao.r4; package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext; 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.BaseSearchParamExtractor;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSearchParameter; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSearchParameter;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.ElementUtil; 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.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.*;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -76,10 +80,10 @@ public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4<SearchPa
FhirContext context = getContext(); FhirContext context = getContext();
Enum<?> type = theResource.getType(); 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) { if (theStatus == null) {
throw new UnprocessableEntityException("SearchParameter.status is missing or invalid"); 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()); 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 } // 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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. * limitations under the License.
* #L% * #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.StringUtils;
import org.apache.commons.lang3.tuple.Pair; 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.context.IWorkerContext;
import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; import org.hl7.fhir.r4.hapi.ctx.IValidationSupport;
import org.hl7.fhir.r4.model.*; 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.Location.LocationPositionComponent;
import org.hl7.fhir.r4.model.Patient.PatientCommunicationComponent; import org.hl7.fhir.r4.model.Patient.PatientCommunicationComponent;
import org.hl7.fhir.r4.utils.FHIRPathEngine; 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 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 static org.apache.commons.lang3.StringUtils.isBlank;
import ca.uhn.fhir.jpa.dao.*; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements ISearchParamExtractor { public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements ISearchParamExtractor {
@ -69,6 +71,17 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
myValidationSupport = theValidationSupport; 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) { private void addSearchTerm(ResourceTable theEntity, Set<ResourceIndexedSearchParamString> retVal, String resourceName, String searchTerm) {
if (isBlank(searchTerm)) { if (isBlank(searchTerm)) {
return; return;
@ -91,6 +104,23 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
retVal.add(nextEntity); 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 @Override
public Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) { public Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
// TODO: implement // TODO: implement
@ -99,7 +129,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamDates(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource) * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamDates(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/ */
@Override @Override
@ -182,7 +212,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamNumber(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource) * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamNumber(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/ */
@Override @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<?>>) * @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)) { * 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); * @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); * 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; } * newValue.setCode(UCUM.DAY.getSymbol()); newValue.setValue(dayValue); nextValue=newValue; }
@ -285,7 +315,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamQuantity(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource) * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamQuantity(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/ */
@Override @Override
@ -336,20 +366,9 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
return retVal; 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) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamStrings(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource) * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamStrings(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/ */
@Override @Override
@ -449,7 +468,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamTokens(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource) * @see ca.uhn.fhir.jpa.dao.ISearchParamExtractor#extractSearchParamTokens(ca.uhn.fhir.jpa.entity.ResourceTable, ca.uhn.fhir.model.api.IBaseResource)
*/ */
@Override @Override
@ -658,14 +677,14 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
} }
private void extractTokensFromCodeableConcept(List<String> theSystems, List<String> theCodes, CodeableConcept theCodeableConcept, ResourceTable theEntity, 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()) { for (Coding nextCoding : theCodeableConcept.getCoding()) {
extractTokensFromCoding(theSystems, theCodes, theEntity, theListToPopulate, theParameterDef, nextCoding); extractTokensFromCoding(theSystems, theCodes, theEntity, theListToPopulate, theParameterDef, nextCoding);
} }
} }
private void extractTokensFromCoding(List<String> theSystems, List<String> theCodes, ResourceTable theEntity, Set<BaseResourceIndexedSearchParam> theListToPopulate, 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()) { if (nextCoding != null && !nextCoding.isEmpty()) {
String nextSystem = nextCoding.getSystemElement().getValueAsString(); String nextSystem = nextCoding.getSystemElement().getValueAsString();
@ -691,16 +710,18 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
FHIRPathEngine fp = new FHIRPathEngine(worker); FHIRPathEngine fp = new FHIRPathEngine(worker);
List<Object> values = new ArrayList<>(); List<Object> values = new ArrayList<>();
try { String[] nextPathsSplit = SPLIT.split(thePaths);
String[] nextPathsSplit = SPLIT.split(thePaths); for (String nextPath : nextPathsSplit) {
for (String nextPath : nextPathsSplit) { List<Base> allValues;
List<Base> allValues = fp.evaluate((Base) theResource, nextPath); try {
if (allValues.isEmpty() == false) { allValues = fp.evaluate((Base) theResource, nextPath);
values.addAll(allValues); } 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++) { for (int i = 0; i < values.size(); i++) {
@ -715,23 +736,6 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
return values; 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 @VisibleForTesting
void setValidationSupportForTesting(org.hl7.fhir.r4.hapi.ctx.IValidationSupport theValidationSupport) { void setValidationSupportForTesting(org.hl7.fhir.r4.hapi.ctx.IValidationSupport theValidationSupport) {
myValidationSupport = 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.context.FhirContext;
import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.IParser;
/**
* @see ResourceHistoryTable#ENCODING_COL_LENGTH
*/
public enum ResourceEncodingEnum { public enum ResourceEncodingEnum {
/* /*

View File

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

View File

@ -51,6 +51,7 @@ public class Search implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
public static final int MAX_SEARCH_QUERY_STRING = 10000; public static final int MAX_SEARCH_QUERY_STRING = 10000;
public static final int UUID_COLUMN_LENGTH = 36;
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
@Column(name="CREATED", nullable=false, updatable=false) @Column(name="CREATED", nullable=false, updatable=false)
@ -118,7 +119,7 @@ public class Search implements Serializable {
@Column(name="TOTAL_COUNT", nullable=true) @Column(name="TOTAL_COUNT", nullable=true)
private Integer myTotalCount; 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; 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.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Nullable;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
@ -408,7 +409,11 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
myManagedTxManager = theTxManager; 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; int pageSize = theToIndex - theFromIndex;
if (pageSize < 1) { if (pageSize < 1) {
return null; return null;

View File

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

View File

@ -13,14 +13,12 @@ import ca.uhn.fhir.model.dstu2.valueset.*;
import ca.uhn.fhir.model.primitive.*; import ca.uhn.fhir.model.primitive.*;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*; 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.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.AfterClass; import org.junit.*;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.internal.util.collections.ListUtil; import org.mockito.internal.util.collections.ListUtil;
import java.util.List; import java.util.List;
@ -37,6 +35,12 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu
myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden());
} }
@After
public void after() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(new DaoConfig().isValidateSearchParameterExpressionsOnSave());
}
@Test @Test
public void testCreateInvalidNoBase() { public void testCreateInvalidNoBase() {
SearchParameter fooSp = new SearchParameter(); 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 @Test
public void testCreateInvalidParamInvalidResourceName() { public void testCreateInvalidParamInvalidResourceName() {
SearchParameter fooSp = new SearchParameter(); SearchParameter fooSp = new SearchParameter();

View File

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

View File

@ -1,9 +1,11 @@
package ca.uhn.fhir.jpa.dao.dstu3; 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.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*; 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.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil; 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.dstu3.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -29,6 +32,11 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
myDaoConfig.setReuseCachedSearchResultsForMillis(null); myDaoConfig.setReuseCachedSearchResultsForMillis(null);
} }
@After
public void after() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(new DaoConfig().isValidateSearchParameterExpressionsOnSave());
}
@Test @Test
public void testCreateInvalidNoBase() { public void testCreateInvalidNoBase() {
SearchParameter fooSp = new SearchParameter(); 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 * 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.exceptions.*;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Bundle.*; import org.hl7.fhir.dstu3.model.Bundle.*;
@ -152,36 +153,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
return input; 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") @SuppressWarnings("unchecked")
private <T extends org.hl7.fhir.dstu3.model.Resource> T find(Bundle theBundle, Class<T> theType, int theIndex) { private <T extends org.hl7.fhir.dstu3.model.Resource> T find(Bundle theBundle, Class<T> theType, int theIndex) {
int count = 0; 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 @Test
public void testSystemMetaOperation() { 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 @Test
public void testTransactionWIthInvalidPlaceholder() throws Exception { public void testTransactionWIthInvalidPlaceholder() throws Exception {
Bundle res = new Bundle(); Bundle res = new Bundle();

View File

@ -92,6 +92,9 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
@Qualifier("myBundleDaoR4") @Qualifier("myBundleDaoR4")
protected IFhirResourceDao<Bundle> myBundleDao; protected IFhirResourceDao<Bundle> myBundleDao;
@Autowired @Autowired
@Qualifier("myCommunicationDaoR4")
protected IFhirResourceDao<Communication> myCommunicationDao;
@Autowired
@Qualifier("myCarePlanDaoR4") @Qualifier("myCarePlanDaoR4")
protected IFhirResourceDao<CarePlan> myCarePlanDao; protected IFhirResourceDao<CarePlan> myCarePlanDao;
@Autowired @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.model.api.Include;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*; 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.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil; 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.*;
import org.hl7.fhir.r4.model.Appointment.AppointmentStatus; import org.hl7.fhir.r4.model.Appointment.AppointmentStatus;
import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -30,6 +32,11 @@ import static org.junit.Assert.*;
public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test { public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4SearchCustomSearchParamTest.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4SearchCustomSearchParamTest.class);
@After
public void after() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(new DaoConfig().isValidateSearchParameterExpressionsOnSave());
}
@Before @Before
public void beforeDisableResultReuse() { public void beforeDisableResultReuse() {
myDaoConfig.setReuseCachedSearchResultsForMillis(null); myDaoConfig.setReuseCachedSearchResultsForMillis(null);
@ -71,7 +78,6 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
} }
} }
@Test @Test
public void testCreateInvalidParamNoPath() { public void testCreateInvalidParamNoPath() {
SearchParameter fooSp = new SearchParameter(); 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 @Test
public void testOverrideAndDisableBuiltInSearchParametersWithOverridingDisabled() { public void testOverrideAndDisableBuiltInSearchParametersWithOverridingDisabled() {
myDaoConfig.setDefaultSearchParamsCanBeOverridden(false); myDaoConfig.setDefaultSearchParamsCanBeOverridden(false);
@ -387,6 +417,23 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
assertThat(results, contains(mrId)); 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 @Test
public void testSearchForExtensionReferenceWithNonMatchingTarget() { public void testSearchForExtensionReferenceWithNonMatchingTarget() {
SearchParameter siblingSp = new SearchParameter(); 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; 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.Date;
import java.util.List;
/* /*
* #%L * #%L
@ -22,51 +26,89 @@ import java.util.Date;
* #L% * #L%
*/ */
import java.util.List;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
public interface IBundleProvider { 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 * 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, * 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 * 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. * additional 20 resources which matched a client's _include specification.
* * <p>
* @param theFromIndex * Note that if this bundle provider was loaded using a
* The low index (inclusive) to return * page ID (i.e. via {@link ca.uhn.fhir.rest.server.IPagingProvider#retrieveResultList(String, String)}
* @param theToIndex * because {@link #getNextPageId()} provided a value on the
* The high index (exclusive) to return * 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>. * @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); 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 * Returns the UUID associated with this search. Note that this
* does not need to return a non-null value unless it a * 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 * IPagingProvider implementation you might use this method to communicate
* the search ID back to the provider. * the search ID back to the provider.
* </p> * </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 { public interface IPagingProvider {
int getDefaultPageSize(); int getDefaultPageSize();
int getMaximumPageSize(); 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 * 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 String myServerVersion = createPoweredByHeaderProductVersion();
private boolean myStarted; private boolean myStarted;
private Map<String, IResourceProvider> myTypeToProvider = new HashMap<>();
private boolean myUncompressIncomingContents = true; private boolean myUncompressIncomingContents = true;
private boolean myUseBrowserFriendlyContentTypes; private boolean myUseBrowserFriendlyContentTypes;
private ITenantIdentificationStrategy myTenantIdentificationStrategy; private ITenantIdentificationStrategy myTenantIdentificationStrategy;
@ -376,7 +375,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
try { try {
count += findResourceMethods(theProvider, clazz); count += findResourceMethods(theProvider, clazz);
} catch (ConfigurationException e) { } 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) { if (count == 0) {
throw new ConfigurationException("Did not find any annotated RESTful methods on provider class " + theProvider.getClass().getCanonicalName()); 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"); throw new NullPointerException("getResourceType() on class '" + rsrcProvider.getClass().getCanonicalName() + "' returned null");
} }
String resourceName = getFhirContext().getResourceDefinition(resourceType).getName(); 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) { if (!inInit) {
myResourceProviders.add(rsrcProvider); myResourceProviders.add(rsrcProvider);
} }
myTypeToProvider.put(resourceName, rsrcProvider);
providedResourceScanner.scanForProvidedResources(rsrcProvider); providedResourceScanner.scanForProvidedResources(rsrcProvider);
newResourceProviders.add(rsrcProvider); newResourceProviders.add(rsrcProvider);
} else { } else {
@ -1384,7 +1378,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
} }
if (!newResourceProviders.isEmpty()) { 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) { for (IResourceProvider provider : newResourceProviders) {
assertProviderIsValid(provider); assertProviderIsValid(provider);
findResourceMethods(provider); findResourceMethods(provider);
@ -1594,7 +1588,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
IResourceProvider rsrcProvider = (IResourceProvider) provider; IResourceProvider rsrcProvider = (IResourceProvider) provider;
Class<? extends IBaseResource> resourceType = rsrcProvider.getResourceType(); Class<? extends IBaseResource> resourceType = rsrcProvider.getResourceType();
String resourceName = getFhirContext().getResourceDefinition(resourceType).getName(); String resourceName = getFhirContext().getResourceDefinition(resourceType).getName();
myTypeToProvider.remove(resourceName);
providedResourceScanner.removeProvidedResources(rsrcProvider); providedResourceScanner.removeProvidedResources(rsrcProvider);
} else { } else {
myPlainProviders.remove(provider); 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, public static String createPagingLink(Set<Include> theIncludes, String theServerBase, String theSearchId, int theOffset, int theCount, Map<String, String[]> theRequestParameters, boolean thePrettyPrint,
BundleTypeEnum theBundleType) { 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(); StringBuilder b = new StringBuilder();
b.append(theServerBase); b.append(theServerBase);
b.append('?'); b.append('?');
@ -137,14 +149,24 @@ public class RestfulServerUtils {
b.append('='); b.append('=');
b.append(UrlUtil.escapeUrlParam(theSearchId)); b.append(UrlUtil.escapeUrlParam(theSearchId));
b.append('&'); if (theOffset != null) {
b.append(Constants.PARAM_PAGINGOFFSET); b.append('&');
b.append('='); b.append(Constants.PARAM_PAGINGOFFSET);
b.append(theOffset); b.append('=');
b.append('&'); b.append(theOffset);
b.append(Constants.PARAM_COUNT); }
b.append('='); if (theCount != null) {
b.append(theCount); 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); String[] strings = theRequestParameters.get(Constants.PARAM_FORMAT);
if (strings != null && strings.length > 0) { if (strings != null && strings.length > 0) {
b.append('&'); b.append('&');
@ -442,6 +464,18 @@ public class RestfulServerUtils {
return retVal; 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) { private static ResponseEncoding getEncodingForContentType(FhirContext theFhirContext, boolean theStrict, String theContentType) {
EncodingEnum encoding; EncodingEnum encoding;
if (theStrict) { if (theStrict) {
@ -476,18 +510,6 @@ public class RestfulServerUtils {
return parser; 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) { public static Set<String> parseAcceptHeaderAndReturnHighestRankedOptions(HttpServletRequest theRequest) {
Set<String> retVal = new HashSet<String>(); Set<String> retVal = new HashSet<String>();
@ -725,7 +747,7 @@ public class RestfulServerUtils {
try { try {
return Integer.parseInt(retVal[0]); return Integer.parseInt(retVal[0]);
} catch (NumberFormatException e) { } 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; return null;
} }
} }

View File

@ -20,31 +20,62 @@ package ca.uhn.fhir.rest.server;
* #L% * #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.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider; 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 { 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) { public SimpleBundleProvider(List<IBaseResource> theList) {
myList = theList; this(theList, null);
} }
public SimpleBundleProvider(IBaseResource theResource) { public SimpleBundleProvider(IBaseResource theResource) {
myList = Collections.singletonList(theResource); this(Collections.singletonList(theResource));
} }
/** /**
* Create an empty bundle * Create an empty bundle
*/ */
public SimpleBundleProvider() { 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 @Override
@ -53,23 +84,38 @@ public class SimpleBundleProvider implements IBundleProvider {
} }
@Override @Override
public Integer size() { public String getUuid() {
return myList.size(); return myUuid;
}
@Override
public InstantDt getPublished() {
return InstantDt.withCurrentTime();
} }
/**
* Defaults to null
*/
@Override @Override
public Integer preferredPageSize() { 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 @Override
public String getUuid() { public Integer size() {
return null; 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, IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes,
IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) { IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) {
IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory(); IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
int numToReturn; int numToReturn;
@ -152,7 +152,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
numToReturn = Math.min(numToReturn, numTotalResults - theOffset); numToReturn = Math.min(numToReturn, numTotalResults - theOffset);
} }
if (numToReturn > 0) { if (numToReturn > 0 || theResult.getCurrentPageId() != null) {
resourceList = theResult.getResources(theOffset, numToReturn + theOffset); resourceList = theResult.getResources(theOffset, numToReturn + theOffset);
} else { } else {
resourceList = Collections.emptyList(); resourceList = Collections.emptyList();
@ -166,6 +166,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
searchId = pagingProvider.storeResultList(theResult); searchId = pagingProvider.storeResultList(theResult);
if (isBlank(searchId)) { if (isBlank(searchId)) {
ourLog.info("Found {} results but paging provider did not provide an ID to use for paging", numTotalResults); 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) { if (hasNull) {
for (Iterator<IBaseResource> iter = resourceList.iterator(); iter.hasNext(); ) { resourceList.removeIf(Objects::isNull);
if (iter.next() == null) {
iter.remove();
}
}
} }
/* /*
@ -207,7 +204,18 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
String linkPrev = null; String linkPrev = null;
String linkNext = 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) { if (numTotalResults == null || theOffset + numToReturn < numTotalResults) {
linkNext = (RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, theOffset + numToReturn, numToReturn, theRequest.getParameters(), prettyPrint, theBundleType)); 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. * limitations under the License.
* #L% * #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.context.FhirContext;
import ca.uhn.fhir.model.api.IResource; 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.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 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 { public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
private final Integer myIdParamIndex; private final Integer myIdParamIndex;
private String myResourceName;
private final RestOperationTypeEnum myResourceOperationType; private final RestOperationTypeEnum myResourceOperationType;
private String myResourceName;
public HistoryMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { public HistoryMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
super(toReturnType(theMethod, theProvider), theMethod, theContext, theProvider); super(toReturnType(theMethod, theProvider), theMethod, theContext, theProvider);
@ -87,13 +87,13 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
} }
@Override @Override
public RestOperationTypeEnum getRestOperationType() { protected BundleTypeEnum getResponseBundleType() {
return myResourceOperationType; return BundleTypeEnum.HISTORY;
} }
@Override @Override
protected BundleTypeEnum getResponseBundleType() { public RestOperationTypeEnum getRestOperationType() {
return BundleTypeEnum.HISTORY; return myResourceOperationType;
} }
@Override @Override
@ -128,7 +128,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
return true; return true;
} }
@Override @Override
public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { 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); Object response = invokeServerMethod(theServer, theRequest, theMethodParams);
final IBundleProvider resources = toResourceList(response); final IBundleProvider resources = toResourceList(response);
/* /*
* We wrap the response so we can verify that it has the ID and version set, * We wrap the response so we can verify that it has the ID and version set,
* as is the contract for history * as is the contract for history
*/ */
return new IBundleProvider() { 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 @Override
public IPrimitiveType<Date> getPublished() { public IPrimitiveType<Date> getPublished() {
return resources.getPublished(); return resources.getPublished();
} }
@Override @Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) { public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
List<IBaseResource> retVal = resources.getResources(theFromIndex, theToIndex); List<IBaseResource> retVal = resources.getResources(theFromIndex, theToIndex);
@ -170,10 +185,10 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
} }
return retVal; return retVal;
} }
@Override @Override
public Integer size() { public String getUuid() {
return resources.size(); return resources.getUuid();
} }
@Override @Override
@ -182,8 +197,8 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
} }
@Override @Override
public String getUuid() { public Integer size() {
return resources.getUuid(); return resources.size();
} }
}; };
} }

View File

@ -20,18 +20,7 @@ package ca.uhn.fhir.rest.server.method;
* #L% * #L%
*/ */
import static org.apache.commons.lang3.StringUtils.isNotBlank; import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
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.ConfigurationException; import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Include; 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.param.binder.CollectionBinder;
import ca.uhn.fhir.rest.server.method.OperationParameter.IOperationParamConverter; import ca.uhn.fhir.rest.server.method.OperationParameter.IOperationParamConverter;
import ca.uhn.fhir.rest.server.method.ResourceParameter.Mode; 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.ParametersUtil;
import ca.uhn.fhir.util.ReflectionUtil; 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 class MethodUtil {
public static void extractDescription(SearchParameter theParameter, Annotation[] theAnnotations) { public static void extractDescription(SearchParameter theParameter, Annotation[] theAnnotations) {
for (Annotation annotation : theAnnotations) { for (Annotation annotation : theAnnotations) {
if (annotation instanceof Description) { if (annotation instanceof Description) {
@ -62,7 +65,7 @@ public class MethodUtil {
} }
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static List<IParameter> getResourceParameters(final FhirContext theContext, Method theMethod, Object theProvider, RestOperationTypeEnum theRestfulOperationTypeEnum) { public static List<IParameter> getResourceParameters(final FhirContext theContext, Method theMethod, Object theProvider, RestOperationTypeEnum theRestfulOperationTypeEnum) {
List<IParameter> parameters = new ArrayList<IParameter>(); List<IParameter> parameters = new ArrayList<IParameter>();
@ -90,7 +93,26 @@ public class MethodUtil {
} }
if (Collection.class.isAssignableFrom(parameterType)) { if (Collection.class.isAssignableFrom(parameterType)) {
throw new ConfigurationException("Argument #" + paramIndex + " of Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName() 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; specType = String.class;
} else if ((parameterType != Include.class) || innerCollectionType == null || outerCollectionType != null) { } 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<" 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 { } else {
instantiableCollectionType = (Class<? extends Collection<Include>>) CollectionBinder.getInstantiableCollectionType(innerCollectionType, "Method '" + theMethod.getName() + "'"); instantiableCollectionType = (Class<? extends Collection<Include>>) CollectionBinder.getInstantiableCollectionType(innerCollectionType, "Method '" + theMethod.getName() + "'");
specType = parameterType; specType = parameterType;
@ -198,7 +220,7 @@ public class MethodUtil {
} else if (nextAnnotation instanceof Validate.Mode) { } else if (nextAnnotation instanceof Validate.Mode) {
if (parameterType.equals(ValidationModeEnum.class) == false) { if (parameterType.equals(ValidationModeEnum.class) == false) {
throw new ConfigurationException( 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() { param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1).setConverter(new IOperationParamConverter() {
@Override @Override
@ -221,7 +243,7 @@ public class MethodUtil {
} else if (nextAnnotation instanceof Validate.Profile) { } else if (nextAnnotation instanceof Validate.Profile) {
if (parameterType.equals(String.class) == false) { if (parameterType.equals(String.class) == false) {
throw new ConfigurationException( 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() { param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1).setConverter(new IOperationParamConverter() {
@Override @Override
@ -244,8 +266,8 @@ public class MethodUtil {
if (param == null) { if (param == null) {
throw new ConfigurationException( throw new ConfigurationException(
"Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() "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"); + "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter");
} }
param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType);
@ -256,5 +278,5 @@ public class MethodUtil {
return parameters; return parameters;
} }
} }

View File

@ -20,23 +20,30 @@ package ca.uhn.fhir.rest.server.method;
* #L% * #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.lang.reflect.Method;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.hl7.fhir.instance.model.api.IBaseResource; import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
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.*;
public class PageMethodBinding extends BaseResourceReturningMethodBinding { public class PageMethodBinding extends BaseResourceReturningMethodBinding {
@ -75,34 +82,51 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding {
if (pagingProvider == null) { if (pagingProvider == null) {
throw new InvalidRequestException("This server does not support paging"); 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) { if (resultList == null) {
ourLog.info("Client requested unknown paging ID[{}]", thePagingAction); ourLog.info("Client requested unknown paging ID[{}]", thePagingAction);
String msg = getContext().getLocalizer().getMessage(PageMethodBinding.class, "unknownSearchId", thePagingAction); String msg = getContext().getLocalizer().getMessage(PageMethodBinding.class, "unknownSearchId", thePagingAction);
throw new ResourceGoneException(msg); 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()); 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); String[] reqIncludes = theRequest.getParameters().get(Constants.PARAM_INCLUDE);
if (reqIncludes != null) { if (reqIncludes != null) {
for (String nextInclude : reqIncludes) { for (String nextInclude : reqIncludes) {
@ -125,7 +149,14 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding {
if (responseEncoding != null) { if (responseEncoding != null) {
encodingEnum = responseEncoding.getEncoding(); 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); 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])) { if (pageId == null || pageId.length == 0 || isBlank(pageId[0])) {
return false; return false;
} }
if (theRequest.getRequestType() != RequestTypeEnum.GET) { return theRequest.getRequestType() == RequestTypeEnum.GET;
return false;
}
return true;
} }

View File

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

View File

@ -20,7 +20,12 @@ package ca.uhn.fhir.rest.server.provider;
* #L% * #L%
*/ */
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.FhirContext; 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.annotation.*;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.param.TokenAndListParam; 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.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 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.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -58,7 +65,9 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
private final Class<T> myResourceType; private final Class<T> myResourceType;
private final FhirContext myFhirContext; private final FhirContext myFhirContext;
private final String myResourceName; 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 long myNextId;
private AtomicLong myDeleteCount = new AtomicLong(0); private AtomicLong myDeleteCount = new AtomicLong(0);
private AtomicLong mySearchCount = new AtomicLong(0); private AtomicLong mySearchCount = new AtomicLong(0);
@ -86,6 +95,8 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
public void clear() { public void clear() {
myNextId = 1; myNextId = 1;
myIdToVersionToResourceMap.clear(); myIdToVersionToResourceMap.clear();
myIdToHistory.clear();
myTypeHistory.clear();
} }
/** /**
@ -183,6 +194,21 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
return myIdToVersionToResourceMap.get(theIdPart); 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) @Read(version = true)
public IBaseResource read(@IdParam IIdType theId) { public IBaseResource read(@IdParam IIdType theId) {
TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart()); TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart());
@ -213,8 +239,23 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
} }
@Search @Search
public List<IBaseResource> search( public List<IBaseResource> searchAll() {
@OptionalParam(name = "_id") TokenAndListParam theIds) { 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<>(); 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) { private IIdType store(@ResourceParam T theResource, String theIdPart, Long theVersionIdPart) {
IIdType id = myFhirContext.getVersion().newIdType(); 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) { if (theResource != null) {
theResource.setId(id); 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()); 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; 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 @Override
public IContextValidationSupport<?, ?, ?, ?, ?, ?> createValidationSupport() { public IContextValidationSupport<?, ?, ?, ?, ?, ?> createValidationSupport() {
return ReflectionUtil.newInstanceOfFhirProfileValidationSupport("org.hl7.fhir.r4.hapi.validation.DefaultProfileValidationSupport"); return ReflectionUtil.newInstanceOfFhirProfileValidationSupport("org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport");
} }
@Override @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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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) { for (IBaseResource next : includedResources) {
BundleEntryComponent entry = myBundle.addEntry(); BundleEntryComponent entry = myBundle.addEntry();
entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE); entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE);
@ -195,7 +195,7 @@ public class R4BundleFactory implements IVersionSpecificBundleFactory {
includedResources.addAll(addedResourcesThisPass); includedResources.addAll(addedResourcesThisPass);
// Linked resources may themselves have linked resources // Linked resources may themselves have linked resources
references = new ArrayList<ResourceReferenceInfo>(); references = new ArrayList<>();
for (IAnyResource iResource : addedResourcesThisPass) { for (IAnyResource iResource : addedResourcesThisPass) {
List<ResourceReferenceInfo> newReferences = myContext.newTerser().getAllResourceReferences(iResource); List<ResourceReferenceInfo> newReferences = myContext.newTerser().getAllResourceReferences(iResource);
references.addAll(newReferences); 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) { for (IAnyResource next : includedResources) {
BundleEntryComponent entry = myBundle.addEntry(); BundleEntryComponent entry = myBundle.addEntry();
entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE); 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(); ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort); ourServer = new Server(ourPort);
PatientProvider patientProvider = new PatientProvider(); PatientProviderCreate patientProviderCreate = new PatientProviderCreate();
PatientProviderRead patientProviderRead = new PatientProviderRead();
PatientProviderSearch patientProviderSearch = new PatientProviderSearch();
ServletHandler proxyHandler = new ServletHandler(); ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx); RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setResourceProviders(patientProvider); servlet.setResourceProviders(patientProviderCreate, patientProviderRead, patientProviderSearch);
ServletHolder servletHolder = new ServletHolder(servlet); ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*"); proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler); ourServer.setHandler(proxyHandler);
@ -276,21 +278,7 @@ public class CreateR4Test {
ourClient = builder.build(); ourClient = builder.build();
} }
public static class PatientProviderRead implements IResourceProvider {
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;
}
@Read() @Read()
public MyPatientWithExtensions read(@IdParam IdType theIdParam) { public MyPatientWithExtensions read(@IdParam IdType theIdParam) {
@ -300,6 +288,35 @@ public class CreateR4Test {
return p0; 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 @Search
public List<IBaseResource> search() { public List<IBaseResource> search() {
ArrayList<IBaseResource> retVal = new ArrayList<IBaseResource>(); ArrayList<IBaseResource> retVal = new ArrayList<IBaseResource>();

View File

@ -1,13 +1,14 @@
package ca.uhn.fhir.rest.server; package ca.uhn.fhir.rest.server;
import static org.junit.Assert.*; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.*;
import java.util.ArrayList; import ca.uhn.fhir.rest.param.DateRangeParam;
import java.util.List; import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import java.util.concurrent.TimeUnit; 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.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.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder; 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.server.Server;
import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder; 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.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ca.uhn.fhir.context.FhirContext; import java.util.ArrayList;
import ca.uhn.fhir.model.dstu2.resource.Bundle; import java.util.Date;
import ca.uhn.fhir.model.dstu2.resource.Patient; import java.util.List;
import ca.uhn.fhir.model.primitive.IdDt; import java.util.concurrent.TimeUnit;
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;
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 CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forDstu2(); private static FhirContext ourCtx = FhirContext.forR4();
private static DateRangeParam ourLastAt; private static DateRangeParam ourLastAt;
private static InstantType ourLastSince;
private static InstantDt ourLastSince; private static IPrimitiveType<Date> ourLastSince2;
private static IPrimitiveType<String> ourLastSince3;
private static IPrimitiveType<?> ourLastSince4;
private static int ourPort; private static int ourPort;
private static Server ourServer; private static Server ourServer;
@Before @Before
public void before() { public void before() {
ourLastAt = null; ourLastAt = null;
ourLastSince = null; ourLastSince = null;
} ourLastSince2 = null;
ourLastSince3 = null;
@Test ourLastSince4 = null;
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());
}
} }
@Test @Test
public void testAt() throws Exception { public void testAt() throws Exception {
{ {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/_history?_at=gt2001&_at=lt2005"); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/_history?_at=gt2001&_at=lt2005");
HttpResponse status = ourClient.execute(httpGet); try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
IOUtils.closeQuietly(status.getEntity().getContent()); String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(200, status.getStatusLine().getStatusCode());
}
assertEquals(ParamPrefixEnum.GREATERTHAN, ourLastAt.getLowerBound().getPrefix()); assertEquals(ParamPrefixEnum.GREATERTHAN, ourLastAt.getLowerBound().getPrefix());
assertEquals("2001", ourLastAt.getLowerBound().getValueAsString()); assertEquals("2001", ourLastAt.getLowerBound().getValueAsString());
@ -86,56 +75,79 @@ public class HistoryDstu2Test {
public void testInstanceHistory() throws Exception { public void testInstanceHistory() throws Exception {
{ {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/_history?_pretty=true"); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/_history?_pretty=true");
HttpResponse status = ourClient.execute(httpGet); String responseContent;
String responseContent = IOUtils.toString(status.getEntity().getContent()); try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
IOUtils.closeQuietly(status.getEntity().getContent()); responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
ourLog.info(responseContent); assertEquals(200, status.getStatusLine().getStatusCode());
}
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent); Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent);
assertEquals(2, bundle.getEntry().size()); 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/1", bundle.getEntry().get(0).getResource().getId());
assertEquals("http://localhost:" + ourPort + "/Patient/ih1/_history/2", bundle.getEntry().get(1).getResource().getId().getValue()); 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 @Test
public void testServerHistory() throws Exception { public void testServerHistory() throws Exception {
{ {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/_history"); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/_history");
HttpResponse status = ourClient.execute(httpGet); String responseContent;
String responseContent = IOUtils.toString(status.getEntity().getContent()); try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
IOUtils.closeQuietly(status.getEntity().getContent()); responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(200, status.getStatusLine().getStatusCode());
}
Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent); Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent);
assertEquals(2, bundle.getEntry().size()); 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/1", bundle.getEntry().get(0).getResource().getId());
assertEquals("http://localhost:" + ourPort + "/Patient/h1/_history/2", bundle.getEntry().get(1).getResource().getId().getValue()); 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 @Test
public void testTypeHistory() throws Exception { public void testTypeHistory() throws Exception {
{ {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/_history"); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/_history");
HttpResponse status = ourClient.execute(httpGet); String responseContent;
String responseContent = IOUtils.toString(status.getEntity().getContent()); try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
IOUtils.closeQuietly(status.getEntity().getContent()); responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(200, status.getStatusLine().getStatusCode());
}
assertNull(ourLastAt); assertNull(ourLastAt);
Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent); Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent);
assertEquals(2, bundle.getEntry().size()); 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/1", bundle.getEntry().get(0).getResource().getId());
assertEquals("http://localhost:" + ourPort + "/Patient/th1/_history/2", bundle.getEntry().get(1).getResource().getId().getValue()); 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 { public void testVread() throws Exception {
{ {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/_history/456"); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/_history/456");
HttpResponse status = ourClient.execute(httpGet); String responseContent;
String responseContent = IOUtils.toString(status.getEntity().getContent()); try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
IOUtils.closeQuietly(status.getEntity().getContent()); responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(200, status.getStatusLine().getStatusCode());
}
Patient bundle = ourCtx.newXmlParser().parseResource(Patient.class, responseContent); 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 { public static class DummyPlainProvider {
@History @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; ourLastAt = theAt;
ourLastSince = theSince; ourLastSince = theSince;
ourLastSince2 = theSince2;
ourLastSince3 = theSince3;
ourLastSince4 = theSince4;
ArrayList<Patient> retVal = new ArrayList<Patient>(); ArrayList<Patient> retVal = new ArrayList<>();
Patient patient = new Patient(); Patient patient = new Patient();
patient.setId("Patient/h1/_history/1"); patient.setId("Patient/h1/_history/1");
patient.addName().addFamily("history"); patient.addName().setFamily("history");
retVal.add(patient); retVal.add(patient);
Patient patient2 = new Patient(); Patient patient2 = new Patient();
patient2.setId("Patient/h1/_history/2"); patient2.setId("Patient/h1/_history/2");
patient2.addName().addFamily("history"); patient2.addName().setFamily("history");
retVal.add(patient2); retVal.add(patient2);
return retVal; return retVal;
@ -220,17 +240,17 @@ public class HistoryDstu2Test {
} }
@History @History
public List<Patient> instanceHistory(@IdParam IdDt theId) { public List<Patient> instanceHistory(@IdParam IdType theId) {
ArrayList<Patient> retVal = new ArrayList<Patient>(); ArrayList<Patient> retVal = new ArrayList<>();
Patient patient = new Patient(); Patient patient = new Patient();
patient.setId("Patient/ih1/_history/1"); patient.setId("Patient/ih1/_history/1");
patient.addName().addFamily("history"); patient.addName().setFamily("history");
retVal.add(patient); retVal.add(patient);
Patient patient2 = new Patient(); Patient patient2 = new Patient();
patient2.setId("Patient/ih1/_history/2"); patient2.setId("Patient/ih1/_history/2");
patient2.addName().addFamily("history"); patient2.addName().setFamily("history");
retVal.add(patient2); retVal.add(patient2);
return retVal; return retVal;
@ -238,25 +258,25 @@ public class HistoryDstu2Test {
@History @History
public List<Patient> typeHistory() { public List<Patient> typeHistory() {
ArrayList<Patient> retVal = new ArrayList<Patient>(); ArrayList<Patient> retVal = new ArrayList<>();
Patient patient = new Patient(); Patient patient = new Patient();
patient.setId("Patient/th1/_history/1"); patient.setId("Patient/th1/_history/1");
patient.addName().addFamily("history"); patient.addName().setFamily("history");
retVal.add(patient); retVal.add(patient);
Patient patient2 = new Patient(); Patient patient2 = new Patient();
patient2.setId("Patient/th1/_history/2"); patient2.setId("Patient/th1/_history/2");
patient2.addName().addFamily("history"); patient2.addName().setFamily("history");
retVal.add(patient2); retVal.add(patient2);
return retVal; return retVal;
} }
@Read(version = true) @Read(version = true)
public Patient vread(@IdParam IdDt theId) { public Patient vread(@IdParam IdType theId) {
Patient retVal = new Patient(); Patient retVal = new Patient();
retVal.addName().addFamily("vread"); retVal.addName().setFamily("vread");
retVal.setId(theId); retVal.setId(theId);
return retVal; 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.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient; 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.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
@ -19,17 +20,19 @@ import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.Assert.*; import static org.junit.Assert.*;
public class HashMapResourceProviderTest { public class HashMapResourceProviderTest {
private static final Logger ourLog = LoggerFactory.getLogger(HashMapResourceProviderTest.class);
private static MyRestfulServer ourRestServer; private static MyRestfulServer ourRestServer;
private static Server ourListenerServer; private static Server ourListenerServer;
private static IGenericClient ourClient; 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 @Test
public void testSearchAll() { public void testSearchAll() {
// Create // Create
@ -112,7 +202,11 @@ public class HashMapResourceProviderTest {
} }
// Search // 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.getTotal());
assertEquals(100, resp.getEntry().size()); assertEquals(100, resp.getEntry().size());

View File

@ -86,7 +86,7 @@
Resource loading logic for the JPA server has been optimized to Resource loading logic for the JPA server has been optimized to
reduce the number of database round trips required when loading reduce the number of database round trips required when loading
search results where many of the entries have a "forced ID" (an alphanumeric 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! request!
</action> </action>
<action type="add"> <action type="add">
@ -163,6 +163,82 @@
"Repreentation: OperationOutcome" value. "Repreentation: OperationOutcome" value.
Thanks to Ana Maria Radu for the pul request! Thanks to Ana Maria Radu for the pul request!
</action> </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>
<release version="3.4.0" date="2018-05-28"> <release version="3.4.0" date="2018-05-28">
<action type="add"> <action type="add">

View File

@ -336,7 +336,34 @@
</macro> </macro>
</subsection> </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> </section>