Merge remote-tracking branch 'remotes/origin/master' into spring-batch-integration

This commit is contained in:
Ken Stevens 2020-06-09 15:52:54 -04:00
commit 7fdd0f1753
25 changed files with 646 additions and 112 deletions

View File

@ -47,6 +47,7 @@ page.server_jpa.search=Search
page.server_jpa.performance=Performance
page.server_jpa.upgrading=Upgrade Guide
page.server_jpa.diff=Diff Operation
page.server_jpa.lastn=LastN Operation
section.server_jpa_empi.title=JPA Server: EMPI
page.server_jpa_empi.empi=Enterprise Master Patient Index

View File

@ -0,0 +1,45 @@
# LastN Operation
HAPI FHIR 5.1.0 introduced preliminary support for the `$lastn` operation described [here](http://hl7.org/fhir/observation-operation-lastn.html).
This implementation of the `$lastn` operation requires an external Elasticsearch server implementation which is used to implement the indexes required by this operation. The following sections describe the current functionality supported by this operation and the configuration needed to enable this operation.
# Functional Overview and Parameters
As described in the [FHIR specification](http://hl7.org/fhir/observation-operation-lastn.html), the `$lastn` can be used to retrieve the most recent or last n=number of observations for one or more subjects. This implementation supports the following search parameters:
* `subject=` or `patient=`: Identifier(s) of patient(s) to return Observation resources for. If not specified, returns most recent observations for all patients.
* `category=`: One or more category code search parameters used to filter Observations.
* `Observation.code=`: One or more `Observation.code` search parameters use to filter and group observations. If not specified, returns most recent observations for all `Observation.code` values.
* `date=`: Date search parameters used to filter Observations by `Observation.effectiveDtm`.
* `max=`: The maximum number of observations to return for each `Observation.code`. If not specified, returns only the most recent observation in each group.
# Limitations
Currently only Elasticsearch versions up to 6.5.4 are supported.
Search parameters other than those listed above are currently not supported.
The grouping of Observation resources by `Observation.code` means that the `$lastn` operation will not work in cases where `Observation.code` has more than one coding.
# Deployment and Configuration
The `$lastn` operation is disabled by default. The operation can be enabled by setting the DaoConfig#setLastNEnabled property (see [JavaDoc](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html#setLastNEnabled(boolean))).
In addition, the Elasticsearch client service, `ElasticsearchSvcImpl` will need to be instantiated with parameters specifying how to connect to the Elasticsearch server, for e.g.:
```java
@Bean()
public ElasticsearchSvcImpl elasticsearchSvc() {
String elasticsearchHost = "localhost";
String elasticsearchUserId = "elastic";
String elasticsearchPassword = "changeme";
int elasticsearchPort = 9301;
return new ElasticsearchSvcImpl(elasticsearchHost, elasticsearchPort, elasticsearchUserId, elasticsearchPassword);
}
```
The Elasticsearch client service requires that security be enabled in the Elasticsearch clusters, and that an Elasticsearch user be available with permissions to create an index and to index, update and delete documents as needed.
See the [JavaDoc](/hapi-fhir/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/search/lastn/IElasticsearchSvc.html) for more information regarding the Elasticsearch client service.

View File

@ -222,6 +222,11 @@ public class DaoConfig {
*/
private boolean myLastNEnabled = false;
/**
* @since 5.1.0
*/
private boolean myPreloadBlobFromInputStream = false;
/**
* Constructor
*/
@ -2083,4 +2088,42 @@ public class DaoConfig {
myMaximumDeleteConflictQueryCount = theMaximumDeleteConflictQueryCount;
}
/**
* <p>
* This determines whether $binary-access-write operations should first load the InputStream into memory before persisting the
* contents to the database. This needs to be enabled for MS SQL Server as this DB requires that the blob size be known
* in advance.
* </p>
* <p>
* Note that this setting should be enabled with caution as it can lead to significant demands on memory.
* </p>
* <p>
* The default value for this setting is {@code false}.
* </p>
*
* @since 5.1.0
*/
public boolean isPreloadBlobFromInputStream() {
return myPreloadBlobFromInputStream;
}
/**
* <p>
* This determines whether $binary-access-write operations should first load the InputStream into memory before persisting the
* contents to the database. This needs to be enabled for MS SQL Server as this DB requires that the blob size be known
* in advance.
* </p>
* <p>
* Note that this setting should be enabled with caution as it can lead to significant demands on memory.
* </p>
* <p>
* The default value for this setting is {@code false}.
* </p>
*
* @since 5.1.0
*/
public void setPreloadBlobFromInputStream(Boolean thePreloadBlobFromInputStream) {
myPreloadBlobFromInputStream = thePreloadBlobFromInputStream;
}
}

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.binstore;
* #L%
*/
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.IBinaryStorageEntityDao;
import ca.uhn.fhir.jpa.model.entity.BinaryStorageEntity;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
@ -55,10 +56,12 @@ public class DatabaseBlobBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
private IBinaryStorageEntityDao myBinaryStorageEntityDao;
@Autowired
private PlatformTransactionManager myPlatformTransactionManager;
@Autowired
private DaoConfig myDaoConfig;
@Override
@Transactional(Transactional.TxType.SUPPORTS)
public StoredDetails storeBlob(IIdType theResourceId, String theBlobIdOrNull, String theContentType, InputStream theInputStream) {
public StoredDetails storeBlob(IIdType theResourceId, String theBlobIdOrNull, String theContentType, InputStream theInputStream) throws IOException {
Date publishedDate = new Date();
HashingInputStream hashingInputStream = createHashingInputStream(theInputStream);
@ -74,7 +77,13 @@ public class DatabaseBlobBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
Session session = (Session) myEntityManager.getDelegate();
LobHelper lobHelper = session.getLobHelper();
Blob dataBlob = lobHelper.createBlob(countingInputStream, 0);
Blob dataBlob;
if (myDaoConfig.isPreloadBlobFromInputStream()) {
byte[] loadedStream = IOUtils.toByteArray(countingInputStream);
dataBlob = lobHelper.createBlob(loadedStream);
} else {
dataBlob = lobHelper.createBlob(countingInputStream, 0);
}
entity.setBlob(dataBlob);
// Save the entity

View File

@ -311,9 +311,6 @@ public class SearchBuilder implements ISearchBuilder {
throw new InvalidRequestException("LastN operation is not enabled on this service, can not process this request");
}
}
if(myParams.getLastNMax() == null) {
throw new InvalidRequestException("Max parameter is required for $lastn operation");
}
List<String> lastnResourceIds = myIElasticsearchSvc.executeLastN(myParams, myContext, theMaximumResults);
for (String lastnResourceId : lastnResourceIds) {
pids.add(myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, lastnResourceId));

View File

@ -60,6 +60,10 @@ public class BaseJpaResourceProviderObservationDstu3 extends JpaResourceProvider
@OperationParam(name="code")
TokenAndListParam theCode,
@Description(shortDefinition="The effective date of the observation")
@OperationParam(name="date")
DateAndListParam theDate,
@Description(shortDefinition="The subject that the observation is about (if patient)")
@OperationParam(name="patient")
ReferenceAndListParam thePatient,
@ -78,13 +82,16 @@ public class BaseJpaResourceProviderObservationDstu3 extends JpaResourceProvider
SearchParameterMap paramMap = new SearchParameterMap();
paramMap.add(Observation.SP_CATEGORY, theCategory);
paramMap.add(Observation.SP_CODE, theCode);
paramMap.add(Observation.SP_DATE, theDate);
if (thePatient != null) {
paramMap.add("patient", thePatient);
paramMap.add(Observation.SP_PATIENT, thePatient);
}
if (theSubject != null) {
paramMap.add("subject", theSubject);
paramMap.add(Observation.SP_SUBJECT, theSubject);
}
if (theMax != null) {
paramMap.setLastNMax(theMax.getValue());
}
paramMap.setLastNMax(theMax.getValue());
if (theCount != null) {
paramMap.setCount(theCount.getValue());
}

View File

@ -60,6 +60,10 @@ public class BaseJpaResourceProviderObservationR4 extends JpaResourceProviderR4<
@OperationParam(name="code")
TokenAndListParam theCode,
@Description(shortDefinition="The effective date of the observation")
@OperationParam(name="date")
DateAndListParam theDate,
@Description(shortDefinition="The subject that the observation is about (if patient)")
@OperationParam(name="patient")
ReferenceAndListParam thePatient,
@ -78,13 +82,16 @@ public class BaseJpaResourceProviderObservationR4 extends JpaResourceProviderR4<
SearchParameterMap paramMap = new SearchParameterMap();
paramMap.add(Observation.SP_CATEGORY, theCategory);
paramMap.add(Observation.SP_CODE, theCode);
paramMap.add(Observation.SP_DATE, theDate);
if (thePatient != null) {
paramMap.add(Observation.SP_PATIENT, thePatient);
}
if (theSubject != null) {
paramMap.add(Observation.SP_SUBJECT, theSubject);
}
paramMap.setLastNMax(theMax.getValue());
if(theMax != null) {
paramMap.setLastNMax(theMax.getValue());
}
if (theCount != null) {
paramMap.setCount(theCount.getValue());
}

View File

@ -60,6 +60,10 @@ public class BaseJpaResourceProviderObservationR5 extends JpaResourceProviderR5<
@OperationParam(name="code")
TokenAndListParam theCode,
@Description(shortDefinition="The effective date of the observation")
@OperationParam(name="date")
DateAndListParam theDate,
@Description(shortDefinition="The subject that the observation is about (if patient)")
@OperationParam(name="patient")
ReferenceAndListParam thePatient,
@ -78,13 +82,16 @@ public class BaseJpaResourceProviderObservationR5 extends JpaResourceProviderR5<
SearchParameterMap paramMap = new SearchParameterMap();
paramMap.add(Observation.SP_CATEGORY, theCategory);
paramMap.add(Observation.SP_CODE, theCode);
paramMap.add(Observation.SP_DATE, theDate);
if (thePatient != null) {
paramMap.add(Observation.SP_PATIENT, thePatient);
}
if (theSubject != null) {
paramMap.add(Observation.SP_SUBJECT, theSubject);
}
paramMap.setLastNMax(theMax.getValue());
if (theMax != null) {
paramMap.setLastNMax(theMax.getValue());
}
if (theCount != null) {
paramMap.setCount(theCount.getValue());
}

View File

@ -27,6 +27,8 @@ import ca.uhn.fhir.jpa.search.lastn.json.ObservationJson;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -46,7 +48,9 @@ import org.shadehapi.elasticsearch.client.RequestOptions;
import org.shadehapi.elasticsearch.client.RestHighLevelClient;
import org.shadehapi.elasticsearch.common.xcontent.XContentType;
import org.shadehapi.elasticsearch.index.query.BoolQueryBuilder;
import org.shadehapi.elasticsearch.index.query.MatchQueryBuilder;
import org.shadehapi.elasticsearch.index.query.QueryBuilders;
import org.shadehapi.elasticsearch.index.query.RangeQueryBuilder;
import org.shadehapi.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.shadehapi.elasticsearch.search.SearchHit;
import org.shadehapi.elasticsearch.search.SearchHits;
@ -77,6 +81,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
public class ElasticsearchSvcImpl implements IElasticsearchSvc {
// Index Constants
public static final String OBSERVATION_INDEX = "observation_index";
public static final String OBSERVATION_CODE_INDEX = "code_index";
public static final String OBSERVATION_DOCUMENT_TYPE = "ca.uhn.fhir.jpa.model.entity.ObservationIndexedSearchParamLastNEntity";
@ -84,15 +89,34 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
public static final String OBSERVATION_INDEX_SCHEMA_FILE = "ObservationIndexSchema.json";
public static final String OBSERVATION_CODE_INDEX_SCHEMA_FILE = "ObservationCodeIndexSchema.json";
private final RestHighLevelClient myRestHighLevelClient;
private final ObjectMapper objectMapper = new ObjectMapper();
// Aggregation Constants
private final String GROUP_BY_SUBJECT = "group_by_subject";
private final String GROUP_BY_SYSTEM = "group_by_system";
private final String GROUP_BY_CODE = "group_by_code";
private final String OBSERVATION_IDENTIFIER_FIELD_NAME = "identifier";
private final String MOST_RECENT_EFFECTIVE = "most_recent_effective";
// Observation index document element names
private final String OBSERVATION_IDENTIFIER_FIELD_NAME = "identifier";
private final String OBSERVATION_SUBJECT_FIELD_NAME = "subject";
private final String OBSERVATION_CODEVALUE_FIELD_NAME = "codeconceptcodingcode";
private final String OBSERVATION_CODESYSTEM_FIELD_NAME = "codeconceptcodingsystem";
private final String OBSERVATION_CODEHASH_FIELD_NAME = "codeconceptcodingcode_system_hash";
private final String OBSERVATION_CODEDISPLAY_FIELD_NAME = "codeconceptcodingdisplay";
private final String OBSERVATION_CODE_TEXT_FIELD_NAME = "codeconcepttext";
private final String OBSERVATION_EFFECTIVEDTM_FIELD_NAME = "effectivedtm";
private final String OBSERVATION_CATEGORYHASH_FIELD_NAME = "categoryconceptcodingcode_system_hash";
private final String OBSERVATION_CATEGORYVALUE_FIELD_NAME = "categoryconceptcodingcode";
private final String OBSERVATION_CATEGORYSYSTEM_FIELD_NAME = "categoryconceptcodingsystem";
private final String OBSERVATION_CATEGORYDISPLAY_FIELD_NAME = "categoryconceptcodingdisplay";
private final String OBSERVATION_CATEGORYTEXT_FIELD_NAME = "categoryconcepttext";
// Code index document element names
private final String CODE_HASH = "codingcode_system_hash";
private final String CODE_TEXT = "text";
private final RestHighLevelClient myRestHighLevelClient;
private final ObjectMapper objectMapper = new ObjectMapper();
public ElasticsearchSvcImpl(String theHostname, int thePort, String theUsername, String thePassword) {
myRestHighLevelClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(theHostname, thePort, theUsername, thePassword);
@ -171,7 +195,7 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
break;
}
SearchRequest myLastNRequest = buildObservationsSearchRequest(subject, theSearchParameterMap, theFhirContext,
createObservationSubjectAggregationBuilder(theSearchParameterMap.getLastNMax(), topHitsInclude));
createObservationSubjectAggregationBuilder(getMaxParameter(theSearchParameterMap), topHitsInclude));
try {
SearchResponse lastnResponse = executeSearchRequest(myLastNRequest);
searchResults.addAll(buildObservationList(lastnResponse, setValue, theSearchParameterMap, theFhirContext,
@ -182,7 +206,7 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
}
} else {
SearchRequest myLastNRequest = buildObservationsSearchRequest(theSearchParameterMap, theFhirContext,
createObservationCodeAggregationBuilder(theSearchParameterMap.getLastNMax(), topHitsInclude));
createObservationCodeAggregationBuilder(getMaxParameter(theSearchParameterMap), topHitsInclude));
try {
SearchResponse lastnResponse = executeSearchRequest(myLastNRequest);
searchResults.addAll(buildObservationList(lastnResponse, setValue, theSearchParameterMap, theFhirContext,
@ -194,6 +218,14 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
return searchResults;
}
private int getMaxParameter(SearchParameterMap theSearchParameterMap) {
if (theSearchParameterMap.getLastNMax() == null) {
return 1;
} else {
return theSearchParameterMap.getLastNMax();
}
}
private List<String> getSubjectReferenceCriteria(String thePatientParamName, String theSubjectParamName, SearchParameterMap theSearchParameterMap) {
List<String> subjectReferenceCriteria = new ArrayList<>();
@ -227,8 +259,8 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
return referenceList;
}
private CompositeAggregationBuilder createObservationSubjectAggregationBuilder(int theMaxNumberObservationsPerCode, String[] theTopHitsInclude) {
CompositeValuesSourceBuilder<?> subjectValuesBuilder = new TermsValuesSourceBuilder("subject").field("subject");
private CompositeAggregationBuilder createObservationSubjectAggregationBuilder(Integer theMaxNumberObservationsPerCode, String[] theTopHitsInclude) {
CompositeValuesSourceBuilder<?> subjectValuesBuilder = new TermsValuesSourceBuilder(OBSERVATION_SUBJECT_FIELD_NAME).field(OBSERVATION_SUBJECT_FIELD_NAME);
List<CompositeValuesSourceBuilder<?>> compositeAggSubjectSources = new ArrayList<>();
compositeAggSubjectSources.add(subjectValuesBuilder);
CompositeAggregationBuilder compositeAggregationSubjectBuilder = new CompositeAggregationBuilder(GROUP_BY_SUBJECT, compositeAggSubjectSources);
@ -239,14 +271,14 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
}
private TermsAggregationBuilder createObservationCodeAggregationBuilder(int theMaxNumberObservationsPerCode, String[] theTopHitsInclude) {
TermsAggregationBuilder observationCodeCodeAggregationBuilder = new TermsAggregationBuilder(GROUP_BY_CODE, ValueType.STRING).field("codeconceptcodingcode");
TermsAggregationBuilder observationCodeCodeAggregationBuilder = new TermsAggregationBuilder(GROUP_BY_CODE, ValueType.STRING).field(OBSERVATION_CODEVALUE_FIELD_NAME);
observationCodeCodeAggregationBuilder.order(BucketOrder.key(true));
// Top Hits Aggregation
observationCodeCodeAggregationBuilder.subAggregation(AggregationBuilders.topHits("most_recent_effective")
.sort("effectivedtm", SortOrder.DESC)
observationCodeCodeAggregationBuilder.subAggregation(AggregationBuilders.topHits(MOST_RECENT_EFFECTIVE)
.sort(OBSERVATION_EFFECTIVEDTM_FIELD_NAME, SortOrder.DESC)
.fetchSource(theTopHitsInclude, null).size(theMaxNumberObservationsPerCode));
observationCodeCodeAggregationBuilder.size(10000);
TermsAggregationBuilder observationCodeSystemAggregationBuilder = new TermsAggregationBuilder(GROUP_BY_SYSTEM, ValueType.STRING).field("codeconceptcodingsystem");
TermsAggregationBuilder observationCodeSystemAggregationBuilder = new TermsAggregationBuilder(GROUP_BY_SYSTEM, ValueType.STRING).field(OBSERVATION_CODESYSTEM_FIELD_NAME);
observationCodeSystemAggregationBuilder.order(BucketOrder.key(true));
observationCodeSystemAggregationBuilder.subAggregation(observationCodeCodeAggregationBuilder);
return observationCodeSystemAggregationBuilder;
@ -328,7 +360,7 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
private SearchHit[] getLastNMatches(Terms.Bucket theObservationCodeBucket) {
Aggregations topHitObservationCodes = theObservationCodeBucket.getAggregations();
ParsedTopHits parsedTopHits = topHitObservationCodes.get("most_recent_effective");
ParsedTopHits parsedTopHits = topHitObservationCodes.get(MOST_RECENT_EFFECTIVE);
return parsedTopHits.getHits().getHits();
}
@ -342,6 +374,7 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
addCategoriesCriteria(boolQueryBuilder, theSearchParameterMap, theFhirContext);
addObservationCodeCriteria(boolQueryBuilder, theSearchParameterMap, theFhirContext);
addDateCriteria(boolQueryBuilder, theSearchParameterMap, theFhirContext);
searchSourceBuilder.query(boolQueryBuilder);
}
searchSourceBuilder.size(0);
@ -359,9 +392,10 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// Query
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.must(QueryBuilders.termQuery("subject", theSubjectParam));
boolQueryBuilder.must(QueryBuilders.termQuery(OBSERVATION_SUBJECT_FIELD_NAME, theSubjectParam));
addCategoriesCriteria(boolQueryBuilder, theSearchParameterMap, theFhirContext);
addObservationCodeCriteria(boolQueryBuilder, theSearchParameterMap, theFhirContext);
addDateCriteria(boolQueryBuilder, theSearchParameterMap, theFhirContext);
searchSourceBuilder.query(boolQueryBuilder);
searchSourceBuilder.size(0);
@ -395,19 +429,19 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
textOnlyList.addAll(getCodingTextOnlyValues(nextAnd));
}
if (codeSystemHashList.size() > 0) {
theBoolQueryBuilder.must(QueryBuilders.termsQuery("categoryconceptcodingcode_system_hash", codeSystemHashList));
theBoolQueryBuilder.must(QueryBuilders.termsQuery(OBSERVATION_CATEGORYHASH_FIELD_NAME, codeSystemHashList));
}
if (codeOnlyList.size() > 0) {
theBoolQueryBuilder.must(QueryBuilders.termsQuery("categoryconceptcodingcode", codeOnlyList));
theBoolQueryBuilder.must(QueryBuilders.termsQuery(OBSERVATION_CATEGORYVALUE_FIELD_NAME, codeOnlyList));
}
if (systemOnlyList.size() > 0) {
theBoolQueryBuilder.must(QueryBuilders.termsQuery("categoryconceptcodingsystem", systemOnlyList));
theBoolQueryBuilder.must(QueryBuilders.termsQuery(OBSERVATION_CATEGORYSYSTEM_FIELD_NAME, systemOnlyList));
}
if (textOnlyList.size() > 0) {
BoolQueryBuilder myTextBoolQueryBuilder = QueryBuilders.boolQuery();
for (String textOnlyParam : textOnlyList) {
myTextBoolQueryBuilder.should(QueryBuilders.matchPhraseQuery("categoryconceptcodingdisplay", textOnlyParam));
myTextBoolQueryBuilder.should(QueryBuilders.matchPhraseQuery("categoryconcepttext", textOnlyParam));
myTextBoolQueryBuilder.should(QueryBuilders.matchPhrasePrefixQuery(OBSERVATION_CATEGORYDISPLAY_FIELD_NAME, textOnlyParam));
myTextBoolQueryBuilder.should(QueryBuilders.matchPhrasePrefixQuery(OBSERVATION_CATEGORYTEXT_FIELD_NAME, textOnlyParam));
}
theBoolQueryBuilder.must(myTextBoolQueryBuilder);
}
@ -493,19 +527,19 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
textOnlyList.addAll(getCodingTextOnlyValues(nextAnd));
}
if (codeSystemHashList.size() > 0) {
theBoolQueryBuilder.must(QueryBuilders.termsQuery("codeconceptcodingcode_system_hash", codeSystemHashList));
theBoolQueryBuilder.must(QueryBuilders.termsQuery(OBSERVATION_CODEHASH_FIELD_NAME, codeSystemHashList));
}
if (codeOnlyList.size() > 0) {
theBoolQueryBuilder.must(QueryBuilders.termsQuery("codeconceptcodingcode", codeOnlyList));
theBoolQueryBuilder.must(QueryBuilders.termsQuery(OBSERVATION_CODEVALUE_FIELD_NAME, codeOnlyList));
}
if (systemOnlyList.size() > 0) {
theBoolQueryBuilder.must(QueryBuilders.termsQuery("codeconceptcodingsystem", systemOnlyList));
theBoolQueryBuilder.must(QueryBuilders.termsQuery(OBSERVATION_CODESYSTEM_FIELD_NAME, systemOnlyList));
}
if (textOnlyList.size() > 0) {
BoolQueryBuilder myTextBoolQueryBuilder = QueryBuilders.boolQuery();
for (String textOnlyParam : textOnlyList) {
myTextBoolQueryBuilder.should(QueryBuilders.matchPhraseQuery("codeconceptcodingdisplay", textOnlyParam));
myTextBoolQueryBuilder.should(QueryBuilders.matchPhraseQuery("codeconcepttext", textOnlyParam));
myTextBoolQueryBuilder.should(QueryBuilders.matchPhrasePrefixQuery(OBSERVATION_CODEDISPLAY_FIELD_NAME, textOnlyParam));
myTextBoolQueryBuilder.should(QueryBuilders.matchPhrasePrefixQuery(OBSERVATION_CODE_TEXT_FIELD_NAME, textOnlyParam));
}
theBoolQueryBuilder.must(myTextBoolQueryBuilder);
}
@ -513,6 +547,41 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
}
private void addDateCriteria(BoolQueryBuilder theBoolQueryBuilder, SearchParameterMap theSearchParameterMap, FhirContext theFhirContext) {
String dateParamName = LastNParameterHelper.getEffectiveParamName(theFhirContext);
if (theSearchParameterMap.containsKey(dateParamName)) {
List<List<IQueryParameterType>> andOrParams = theSearchParameterMap.get(dateParamName);
for (List<? extends IQueryParameterType> nextAnd : andOrParams) {
BoolQueryBuilder myDateBoolQueryBuilder = new BoolQueryBuilder();
for (IQueryParameterType nextOr : nextAnd) {
if (nextOr instanceof DateParam) {
DateParam myDate = (DateParam) nextOr;
createDateCriteria(myDate, myDateBoolQueryBuilder);
}
}
theBoolQueryBuilder.must(myDateBoolQueryBuilder);
}
}
}
private void createDateCriteria(DateParam theDate, BoolQueryBuilder theBoolQueryBuilder) {
Long dateInstant = theDate.getValue().getTime();
RangeQueryBuilder myRangeQueryBuilder = new RangeQueryBuilder(OBSERVATION_EFFECTIVEDTM_FIELD_NAME);
ParamPrefixEnum prefix = theDate.getPrefix();
if (prefix == ParamPrefixEnum.GREATERTHAN || prefix == ParamPrefixEnum.STARTS_AFTER) {
theBoolQueryBuilder.should(myRangeQueryBuilder.gt(dateInstant));
} else if (prefix == ParamPrefixEnum.LESSTHAN || prefix == ParamPrefixEnum.ENDS_BEFORE) {
theBoolQueryBuilder.should(myRangeQueryBuilder.lt(dateInstant));
} else if (prefix == ParamPrefixEnum.LESSTHAN_OR_EQUALS) {
theBoolQueryBuilder.should(myRangeQueryBuilder.lte(dateInstant));
} else if (prefix == ParamPrefixEnum.GREATERTHAN_OR_EQUALS) {
theBoolQueryBuilder.should(myRangeQueryBuilder.gte(dateInstant));
} else {
theBoolQueryBuilder.should(new MatchQueryBuilder(OBSERVATION_EFFECTIVEDTM_FIELD_NAME, dateInstant));
}
}
@VisibleForTesting
public List<ObservationJson> executeLastNWithAllFieldsForTest(SearchParameterMap theSearchParameterMap, FhirContext theFhirContext) {
return buildAndExecuteSearch(theSearchParameterMap, theFhirContext, null, t -> t, 100);
@ -604,9 +673,9 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if (theCodeSystemHash != null) {
boolQueryBuilder.must(QueryBuilders.termQuery("codingcode_system_hash", theCodeSystemHash));
boolQueryBuilder.must(QueryBuilders.termQuery(CODE_HASH, theCodeSystemHash));
} else {
boolQueryBuilder.must(QueryBuilders.matchPhraseQuery("text", theText));
boolQueryBuilder.must(QueryBuilders.matchPhraseQuery(CODE_TEXT, theText));
}
searchSourceBuilder.query(boolQueryBuilder);
@ -644,6 +713,11 @@ public class ElasticsearchSvcImpl implements IElasticsearchSvc {
return (indexResponse.getResult() == DocWriteResponse.Result.CREATED) || (indexResponse.getResult() == DocWriteResponse.Result.UPDATED);
}
@Override
public void close() throws IOException {
myRestHighLevelClient.close();
}
private IndexRequest createIndexRequest(String theIndexName, String theDocumentId, String theObservationDocument, String theDocumentType) {
IndexRequest request = new IndexRequest(theIndexName);
request.id(theDocumentId);

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.search.lastn.json.CodeJson;
import ca.uhn.fhir.jpa.search.lastn.json.ObservationJson;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import java.io.IOException;
import java.util.List;
public interface IElasticsearchSvc {
@ -75,4 +76,9 @@ public interface IElasticsearchSvc {
*/
void deleteObservationDocument(String theDocumentId);
/**
* Invoked when shutting down.
*/
void close() throws IOException;
}

View File

@ -18,10 +18,10 @@
"type" : "keyword"
},
"codeconceptcodingdisplay" : {
"type" : "keyword"
"type" : "text"
},
"categoryconcepttext" : {
"type" : "keyword"
"type" : "text"
},
"categoryconceptcodingcode" : {
"type" : "keyword"
@ -33,7 +33,7 @@
"type" : "keyword"
},
"categoryconceptcodingdisplay" : {
"type" : "keyword"
"type" : "text"
},
"effectivedtm" : {
"type" : "date"

View File

@ -1,9 +1,12 @@
package ca.uhn.fhir.jpa.binstore;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.model.entity.BinaryStorageEntity;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.r4.model.IdType;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@ -39,6 +42,21 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
@Qualifier("databaseBlobBinaryStorageSvc")
private IBinaryStorageSvc mySvc;
@Autowired
private DaoConfig myDaoConfig;
@Before
public void backupDaoConfig() {
defaultPreloadBlobFromInputStream = myDaoConfig.isPreloadBlobFromInputStream();
}
@After
public void restoreDaoConfig() {
myDaoConfig.setPreloadBlobFromInputStream(defaultPreloadBlobFromInputStream);
}
boolean defaultPreloadBlobFromInputStream;
@Test
public void testStoreAndRetrieve() throws IOException {
@ -85,6 +103,12 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
assertArrayEquals(SOME_BYTES, mySvc.fetchBlob(resourceId, outcome.getBlobId()));
}
@Test
public void testStoreAndRetrieveWithPreload() throws IOException {
myDaoConfig.setPreloadBlobFromInputStream(true);
testStoreAndRetrieve();
}
@Test
public void testStoreAndRetrieveWithManualId() throws IOException {

View File

@ -4,6 +4,9 @@ import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PreDestroy;
import java.io.IOException;
@Configuration
public class TestR4ConfigWithElasticsearchClient extends TestR4ConfigWithElasticSearch {
@ -13,4 +16,9 @@ public class TestR4ConfigWithElasticsearchClient extends TestR4ConfigWithElastic
return new ElasticsearchSvcImpl(elasticsearchHost, elasticsearchPort, elasticsearchUserId, elasticsearchPassword);
}
@PreDestroy
public void stopEsClient() throws IOException {
myElasticsearchSvc().close();
}
}

View File

@ -8,6 +8,10 @@ import ca.uhn.fhir.jpa.config.TestR4ConfigWithElasticsearchClient;
import ca.uhn.fhir.jpa.dao.BaseJpaTest;
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.DateAndListParam;
import ca.uhn.fhir.rest.param.DateOrListParam;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.ReferenceAndListParam;
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
@ -115,6 +119,8 @@ public class BaseR4SearchLastN extends BaseJpaTest {
private static final Map<String, String> observationCodeMap = new HashMap<>();
private static final Map<String, Date> observationEffectiveMap = new HashMap<>();
private static Calendar observationDate = new GregorianCalendar();
@Before
public void beforeCreateTestPatientsAndObservations() throws IOException {
// Using a static flag to ensure that test data and elasticsearch index is only created once.
@ -154,15 +160,13 @@ public class BaseR4SearchLastN extends BaseJpaTest {
private void createFiveObservationsForPatientCodeCategory(IIdType thePatientId, String theObservationCode, String theCategoryCode,
Integer theTimeOffset) {
Calendar observationDate = new GregorianCalendar();
for (int idx=0; idx<5; idx++ ) {
Observation obs = new Observation();
obs.getSubject().setReferenceElement(thePatientId);
obs.getCode().addCoding().setCode(theObservationCode).setSystem(codeSystem);
obs.setValue(new StringType(theObservationCode + "_0"));
observationDate.add(Calendar.HOUR, -theTimeOffset+idx);
Date effectiveDtm = observationDate.getTime();
Date effectiveDtm = calculateObservationDateFromOffset(theTimeOffset, idx);
obs.setEffective(new DateTimeType(effectiveDtm));
obs.getCategoryFirstRep().addCoding().setCode(theCategoryCode).setSystem(categorySystem);
String observationId = myObservationDao.create(obs, mockSrd()).getId().toUnqualifiedVersionless().getValue();
@ -173,6 +177,12 @@ public class BaseR4SearchLastN extends BaseJpaTest {
}
}
private Date calculateObservationDateFromOffset(Integer theTimeOffset, Integer theObservationIndex) {
int milliSecondsPerHour = 3600*1000;
// Generate a Date by subtracting a calculated number of hours from the static observationDate property.
return new Date(observationDate.getTimeInMillis() - (milliSecondsPerHour*(theTimeOffset+theObservationIndex)));
}
protected ServletRequestDetails mockSrd() {
return mySrd;
}
@ -204,7 +214,6 @@ public class BaseR4SearchLastN extends BaseJpaTest {
public void testLastNNoPatients() {
SearchParameterMap params = new SearchParameterMap();
params.setLastNMax(1);
params.setLastN(true);
Map<String, String[]> requestParameters = new HashMap<>();
@ -548,6 +557,53 @@ public class BaseR4SearchLastN extends BaseJpaTest {
return new TokenAndListParam().addAnd(myTokenOrListParam);
}
@Test
public void testLastNSingleDate() {
SearchParameterMap params = new SearchParameterMap();
ReferenceParam subjectParam = new ReferenceParam("Patient", "", patient0Id.getValue());
params.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
DateParam myDateParam = new DateParam(ParamPrefixEnum.LESSTHAN, new Date(observationDate.getTimeInMillis() - (3600*1000*9)));
params.add(Observation.SP_DATE, myDateParam);
List<String> sortedPatients = new ArrayList<>();
sortedPatients.add(patient0Id.getValue());
List<String> sortedObservationCodes = new ArrayList<>();
sortedObservationCodes.add(observationCd0);
sortedObservationCodes.add(observationCd1);
executeTestCase(params, sortedPatients,sortedObservationCodes, null,15);
}
@Test
public void testLastNMultipleDates() {
SearchParameterMap params = new SearchParameterMap();
ReferenceParam subjectParam = new ReferenceParam("Patient", "", patient0Id.getValue());
params.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
DateParam lowDateParam = new DateParam(ParamPrefixEnum.LESSTHAN, new Date(observationDate.getTimeInMillis() - (3600*1000*(9))));
DateParam highDateParam = new DateParam(ParamPrefixEnum.GREATERTHAN, new Date(observationDate.getTimeInMillis() - (3600*1000*(15))));
DateAndListParam myDateAndListParam = new DateAndListParam();
myDateAndListParam.addAnd(new DateOrListParam().addOr(lowDateParam));
myDateAndListParam.addAnd(new DateOrListParam().addOr(highDateParam));
params.add(Observation.SP_DATE, myDateAndListParam);
List<String> sortedPatients = new ArrayList<>();
sortedPatients.add(patient0Id.getValue());
List<String> sortedObservationCodes = new ArrayList<>();
sortedObservationCodes.add(observationCd0);
sortedObservationCodes.add(observationCd1);
executeTestCase(params, sortedPatients,sortedObservationCodes, null,10);
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();

View File

@ -51,10 +51,13 @@ public class LastNElasticsearchSvcMultipleObservationsIT {
private static ObjectMapper ourMapperNonPrettyPrint;
private static boolean indexLoaded = false;
private final Map<String, Map<String, List<Date>>> createdPatientObservationMap = new HashMap<>();
private final FhirContext myFhirContext = FhirContext.forR4();
static private final Calendar baseObservationDate = new GregorianCalendar();
@BeforeClass
public static void beforeClass() {
@ -66,15 +69,10 @@ public class LastNElasticsearchSvcMultipleObservationsIT {
@Before
public void before() throws IOException {
createMultiplePatientsAndObservations();
}
@After
public void after() throws IOException {
elasticsearchSvc.deleteAllDocumentsForTest(ElasticsearchSvcImpl.OBSERVATION_INDEX);
elasticsearchSvc.deleteAllDocumentsForTest(ElasticsearchSvcImpl.OBSERVATION_CODE_INDEX);
elasticsearchSvc.refreshIndex(ElasticsearchSvcImpl.OBSERVATION_INDEX);
elasticsearchSvc.refreshIndex(ElasticsearchSvcImpl.OBSERVATION_CODE_INDEX);
if (!indexLoaded) {
createMultiplePatientsAndObservations();
indexLoaded = true;
}
}
@Test
@ -218,9 +216,9 @@ public class LastNElasticsearchSvcMultipleObservationsIT {
SearchParameterMap searchParameterMap = new SearchParameterMap();
ReferenceParam subjectParam = new ReferenceParam("Patient", "", "3");
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
TokenParam categoryParam = new TokenParam("test-heart-rate");
TokenParam categoryParam = new TokenParam(null, "test-heart-rate");
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(categoryParam));
TokenParam codeParam = new TokenParam("test-code-1");
TokenParam codeParam = new TokenParam(null,"test-code-1");
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(codeParam));
searchParameterMap.setLastNMax(100);
@ -251,11 +249,13 @@ public class LastNElasticsearchSvcMultipleObservationsIT {
public void testLastNCodeCodeTextCategoryTextOnly() {
SearchParameterMap searchParameterMap = new SearchParameterMap();
ReferenceParam subjectParam = new ReferenceParam("Patient", "", "3");
// Check case match
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
TokenParam categoryParam = new TokenParam("test-heart-rate display");
TokenParam categoryParam = new TokenParam("Heart");
categoryParam.setModifier(TokenParamModifier.TEXT);
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(categoryParam));
TokenParam codeParam = new TokenParam("test-code-1 display");
TokenParam codeParam = new TokenParam("Code1");
codeParam.setModifier(TokenParamModifier.TEXT);
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(codeParam));
searchParameterMap.setLastNMax(100);
@ -264,31 +264,90 @@ public class LastNElasticsearchSvcMultipleObservationsIT {
assertEquals(5, observations.size());
// Check case not match
searchParameterMap = new SearchParameterMap();
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
categoryParam = new TokenParam("heart");
categoryParam.setModifier(TokenParamModifier.TEXT);
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(categoryParam));
codeParam = new TokenParam("code1");
codeParam.setModifier(TokenParamModifier.TEXT);
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(codeParam));
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(5, observations.size());
// Check hyphenated strings
searchParameterMap = new SearchParameterMap();
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
categoryParam = new TokenParam("heart-rate");
categoryParam.setModifier(TokenParamModifier.TEXT);
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(categoryParam));
codeParam = new TokenParam("code1");
codeParam.setModifier(TokenParamModifier.TEXT);
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(codeParam));
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(5, observations.size());
// Check partial strings
searchParameterMap = new SearchParameterMap();
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
categoryParam = new TokenParam("hear");
categoryParam.setModifier(TokenParamModifier.TEXT);
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(categoryParam));
codeParam = new TokenParam("1-obs");
codeParam.setModifier(TokenParamModifier.TEXT);
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(codeParam));
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(5, observations.size());
}
@Test
public void testLastNNoMatchQueries() {
// Invalid Patient
ReferenceParam validPatientParam = new ReferenceParam("Patient", "", "9");
TokenParam validCategoryCodeParam = new TokenParam("http://mycodes.org/fhir/observation-category","test-heart-rate");
TokenParam validObservationCodeParam = new TokenParam("http://mycodes.org/fhir/observation-code", "test-code-1");
DateParam validDateParam = new DateParam(ParamPrefixEnum.EQUAL, new Date(baseObservationDate.getTimeInMillis() - (9*3600*1000)));
// Ensure that valid parameters are indeed valid
SearchParameterMap searchParameterMap = new SearchParameterMap();
ReferenceParam patientParam = new ReferenceParam("Patient", "", "10");
searchParameterMap.add(Observation.SP_PATIENT, buildReferenceAndListParam(patientParam));
TokenParam categoryParam = new TokenParam("http://mycodes.org/fhir/observation-category", "test-heart-rate");
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(categoryParam));
TokenParam codeParam = new TokenParam("http://mycodes.org/fhir/observation-code", "test-code-1");
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(codeParam));
searchParameterMap.add(Observation.SP_PATIENT, buildReferenceAndListParam(validPatientParam));
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(validCategoryCodeParam));
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(validObservationCodeParam));
searchParameterMap.add(Observation.SP_DATE, validDateParam);
searchParameterMap.setLastNMax(100);
List<String> observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(1, observations.size());
// Invalid Patient
searchParameterMap = new SearchParameterMap();
ReferenceParam patientParam = new ReferenceParam("Patient", "", "10");
searchParameterMap.add(Observation.SP_PATIENT, buildReferenceAndListParam(patientParam));
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(validCategoryCodeParam));
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(validObservationCodeParam));
searchParameterMap.add(Observation.SP_DATE, validDateParam);
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(0, observations.size());
// Invalid subject
searchParameterMap = new SearchParameterMap();
ReferenceParam subjectParam = new ReferenceParam("Patient", "", "10");
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
categoryParam = new TokenParam("http://mycodes.org/fhir/observation-category", "test-heart-rate");
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(categoryParam));
codeParam = new TokenParam("http://mycodes.org/fhir/observation-code", "test-code-1");
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(codeParam));
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(patientParam));
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(validCategoryCodeParam));
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(validObservationCodeParam));
searchParameterMap.add(Observation.SP_DATE, validDateParam);
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
@ -296,12 +355,11 @@ public class LastNElasticsearchSvcMultipleObservationsIT {
// Invalid observation code
searchParameterMap = new SearchParameterMap();
subjectParam = new ReferenceParam("Patient", "", "9");
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
categoryParam = new TokenParam("http://mycodes.org/fhir/observation-category", "test-heart-rate");
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(categoryParam));
codeParam = new TokenParam("http://mycodes.org/fhir/observation-code", "test-code-999");
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(validPatientParam));
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(validCategoryCodeParam));
TokenParam codeParam = new TokenParam("http://mycodes.org/fhir/observation-code", "test-code-999");
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(codeParam));
searchParameterMap.add(Observation.SP_DATE, validDateParam);
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
@ -309,17 +367,112 @@ public class LastNElasticsearchSvcMultipleObservationsIT {
// Invalid category code
searchParameterMap = new SearchParameterMap();
subjectParam = new ReferenceParam("Patient", "", "9");
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
categoryParam = new TokenParam("http://mycodes.org/fhir/observation-category", "test-not-a-category");
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(validPatientParam));
TokenParam categoryParam = new TokenParam("http://mycodes.org/fhir/observation-category", "test-not-a-category");
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(categoryParam));
codeParam = new TokenParam("http://mycodes.org/fhir/observation-code", "test-code-1");
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(codeParam));
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(validObservationCodeParam));
searchParameterMap.add(Observation.SP_DATE, validDateParam);
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(0, observations.size());
// Invalid date
searchParameterMap = new SearchParameterMap();
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(validPatientParam));
searchParameterMap.add(Observation.SP_CATEGORY, buildTokenAndListParam(validCategoryCodeParam));
searchParameterMap.add(Observation.SP_CODE, buildTokenAndListParam(validObservationCodeParam));
searchParameterMap.add(Observation.SP_DATE, new DateParam(ParamPrefixEnum.GREATERTHAN, baseObservationDate.getTime()));
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(0, observations.size());
}
@Test
public void testLastNEffectiveDates() {
Date highDate = new Date(baseObservationDate.getTimeInMillis() - (3600*1000));
Date lowDate = new Date(baseObservationDate.getTimeInMillis() - (10*3600*1000));
SearchParameterMap searchParameterMap = new SearchParameterMap();
ReferenceParam subjectParam = new ReferenceParam("Patient", "", "3");
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
DateParam dateParam = new DateParam(ParamPrefixEnum.EQUAL, lowDate);
searchParameterMap.add(Observation.SP_DATE, dateParam);
searchParameterMap.setLastNMax(100);
List<String> observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(1, observations.size());
searchParameterMap = new SearchParameterMap();
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
dateParam = new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, lowDate);
searchParameterMap.add(Observation.SP_DATE, dateParam);
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(10, observations.size());
searchParameterMap = new SearchParameterMap();
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
dateParam = new DateParam(ParamPrefixEnum.GREATERTHAN, lowDate);
searchParameterMap.add(Observation.SP_DATE, dateParam);
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(9, observations.size());
searchParameterMap = new SearchParameterMap();
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
dateParam = new DateParam(ParamPrefixEnum.STARTS_AFTER, lowDate);
searchParameterMap.add(Observation.SP_DATE, dateParam);
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(9, observations.size());
searchParameterMap = new SearchParameterMap();
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
dateParam = new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, highDate);
searchParameterMap.add(Observation.SP_DATE, dateParam);
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(10, observations.size());
searchParameterMap = new SearchParameterMap();
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
dateParam = new DateParam(ParamPrefixEnum.LESSTHAN, highDate);
searchParameterMap.add(Observation.SP_DATE, dateParam);
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(9, observations.size());
searchParameterMap = new SearchParameterMap();
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
dateParam = new DateParam(ParamPrefixEnum.ENDS_BEFORE, highDate);
searchParameterMap.add(Observation.SP_DATE, dateParam);
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(9, observations.size());
searchParameterMap = new SearchParameterMap();
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
DateParam startDateParam = new DateParam(ParamPrefixEnum.GREATERTHAN, new Date(baseObservationDate.getTimeInMillis() - (4*3600*1000)));
DateAndListParam dateAndListParam = new DateAndListParam();
dateAndListParam.addAnd(new DateOrListParam().addOr(startDateParam));
dateParam = new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, highDate);
dateAndListParam.addAnd(new DateOrListParam().addOr(dateParam));
searchParameterMap.add(Observation.SP_DATE, dateAndListParam);
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(3, observations.size());
searchParameterMap = new SearchParameterMap();
searchParameterMap.add(Observation.SP_SUBJECT, buildReferenceAndListParam(subjectParam));
startDateParam = new DateParam(ParamPrefixEnum.GREATERTHAN, new Date(baseObservationDate.getTimeInMillis() - (4*3600*1000)));
searchParameterMap.add(Observation.SP_DATE, startDateParam);
dateParam = new DateParam(ParamPrefixEnum.LESSTHAN, lowDate);
searchParameterMap.add(Observation.SP_DATE, dateParam);
searchParameterMap.setLastNMax(100);
observations = elasticsearchSvc.executeLastN(searchParameterMap, myFhirContext, 100);
assertEquals(0, observations.size());
}
private void createMultiplePatientsAndObservations() throws IOException {
@ -328,30 +481,30 @@ public class LastNElasticsearchSvcMultipleObservationsIT {
CodeJson codeJson1 = new CodeJson();
codeJson1.setCodeableConceptText("Test Codeable Concept Field for First Code");
codeJson1.setCodeableConceptId(codeableConceptId1);
codeJson1.addCoding("http://mycodes.org/fhir/observation-code", "test-code-1", "test-code-1 display");
codeJson1.addCoding("http://mycodes.org/fhir/observation-code", "test-code-1", "1-Observation Code1");
String codeableConceptId2 = UUID.randomUUID().toString();
CodeJson codeJson2 = new CodeJson();
codeJson2.setCodeableConceptText("Test Codeable Concept Field for Second Code");
codeJson2.setCodeableConceptId(codeableConceptId1);
codeJson2.addCoding("http://mycodes.org/fhir/observation-code", "test-code-2", "test-code-2 display");
codeJson2.addCoding("http://mycodes.org/fhir/observation-code", "test-code-2", "2-Observation Code2");
// Create CodeableConcepts for two categories, each with three codings.
// Create three codings and first category CodeableConcept
List<CodeJson> categoryConcepts1 = new ArrayList<>();
CodeJson categoryCodeableConcept1 = new CodeJson();
categoryCodeableConcept1.setCodeableConceptText("Test Codeable Concept Field for first category");
categoryCodeableConcept1.addCoding("http://mycodes.org/fhir/observation-category", "test-heart-rate", "test-heart-rate display");
categoryCodeableConcept1.addCoding("http://myalternatecodes.org/fhir/observation-category", "test-alt-heart-rate", "test-alt-heart-rate display");
categoryCodeableConcept1.addCoding("http://mysecondaltcodes.org/fhir/observation-category", "test-2nd-alt-heart-rate", "test-2nd-alt-heart-rate display");
categoryCodeableConcept1.addCoding("http://mycodes.org/fhir/observation-category", "test-heart-rate", "Test Heart Rate");
categoryCodeableConcept1.addCoding("http://myalternatecodes.org/fhir/observation-category", "test-alt-heart-rate", "Test Heartrate");
categoryCodeableConcept1.addCoding("http://mysecondaltcodes.org/fhir/observation-category", "test-2nd-alt-heart-rate", "Test Heart-Rate");
categoryConcepts1.add(categoryCodeableConcept1);
// Create three codings and second category CodeableConcept
List<CodeJson> categoryConcepts2 = new ArrayList<>();
CodeJson categoryCodeableConcept2 = new CodeJson();
categoryCodeableConcept2.setCodeableConceptText("Test Codeable Concept Field for second category");
categoryCodeableConcept2.addCoding("http://mycodes.org/fhir/observation-category", "test-vital-signs", "test-vital-signs display");
categoryCodeableConcept2.addCoding("http://myalternatecodes.org/fhir/observation-category", "test-alt-vitals", "test-alt-vitals display");
categoryCodeableConcept2.addCoding("http://mysecondaltcodes.org/fhir/observation-category", "test-2nd-alt-vitals", "test-2nd-alt-vitals display");
categoryCodeableConcept2.addCoding("http://mycodes.org/fhir/observation-category", "test-vital-signs", "Test Vital Signs");
categoryCodeableConcept2.addCoding("http://myalternatecodes.org/fhir/observation-category", "test-alt-vitals", "Test Vital-Signs");
categoryCodeableConcept2.addCoding("http://mysecondaltcodes.org/fhir/observation-category", "test-2nd-alt-vitals", "Test Vitals");
categoryConcepts2.add(categoryCodeableConcept2);
for (int patientCount = 0; patientCount < 10; patientCount++) {
@ -377,9 +530,7 @@ public class LastNElasticsearchSvcMultipleObservationsIT {
assertTrue(elasticsearchSvc.createOrUpdateObservationCodeIndex(codeableConceptId2, codeJson2));
}
Calendar observationDate = new GregorianCalendar();
observationDate.add(Calendar.HOUR, -10 + entryCount);
Date effectiveDtm = observationDate.getTime();
Date effectiveDtm = new Date(baseObservationDate.getTimeInMillis() - ((10-entryCount)*3600*1000));
observationJson.setEffectiveDtm(effectiveDtm);
assertTrue(elasticsearchSvc.createOrUpdateObservationIndex(identifier, observationJson));

View File

@ -7,6 +7,8 @@ import ca.uhn.fhir.jpa.search.lastn.json.CodeJson;
import ca.uhn.fhir.jpa.search.lastn.json.ObservationJson;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.dstu2.resource.Observation;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.ReferenceAndListParam;
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
@ -108,6 +110,7 @@ public class LastNElasticsearchSvcSingleObservationIT {
searchParameterMap.add(Observation.SP_CATEGORY, new TokenAndListParam().addAnd(new TokenOrListParam().addOr(categoryParam)));
TokenParam codeParam = new TokenParam(CODEFIRSTCODINGSYSTEM, CODEFIRSTCODINGCODE);
searchParameterMap.add(Observation.SP_CODE, new TokenAndListParam().addAnd(new TokenOrListParam().addOr(codeParam)));
searchParameterMap.add(Observation.SP_DATE, new DateParam(ParamPrefixEnum.EQUAL, EFFECTIVEDTM));
searchParameterMap.setLastNMax(3);

View File

@ -22,14 +22,14 @@ public class TestElasticsearchConfig {
@Bean()
public ElasticsearchSvcImpl myElasticsearchSvc() throws IOException {
public ElasticsearchSvcImpl myElasticsearchSvc() {
int elasticsearchPort = embeddedElasticSearch().getHttpPort();
return new ElasticsearchSvcImpl(elasticsearchHost, elasticsearchPort, elasticsearchUserId, elasticsearchPassword);
}
@Bean
public EmbeddedElastic embeddedElasticSearch() {
EmbeddedElastic embeddedElastic = null;
EmbeddedElastic embeddedElastic;
try {
embeddedElastic = EmbeddedElastic.builder()
.withElasticVersion(ELASTIC_VERSION)
@ -46,4 +46,10 @@ public class TestElasticsearchConfig {
return embeddedElastic;
}
@PreDestroy
public void stop() throws IOException {
myElasticsearchSvc().close();
embeddedElasticSearch().stop();
}
}

View File

@ -93,8 +93,9 @@ public class EmpiPersonFindingSvc {
List<CanonicalEID> eidFromResource = myEIDHelper.getExternalEid(theBaseResource);
if (!eidFromResource.isEmpty()) {
for (CanonicalEID eid : eidFromResource) {
IBaseResource foundPerson = myEmpiResourceDaoSvc.searchPersonByEid(eid.getValue());
if (foundPerson != null) {
Optional<IAnyResource> oFoundPerson = myEmpiResourceDaoSvc.searchPersonByEid(eid.getValue());
if (oFoundPerson.isPresent()) {
IAnyResource foundPerson = oFoundPerson.get();
Long pidOrNull = myIdHelperService.getPidOrNull(foundPerson);
MatchedPersonCandidate mpc = new MatchedPersonCandidate(new ResourcePersistentId(pidOrNull), EmpiMatchResultEnum.MATCH);
ourLog.debug("Matched {} by EID {}", foundPerson.getIdElement(), eid);

View File

@ -20,7 +20,9 @@ package ca.uhn.fhir.jpa.empi.svc;
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
@ -28,6 +30,7 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -35,9 +38,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class EmpiResourceDaoSvc {
private static final int MAX_MATCHING_PERSONS = 1000;
@Autowired
DaoRegistry myDaoRegistry;
@Autowired
@ -74,16 +81,33 @@ public class EmpiResourceDaoSvc {
return (IAnyResource) myPersonDao.readByPid(thePersonPid);
}
public IAnyResource searchPersonByEid(String theEidFromResource) {
public Optional<IAnyResource> searchPersonByEid(String theEid) {
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add("identifier", new TokenParam(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem(), theEidFromResource));
map.add("identifier", new TokenParam(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem(), theEid));
map.add("active", new TokenParam("true"));
IBundleProvider search = myPersonDao.search(map);
if (search.isEmpty()) {
return null;
// Could add the meta tag to the query, but it's probably more efficient to filter on it afterwards since in practice
// it will always be present.
List<IBaseResource> list = search.getResources(0, MAX_MATCHING_PERSONS).stream()
.filter(EmpiUtil::isEmpiManaged)
.collect(Collectors.toList());
if (list.isEmpty()) {
return Optional.empty();
} else if (list.size() > 1) {
throw new InternalErrorException("Found more than one active " +
EmpiConstants.CODE_HAPI_EMPI_MANAGED +
" Person with EID " +
theEid +
": " +
list.get(0).getIdElement().getValue() +
", " +
list.get(1).getIdElement().getValue()
);
} else {
return (IAnyResource) search.getResources(0, 1).get(0);
return Optional.of((IAnyResource) list.get(0));
}
}
}

View File

@ -256,6 +256,11 @@ abstract public class BaseEmpiR4Test extends BaseJpaR4Test {
return thePatient;
}
protected Person addExternalEID(Person thePerson, String theEID) {
thePerson.addIdentifier().setSystem(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem()).setValue(theEID);
return thePerson;
}
protected Patient clearExternalEIDs(Patient thePatient) {
thePatient.getIdentifier().removeIf(theIdentifier -> theIdentifier.getSystem().equalsIgnoreCase(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem()));
return thePatient;

View File

@ -12,8 +12,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class EmpiLinkDaoSvcTest extends BaseEmpiR4Test {
@Autowired
@ -27,7 +27,7 @@ public class EmpiLinkDaoSvcTest extends BaseEmpiR4Test {
myEmpiLinkDaoSvc.save(empiLink);
assertThat(empiLink.getCreated(), is(notNullValue()));
assertThat(empiLink.getUpdated(), is(notNullValue()));
assertEquals(empiLink.getCreated(), empiLink.getUpdated());
assertTrue(empiLink.getUpdated().getTime() - empiLink.getCreated().getTime() < 1000);
}
@Test

View File

@ -1,15 +1,14 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.model.CanonicalEID;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Person;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
@ -33,12 +32,12 @@ import static org.slf4j.LoggerFactory.getLogger;
public class EmpiMatchLinkSvcMultipleEidModeTest extends BaseEmpiR4Test {
private static final Logger ourLog = getLogger(EmpiMatchLinkSvcMultipleEidModeTest.class);
@Autowired
IEmpiLinkSvc myEmpiLinkSvc;
@Autowired
private EIDHelper myEidHelper;
@Autowired
private PersonHelper myPersonHelper;
@Before
public void before() {
super.loadEmpiSearchParameters();
}
@Test
public void testIncomingPatientWithEIDThatMatchesPersonWithHapiEidAddsExternalEidsToPerson() {

View File

@ -18,6 +18,7 @@ import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.Practitioner;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
@ -47,6 +48,11 @@ public class EmpiMatchLinkSvcTest extends BaseEmpiR4Test {
@Autowired
private PersonHelper myPersonHelper;
@Before
public void before() {
super.loadEmpiSearchParameters();
}
@Test
public void testAddPatientLinksToNewPersonIfNoneFound() {
createPatientAndUpdateLinks(buildJanePatient());

View File

@ -0,0 +1,52 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.r4.model.Person;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Optional;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
public class EmpiResourceDaoSvcTest extends BaseEmpiR4Test {
private static final String TEST_EID = "TEST_EID";
@Autowired
EmpiResourceDaoSvc myResourceDaoSvc;
@Before
public void before() {
super.loadEmpiSearchParameters();
}
@Test
public void testSearchPersonByEidExcludesInactive() {
Person goodPerson = addExternalEID(createPerson(), TEST_EID);
myPersonDao.update(goodPerson);
Person badPerson = addExternalEID(createPerson(), TEST_EID);
badPerson.setActive(false);
myPersonDao.update(badPerson);
Optional<IAnyResource> foundPerson = myResourceDaoSvc.searchPersonByEid(TEST_EID);
assertTrue(foundPerson.isPresent());
assertThat(foundPerson.get().getIdElement().toUnqualifiedVersionless().getValue(), is(goodPerson.getIdElement().toUnqualifiedVersionless().getValue()));
}
@Test
public void testSearchPersonByEidExcludesNonEmpiManaged() {
Person goodPerson = addExternalEID(createPerson(), TEST_EID);
myPersonDao.update(goodPerson);
Person badPerson = addExternalEID(createPerson(new Person(), false), TEST_EID);
myPersonDao.update(badPerson);
Optional<IAnyResource> foundPerson = myResourceDaoSvc.searchPersonByEid(TEST_EID);
assertTrue(foundPerson.isPresent());
assertThat(foundPerson.get().getIdElement().toUnqualifiedVersionless().getValue(), is(goodPerson.getIdElement().toUnqualifiedVersionless().getValue()));
}
}

View File

@ -58,17 +58,20 @@ public class LastNParameterHelper {
private static boolean isLastNParameterDstu3(String theParamName) {
return (theParamName.equals(org.hl7.fhir.dstu3.model.Observation.SP_SUBJECT) || theParamName.equals(org.hl7.fhir.dstu3.model.Observation.SP_PATIENT)
|| theParamName.equals(org.hl7.fhir.dstu3.model.Observation.SP_CATEGORY) || theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_CODE));
|| theParamName.equals(org.hl7.fhir.dstu3.model.Observation.SP_CATEGORY) || theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_CODE))
|| theParamName.equals(org.hl7.fhir.dstu3.model.Observation.SP_DATE);
}
private static boolean isLastNParameterR4(String theParamName) {
return (theParamName.equals(org.hl7.fhir.r4.model.Observation.SP_SUBJECT) || theParamName.equals(org.hl7.fhir.r4.model.Observation.SP_PATIENT)
|| theParamName.equals(org.hl7.fhir.r4.model.Observation.SP_CATEGORY) || theParamName.equals(org.hl7.fhir.r4.model.Observation.SP_CODE));
|| theParamName.equals(org.hl7.fhir.r4.model.Observation.SP_CATEGORY) || theParamName.equals(org.hl7.fhir.r4.model.Observation.SP_CODE))
|| theParamName.equals(org.hl7.fhir.r4.model.Observation.SP_DATE);
}
private static boolean isLastNParameterR5(String theParamName) {
return (theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_SUBJECT) || theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_PATIENT)
|| theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_CATEGORY) || theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_CODE));
|| theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_CATEGORY) || theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_CODE))
|| theParamName.equals(org.hl7.fhir.r5.model.Observation.SP_DATE);
}
public static String getSubjectParamName(FhirContext theContext) {