From f6d4a403880550ba8d1d8025b4fbd02efed936d9 Mon Sep 17 00:00:00 2001 From: Frank Tao <38163583+frankjtao@users.noreply.github.com> Date: Sun, 10 Jan 2021 21:05:24 -0500 Subject: [PATCH] UCUM Service Support (#2261) * Added UcumServiceUtil * Removed ucum-essense.xml * Added ucum service existing test cases passed * Added more test cases * Merge branch 'master' into ft-ucum-support # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit. * Added back extractReferenceParamsAsQueryTokens * Added changelog and migration script * Added length for the string type * Moved UCUM util to hapi-fhir-jpaserver-model * Renamed UCUM support to Normalized Quantity Search Support * Moved setNormalizedQuantitySearchNotSupported to AfterEach * Changed migrate task to 530 * Removed comments * Fixed in memory search issue and added more test cases * Fixed the version order * License header updates * Updated based on review comments * Updated the migration task based on review comments * Fixed RES_TYPE with type String and length 100 * Changed myParamsQuantityNormalizedPopulated type to Boolean class --- .../changelog/5_3_0/2261-ucum-support.yaml | 5 + .../ca/uhn/fhir/jpa/config/BaseConfig.java | 7 + ...dexedSearchParamQuantityNormalizedDao.java | 34 ++ .../dao/expunge/ExpungeEverythingService.java | 2 + .../dao/expunge/ResourceExpungeService.java | 6 + .../dao/expunge/ResourceTableFKProvider.java | 1 + .../dao/index/DaoSearchParamSynchronizer.java | 2 + .../fhir/jpa/search/builder/QueryStack.java | 27 +- .../QuantityBasePredicateBuilder.java | 131 ++++++ .../QuantityNormalizedPredicateBuilder.java | 38 ++ .../predicate/QuantityPredicateBuilder.java | 82 +--- .../builder/sql/SearchQueryBuilder.java | 11 + .../search/builder/sql/SqlObjectFactory.java | 5 + .../reindex/ResourceReindexingSvcImpl.java | 4 + .../jpa/dao/dstu3/CustomObservationDstu3.java | 2 +- .../ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java | 3 + .../fhir/jpa/dao/r4/CustomObservationR4.java | 2 +- .../dao/r4/FhirResourceDaoR4CreateTest.java | 209 +++++++-- ...ourceDaoR4SearchCustomSearchParamTest.java | 43 ++ .../FhirResourceDaoR4SearchMissingTest.java | 123 ++++++ .../r4/FhirResourceDaoR4SearchNoFtTest.java | 350 +++++++++++---- .../FhirResourceDaoR4SearchNoHashesTest.java | 105 +++++ .../jpa/dao/r4/FhirResourceDaoR4Test.java | 164 ++++++- .../fhir/jpa/dao/r4/FhirSystemDaoR4Test.java | 75 ++++ .../dao/r4/SearchParamExtractorR4Test.java | 107 +++-- .../fhir/jpa/provider/r4/ExpungeR4Test.java | 57 +++ .../r4/ResourceProviderHasParamR4Test.java | 1 - .../provider/r4/ResourceProviderR4Test.java | 410 +++++++++++++++--- .../InMemorySubscriptionMatcherR4Test.java | 95 +++- .../tasks/HapiFhirJpaMigrationTasks.java | 36 +- hapi-fhir-jpaserver-model/pom.xml | 7 + .../fhir/jpa/model/entity/ModelConfig.java | 43 ++ .../entity/NormalizedQuantitySearchLevel.java | 37 ++ ...esourceIndexedSearchParamBaseQuantity.java | 147 +++++++ .../ResourceIndexedSearchParamQuantity.java | 226 +++------- ...eIndexedSearchParamQuantityNormalized.java | 239 ++++++++++ .../fhir/jpa/model/entity/ResourceTable.java | 49 ++- .../fhir/jpa/model/util/UcumServiceUtil.java | 100 +++++ ...exedSearchParamQuantityNormalizedTest.java | 82 ++++ .../jpa/model/util/UcumServiceUtilTest.java | 55 +++ .../extractor/BaseSearchParamExtractor.java | 97 ++++- .../extractor/ISearchParamExtractor.java | 36 +- .../ResourceIndexedSearchParams.java | 29 +- .../SearchParamExtractorService.java | 16 +- .../module/SubscriptionTestConfig.java | 2 +- 45 files changed, 2789 insertions(+), 513 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2261-ucum-support.yaml create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamQuantityNormalizedDao.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/QuantityBasePredicateBuilder.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/QuantityNormalizedPredicateBuilder.java create mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NormalizedQuantitySearchLevel.java create mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamBaseQuantity.java create mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityNormalized.java create mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/UcumServiceUtil.java create mode 100644 hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityNormalizedTest.java create mode 100644 hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/UcumServiceUtilTest.java diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2261-ucum-support.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2261-ucum-support.yaml new file mode 100644 index 00000000000..aaed233cca6 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2261-ucum-support.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 2261 +title: "Optionally supports storage and search in canonical form of the quantity value which is defined by 'http://unitsofmeasure.org'; + please check ModelConfig for the configuration. No changes were made to the existing behaviour." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index bfe36f6d018..bdffba29f46 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -92,6 +92,7 @@ import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ForcedIdPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.QuantityPredicateBuilder; +import ca.uhn.fhir.jpa.search.builder.predicate.QuantityNormalizedPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder; @@ -551,6 +552,12 @@ public abstract class BaseConfig { return new QuantityPredicateBuilder(theSearchBuilder); } + @Bean + @Scope("prototype") + public QuantityNormalizedPredicateBuilder newQuantityNormalizedPredicateBuilder(SearchQueryBuilder theSearchBuilder) { + return new QuantityNormalizedPredicateBuilder(theSearchBuilder); + } + @Bean @Scope("prototype") public ResourceLinkPredicateBuilder newResourceLinkPredicateBuilder(QueryStack theQueryStack, SearchQueryBuilder theSearchBuilder, boolean theReversed) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamQuantityNormalizedDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamQuantityNormalizedDao.java new file mode 100644 index 00000000000..65fdaeadf87 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamQuantityNormalizedDao.java @@ -0,0 +1,34 @@ +package ca.uhn.fhir.jpa.dao.data; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * 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.springframework.data.jpa.repository.JpaRepository; + +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface IResourceIndexedSearchParamQuantityNormalizedDao extends JpaRepository { + @Modifying + @Query("delete from ResourceIndexedSearchParamQuantityNormalized t WHERE t.myResourcePid = :resid") + void deleteByResourceId(@Param("resid") Long theResourcePid); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java index d6a4369253c..5da2372c9b6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java @@ -53,6 +53,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; @@ -126,6 +127,7 @@ public class ExpungeEverythingService { counter.addAndGet(expungeEverythingByType(ResourceIndexedSearchParamDate.class)); counter.addAndGet(expungeEverythingByType(ResourceIndexedSearchParamNumber.class)); counter.addAndGet(expungeEverythingByType(ResourceIndexedSearchParamQuantity.class)); + counter.addAndGet(expungeEverythingByType(ResourceIndexedSearchParamQuantityNormalized.class)); counter.addAndGet(expungeEverythingByType(ResourceIndexedSearchParamString.class)); counter.addAndGet(expungeEverythingByType(ResourceIndexedSearchParamToken.class)); counter.addAndGet(expungeEverythingByType(ResourceIndexedSearchParamUri.class)); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java index 8c173d928e1..525cbd16b13 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java @@ -33,6 +33,7 @@ import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamCoordsDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamDateDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamNumberDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamQuantityDao; +import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamQuantityNormalizedDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamStringDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamTokenDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao; @@ -90,6 +91,8 @@ public class ResourceExpungeService implements IResourceExpungeService { @Autowired private IResourceIndexedSearchParamQuantityDao myResourceIndexedSearchParamQuantityDao; @Autowired + private IResourceIndexedSearchParamQuantityNormalizedDao myResourceIndexedSearchParamQuantityNormalizedDao; + @Autowired private IResourceIndexedSearchParamCoordsDao myResourceIndexedSearchParamCoordsDao; @Autowired private IResourceIndexedSearchParamNumberDao myResourceIndexedSearchParamNumberDao; @@ -279,6 +282,9 @@ public class ResourceExpungeService implements IResourceExpungeService { if (resource == null || resource.isParamsQuantityPopulated()) { myResourceIndexedSearchParamQuantityDao.deleteByResourceId(theResourceId); } + if (resource == null || resource.isParamsQuantityNormalizedPopulated()) { + myResourceIndexedSearchParamQuantityNormalizedDao.deleteByResourceId(theResourceId); + } if (resource == null || resource.isParamsStringPopulated()) { myResourceIndexedSearchParamStringDao.deleteByResourceId(theResourceId); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceTableFKProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceTableFKProvider.java index 1eea4c16324..0175bf37b3f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceTableFKProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceTableFKProvider.java @@ -50,6 +50,7 @@ public class ResourceTableFKProvider { retval.add(new ResourceForeignKey("HFJ_SPIDX_DATE", "RES_ID")); retval.add(new ResourceForeignKey("HFJ_SPIDX_NUMBER", "RES_ID")); retval.add(new ResourceForeignKey("HFJ_SPIDX_QUANTITY", "RES_ID")); + retval.add(new ResourceForeignKey("HFJ_SPIDX_QUANTITY_NRML", "RES_ID")); retval.add(new ResourceForeignKey("HFJ_SPIDX_STRING", "RES_ID")); retval.add(new ResourceForeignKey("HFJ_SPIDX_TOKEN", "RES_ID")); retval.add(new ResourceForeignKey("HFJ_SPIDX_URI", "RES_ID")); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java index 0e7708426d2..61cc169ffb8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java @@ -56,6 +56,7 @@ public class DaoSearchParamSynchronizer { synchronize(theEntity, retVal, theParams.myTokenParams, existingParams.myTokenParams); synchronize(theEntity, retVal, theParams.myNumberParams, existingParams.myNumberParams); synchronize(theEntity, retVal, theParams.myQuantityParams, existingParams.myQuantityParams); + synchronize(theEntity, retVal, theParams.myQuantityNormalizedParams, existingParams.myQuantityNormalizedParams); synchronize(theEntity, retVal, theParams.myDateParams, existingParams.myDateParams); synchronize(theEntity, retVal, theParams.myUriParams, existingParams.myUriParams); synchronize(theEntity, retVal, theParams.myCoordsParams, existingParams.myCoordsParams); @@ -87,6 +88,7 @@ public class DaoSearchParamSynchronizer { for (T next : paramsToRemove) { myEntityManager.remove(next); theEntity.getParamsQuantity().remove(next); + theEntity.getParamsQuantityNormalized().remove(next); } for (T next : paramsToAdd) { myEntityManager.merge(next); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java index 1a23e07da81..a52a65a02b1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java @@ -1,6 +1,6 @@ package ca.uhn.fhir.jpa.search.builder; -/*- +/* * #%L * HAPI FHIR JPA Server * %% @@ -38,7 +38,7 @@ import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ForcedIdPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder; -import ca.uhn.fhir.jpa.search.builder.predicate.QuantityPredicateBuilder; +import ca.uhn.fhir.jpa.search.builder.predicate.QuantityBasePredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder; @@ -185,7 +185,14 @@ public class QueryStack { public void addSortOnQuantity(String theResourceName, String theParamName, boolean theAscending) { BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); - QuantityPredicateBuilder sortPredicateBuilder = mySqlBuilder.addQuantityPredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); + + QuantityBasePredicateBuilder sortPredicateBuilder = null; + if (myModelConfig.isNormalizedQuantitySearchSupported()) { + sortPredicateBuilder = mySqlBuilder.addQuantityNormalizedPredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); + } else { + sortPredicateBuilder = mySqlBuilder.addQuantityPredicateBuilder(firstPredicateBuilder.getResourceIdColumn()); + + } Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName); mySqlBuilder.addPredicate(hashIdentityPredicate); @@ -635,9 +642,14 @@ public class QueryStack { List theList, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { - - QuantityPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult(); - + + QuantityBasePredicateBuilder join = null; + + if (myModelConfig.isNormalizedQuantitySearchSupported()) { + join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn)).getResult(); + } else { + join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult(); + } if (theList.get(0).getMissing() != null) { return join.createPredicateParamMissingForNonReference(theResourceName, theSearchParam.getName(), theList.get(0).getMissing(), theRequestPartitionId); } @@ -911,7 +923,7 @@ public class QueryStack { @Nullable public Condition searchForIdsWithAndOr(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theParamName, List> theAndOrParams, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { - + if (theAndOrParams.isEmpty()) { return null; } @@ -1042,6 +1054,7 @@ public class QueryStack { return toAndPredicate(andPredicates); } + } public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/QuantityBasePredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/QuantityBasePredicateBuilder.java new file mode 100644 index 00000000000..7b610a72dae --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/QuantityBasePredicateBuilder.java @@ -0,0 +1,131 @@ +package ca.uhn.fhir.jpa.search.builder.predicate; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * 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 static org.apache.commons.lang3.ObjectUtils.defaultIfNull; +import static org.apache.commons.lang3.StringUtils.isBlank; + +import java.math.BigDecimal; + +import javax.persistence.criteria.CriteriaBuilder; + +import org.fhir.ucum.Pair; +import org.springframework.beans.factory.annotation.Autowired; + +import com.healthmarketscience.sqlbuilder.BinaryCondition; +import com.healthmarketscience.sqlbuilder.ComboCondition; +import com.healthmarketscience.sqlbuilder.Condition; +import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; +import com.healthmarketscience.sqlbuilder.dbspec.basic.DbTable; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamBaseQuantity; +import ca.uhn.fhir.jpa.search.builder.QueryStack; +import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.model.base.composite.BaseQuantityDt; +import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import ca.uhn.fhir.rest.param.QuantityParam; +import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; + + +public abstract class QuantityBasePredicateBuilder extends BaseSearchParamPredicateBuilder { + + protected DbColumn myColumnHashIdentitySystemUnits; + protected DbColumn myColumnHashIdentityUnits; + protected DbColumn myColumnValue; + + @Autowired + private FhirContext myFhirContext; + + @Autowired + private ModelConfig myModelConfig; + + + /** + * Constructor + */ + public QuantityBasePredicateBuilder(SearchQueryBuilder theSearchSqlBuilder, DbTable theTable) { + super(theSearchSqlBuilder, theTable); + } + + public Condition createPredicateQuantity(IQueryParameterType theParam, String theResourceName, String theParamName, CriteriaBuilder theBuilder, QuantityBasePredicateBuilder theFrom, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { + + String systemValue; + String unitsValue; + ParamPrefixEnum cmpValue; + BigDecimal valueValue; + + if (theParam instanceof BaseQuantityDt) { + BaseQuantityDt param = (BaseQuantityDt) theParam; + systemValue = param.getSystemElement().getValueAsString(); + unitsValue = param.getUnitsElement().getValueAsString(); + cmpValue = ParamPrefixEnum.forValue(param.getComparatorElement().getValueAsString()); + valueValue = param.getValueElement().getValue(); + } else if (theParam instanceof QuantityParam) { + QuantityParam param = (QuantityParam) theParam; + systemValue = param.getSystem(); + unitsValue = param.getUnits(); + cmpValue = param.getPrefix(); + valueValue = param.getValue(); + } else { + throw new IllegalArgumentException("Invalid quantity type: " + theParam.getClass()); + } + + if (myModelConfig.isNormalizedQuantitySearchSupported()) { + //-- convert the value/unit to the canonical form if any to use by the search + Pair canonicalForm = UcumServiceUtil.getCanonicalForm(systemValue, valueValue, unitsValue); + if (canonicalForm != null) { + valueValue = new BigDecimal(canonicalForm.getValue().asDecimal()); + unitsValue = canonicalForm.getCode(); + } + } + + Condition hashPredicate; + if (!isBlank(systemValue) && !isBlank(unitsValue)) { + long hash = ResourceIndexedSearchParamBaseQuantity.calculateHashSystemAndUnits(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName, systemValue, unitsValue); + hashPredicate = BinaryCondition.equalTo(myColumnHashIdentitySystemUnits, generatePlaceholder(hash)); + } else if (!isBlank(unitsValue)) { + long hash = ResourceIndexedSearchParamBaseQuantity.calculateHashUnits(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName, unitsValue); + hashPredicate = BinaryCondition.equalTo(myColumnHashIdentityUnits, generatePlaceholder(hash)); + } else { + long hash = BaseResourceIndexedSearchParam.calculateHashIdentity(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName); + hashPredicate = BinaryCondition.equalTo(getColumnHashIdentity(), generatePlaceholder(hash)); + } + + SearchFilterParser.CompareOperation operation = theOperation; + if (operation == null && cmpValue != null) { + operation = QueryStack.toOperation(cmpValue); + } + operation = defaultIfNull(operation, SearchFilterParser.CompareOperation.eq); + Condition numericPredicate = NumberPredicateBuilder.createPredicateNumeric(this, operation, valueValue, myColumnValue, "invalidQuantityPrefix", myFhirContext, theParam); + + return ComboCondition.and(hashPredicate, numericPredicate); + } + + public DbColumn getColumnValue() { + return myColumnValue; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/QuantityNormalizedPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/QuantityNormalizedPredicateBuilder.java new file mode 100644 index 00000000000..5ba6d6acd5f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/QuantityNormalizedPredicateBuilder.java @@ -0,0 +1,38 @@ +package ca.uhn.fhir.jpa.search.builder.predicate; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * 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 ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; + +public class QuantityNormalizedPredicateBuilder extends QuantityBasePredicateBuilder { + + /** + * Constructor + */ + public QuantityNormalizedPredicateBuilder(SearchQueryBuilder theSearchSqlBuilder) { + super(theSearchSqlBuilder, theSearchSqlBuilder.addTable("HFJ_SPIDX_QUANTITY_NRML")); + + myColumnHashIdentitySystemUnits = getTable().addColumn("HASH_IDENTITY_SYS_UNITS"); + myColumnHashIdentityUnits = getTable().addColumn("HASH_IDENTITY_AND_UNITS"); + myColumnValue = getTable().addColumn("SP_VALUE"); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/QuantityPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/QuantityPredicateBuilder.java index 7bdfcd52d73..2c973b7471e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/QuantityPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/QuantityPredicateBuilder.java @@ -20,96 +20,20 @@ package ca.uhn.fhir.jpa.search.builder.predicate; * #L% */ -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.interceptor.model.RequestPartitionId; -import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; -import ca.uhn.fhir.jpa.search.builder.QueryStack; -import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; -import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.model.base.composite.BaseQuantityDt; -import ca.uhn.fhir.rest.param.ParamPrefixEnum; -import ca.uhn.fhir.rest.param.QuantityParam; -import com.healthmarketscience.sqlbuilder.BinaryCondition; -import com.healthmarketscience.sqlbuilder.ComboCondition; -import com.healthmarketscience.sqlbuilder.Condition; -import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; -import org.springframework.beans.factory.annotation.Autowired; -import javax.persistence.criteria.CriteriaBuilder; -import java.math.BigDecimal; +public class QuantityPredicateBuilder extends QuantityBasePredicateBuilder { -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static org.apache.commons.lang3.StringUtils.isBlank; - -public class QuantityPredicateBuilder extends BaseSearchParamPredicateBuilder { - - private final DbColumn myColumnHashIdentitySystemUnits; - private final DbColumn myColumnHashIdentityUnits; - private final DbColumn myColumnValue; - @Autowired - private FhirContext myFhirContext; /** * Constructor */ public QuantityPredicateBuilder(SearchQueryBuilder theSearchSqlBuilder) { super(theSearchSqlBuilder, theSearchSqlBuilder.addTable("HFJ_SPIDX_QUANTITY")); - + myColumnHashIdentitySystemUnits = getTable().addColumn("HASH_IDENTITY_SYS_UNITS"); myColumnHashIdentityUnits = getTable().addColumn("HASH_IDENTITY_AND_UNITS"); myColumnValue = getTable().addColumn("SP_VALUE"); } - - - public Condition createPredicateQuantity(IQueryParameterType theParam, String theResourceName, String theParamName, CriteriaBuilder theBuilder, QuantityPredicateBuilder theFrom, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { - - String systemValue; - String unitsValue; - ParamPrefixEnum cmpValue; - BigDecimal valueValue; - - if (theParam instanceof BaseQuantityDt) { - BaseQuantityDt param = (BaseQuantityDt) theParam; - systemValue = param.getSystemElement().getValueAsString(); - unitsValue = param.getUnitsElement().getValueAsString(); - cmpValue = ParamPrefixEnum.forValue(param.getComparatorElement().getValueAsString()); - valueValue = param.getValueElement().getValue(); - } else if (theParam instanceof QuantityParam) { - QuantityParam param = (QuantityParam) theParam; - systemValue = param.getSystem(); - unitsValue = param.getUnits(); - cmpValue = param.getPrefix(); - valueValue = param.getValue(); - } else { - throw new IllegalArgumentException("Invalid quantity type: " + theParam.getClass()); - } - - Condition hashPredicate; - if (!isBlank(systemValue) && !isBlank(unitsValue)) { - long hash = ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName, systemValue, unitsValue); - hashPredicate = BinaryCondition.equalTo(myColumnHashIdentitySystemUnits, generatePlaceholder(hash)); - } else if (!isBlank(unitsValue)) { - long hash = ResourceIndexedSearchParamQuantity.calculateHashUnits(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName, unitsValue); - hashPredicate = BinaryCondition.equalTo(myColumnHashIdentityUnits, generatePlaceholder(hash)); - } else { - long hash = BaseResourceIndexedSearchParam.calculateHashIdentity(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName); - hashPredicate = BinaryCondition.equalTo(getColumnHashIdentity(), generatePlaceholder(hash)); - } - - SearchFilterParser.CompareOperation operation = theOperation; - if (operation == null && cmpValue != null) { - operation = QueryStack.toOperation(cmpValue); - } - operation = defaultIfNull(operation, SearchFilterParser.CompareOperation.eq); - Condition numericPredicate = NumberPredicateBuilder.createPredicateNumeric(this, operation, valueValue, myColumnValue, "invalidQuantityPrefix", myFhirContext, theParam); - - return ComboCondition.and(hashPredicate, numericPredicate); - } - - - public DbColumn getColumnValue() { - return myColumnValue; - } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/sql/SearchQueryBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/sql/SearchQueryBuilder.java index c8ba5170c9f..bffe7e05c5a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/sql/SearchQueryBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/sql/SearchQueryBuilder.java @@ -33,6 +33,7 @@ import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ForcedIdPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.QuantityPredicateBuilder; +import ca.uhn.fhir.jpa.search.builder.predicate.QuantityNormalizedPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder; @@ -201,11 +202,21 @@ public class SearchQueryBuilder { * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a QUANTITY search parameter */ public QuantityPredicateBuilder addQuantityPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { + QuantityPredicateBuilder retVal = mySqlBuilderFactory.quantityIndexTable(this); addTable(retVal, theSourceJoinColumn); + return retVal; } + public QuantityNormalizedPredicateBuilder addQuantityNormalizedPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) { + + QuantityNormalizedPredicateBuilder retVal = mySqlBuilderFactory.quantityNormalizedIndexTable(this); + addTable(retVal, theSourceJoinColumn); + + return retVal; + } + /** * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a _source search parameter */ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/sql/SqlObjectFactory.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/sql/SqlObjectFactory.java index 1d089491b0b..e0fcdbb0916 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/sql/SqlObjectFactory.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/sql/SqlObjectFactory.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ForcedIdPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.QuantityPredicateBuilder; +import ca.uhn.fhir.jpa.search.builder.predicate.QuantityNormalizedPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder; @@ -68,6 +69,10 @@ public class SqlObjectFactory { return myApplicationContext.getBean(QuantityPredicateBuilder.class, theSearchSqlBuilder); } + public QuantityNormalizedPredicateBuilder quantityNormalizedIndexTable(SearchQueryBuilder theSearchSqlBuilder) { + return myApplicationContext.getBean(QuantityNormalizedPredicateBuilder.class, theSearchSqlBuilder); + } + public ResourceLinkPredicateBuilder referenceIndexTable(QueryStack theQueryStack, SearchQueryBuilder theSearchSqlBuilder, boolean theReversed) { return myApplicationContext.getBean(ResourceLinkPredicateBuilder.class, theQueryStack, theSearchSqlBuilder, theReversed); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java index 12a8322c5b8..592118a685c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java @@ -488,6 +488,10 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { q.setParameter("id", theId); q.executeUpdate(); + q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamQuantityNormalized t WHERE t.myResourcePid = :id"); + q.setParameter("id", theId); + q.executeUpdate(); + q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamString t WHERE t.myResourcePid = :id"); q.setParameter("id", theId); q.executeUpdate(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/CustomObservationDstu3.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/CustomObservationDstu3.java index 7365ec07d95..bc013681ae2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/CustomObservationDstu3.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/CustomObservationDstu3.java @@ -4,7 +4,7 @@ package ca.uhn.fhir.jpa.dao.dstu3; * #%L * HAPI FHIR JPA Server * %% - * Copyright (C) 2014 - 2019 University Health Network + * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index 608d9aca363..efa965e4f84 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamCoordsDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamDateDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamQuantityDao; +import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamQuantityNormalizedDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamStringDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamTokenDao; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; @@ -222,6 +223,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil @Autowired protected IResourceIndexedSearchParamQuantityDao myResourceIndexedSearchParamQuantityDao; @Autowired + protected IResourceIndexedSearchParamQuantityNormalizedDao myResourceIndexedSearchParamQuantityNormalizedDao; + @Autowired protected IResourceIndexedSearchParamDateDao myResourceIndexedSearchParamDateDao; @Autowired protected IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/CustomObservationR4.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/CustomObservationR4.java index a14da2f9b93..4c7bc57365b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/CustomObservationR4.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/CustomObservationR4.java @@ -4,7 +4,7 @@ package ca.uhn.fhir.jpa.dao.r4; * #%L * HAPI FHIR JPA Server * %% - * Copyright (C) 2014 - 2019 University Health Network + * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java index 9860da500f3..16197adb501 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java @@ -1,33 +1,5 @@ package ca.uhn.fhir.jpa.dao.r4; -import ca.uhn.fhir.jpa.api.config.DaoConfig; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.rest.param.StringParam; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; -import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.util.TestUtil; -import org.apache.commons.lang3.time.DateUtils; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.DateType; -import org.hl7.fhir.r4.model.Enumerations; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Observation; -import org.hl7.fhir.r4.model.Organization; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.SampledData; -import org.hl7.fhir.r4.model.SearchParameter; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.PageRequest; - -import java.io.IOException; -import java.util.Date; - import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; @@ -36,6 +8,41 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang3.time.DateUtils; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Quantity; +import org.hl7.fhir.r4.model.SampledData; +import org.hl7.fhir.r4.model.SearchParameter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.PageRequest; + +import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.QuantityParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; + public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4CreateTest.class); @@ -44,6 +51,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { myDaoConfig.setResourceServerIdStrategy(new DaoConfig().getResourceServerIdStrategy()); myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy()); myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); + myModelConfig.setNormalizedQuantitySearchNotSupported(); } @Test @@ -71,7 +79,6 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { map.setLoadSynchronous(true); map.add(Patient.SP_GIVEN, new StringParam("수")); // rightmost character only assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), empty()); - } @Test @@ -329,6 +336,150 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { } + @Test + public void testCreateWithNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Observation obs = new Observation(); + obs.setStatus(Observation.ObservationStatus.FINAL); + Quantity q = new Quantity(); + q.setValueElement(new DecimalType(1.2)); + q.setUnit("CM"); + q.setSystem("http://unitsofmeasure.org"); + q.setCode("cm"); + obs.setValue(q); + + ourLog.info("Observation1: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + + assertTrue(myObservationDao.create(obs).getCreated()); + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + QuantityParam qp = new QuantityParam(); + qp.setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL); + qp.setValue(new BigDecimal("0.012")); + qp.setUnits("m"); + + map.add(Observation.SP_VALUE_QUANTITY, qp); + + IBundleProvider found = myObservationDao.search(map); + List ids = toUnqualifiedVersionlessIdValues(found); + + List resources = found.getResources(0, found.size()); + + assertEquals(1, ids.size()); + + ourLog.info("Observation2: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resources.get(0))); + } + @Test + public void testCreateWithNormalizedQuantitySearchSupportedWithVerySmallNumber() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Observation obs = new Observation(); + obs.setStatus(Observation.ObservationStatus.FINAL); + Quantity q = new Quantity(); + q.setValueElement(new DecimalType(0.0000012)); + q.setUnit("MM"); + q.setSystem("http://unitsofmeasure.org"); + q.setCode("mm"); + obs.setValue(q); + + ourLog.info("Observation1: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + + assertTrue(myObservationDao.create(obs).getCreated()); + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + QuantityParam qp = new QuantityParam(); + qp.setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL); + qp.setValue(new BigDecimal("0.0000000012")); + qp.setUnits("m"); + + map.add(Observation.SP_VALUE_QUANTITY, qp); + + IBundleProvider found = myObservationDao.search(map); + List ids = toUnqualifiedVersionlessIdValues(found); + + List resources = found.getResources(0, found.size()); + + assertEquals(1, ids.size()); + + ourLog.info("Observation2: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resources.get(0))); + + } + @Test + public void testCreateWithNormalizedQuantitySearchSupportedWithVerySmallNumber2() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Observation obs = new Observation(); + obs.setStatus(Observation.ObservationStatus.FINAL); + Quantity q = new Quantity(); + q.setValueElement(new DecimalType(149597.870691)); + q.setUnit("MM"); + q.setSystem("http://unitsofmeasure.org"); + q.setCode("mm"); + obs.setValue(q); + + ourLog.info("Observation1: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + + assertTrue(myObservationDao.create(obs).getCreated()); + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + QuantityParam qp = new QuantityParam(); + qp.setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL); + qp.setValue(new BigDecimal("149.597870691")); + qp.setUnits("m"); + + map.add(Observation.SP_VALUE_QUANTITY, qp); + + IBundleProvider found = myObservationDao.search(map); + List ids = toUnqualifiedVersionlessIdValues(found); + + List resources = found.getResources(0, found.size()); + + assertEquals(1, ids.size()); + + ourLog.info("Observation2: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resources.get(0))); + + } + + @Test + public void testCreateWithNormalizedQuantitySearchSupportedWithLargeNumber() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Observation obs = new Observation(); + obs.setStatus(Observation.ObservationStatus.FINAL); + Quantity q = new Quantity(); + q.setValueElement(new DecimalType(95.7412345)); + q.setUnit("kg/dL"); + q.setSystem("http://unitsofmeasure.org"); + q.setCode("kg/dL"); + obs.setValue(q); + + ourLog.info("Observation1: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + + assertTrue(myObservationDao.create(obs).getCreated()); + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + QuantityParam qp = new QuantityParam(); + qp.setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL); + qp.setValue(new BigDecimal("957412345")); + qp.setUnits("g.m-3"); + + map.add(Observation.SP_VALUE_QUANTITY, qp); + + IBundleProvider found = myObservationDao.search(map); + List ids = toUnqualifiedVersionlessIdValues(found); + + List resources = found.getResources(0, found.size()); + + assertEquals(1, ids.size()); + + ourLog.info("Observation2: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resources.get(0))); + + } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java index 19a5babacf1..8d828d8ceb8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java @@ -86,6 +86,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test @AfterEach public void after() { myDaoConfig.setValidateSearchParameterExpressionsOnSave(new DaoConfig().isValidateSearchParameterExpressionsOnSave()); + myModelConfig.setNormalizedQuantitySearchNotSupported(); } @BeforeEach @@ -97,6 +98,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test @Test public void testStoreSearchParamWithBracketsInExpression() { + myDaoConfig.setMarkResourcesForReindexingUponSearchParameterChange(true); SearchParameter fooSp = new SearchParameter(); @@ -113,6 +115,47 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test mySearchParamRegistry.forceRefresh(); } + @Test + public void testStoreSearchParamWithBracketsInExpressionNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + myDaoConfig.setMarkResourcesForReindexingUponSearchParameterChange(true); + + SearchParameter fooSp = new SearchParameter(); + fooSp.setCode("foo"); + fooSp.addBase("ActivityDefinition"); + fooSp.setType(Enumerations.SearchParamType.REFERENCE); + fooSp.setTitle("FOO SP"); + fooSp.setExpression("(ActivityDefinition.useContext.value as Quantity) | (ActivityDefinition.useContext.value as Range)"); + fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL); + fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); + + // Ensure that no exceptions are thrown + mySearchParameterDao.create(fooSp, mySrd); + mySearchParamRegistry.forceRefresh(); + + } + + @Test + public void testStoreSearchParamWithBracketsInExpressionNormalizedQuantityStorageSupported() { + + myModelConfig.setNormalizedQuantityStorageSupported(); + myDaoConfig.setMarkResourcesForReindexingUponSearchParameterChange(true); + + SearchParameter fooSp = new SearchParameter(); + fooSp.setCode("foo"); + fooSp.addBase("ActivityDefinition"); + fooSp.setType(Enumerations.SearchParamType.REFERENCE); + fooSp.setTitle("FOO SP"); + fooSp.setExpression("(ActivityDefinition.useContext.value as Quantity) | (ActivityDefinition.useContext.value as Range)"); + fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL); + fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); + + // Ensure that no exceptions are thrown + mySearchParameterDao.create(fooSp, mySrd); + mySearchParamRegistry.forceRefresh(); + } + /** * See #2023 */ diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchMissingTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchMissingTest.java index 347af8a40a6..49484ef60be 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchMissingTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchMissingTest.java @@ -11,6 +11,7 @@ import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -30,6 +31,11 @@ public class FhirResourceDaoR4SearchMissingTest extends BaseJpaR4Test { public void beforeResetMissing() { myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.ENABLED); } + + @AfterEach + public void afterResetSearch() { + myModelConfig.setNormalizedQuantitySearchNotSupported(); + } @Test public void testIndexMissingFieldsDisabledDontAllowInSearch_NonReference() { @@ -72,6 +78,40 @@ public class FhirResourceDaoR4SearchMissingTest extends BaseJpaR4Test { } + @Test + public void testIndexMissingFieldsDisabledDontCreateIndexesWithNormalizedQuantitySearchSupported() { + + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); + myModelConfig.setNormalizedQuantitySearchSupported(); + Organization org = new Organization(); + org.setActive(true); + myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + + assertThat(mySearchParamPresentDao.findAll(), empty()); + assertThat(myResourceIndexedSearchParamStringDao.findAll(), empty()); + assertThat(myResourceIndexedSearchParamDateDao.findAll(), empty()); + assertThat(myResourceIndexedSearchParamTokenDao.findAll(), hasSize(1)); + assertThat(myResourceIndexedSearchParamQuantityDao.findAll(), empty()); + + } + + @Test + public void testIndexMissingFieldsDisabledDontCreateIndexesWithNormalizedQuantityStorageSupported() { + + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); + myModelConfig.setNormalizedQuantityStorageSupported(); + Organization org = new Organization(); + org.setActive(true); + myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + + assertThat(mySearchParamPresentDao.findAll(), empty()); + assertThat(myResourceIndexedSearchParamStringDao.findAll(), empty()); + assertThat(myResourceIndexedSearchParamDateDao.findAll(), empty()); + assertThat(myResourceIndexedSearchParamTokenDao.findAll(), hasSize(1)); + assertThat(myResourceIndexedSearchParamQuantityDao.findAll(), empty()); + + } + @SuppressWarnings("unused") @Test public void testSearchResourceReferenceMissingChain() { @@ -267,6 +307,89 @@ public class FhirResourceDaoR4SearchMissingTest extends BaseJpaR4Test { } } + @Test + public void testSearchWithMissingQuantityWithNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + IIdType notMissing; + IIdType missing; + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("001"); + missing = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + } + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("002"); + obs.setValue(new Quantity(123)); + notMissing = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + } + // Quantity Param + { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + QuantityParam param = new QuantityParam(); + param.setMissing(false); + params.add(Observation.SP_VALUE_QUANTITY, param); + List patients = toUnqualifiedVersionlessIds(myObservationDao.search(params)); + assertThat(patients, not(containsInRelativeOrder(missing))); + assertThat(patients, containsInRelativeOrder(notMissing)); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + QuantityParam param = new QuantityParam(); + param.setMissing(true); + params.add(Observation.SP_VALUE_QUANTITY, param); + List patients = toUnqualifiedVersionlessIds(myObservationDao.search(params)); + assertThat(patients, containsInRelativeOrder(missing)); + assertThat(patients, not(containsInRelativeOrder(notMissing))); + } + + } + + @Test + public void testSearchWithMissingQuantityWithNormalizedQuantityStorageSupported() { + + myModelConfig.setNormalizedQuantityStorageSupported(); + IIdType notMissing; + IIdType missing; + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("001"); + missing = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + } + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("002"); + obs.setValue(new Quantity(123)); + notMissing = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + } + // Quantity Param + { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + QuantityParam param = new QuantityParam(); + param.setMissing(false); + params.add(Observation.SP_VALUE_QUANTITY, param); + List patients = toUnqualifiedVersionlessIds(myObservationDao.search(params)); + assertThat(patients, not(containsInRelativeOrder(missing))); + assertThat(patients, containsInRelativeOrder(notMissing)); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + QuantityParam param = new QuantityParam(); + param.setMissing(true); + params.add(Observation.SP_VALUE_QUANTITY, param); + List patients = toUnqualifiedVersionlessIds(myObservationDao.search(params)); + assertThat(patients, containsInRelativeOrder(missing)); + assertThat(patients, not(containsInRelativeOrder(notMissing))); + } + + } + + @Test public void testSearchWithMissingReference() { IIdType orgId = myOrganizationDao.create(new Organization(), mySrd).getId().toUnqualifiedVersionless(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index 5a86ce3fa06..b1c66e1ad73 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java @@ -1,58 +1,39 @@ package ca.uhn.fhir.jpa.dao.r4; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.interceptor.api.HookParams; -import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor; -import ca.uhn.fhir.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.api.config.DaoConfig; -import ca.uhn.fhir.jpa.entity.Search; -import ca.uhn.fhir.jpa.model.config.PartitionSettings; -import ca.uhn.fhir.jpa.model.entity.ModelConfig; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; -import ca.uhn.fhir.jpa.model.entity.ResourceLink; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; -import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; -import ca.uhn.fhir.jpa.searchparam.MatchUrlService; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; -import ca.uhn.fhir.jpa.util.SqlQuery; -import ca.uhn.fhir.jpa.util.TestUtil; -import ca.uhn.fhir.model.api.Include; -import ca.uhn.fhir.model.api.TemporalPrecisionEnum; -import ca.uhn.fhir.model.primitive.InstantDt; -import ca.uhn.fhir.parser.StrictErrorHandler; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.param.CompositeParam; -import ca.uhn.fhir.rest.param.DateParam; -import ca.uhn.fhir.rest.param.DateRangeParam; -import ca.uhn.fhir.rest.param.HasAndListParam; -import ca.uhn.fhir.rest.param.HasOrListParam; -import ca.uhn.fhir.rest.param.HasParam; -import ca.uhn.fhir.rest.param.NumberParam; -import ca.uhn.fhir.rest.param.ParamPrefixEnum; -import ca.uhn.fhir.rest.param.QuantityParam; -import ca.uhn.fhir.rest.param.ReferenceAndListParam; -import ca.uhn.fhir.rest.param.ReferenceOrListParam; -import ca.uhn.fhir.rest.param.ReferenceParam; -import ca.uhn.fhir.rest.param.StringAndListParam; -import ca.uhn.fhir.rest.param.StringOrListParam; -import ca.uhn.fhir.rest.param.StringParam; -import ca.uhn.fhir.rest.param.TokenAndListParam; -import ca.uhn.fhir.rest.param.TokenOrListParam; -import ca.uhn.fhir.rest.param.TokenParam; -import ca.uhn.fhir.rest.param.TokenParamModifier; -import ca.uhn.fhir.rest.param.UriParam; -import ca.uhn.fhir.rest.param.UriParamQualifierEnum; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; -import ca.uhn.fhir.util.HapiExtensions; -import com.google.common.collect.Lists; +import static ca.uhn.fhir.rest.api.Constants.PARAM_TYPE; +import static org.apache.commons.lang3.StringUtils.countMatches; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; + import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateUtils; @@ -134,38 +115,61 @@ import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; -import java.util.stream.Collectors; +import com.google.common.collect.Lists; -import static ca.uhn.fhir.rest.api.Constants.PARAM_TYPE; -import static org.apache.commons.lang3.StringUtils.countMatches; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.endsWith; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.hasItems; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.not; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.interceptor.api.HookParams; +import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.entity.Search; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; +import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; +import ca.uhn.fhir.jpa.searchparam.MatchUrlService; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; +import ca.uhn.fhir.jpa.util.SqlQuery; +import ca.uhn.fhir.jpa.util.TestUtil; +import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.model.api.TemporalPrecisionEnum; +import ca.uhn.fhir.parser.StrictErrorHandler; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.CompositeParam; +import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.HasAndListParam; +import ca.uhn.fhir.rest.param.HasOrListParam; +import ca.uhn.fhir.rest.param.HasParam; +import ca.uhn.fhir.rest.param.NumberParam; +import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import ca.uhn.fhir.rest.param.QuantityParam; +import ca.uhn.fhir.rest.param.ReferenceAndListParam; +import ca.uhn.fhir.rest.param.ReferenceOrListParam; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.TokenParamModifier; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.param.UriParamQualifierEnum; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import ca.uhn.fhir.util.HapiExtensions; +import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; @SuppressWarnings({"unchecked", "Duplicates"}) public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { @@ -181,6 +185,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches()); myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds()); myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields()); + myModelConfig.setNormalizedQuantitySearchNotSupported(); } @BeforeEach @@ -1217,11 +1222,139 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { assertEquals(2, results.size()); }); + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamQuantityNormalized.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(0, results.size()); + }); + List actual = toUnqualifiedVersionlessIds( mySubstanceDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Substance.SP_QUANTITY, new QuantityParam(null, 123, "http://foo", "UNIT")))); assertThat(actual, contains(id)); } + @Test + public void testIndexNoDuplicatesQuantityWithNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Substance res = new Substance(); + res.addInstance().getQuantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("m").setValue(123); + res.addInstance().getQuantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("m").setValue(123); + res.addInstance().getQuantity().setSystem("http://foo2").setCode("UNIT2").setValue(1232); + res.addInstance().getQuantity().setSystem("http://foo2").setCode("UNIT2").setValue(1232); + + IIdType id = mySubstanceDao.create(res, mySrd).getId().toUnqualifiedVersionless(); + + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamQuantity.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(2, results.size()); + }); + + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamQuantityNormalized.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(2, results.size()); + }); + + List actual = toUnqualifiedVersionlessIds( + mySubstanceDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Substance.SP_QUANTITY, new QuantityParam(null, 12300, UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")))); + assertThat(actual, contains(id)); + + } + + @Test + public void testQuantityWithNormalizedQuantitySearchSupported_InvalidUCUMCode() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Substance res = new Substance(); + res.addInstance().getQuantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("FOO").setValue(123); + + IIdType id = mySubstanceDao.create(res, mySrd).getId().toUnqualifiedVersionless(); + + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamQuantity.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(1, results.size()); + }); + + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamQuantityNormalized.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(1, results.size()); + }); + + List actual = toUnqualifiedVersionlessIds( + mySubstanceDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Substance.SP_QUANTITY, new QuantityParam(null, 123, UcumServiceUtil.UCUM_CODESYSTEM_URL, "FOO")))); + assertThat(actual, contains(id)); + + } + + @Test + public void testQuantityWithNormalizedQuantitySearchSupported_NotUCUM() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Substance res = new Substance(); + res.addInstance().getQuantity().setSystem("http://bar").setCode("FOO").setValue(123); + + IIdType id = mySubstanceDao.create(res, mySrd).getId().toUnqualifiedVersionless(); + + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamQuantity.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(1, results.size()); + }); + + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamQuantityNormalized.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(1, results.size()); + }); + + List actual = toUnqualifiedVersionlessIds( + mySubstanceDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Substance.SP_QUANTITY, new QuantityParam(null, 123, "http://bar", "FOO")))); + assertThat(actual, contains(id)); + + } + + @Test + public void testIndexNoDuplicatesQuantityWithNormalizedQuantityStorageSupported() { + + myModelConfig.setNormalizedQuantityStorageSupported(); + Substance res = new Substance(); + res.addInstance().getQuantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("m").setValue(123); + res.addInstance().getQuantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("m").setValue(123); + res.addInstance().getQuantity().setSystem("http://foo2").setCode("UNIT2").setValue(1232); + res.addInstance().getQuantity().setSystem("http://foo2").setCode("UNIT2").setValue(1232); + + IIdType id = mySubstanceDao.create(res, mySrd).getId().toUnqualifiedVersionless(); + + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamQuantity.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(2, results.size()); + }); + + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamQuantityNormalized.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(2, results.size()); + }); + + List actual = toUnqualifiedVersionlessIds( + mySubstanceDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Substance.SP_QUANTITY, new QuantityParam(null, 123, UcumServiceUtil.UCUM_CODESYSTEM_URL, "m")))); + assertThat(actual, contains(id)); + } + @Test public void testIndexNoDuplicatesReference() { ServiceRequest pr = new ServiceRequest(); @@ -2618,7 +2751,32 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { } + @Test + public void testSearchByMoneyParamWithNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + ChargeItem ci = new ChargeItem(); + ci.getPriceOverride().setValue(123).setCurrency("$"); + myChargeItemDao.create(ci); + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(ChargeItem.SP_PRICE_OVERRIDE, new QuantityParam().setValue(123)); + assertEquals(1, myChargeItemDao.search(map).size().intValue()); + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(ChargeItem.SP_PRICE_OVERRIDE, new QuantityParam().setValue(123).setUnits("$")); + assertEquals(1, myChargeItemDao.search(map).size().intValue()); + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(ChargeItem.SP_PRICE_OVERRIDE, new QuantityParam().setValue(123).setUnits("$").setSystem("urn:iso:std:iso:4217")); + assertEquals(1, myChargeItemDao.search(map).size().intValue()); + + } + @Test public void testSearchNameParam() { IIdType id1; @@ -2959,6 +3117,30 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { } + @Test + public void testSearchQuantityWithNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Condition c1 = new Condition(); + c1.setAbatement(new Range().setLow(new SimpleQuantity().setValue(1L)).setHigh(new SimpleQuantity().setValue(1L))); + String id1 = myConditionDao.create(c1).getId().toUnqualifiedVersionless().getValue(); + + Condition c2 = new Condition(); + c2.setOnset(new Range().setLow(new SimpleQuantity().setValue(1L)).setHigh(new SimpleQuantity().setValue(1L))); + String id2 = myConditionDao.create(c2).getId().toUnqualifiedVersionless().getValue(); + + { + IBundleProvider found = myConditionDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Condition.SP_ABATEMENT_AGE, new QuantityParam("1"))); + assertThat(toUnqualifiedVersionlessIdValues(found), containsInAnyOrder(id1)); + assertEquals(1, found.size().intValue()); + } + { + IBundleProvider found = myConditionDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Condition.SP_ONSET_AGE, new QuantityParam("1"))); + assertThat(toUnqualifiedVersionlessIdValues(found), containsInAnyOrder(id2)); + assertEquals(1, found.size().intValue()); + } + } + @Test public void testSearchResourceLinkOnCanonical() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java index bf98bcde62d..7e6d9f6ede2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java @@ -42,6 +42,8 @@ import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.param.UriParamQualifierEnum; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; + import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IAnyResource; @@ -141,6 +143,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { myDaoConfig.setFetchSizeDefaultMaximum(new DaoConfig().getFetchSizeDefaultMaximum()); myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches()); myDaoConfig.setDisableHashBasedSearches(false); + myModelConfig.setNormalizedQuantitySearchNotSupported(); } @BeforeEach @@ -1188,6 +1191,52 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { } } + @Test + public void testComponentQuantityWithNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Observation o1 = new Observation(); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm").setValue(1.2)); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("m"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("mm").setValue(2)); + IIdType id1 = myObservationDao.create(o1, mySrd).getId().toUnqualifiedVersionless(); + + String param = Observation.SP_COMPONENT_VALUE_QUANTITY; + + { + QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 0.012, UcumServiceUtil.UCUM_CODESYSTEM_URL, "m"); + SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, v1); + IBundleProvider result = myObservationDao.search(map); + assertThat("Got: " + toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1.getValue())); + } + } + + @Test + public void testComponentQuantityWithNormalizedQuantityStorageSupported() { + + myModelConfig.setNormalizedQuantityStorageSupported(); + Observation o1 = new Observation(); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm").setValue(1.2)); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("m"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("mm").setValue(2)); + IIdType id1 = myObservationDao.create(o1, mySrd).getId().toUnqualifiedVersionless(); + + String param = Observation.SP_COMPONENT_VALUE_QUANTITY; + + { + QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 1.2, UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm"); + SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, v1); + IBundleProvider result = myObservationDao.search(map); + assertThat("Got: " + toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1.getValue())); + } + } + @Test public void testSearchCompositeParamQuantity() { Observation o1 = new Observation(); @@ -1241,6 +1290,62 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { } } + @Test + public void testSearchCompositeParamQuantityWithNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Observation o1 = new Observation(); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code1"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("dm").setValue(10)); // 0.1m + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code2"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm").setValue(12));// 0.012m + IIdType id1 = myObservationDao.create(o1, mySrd).getId().toUnqualifiedVersionless(); + + Observation o2 = new Observation(); + o2.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code1"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("dm").setValue(20)); //0.2m + o2.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code3"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("dm").setValue(22)); //0.022m + IIdType id2 = myObservationDao.create(o2, mySrd).getId().toUnqualifiedVersionless(); + + String param = Observation.SP_COMPONENT_CODE_VALUE_QUANTITY; + + { + TokenParam v0 = new TokenParam("http://foo", "code1"); + QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 15, UcumServiceUtil.UCUM_CODESYSTEM_URL, "dm"); // 0.15m + CompositeParam val = new CompositeParam<>(v0, v1); + SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, val); + IBundleProvider result = myObservationDao.search(map); + assertThat("Got: " + toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id2.getValue())); + } + { + TokenParam v0 = new TokenParam("http://foo", "code1"); + QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 5, UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm"); //0.05m + CompositeParam val = new CompositeParam<>(v0, v1); + IBundleProvider result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(param, val)); + assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1.getValue(), id2.getValue())); + } + { + TokenParam v0 = new TokenParam("http://foo", "code4"); + QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 5, UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm"); //0.05m + CompositeParam val = new CompositeParam<>(v0, v1); + IBundleProvider result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(param, val)); + assertThat(toUnqualifiedVersionlessIdValues(result), empty()); + } + { + TokenParam v0 = new TokenParam("http://foo", "code1"); + QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 5, UcumServiceUtil.UCUM_CODESYSTEM_URL, "m"); //5m + CompositeParam val = new CompositeParam<>(v0, v1); + IBundleProvider result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(param, val)); + assertThat(toUnqualifiedVersionlessIdValues(result), empty()); + } + + } + @Test public void testSearchDateWrongParam() { Patient p1 = new Patient(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java index ea6d4e1a2dc..109cc815914 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java @@ -42,6 +42,8 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; + import com.google.common.base.Charsets; import com.google.common.collect.Lists; import org.apache.commons.io.IOUtils; @@ -159,6 +161,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { myDaoConfig.setEnforceReferentialIntegrityOnDelete(new DaoConfig().isEnforceReferentialIntegrityOnDelete()); myDaoConfig.setEnforceReferenceTargetTypes(new DaoConfig().isEnforceReferenceTargetTypes()); myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields()); + myModelConfig.setNormalizedQuantitySearchNotSupported(); } @BeforeEach @@ -576,45 +579,149 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { } } + @Test + public void testChoiceParamQuantityWithNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Observation o3 = new Observation(); + o3.getCode().addCoding().setSystem("foo").setCode("testChoiceParam03"); + o3.setValue(new Quantity(QuantityComparator.GREATER_THAN, 123.0, UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm", "cm")); // 0.0123m + IIdType id3 = myObservationDao.create(o3, mySrd).getId(); + + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam(">100", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + assertEquals(1, found.size().intValue()); + assertEquals(id3, found.getResources(0, 1).get(0).getIdElement()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("gt100", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + assertEquals(1, found.size().intValue()); + assertEquals(id3, found.getResources(0, 1).get(0).getIdElement()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("<100", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + assertEquals(0, found.size().intValue()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("lt100", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + assertEquals(0, found.size().intValue()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.0001", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + assertEquals(0, found.size().intValue()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("~120", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + assertEquals(1, found.size().intValue()); + assertEquals(id3, found.getResources(0, 1).get(0).getIdElement()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("ap120", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + assertEquals(1, found.size().intValue()); + assertEquals(id3, found.getResources(0, 1).get(0).getIdElement()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("eq123", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + assertEquals(1, found.size().intValue()); + assertEquals(id3, found.getResources(0, 1).get(0).getIdElement()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("eq120", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + assertEquals(0, found.size().intValue()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("ne120", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + assertEquals(1, found.size().intValue()); + assertEquals(id3, found.getResources(0, 1).get(0).getIdElement()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("ne123", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + assertEquals(0, found.size().intValue()); + } + + } + @Test public void testChoiceParamQuantityPrecision() { Observation o3 = new Observation(); o3.getCode().addCoding().setSystem("foo").setCode("testChoiceParam03"); - o3.setValue(new Quantity(null, 123.01, "foo", "bar", "bar")); + o3.setValue(new Quantity(null, 123.01, UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm", "cm")); // 0.012301 m IIdType id3 = myObservationDao.create(o3, mySrd).getId().toUnqualifiedVersionless(); { - IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123", "foo", "bar")).setLoadSynchronous(true)); + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); List list = toUnqualifiedVersionlessIds(found); assertThat(list, Matchers.empty()); } { - IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.0", "foo", "bar")).setLoadSynchronous(true)); + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.0", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); List list = toUnqualifiedVersionlessIds(found); assertThat(list, Matchers.empty()); } { - IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.01", "foo", "bar")).setLoadSynchronous(true)); + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.01", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); List list = toUnqualifiedVersionlessIds(found); assertThat(list, containsInAnyOrder(id3)); } { - IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.010", "foo", "bar")).setLoadSynchronous(true)); + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.010", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); List list = toUnqualifiedVersionlessIds(found); assertThat(list, containsInAnyOrder(id3)); } { - IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.02", "foo", "bar")).setLoadSynchronous(true)); + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.02", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); List list = toUnqualifiedVersionlessIds(found); assertThat(list, Matchers.empty()); } { - IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.001", "foo", "bar")).setLoadSynchronous(true)); + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.001", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); List list = toUnqualifiedVersionlessIds(found); assertThat(list, Matchers.empty()); } } + @Test + public void testChoiceParamQuantityPrecisionWithNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Observation o3 = new Observation(); + o3.getCode().addCoding().setSystem("foo").setCode("testChoiceParam03"); + o3.setValue(new Quantity(null, 123.01, UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm", "cm")); // 0.012301 m + IIdType id3 = myObservationDao.create(o3, mySrd).getId().toUnqualifiedVersionless(); + + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + List list = toUnqualifiedVersionlessIds(found); + assertThat(list, Matchers.empty()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.0", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + List list = toUnqualifiedVersionlessIds(found); + assertThat(list, Matchers.empty()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.01", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + List list = toUnqualifiedVersionlessIds(found); + assertThat(list, containsInAnyOrder(id3)); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.010", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + List list = toUnqualifiedVersionlessIds(found); + assertThat(list, containsInAnyOrder(id3)); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.02", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + List list = toUnqualifiedVersionlessIds(found); + assertThat(list, Matchers.empty()); + } + { + IBundleProvider found = myObservationDao.search(new SearchParameterMap(Observation.SP_VALUE_QUANTITY, new QuantityParam("123.001", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm")).setLoadSynchronous(true)); + List list = toUnqualifiedVersionlessIds(found); + assertThat(list, Matchers.empty()); + } + + } + @Test public void testChoiceParamString() { @@ -1417,7 +1524,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { myObservationDao.read(obs1id); myObservationDao.read(obs2id); } - + @Test public void testDeleteWithMatchUrl() { String methodName = "testDeleteWithMatchUrl"; @@ -3391,6 +3498,47 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { assertThat(actual, contains(id4, id3, id2, id1)); } + + @Test + public void testSortByQuantityWithNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Observation res; + + res = new Observation(); + res.setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm").setValue(2)); // 0.02m + IIdType id2 = myObservationDao.create(res, mySrd).getId().toUnqualifiedVersionless(); + + res = new Observation(); + res.setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("dm").setValue(0.1)); // 0.01m + IIdType id1 = myObservationDao.create(res, mySrd).getId().toUnqualifiedVersionless(); + + res = new Observation(); + res.setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("m").setValue(0.03)); // 0.03m + IIdType id3 = myObservationDao.create(res, mySrd).getId().toUnqualifiedVersionless(); + + res = new Observation(); + res.setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm").setValue(4)); // 0.04m + IIdType id4 = myObservationDao.create(res, mySrd).getId().toUnqualifiedVersionless(); + + SearchParameterMap pm = new SearchParameterMap(); + pm.setSort(new SortSpec(Observation.SP_VALUE_QUANTITY)); + List actual = toUnqualifiedVersionlessIds(myObservationDao.search(pm)); + assertEquals(4, actual.size()); + assertThat(actual, contains(id1, id2, id3, id4)); + + pm = new SearchParameterMap(); + pm.setSort(new SortSpec(Observation.SP_VALUE_QUANTITY, SortOrderEnum.ASC)); + actual = toUnqualifiedVersionlessIds(myObservationDao.search(pm)); + assertEquals(4, actual.size()); + assertThat(actual, contains(id1, id2, id3, id4)); + + pm = new SearchParameterMap(); + pm.setSort(new SortSpec(Observation.SP_VALUE_QUANTITY, SortOrderEnum.DESC)); + actual = toUnqualifiedVersionlessIds(myObservationDao.search(pm)); + assertEquals(4, actual.size()); + assertThat(actual, contains(id4, id3, id2, id1)); + } @Test public void testSortByReference() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java index 6635ec8b5b2..c7cd84e9db3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java @@ -110,6 +110,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { public void after() { myDaoConfig.setAllowInlineMatchUrlReferences(false); myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete()); + myModelConfig.setNormalizedQuantitySearchNotSupported(); } @BeforeEach @@ -3127,6 +3128,80 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { assertEquals(id.getValue(), id2.getValue()); } + @Test + public void testTransactionWithConditionalUpdateDoesntUpdateIfNoChangeWithNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Observation obs = new Observation(); + obs.addIdentifier() + .setSystem("http://acme.org") + .setValue("ID1"); + obs + .getCode() + .addCoding() + .setSystem("http://loinc.org") + .setCode("29463-7"); + obs.setEffective(new DateTimeType("2011-09-03T11:13:00-04:00")); + obs.setValue(new Quantity() + .setValue(new BigDecimal("123.4")) + .setCode("kg") + .setSystem("http://unitsofmeasure.org") + .setUnit("kg")); + Bundle input = new Bundle(); + input.setType(BundleType.TRANSACTION); + input + .addEntry() + .setFullUrl("urn:uuid:0001") + .setResource(obs) + .getRequest() + .setMethod(HTTPVerb.PUT) + .setUrl("Observation?identifier=http%3A%2F%2Facme.org|ID1"); + + Bundle output = mySystemDao.transaction(mySrd, input); + assertEquals(1, output.getEntry().size()); + IdType id = new IdType(output.getEntry().get(0).getResponse().getLocation()); + assertEquals("Observation", id.getResourceType()); + assertEquals("1", id.getVersionIdPart()); + + /* + * Try again with same contents + */ + + Observation obs2 = new Observation(); + obs2.addIdentifier() + .setSystem("http://acme.org") + .setValue("ID1"); + obs2 + .getCode() + .addCoding() + .setSystem("http://loinc.org") + .setCode("29463-7"); + obs2.setEffective(new DateTimeType("2011-09-03T11:13:00-04:00")); + obs2.setValue(new Quantity() + .setValue(new BigDecimal("123.4")) + .setCode("kg") + .setSystem("http://unitsofmeasure.org") + .setUnit("kg")); + Bundle input2 = new Bundle(); + input2.setType(BundleType.TRANSACTION); + input2 + .addEntry() + .setFullUrl("urn:uuid:0001") + .setResource(obs2) + .getRequest() + .setMethod(HTTPVerb.PUT) + .setUrl("Observation?identifier=http%3A%2F%2Facme.org|ID1"); + + Bundle output2 = mySystemDao.transaction(mySrd, input2); + assertEquals(1, output2.getEntry().size()); + IdType id2 = new IdType(output2.getEntry().get(0).getResponse().getLocation()); + assertEquals("Observation", id2.getResourceType()); + assertEquals("1", id2.getVersionIdPart()); + + assertEquals(id.getValue(), id2.getValue()); + + } + @Test public void testTransactionWithIfMatch() { Patient p = new Patient(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java index b7b413aae58..02655f22356 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java @@ -1,5 +1,40 @@ package ca.uhn.fhir.jpa.dao.r4; +import static java.util.Comparator.comparing; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.hl7.fhir.dstu2.model.SimpleQuantity; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Consent; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Quantity; +import org.hl7.fhir.r4.model.Range; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.SearchParameter; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Sets; + import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.RuntimeResourceDefinition; @@ -12,6 +47,7 @@ import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; @@ -22,36 +58,7 @@ import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.registry.ReadOnlySearchParamCache; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.util.HapiExtensions; -import com.google.common.collect.Sets; -import org.hl7.fhir.r4.model.BooleanType; -import org.hl7.fhir.r4.model.CodeableConcept; -import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.Consent; -import org.hl7.fhir.r4.model.Encounter; -import org.hl7.fhir.r4.model.Extension; -import org.hl7.fhir.r4.model.Observation; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Quantity; -import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.SearchParameter; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import static java.util.Comparator.comparing; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; public class SearchParamExtractorR4Test { @@ -334,6 +341,46 @@ public class SearchParamExtractorR4Test { assertEquals(4, links.size()); } + @Test + public void testExtractComponentQuantityWithNormalizedQuantitySearchSupported() { + + ModelConfig modelConfig = new ModelConfig(); + + modelConfig.setNormalizedQuantitySearchSupported(); + + Observation o1 = new Observation(); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code1"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm").setValue(200)); + + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(modelConfig, new PartitionSettings(), ourCtx, ourValidationSupport, mySearchParamRegistry); + Set links = extractor.extractSearchParamQuantityNormalized(o1); + ourLog.info("Links:\n {}", links.stream().map(t -> t.toString()).collect(Collectors.joining("\n "))); + assertEquals(2, links.size()); + + } + + @Test + public void testExtractComponentQuantityValueWithNormalizedQuantitySearchSupported() { + + ModelConfig modelConfig = new ModelConfig(); + + modelConfig.setNormalizedQuantitySearchSupported(); + + Observation o1 = new Observation(); + + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code1"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm").setValue(200)); + + RuntimeSearchParam existingCodeSp = mySearchParamRegistry.getActiveSearchParams("Observation").get("component-value-quantity"); + + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(modelConfig, new PartitionSettings(), ourCtx, ourValidationSupport, mySearchParamRegistry); + List list = extractor.extractParamValuesAsStrings(existingCodeSp, o1); + + assertEquals(1, list.size()); + } + private static class MySearchParamRegistry implements ISearchParamRegistry { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java index 2df8b6570a3..56aa943a3a1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java @@ -14,14 +14,19 @@ import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.HapiExtensions; +import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; + import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.SearchParameter; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -57,6 +62,7 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { @AfterEach public void afterDisableExpunge() { myDaoConfig.setExpungeEnabled(new DaoConfig().isExpungeEnabled()); + myModelConfig.setNormalizedQuantitySearchNotSupported(); } @BeforeEach @@ -150,11 +156,18 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { myTwoVersionObservationId = myObservationDao.create(o).getId(); o.setStatus(Observation.ObservationStatus.AMENDED); myTwoVersionObservationId = myObservationDao.update(o).getId(); + CodeableConcept cc = o.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + o.setValue(new Quantity().setValueElement(new DecimalType(125.12)).setUnit("CM").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm")); o = new Observation(); o.setStatus(Observation.ObservationStatus.FINAL); myDeletedObservationId = myObservationDao.create(o).getId(); myDeletedObservationId = myObservationDao.delete(myDeletedObservationId).getId(); + cc = o.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + o.setValue(new Quantity().setValueElement(new DecimalType(13.45)).setUnit("DM").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("dm")); + } private IFhirResourceDao getDao(IIdType theId) { @@ -387,6 +400,50 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { assertExpunged(myDeletedObservationId); } + @Test + public void testExpungeSystemEverythingWithNormalizedQuantitySearchSupported() { + myModelConfig.setNormalizedQuantitySearchSupported(); + createStandardPatients(); + + mySystemDao.expunge(new ExpungeOptions() + .setExpungeEverything(true), null); + + // Everything deleted + assertExpunged(myOneVersionPatientId); + assertExpunged(myTwoVersionPatientId.withVersion("1")); + assertExpunged(myTwoVersionPatientId.withVersion("2")); + assertExpunged(myDeletedPatientId.withVersion("1")); + assertExpunged(myDeletedPatientId); + + // Everything deleted + assertExpunged(myOneVersionObservationId); + assertExpunged(myTwoVersionObservationId.withVersion("1")); + assertExpunged(myTwoVersionObservationId.withVersion("2")); + assertExpunged(myDeletedObservationId); + } + + @Test + public void testExpungeSystemEverythingWithNormalizedQuantityStorageSupported() { + myModelConfig.setNormalizedQuantityStorageSupported(); + createStandardPatients(); + + mySystemDao.expunge(new ExpungeOptions() + .setExpungeEverything(true), null); + + // Everything deleted + assertExpunged(myOneVersionPatientId); + assertExpunged(myTwoVersionPatientId.withVersion("1")); + assertExpunged(myTwoVersionPatientId.withVersion("2")); + assertExpunged(myDeletedPatientId.withVersion("1")); + assertExpunged(myDeletedPatientId); + + // Everything deleted + assertExpunged(myOneVersionObservationId); + assertExpunged(myTwoVersionObservationId.withVersion("1")); + assertExpunged(myTwoVersionObservationId.withVersion("2")); + assertExpunged(myDeletedObservationId); + } + @Test public void testExpungeTypeOldVersionsAndDeleted() { createStandardPatients(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderHasParamR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderHasParamR4Test.java index 3265d97bae7..a79a48fceed 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderHasParamR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderHasParamR4Test.java @@ -163,7 +163,6 @@ public class ResourceProviderHasParamR4Test extends BaseResourceProviderR4Test { ourLog.info("uri = " + uri); List ids = searchAndReturnUnqualifiedVersionlessIdValues(uri); - System.out.println("ids.size() = " + ids.size()); assertThat(ids, contains(pid0.getValue())); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index cc344111f56..62576b232cc 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -1,45 +1,52 @@ package ca.uhn.fhir.jpa.provider.r4; -import ca.uhn.fhir.jpa.api.config.DaoConfig; -import ca.uhn.fhir.jpa.config.TestR4Config; -import ca.uhn.fhir.jpa.dao.data.ISearchDao; -import ca.uhn.fhir.jpa.entity.Search; -import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; -import ca.uhn.fhir.jpa.model.util.JpaConstants; -import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; -import ca.uhn.fhir.model.api.TemporalPrecisionEnum; -import ca.uhn.fhir.model.primitive.InstantDt; -import ca.uhn.fhir.model.primitive.UriDt; -import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.parser.StrictErrorHandler; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.api.PreferReturnEnum; -import ca.uhn.fhir.rest.api.SearchTotalModeEnum; -import ca.uhn.fhir.rest.api.SummaryEnum; -import ca.uhn.fhir.rest.client.apache.ResourceEntity; -import ca.uhn.fhir.rest.client.api.IClientInterceptor; -import ca.uhn.fhir.rest.client.api.IGenericClient; -import ca.uhn.fhir.rest.client.api.IHttpRequest; -import ca.uhn.fhir.rest.client.api.IHttpResponse; -import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; -import ca.uhn.fhir.rest.gclient.StringClientParam; -import ca.uhn.fhir.rest.param.DateRangeParam; -import ca.uhn.fhir.rest.param.NumberParam; -import ca.uhn.fhir.rest.param.ParamPrefixEnum; -import ca.uhn.fhir.rest.param.StringAndListParam; -import ca.uhn.fhir.rest.param.StringOrListParam; -import ca.uhn.fhir.rest.param.StringParam; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; -import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; -import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; -import ca.uhn.fhir.util.StopWatch; -import ca.uhn.fhir.util.UrlUtil; -import com.google.common.base.Charsets; -import com.google.common.collect.Lists; +import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast; +import static ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsInRelativeOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.matchesPattern; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.Matchers.stringContainsInOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.math.BigDecimal; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; @@ -80,6 +87,7 @@ import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Condition; import org.hl7.fhir.r4.model.DateTimeType; import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; import org.hl7.fhir.r4.model.Device; import org.hl7.fhir.r4.model.DiagnosticReport; import org.hl7.fhir.r4.model.DocumentManifest; @@ -129,36 +137,58 @@ import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus; import org.hl7.fhir.r4.model.UnsignedIntType; import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.utilities.xhtml.XhtmlNode; -import org.junit.jupiter.api.*; import static org.hamcrest.MatcherAssert.assertThat; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.util.AopTestUtils; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.math.BigDecimal; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketTimeoutException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; -import java.util.stream.Collectors; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; -import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast; -import static ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick; -import static org.apache.commons.lang3.StringUtils.isNotBlank; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; +import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.config.TestR4Config; +import ca.uhn.fhir.jpa.dao.data.ISearchDao; +import ca.uhn.fhir.jpa.entity.Search; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; +import ca.uhn.fhir.model.api.TemporalPrecisionEnum; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.model.primitive.UriDt; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.parser.StrictErrorHandler; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PreferReturnEnum; +import ca.uhn.fhir.rest.api.SearchTotalModeEnum; +import ca.uhn.fhir.rest.api.SummaryEnum; +import ca.uhn.fhir.rest.client.apache.ResourceEntity; +import ca.uhn.fhir.rest.client.api.IClientInterceptor; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.api.IHttpRequest; +import ca.uhn.fhir.rest.client.api.IHttpResponse; +import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; +import ca.uhn.fhir.rest.gclient.StringClientParam; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.NumberParam; +import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; +import ca.uhn.fhir.util.StopWatch; +import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; +import ca.uhn.fhir.util.UrlUtil; @SuppressWarnings("Duplicates") public class ResourceProviderR4Test extends BaseResourceProviderR4Test { @@ -187,6 +217,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE); mySearchCoordinatorSvcRaw.setNeverUseLocalSearchForUnitTests(false); mySearchCoordinatorSvcRaw.cancelAllActiveSearches(); + myDaoConfig.getModelConfig().setNormalizedQuantitySearchNotSupported(); myClient.unregisterInterceptor(myCapturingInterceptor); } @@ -727,7 +758,9 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { ourLog.info(resp); Bundle bundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, resp); ids = toUnqualifiedVersionlessIdValues(bundle); + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); } + return ids; } @@ -4050,6 +4083,154 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { assertTrue(value.before(after), new InstantDt(value) + " should be before " + new InstantDt(after)); } + @Test + public void testSearchWithNormalizedQuantitySearchSupported() throws Exception { + + myDaoConfig.getModelConfig().setNormalizedQuantitySearchSupported(); + IIdType pid0; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester").addGiven("Joe"); + pid0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + obs.setValue(new Quantity().setValueElement(new DecimalType(125.12)).setUnit("CM").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm")); + + myObservationDao.create(obs, mySrd); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + obs.setValue(new Quantity().setValueElement(new DecimalType(13.45)).setUnit("DM").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("dm")); + + myObservationDao.create(obs, mySrd); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + obs.setValue(new Quantity().setValueElement(new DecimalType(1.45)).setUnit("M").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("m")); + + myObservationDao.create(obs, mySrd); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + obs.setValue(new Quantity().setValueElement(new DecimalType(25)).setUnit("CM").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm")); + + myObservationDao.create(obs, mySrd); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + // > 1m + String uri = ourServerBase + "/Observation?code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$gt1|http://unitsofmeasure.org|m"); + ourLog.info("uri = " + uri); + List ids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + assertEquals(3, ids.size()); + + //>= 100cm + uri = ourServerBase + "/Observation?code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$gt100|http://unitsofmeasure.org|cm"); + ourLog.info("uri = " + uri); + ids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + assertEquals(3, ids.size()); + + //>= 10dm + uri = ourServerBase + "/Observation?code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$gt10|http://unitsofmeasure.org|dm"); + ourLog.info("uri = " + uri); + ids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + assertEquals(3, ids.size()); + } + + @Test + public void testSearchWithNormalizedQuantitySearchSupported_CombineUCUMOrNonUCUM() throws Exception { + + myDaoConfig.getModelConfig().setNormalizedQuantitySearchSupported(); + IIdType pid0; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester").addGiven("Joe"); + pid0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + obs.setValue(new Quantity().setValueElement(new DecimalType(1)).setUnit("M").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("m")); + + myObservationDao.create(obs, mySrd); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + obs.setValue(new Quantity().setValueElement(new DecimalType(13.45)).setUnit("DM").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("dm")); + + myObservationDao.create(obs, mySrd); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + obs.setValue(new Quantity().setValueElement(new DecimalType(1.45)).setUnit("M").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("m")); + + myObservationDao.create(obs, mySrd); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + CodeableConcept cc = obs.getCode(); + obs.setValue(new Quantity().setValueElement(new DecimalType(100)).setUnit("CM").setSystem("http://foo").setCode("cm")); + + myObservationDao.create(obs, mySrd); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + // > 1m + String uri = ourServerBase + "/Observation?value-quantity=" + UrlUtil.escapeUrlParam("100|http://unitsofmeasure.org|cm,100|http://foo|cm"); + + ourLog.info("uri = " + uri); + List ids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + assertEquals(2, ids.size()); + } + + @Test public void testSearchReusesNoParams() { List resources = new ArrayList<>(); @@ -5861,6 +6042,109 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { } + @Test + public void testUpdateWithNormalizedQuantitySearchSupported() throws Exception { + + myDaoConfig.getModelConfig().setNormalizedQuantitySearchSupported(); + IIdType pid0; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester").addGiven("Joe"); + pid0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester").addGiven("Joe"); + myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + obs.setValue(new Quantity().setValueElement(new DecimalType(125.12)).setUnit("CM").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm")); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + + IIdType opid1 = myObservationDao.create(obs, mySrd).getId(); + + //-- update quantity + obs = new Observation(); + obs.setId(opid1); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + obs.setValue(new Quantity().setValueElement(new DecimalType(24.12)).setUnit("CM").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm")); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + + myObservationDao.update(obs, mySrd); + } + + + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + obs.setValue(new Quantity().setValueElement(new DecimalType(13.45)).setUnit("DM").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("dm")); + + myObservationDao.create(obs, mySrd); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + obs.setValue(new Quantity().setValueElement(new DecimalType(1.45)).setUnit("M").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("m")); + + myObservationDao.create(obs, mySrd); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.getSubject().setReferenceElement(pid0); + CodeableConcept cc = obs.getCode(); + cc.addCoding().setCode("2345-7").setSystem("http://loinc.org"); + obs.setValue(new Quantity().setValueElement(new DecimalType(25)).setUnit("CM").setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm")); + + myObservationDao.create(obs, mySrd); + + ourLog.info("Observation: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + } + + // > 1m + String uri = ourServerBase + "/Observation?code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$gt1|http://unitsofmeasure.org|m"); + ourLog.info("uri = " + uri); + List ids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + assertEquals(2, ids.size()); + + + //>= 100cm + uri = ourServerBase + "/Observation?code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$gt100|http://unitsofmeasure.org|cm"); + ourLog.info("uri = " + uri); + ids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + assertEquals(2, ids.size()); + + //>= 10dm + uri = ourServerBase + "/Observation?code-value-quantity=http://" + UrlUtil.escapeUrlParam("loinc.org|2345-7$gt10|http://unitsofmeasure.org|dm"); + ourLog.info("uri = " + uri); + ids = searchAndReturnUnqualifiedVersionlessIdValues(uri); + assertEquals(2, ids.size()); + } private String toStr(Date theDate) { return new InstantDt(theDate).getValueAsString(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherR4Test.java index c98ae5ede91..e3764d82969 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherR4Test.java @@ -1,8 +1,11 @@ package ca.uhn.fhir.jpa.subscription.module.matcher; import ca.uhn.fhir.context.FhirContext; + import ca.uhn.fhir.jpa.config.TestR4Config; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult; @@ -61,6 +64,7 @@ import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Subscription; import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.ValueSet; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -71,7 +75,6 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.Locale; import java.util.TimeZone; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -95,6 +98,14 @@ public class InMemorySubscriptionMatcherR4Test { @Autowired MatchUrlService myMatchUrlService; + @Autowired + ModelConfig myModelConfig; + + @AfterEach + public void after() throws Exception { + myModelConfig.setNormalizedQuantitySearchNotSupported(); + } + private void assertMatched(Resource resource, SearchParameterMap params) { InMemoryMatchResult result = match(resource, params); assertTrue(result.supported(), result.getUnsupportedReason()); @@ -235,7 +246,89 @@ public class InMemorySubscriptionMatcherR4Test { SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(param, v1); assertMatched(o1, params); } + + @Test + public void testSearchWithNormalizedQuantitySearchSupported() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + Observation o1 = new Observation(); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("cm"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm").setValue(150)); + + String param1 = Observation.SP_COMPONENT_VALUE_QUANTITY; + + QuantityParam v1 = new QuantityParam(null, 1.5, UcumServiceUtil.UCUM_CODESYSTEM_URL, "m"); + SearchParameterMap params1 = new SearchParameterMap().setLoadSynchronous(true).add(param1, v1); + assertMatched(o1, params1); + + Observation o2 = new Observation(); + o2.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("cm"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("cm").setValue(150)); + + String param2 = Observation.SP_COMPONENT_VALUE_QUANTITY; + + QuantityParam v2 = new QuantityParam(null, 15, UcumServiceUtil.UCUM_CODESYSTEM_URL, "dm"); + SearchParameterMap params2 = new SearchParameterMap().setLoadSynchronous(true).add(param2, v2); + assertMatched(o2, params2); + + v2 = new QuantityParam(null, 150, UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm"); + params2 = new SearchParameterMap().setLoadSynchronous(true).add(param2, v2); + assertMatched(o2, params2); + + } + + @Test + public void testSearchWithNormalizedQuantitySearchSupported_InvalidUCUMUnit() { + myModelConfig.setNormalizedQuantitySearchSupported(); + + Observation o1 = new Observation(); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://bar").setCode("foo"))) + .setValue(new Quantity().setSystem(UcumServiceUtil.UCUM_CODESYSTEM_URL).setCode("foo").setValue(150)); + + String param1 = Observation.SP_COMPONENT_VALUE_QUANTITY; + + QuantityParam v1 = new QuantityParam(null, 150, UcumServiceUtil.UCUM_CODESYSTEM_URL, "foo"); + SearchParameterMap params1 = new SearchParameterMap().setLoadSynchronous(true).add(param1, v1); + assertMatched(o1, params1); + } + + @Test + public void testSearchWithNormalizedQuantitySearchSupported_NoSystem() { + myModelConfig.setNormalizedQuantitySearchSupported(); + + Observation o1 = new Observation(); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://bar").setCode("foo"))) + .setValue(new Quantity().setCode("foo").setValue(150)); + + String param1 = Observation.SP_COMPONENT_VALUE_QUANTITY; + + QuantityParam v1 = new QuantityParam(null, 150, null, "foo"); + SearchParameterMap params1 = new SearchParameterMap().setLoadSynchronous(true).add(param1, v1); + assertMatched(o1, params1); + } + + @Test + public void testSearchWithNormalizedQuantitySearchSupported_NotUcumSystem() { + + myModelConfig.setNormalizedQuantitySearchSupported(); + + Observation o1 = new Observation(); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("cm"))) + .setValue(new Quantity().setSystem("http://bar").setCode("cm").setValue(150)); + + String param1 = Observation.SP_COMPONENT_VALUE_QUANTITY; + + QuantityParam v1 = new QuantityParam(null, 150, "http://bar", "cm"); + SearchParameterMap params1 = new SearchParameterMap().setLoadSynchronous(true).add(param1, v1); + assertMatched(o1, params1); + } + @Test public void testIdSupported() { Observation o1 = new Observation(); diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index 0579a91779b..236230b1679 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -76,12 +76,40 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { private void init530() { Builder version = forVersion(VersionEnum.V5_3_0); + + //-- TRM version .onTable("TRM_VALUESET_CONCEPT") .dropIndex("20210104.1", "IDX_VS_CONCEPT_CS_CODE"); - version - .onTable("TRM_VALUESET_CONCEPT") - .addIndex("20210104.2", "IDX_VS_CONCEPT_CSCD").unique(true).withColumns("VALUESET_PID", "SYSTEM_URL", "CODEVAL"); + + version + .onTable("TRM_VALUESET_CONCEPT") + .addIndex("20210104.2", "IDX_VS_CONCEPT_CSCD").unique(true).withColumns("VALUESET_PID", "SYSTEM_URL", "CODEVAL"); + + //-- Add new Table, HFJ_SPIDX_QUANTITY_NRML + version.addIdGenerator("20210109.1", "SEQ_SPIDX_QUANTITY_NRML"); + Builder.BuilderAddTableByColumns pkg = version.addTableByColumns("20210109.2", "HFJ_SPIDX_QUANTITY_NRML", "SP_ID"); + pkg.addColumn("RES_ID").nonNullable().type(ColumnTypeEnum.LONG); + pkg.addColumn("RES_TYPE").nonNullable().type(ColumnTypeEnum.STRING, 100); + pkg.addColumn("SP_UPDATED").nullable().type(ColumnTypeEnum.DATE_TIMESTAMP); + pkg.addColumn("SP_MISSING").nonNullable().type(ColumnTypeEnum.BOOLEAN); + pkg.addColumn("SP_NAME").nonNullable().type(ColumnTypeEnum.STRING, 100); + pkg.addColumn("SP_ID").nonNullable().type(ColumnTypeEnum.LONG); + pkg.addColumn("SP_SYSTEM").nullable().type(ColumnTypeEnum.STRING, 200); + pkg.addColumn("SP_UNITS").nullable().type(ColumnTypeEnum.STRING, 200); + pkg.addColumn("HASH_IDENTITY_AND_UNITS").nullable().type(ColumnTypeEnum.LONG); + pkg.addColumn("HASH_IDENTITY_SYS_UNITS").nullable().type(ColumnTypeEnum.LONG); + pkg.addColumn("HASH_IDENTITY").nullable().type(ColumnTypeEnum.LONG); + pkg.addColumn("SP_VALUE").nullable().type(ColumnTypeEnum.FLOAT); + pkg.addIndex("20210109.3", "IDX_SP_QNTY_NRML_HASH").unique(false).withColumns("HASH_IDENTITY","SP_VALUE"); + pkg.addIndex("20210109.4", "IDX_SP_QNTY_NRML_HASH_UN").unique(false).withColumns("HASH_IDENTITY_AND_UNITS","SP_VALUE"); + pkg.addIndex("20210109.5", "IDX_SP_QNTY_NRML_HASH_SYSUN").unique(false).withColumns("HASH_IDENTITY_SYS_UNITS","SP_VALUE"); + pkg.addIndex("20210109.6", "IDX_SP_QNTY_NRML_UPDATED").unique(false).withColumns("SP_UPDATED"); + pkg.addIndex("20210109.7", "IDX_SP_QNTY_NRML_RESID").unique(false).withColumns("RES_ID"); + //pkg.addForeignKey("20210109.9", "FK_QNTY_NRML_RESID").toColumn("RES_ID").references("HFJ_RESOURCE", "RES_ID"); + + //-- Link to the resourceTable + version.onTable("HFJ_RESOURCE").addColumn("20210109.10", "SP_QUANTITY_NRML_PRESENT").nullable().type(ColumnTypeEnum.BOOLEAN); } @@ -96,7 +124,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .toColumn("GOLDEN_RESOURCE_PID") .references("HFJ_RESOURCE", "RES_ID"); } - + protected void init510() { Builder version = forVersion(VersionEnum.V5_1_0); diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index f64bd911a98..a33574e8ccf 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -15,6 +15,13 @@ HAPI FHIR Model + + + + org.fhir + ucum + + ca.uhn.hapi.fhir hapi-fhir-base diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java index d58d5597a2f..35402230b88 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java @@ -89,12 +89,15 @@ public class ModelConfig { private IPrimitiveType myPeriodIndexStartOfTime; private IPrimitiveType myPeriodIndexEndOfTime; + private NormalizedQuantitySearchLevel myNormalizedQuantitySearchLevel; + /** * Constructor */ public ModelConfig() { setPeriodIndexStartOfTime(new DateTimeType(DEFAULT_PERIOD_INDEX_START_OF_TIME)); setPeriodIndexEndOfTime(new DateTimeType(DEFAULT_PERIOD_INDEX_END_OF_TIME)); + setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED); } /** @@ -585,4 +588,44 @@ public class ModelConfig { } } + + /** + * Set the UCUM service support level + * + *

+ * The default value is {@link NormalizedQuantitySearchLevel#NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED} which is current behavior. + *

+ *

+ * Here is the UCUM service support level + *

    + *
  • {@link NormalizedQuantitySearchLevel#NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED}, default, Quantity is stored in {@link ResourceIndexedSearchParamQuantity} only and it is used by searching.
  • + *
  • {@link NormalizedQuantitySearchLevel#NORMALIZED_QUANTITY_STORAGE_SUPPORTED}, Quantity is stored in both {@link ResourceIndexedSearchParamQuantity} and {@link ResourceIndexedSearchParamQuantityNormalized}, but {@link ResourceIndexedSearchParamQuantity} is used by searching.
  • + *
  • {@link NormalizedQuantitySearchLevel#NORMALIZED_QUANTITY_SEARCH_SUPPORTED}, Quantity is stored in both {@link ResourceIndexedSearchParamQuantity} and {@link ResourceIndexedSearchParamQuantityNormalized}, {@link ResourceIndexedSearchParamQuantityNormalized} is used by searching.
  • + *
  • {@link NormalizedQuantitySearchLevel#NORMALIZED_QUANTITY_SEARCH_FULL_SUPPORTED}, Quantity is stored in only in {@link ResourceIndexedSearchParamQuantityNormalized}, {@link ResourceIndexedSearchParamQuantityNormalized} is used by searching. NOTE: this option is not supported yet.
  • + *
+ *

+ * + * @since 5.3.0 + */ + public NormalizedQuantitySearchLevel getNormalizedQuantitySearchLevel() { + return myNormalizedQuantitySearchLevel; + } + public void setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel theNormalizedQuantitySearchLevel) { + myNormalizedQuantitySearchLevel = theNormalizedQuantitySearchLevel; + } + public boolean isNormalizedQuantitySearchSupported() { + return myNormalizedQuantitySearchLevel.equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED); + } + public boolean isNormalizedQuantityStorageSupported() { + return myNormalizedQuantitySearchLevel.equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_STORAGE_SUPPORTED); + } + public void setNormalizedQuantitySearchNotSupported() { + myNormalizedQuantitySearchLevel = NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED; + } + public void setNormalizedQuantityStorageSupported() { + myNormalizedQuantitySearchLevel = NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_STORAGE_SUPPORTED; + } + public void setNormalizedQuantitySearchSupported() { + myNormalizedQuantitySearchLevel = NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED; + } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NormalizedQuantitySearchLevel.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NormalizedQuantitySearchLevel.java new file mode 100644 index 00000000000..764a8c26d78 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NormalizedQuantitySearchLevel.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.jpa.model.entity; + +/** + * Support different UCUM services level for FHIR Quantity data type. + * + * @since 5.3.0 + */ + +public enum NormalizedQuantitySearchLevel { + + /** + * default, Quantity is stored in {@link ResourceIndexedSearchParamQuantity} only and it is used by searching. + */ + NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED, + + /** + * Quantity is stored in both {@link ResourceIndexedSearchParamQuantity} + * and {@link ResourceIndexedSearchParamQuantityNormalized}, + * but {@link ResourceIndexedSearchParamQuantity} is used by searching. + */ + NORMALIZED_QUANTITY_STORAGE_SUPPORTED, + + /** + * Quantity is stored in both {@link ResourceIndexedSearchParamQuantity} + * and {@link ResourceIndexedSearchParamQuantityNormalized}, + * {@link ResourceIndexedSearchParamQuantityNormalized} is used by searching. + */ + NORMALIZED_QUANTITY_SEARCH_SUPPORTED, + + /** + * Quantity is stored in only in {@link ResourceIndexedSearchParamQuantityNormalized}, + * {@link ResourceIndexedSearchParamQuantityNormalized} is used by searching. + * The existing non normalized quantity will be not supported + * NOTE: this option is not supported in this release + */ + //NORMALIZED_QUANTITY_SEARCH_FULL_SUPPORTED, +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamBaseQuantity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamBaseQuantity.java new file mode 100644 index 00000000000..9e35c2c5bff --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamBaseQuantity.java @@ -0,0 +1,147 @@ +package ca.uhn.fhir.jpa.model.entity; + +/* + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * 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 javax.persistence.Column; +import javax.persistence.MappedSuperclass; + +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; + +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; + +@MappedSuperclass +public abstract class ResourceIndexedSearchParamBaseQuantity extends BaseResourceIndexedSearchParam { + + private static final int MAX_LENGTH = 200; + + private static final long serialVersionUID = 1L; + + @Column(name = "SP_SYSTEM", nullable = true, length = MAX_LENGTH) + @FullTextField + public String mySystem; + + @Column(name = "SP_UNITS", nullable = true, length = MAX_LENGTH) + @FullTextField + public String myUnits; + + /** + * @since 3.5.0 - At some point this should be made not-null + */ + @Column(name = "HASH_IDENTITY_AND_UNITS", nullable = true) + private Long myHashIdentityAndUnits; + /** + * @since 3.5.0 - At some point this should be made not-null + */ + @Column(name = "HASH_IDENTITY_SYS_UNITS", nullable = true) + private Long myHashIdentitySystemAndUnits; + /** + * @since 3.5.0 - At some point this should be made not-null + */ + @Column(name = "HASH_IDENTITY", nullable = true) + private Long myHashIdentity; + + public ResourceIndexedSearchParamBaseQuantity() { + super(); + } + + @Override + public void calculateHashes() { + String resourceType = getResourceType(); + String paramName = getParamName(); + String units = getUnits(); + String system = getSystem(); + setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); + setHashIdentityAndUnits(calculateHashUnits(getPartitionSettings(), getPartitionId(), resourceType, paramName, units)); + setHashIdentitySystemAndUnits(calculateHashSystemAndUnits(getPartitionSettings(), getPartitionId(), resourceType, paramName, system, units)); + } + + public Long getHashIdentity() { + return myHashIdentity; + } + + public void setHashIdentity(Long theHashIdentity) { + myHashIdentity = theHashIdentity; + } + + public Long getHashIdentityAndUnits() { + return myHashIdentityAndUnits; + } + + public void setHashIdentityAndUnits(Long theHashIdentityAndUnits) { + myHashIdentityAndUnits = theHashIdentityAndUnits; + } + + public Long getHashIdentitySystemAndUnits() { + return myHashIdentitySystemAndUnits; + } + + public void setHashIdentitySystemAndUnits(Long theHashIdentitySystemAndUnits) { + myHashIdentitySystemAndUnits = theHashIdentitySystemAndUnits; + } + + public String getSystem() { + return mySystem; + } + + public void setSystem(String theSystem) { + mySystem = theSystem; + } + + public String getUnits() { + return myUnits; + } + + public void setUnits(String theUnits) { + myUnits = theUnits; + } + + @Override + public int hashCode() { + HashCodeBuilder b = new HashCodeBuilder(); + b.append(getResourceType()); + b.append(getParamName()); + b.append(getHashIdentity()); + b.append(getHashIdentityAndUnits()); + b.append(getHashIdentitySystemAndUnits()); + return b.toHashCode(); + } + + + public static long calculateHashSystemAndUnits(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName, String theSystem, String theUnits) { + RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); + return calculateHashSystemAndUnits(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem, theUnits); + } + + public static long calculateHashSystemAndUnits(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName, String theSystem, String theUnits) { + return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, theSystem, theUnits); + } + + public static long calculateHashUnits(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName, String theUnits) { + RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); + return calculateHashUnits(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theUnits); + } + + public static long calculateHashUnits(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName, String theUnits) { + return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, theUnits); + } +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java index 5813c3b4a93..d435650cda0 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java @@ -20,16 +20,11 @@ package ca.uhn.fhir.jpa.model.entity; * #L% */ -import ca.uhn.fhir.interceptor.model.RequestPartitionId; -import ca.uhn.fhir.jpa.model.config.PartitionSettings; -import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.rest.param.QuantityParam; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ScaledNumberField; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isBlank; + +import java.math.BigDecimal; +import java.util.Objects; import javax.persistence.Column; import javax.persistence.Embeddable; @@ -40,11 +35,16 @@ import javax.persistence.Id; import javax.persistence.Index; import javax.persistence.SequenceGenerator; import javax.persistence.Table; -import java.math.BigDecimal; -import java.util.Objects; -import static org.apache.commons.lang3.StringUtils.defaultString; -import static org.apache.commons.lang3.StringUtils.isBlank; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ScaledNumberField; + +import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.QuantityParam; //@formatter:off @Embeddable @@ -57,49 +57,24 @@ import static org.apache.commons.lang3.StringUtils.isBlank; @Index(name = "IDX_SP_QUANTITY_UPDATED", columnList = "SP_UPDATED"), @Index(name = "IDX_SP_QUANTITY_RESID", columnList = "RES_ID") }) -public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearchParam { - - private static final int MAX_LENGTH = 200; +public class ResourceIndexedSearchParamQuantity extends ResourceIndexedSearchParamBaseQuantity { private static final long serialVersionUID = 1L; - @Column(name = "SP_SYSTEM", nullable = true, length = MAX_LENGTH) - @FullTextField - public String mySystem; - - @Column(name = "SP_UNITS", nullable = true, length = MAX_LENGTH) - @FullTextField - public String myUnits; - @Column(name = "SP_VALUE", nullable = true) - - @ScaledNumberField - public BigDecimal myValue; - + @Id @SequenceGenerator(name = "SEQ_SPIDX_QUANTITY", sequenceName = "SEQ_SPIDX_QUANTITY") @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_QUANTITY") @Column(name = "SP_ID") private Long myId; - /** - * @since 3.5.0 - At some point this should be made not-null - */ - @Column(name = "HASH_IDENTITY_AND_UNITS", nullable = true) - private Long myHashIdentityAndUnits; - /** - * @since 3.5.0 - At some point this should be made not-null - */ - @Column(name = "HASH_IDENTITY_SYS_UNITS", nullable = true) - private Long myHashIdentitySystemAndUnits; - /** - * @since 3.5.0 - At some point this should be made not-null - */ - @Column(name = "HASH_IDENTITY", nullable = true) - private Long myHashIdentity; + + @Column(name = "SP_VALUE", nullable = true) + @ScaledNumberField + public BigDecimal myValue; public ResourceIndexedSearchParamQuantity() { super(); } - public ResourceIndexedSearchParamQuantity(PartitionSettings thePartitionSettings, String theResourceType, String theParamName, BigDecimal theValue, String theSystem, String theUnits) { this(); setPartitionSettings(thePartitionSettings); @@ -118,21 +93,46 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc mySystem = source.mySystem; myUnits = source.myUnits; myValue = source.myValue; - myHashIdentity = source.myHashIdentity; - myHashIdentityAndUnits = source.myHashIdentitySystemAndUnits; - myHashIdentitySystemAndUnits = source.myHashIdentitySystemAndUnits; + setHashIdentity(source.getHashIdentity()); + setHashIdentityAndUnits(source.getHashIdentityAndUnits()); + setHashIdentitySystemAndUnits(source.getHashIdentitySystemAndUnits()); + } + + public BigDecimal getValue() { + return myValue; } + public ResourceIndexedSearchParamQuantity setValue(BigDecimal theValue) { + myValue = theValue; + return this; + } + + @Override + public Long getId() { + return myId; + } @Override - public void calculateHashes() { - String resourceType = getResourceType(); - String paramName = getParamName(); - String units = getUnits(); - String system = getSystem(); - setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); - setHashIdentityAndUnits(calculateHashUnits(getPartitionSettings(), getPartitionId(), resourceType, paramName, units)); - setHashIdentitySystemAndUnits(calculateHashSystemAndUnits(getPartitionSettings(), getPartitionId(), resourceType, paramName, system, units)); + public void setId(Long theId) { + myId = theId; + } + + @Override + public IQueryParameterType toQueryParameterType() { + return new QuantityParam(null, getValue(), getSystem(), getUnits()); + } + + @Override + public String toString() { + ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); + b.append("paramName", getParamName()); + b.append("resourceId", getResourcePid()); + b.append("system", getSystem()); + b.append("units", getUnits()); + b.append("value", getValue()); + b.append("missing", isMissing()); + b.append("hashIdentitySystemAndUnits", getHashIdentitySystemAndUnits()); + return b.build(); } @Override @@ -154,99 +154,13 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc b.append(getHashIdentityAndUnits(), obj.getHashIdentityAndUnits()); b.append(getHashIdentitySystemAndUnits(), obj.getHashIdentitySystemAndUnits()); b.append(isMissing(), obj.isMissing()); + b.append(getValue(), obj.getValue()); return b.isEquals(); } - - public Long getHashIdentity() { - return myHashIdentity; - } - - public void setHashIdentity(Long theHashIdentity) { - myHashIdentity = theHashIdentity; - } - - public Long getHashIdentityAndUnits() { - return myHashIdentityAndUnits; - } - - public void setHashIdentityAndUnits(Long theHashIdentityAndUnits) { - myHashIdentityAndUnits = theHashIdentityAndUnits; - } - - private Long getHashIdentitySystemAndUnits() { - return myHashIdentitySystemAndUnits; - } - - public void setHashIdentitySystemAndUnits(Long theHashIdentitySystemAndUnits) { - myHashIdentitySystemAndUnits = theHashIdentitySystemAndUnits; - } - - @Override - public Long getId() { - return myId; - } - - @Override - public void setId(Long theId) { - myId = theId; - } - - public String getSystem() { - return mySystem; - } - - public void setSystem(String theSystem) { - mySystem = theSystem; - } - - public String getUnits() { - return myUnits; - } - - public void setUnits(String theUnits) { - myUnits = theUnits; - } - - public BigDecimal getValue() { - return myValue; - } - - public ResourceIndexedSearchParamQuantity setValue(BigDecimal theValue) { - myValue = theValue; - return this; - } - - @Override - public int hashCode() { - HashCodeBuilder b = new HashCodeBuilder(); - b.append(getResourceType()); - b.append(getParamName()); - b.append(getHashIdentity()); - b.append(getHashIdentityAndUnits()); - b.append(getHashIdentitySystemAndUnits()); - return b.toHashCode(); - } - - @Override - public IQueryParameterType toQueryParameterType() { - return new QuantityParam(null, getValue(), getSystem(), getUnits()); - } - - @Override - public String toString() { - ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); - b.append("paramName", getParamName()); - b.append("resourceId", getResourcePid()); - b.append("system", getSystem()); - b.append("units", getUnits()); - b.append("value", getValue()); - b.append("missing", isMissing()); - b.append("hashIdentitySystemAndUnits", myHashIdentitySystemAndUnits); - return b.build(); - } - + @Override public boolean matches(IQueryParameterType theParam) { + if (!(theParam instanceof QuantityParam)) { return false; } @@ -279,26 +193,8 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc } } } + return retval; } - public static long calculateHashSystemAndUnits(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName, String theSystem, String theUnits) { - RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); - return calculateHashSystemAndUnits(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem, theUnits); - } - - public static long calculateHashSystemAndUnits(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName, String theSystem, String theUnits) { - return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, theSystem, theUnits); - } - - public static long calculateHashUnits(PartitionSettings thePartitionSettings, PartitionablePartitionId theRequestPartitionId, String theResourceType, String theParamName, String theUnits) { - RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); - return calculateHashUnits(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theUnits); - } - - public static long calculateHashUnits(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName, String theUnits) { - return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, theUnits); - } - - -} +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityNormalized.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityNormalized.java new file mode 100644 index 00000000000..0234d0fa4a2 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityNormalized.java @@ -0,0 +1,239 @@ +package ca.uhn.fhir.jpa.model.entity; + +/* + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * 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 static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isBlank; + +import java.math.BigDecimal; +import java.util.Objects; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.fhir.ucum.Pair; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ScaledNumberField; + + +import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.QuantityParam; +import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; + +//@formatter:off +@Embeddable +@Entity +@Table(name = "HFJ_SPIDX_QUANTITY_NRML", indexes = { + @Index(name = "IDX_SP_QNTY_NRML_HASH", columnList = "HASH_IDENTITY,SP_VALUE"), + @Index(name = "IDX_SP_QNTY_NRML_HASH_UN", columnList = "HASH_IDENTITY_AND_UNITS,SP_VALUE"), + @Index(name = "IDX_SP_QNTY_NRML_HASH_SYSUN", columnList = "HASH_IDENTITY_SYS_UNITS,SP_VALUE"), + @Index(name = "IDX_SP_QNTY_NRML_UPDATED", columnList = "SP_UPDATED"), + @Index(name = "IDX_SP_QNTY_NRML_RESID", columnList = "RES_ID") +}) +/** + * Support UCUM service + * @since 5.3.0 + * + */ +public class ResourceIndexedSearchParamQuantityNormalized extends ResourceIndexedSearchParamBaseQuantity { + + private static final long serialVersionUID = 1L; + + @Id + @SequenceGenerator(name = "SEQ_SPIDX_QUANTITY_NRML", sequenceName = "SEQ_SPIDX_QUANTITY_NRML") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_QUANTITY_NRML") + @Column(name = "SP_ID") + private Long myId; + + // Changed to double here for storing the value after converted to the CanonicalForm due to BigDecimal maps NUMBER(19,2) + // The precision may lost even to store 1.2cm which is 0.012m in the CanonicalForm + @Column(name = "SP_VALUE", nullable = true) + @ScaledNumberField + public Double myValue; + + public ResourceIndexedSearchParamQuantityNormalized() { + super(); + } + + public ResourceIndexedSearchParamQuantityNormalized(PartitionSettings thePartitionSettings, String theResourceType, String theParamName, BigDecimal theValue, String theSystem, String theUnits) { + this(); + setPartitionSettings(thePartitionSettings); + setResourceType(theResourceType); + setParamName(theParamName); + setSystem(theSystem); + + //-- convert the value/unit to the canonical form if any, otherwise store the original value/units pair + Pair canonicalForm = UcumServiceUtil.getCanonicalForm(theSystem, theValue, theUnits); + if (canonicalForm != null) { + setValue(Double.parseDouble(canonicalForm.getValue().asDecimal())); + setUnits(canonicalForm.getCode()); + } else { + setValue(theValue); + setUnits(theUnits); + } + + calculateHashes(); + } + + @Override + public void copyMutableValuesFrom(T theSource) { + super.copyMutableValuesFrom(theSource); + ResourceIndexedSearchParamQuantityNormalized source = (ResourceIndexedSearchParamQuantityNormalized) theSource; + mySystem = source.mySystem; + myUnits = source.myUnits; + myValue = source.myValue; + setHashIdentity(source.getHashIdentity()); + setHashIdentityAndUnits(source.getHashIdentityAndUnits()); + setHashIdentitySystemAndUnits(source.getHashIdentitySystemAndUnits()); + } + + //- myValue + public Double getValue() { + return myValue; + } + public ResourceIndexedSearchParamQuantityNormalized setValue(Double theValue) { + myValue = theValue; + return this; + } + public void setValue(BigDecimal theValue) { + if (theValue != null) + myValue = theValue.doubleValue(); + } + public BigDecimal getValueBigDecimal() { + if (myValue == null) + return null; + return new BigDecimal(myValue); + } + + //-- myId + @Override + public Long getId() { + return myId; + } + @Override + public void setId(Long theId) { + myId = theId; + } + + @Override + public IQueryParameterType toQueryParameterType() { + return new QuantityParam(null, getValue(), getSystem(), getUnits()); + } + + @Override + public String toString() { + ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); + b.append("paramName", getParamName()); + b.append("resourceId", getResourcePid()); + b.append("system", getSystem()); + b.append("units", getUnits()); + b.append("value", getValue()); + b.append("missing", isMissing()); + b.append("hashIdentitySystemAndUnits", getHashIdentitySystemAndUnits()); + return b.build(); + } + + @Override + public boolean equals(Object theObj) { + if (this == theObj) { + return true; + } + if (theObj == null) { + return false; + } + if (!(theObj instanceof ResourceIndexedSearchParamQuantityNormalized)) { + return false; + } + ResourceIndexedSearchParamQuantityNormalized obj = (ResourceIndexedSearchParamQuantityNormalized) theObj; + EqualsBuilder b = new EqualsBuilder(); + b.append(getResourceType(), obj.getResourceType()); + b.append(getParamName(), obj.getParamName()); + b.append(getHashIdentity(), obj.getHashIdentity()); + b.append(getHashIdentityAndUnits(), obj.getHashIdentityAndUnits()); + b.append(getHashIdentitySystemAndUnits(), obj.getHashIdentitySystemAndUnits()); + b.append(isMissing(), obj.isMissing()); + b.append(getValue(), obj.getValue()); + return b.isEquals(); + } + + @Override + public boolean matches(IQueryParameterType theParam) { + + if (!(theParam instanceof QuantityParam)) { + return false; + } + QuantityParam quantity = (QuantityParam) theParam; + boolean retval = false; + + String quantitySystem = quantity.getSystem(); + BigDecimal quantityValue = quantity.getValue(); + Double quantityDoubleValue = null; + if (quantityValue != null) + quantityDoubleValue = quantityValue.doubleValue(); + String quantityUnits = defaultString(quantity.getUnits()); + + //-- convert the value/unit to the canonical form if any, otherwise store the original value/units pair + Pair canonicalForm = UcumServiceUtil.getCanonicalForm(quantitySystem, quantityValue, quantityUnits); + if (canonicalForm != null) { + quantityDoubleValue = Double.parseDouble(canonicalForm.getValue().asDecimal()); + quantityUnits = canonicalForm.getCode(); + } + + // Only match on system if it wasn't specified + if (quantitySystem == null && isBlank(quantityUnits)) { + if (Objects.equals(getValue(), quantityDoubleValue)) { + retval = true; + } + } else { + String unitsString = defaultString(getUnits()); + if (quantitySystem == null) { + if (unitsString.equalsIgnoreCase(quantityUnits) && + Objects.equals(getValue(), quantityDoubleValue)) { + retval = true; + } + } else if (isBlank(quantityUnits)) { + if (getSystem().equalsIgnoreCase(quantitySystem) && + Objects.equals(getValue(), quantityDoubleValue)) { + retval = true; + } + } else { + if (getSystem().equalsIgnoreCase(quantitySystem) && + unitsString.equalsIgnoreCase(quantityUnits) && + Objects.equals(getValue(), quantityDoubleValue)) { + retval = true; + } + } + } + + return retval; + } +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java index f0a9c24103d..0b6ac43c384 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java @@ -32,13 +32,10 @@ import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.annotations.OptimisticLock; import org.hibernate.search.engine.backend.types.Projectable; import org.hibernate.search.engine.backend.types.Searchable; -import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate; import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.RoutingBinderRef; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.DocumentId; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ObjectPath; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.PropertyValue; @@ -149,6 +146,23 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas @Column(name = "SP_QUANTITY_PRESENT") @OptimisticLock(excluded = true) private boolean myParamsQuantityPopulated; + + /** + * Added to support UCUM conversion + * since 5.3.0 + */ + @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @OptimisticLock(excluded = true) + private Collection myParamsQuantityNormalized; + + /** + * Added to support UCUM conversion, + * NOTE : use Boolean class instead of boolean primitive, in order to set the existing rows to null + * since 5.3.0 + */ + @Column(name = "SP_QUANTITY_NRML_PRESENT") + @OptimisticLock(excluded = true) + private Boolean myParamsQuantityNormalizedPopulated = Boolean.FALSE; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) @OptimisticLock(excluded = true) @@ -361,6 +375,21 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas getParamsQuantity().addAll(theQuantityParams); } + public Collection getParamsQuantityNormalized() { + if (myParamsQuantityNormalized == null) { + myParamsQuantityNormalized = new ArrayList<>(); + } + return myParamsQuantityNormalized; + } + + public void setParamsQuantityNormalized(Collection theQuantityNormalizedParams) { + if (!isParamsQuantityNormalizedPopulated() && theQuantityNormalizedParams.isEmpty()) { + return; + } + getParamsQuantityNormalized().clear(); + getParamsQuantityNormalized().addAll(theQuantityNormalizedParams); + } + public Collection getParamsString() { if (myParamsString == null) { myParamsString = new ArrayList<>(); @@ -503,6 +532,20 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas public void setParamsQuantityPopulated(boolean theParamsQuantityPopulated) { myParamsQuantityPopulated = theParamsQuantityPopulated; } + + public Boolean isParamsQuantityNormalizedPopulated() { + if (myParamsQuantityNormalizedPopulated == null) + return Boolean.FALSE; + else + return myParamsQuantityNormalizedPopulated; + } + + public void setParamsQuantityNormalizedPopulated(Boolean theParamsQuantityNormalizedPopulated) { + if (theParamsQuantityNormalizedPopulated == null) + myParamsQuantityNormalizedPopulated = Boolean.FALSE; + else + myParamsQuantityNormalizedPopulated = theParamsQuantityNormalizedPopulated; + } public boolean isParamsStringPopulated() { return myParamsStringPopulated; diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/UcumServiceUtil.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/UcumServiceUtil.java new file mode 100644 index 00000000000..3224067fbc0 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/UcumServiceUtil.java @@ -0,0 +1,100 @@ +package ca.uhn.fhir.jpa.model.util; + +/* + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * 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 java.io.InputStream; +import java.math.BigDecimal; + +import org.fhir.ucum.Decimal; +import org.fhir.ucum.Pair; +import org.fhir.ucum.UcumEssenceService; +import org.fhir.ucum.UcumException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ca.uhn.fhir.util.ClasspathUtil; + +/** + * It's a wrapper of UcumEssenceService + * + */ +public class UcumServiceUtil { + + private static final Logger ourLog = LoggerFactory.getLogger(UcumServiceUtil.class); + + public static final String UCUM_CODESYSTEM_URL = "http://unitsofmeasure.org"; + private static final String UCUM_SOURCE = "/ucum-essence.xml"; + + private static UcumEssenceService myUcumEssenceService = null; + + private UcumServiceUtil() { + } + + // lazy load UCUM_SOURCE only once + private static void init() { + + if (myUcumEssenceService != null) + return; + + synchronized (UcumServiceUtil.class) { + InputStream input = ClasspathUtil.loadResourceAsStream(UCUM_SOURCE); + try { + myUcumEssenceService = new UcumEssenceService(input); + + } catch (UcumException e) { + ourLog.warn("Failed to load ucum code from ", UCUM_SOURCE, e); + } finally { + ClasspathUtil.close(input); + } + } + } + + /** + * Get the canonical form of a code, it's define at + * http://unitsofmeasure.org + * + * e.g. 12cm -> 0.12m where m is the canonical form of the length. + * + * @param theSystem must be http://unitsofmeasure.org + * @param theValue the value in the original form e.g. 0.12 + * @param theCode the code in the original form e.g. 'cm' + * @return the CanonicalForm if no error, otherwise return null + */ + public static Pair getCanonicalForm(String theSystem, BigDecimal theValue, String theCode) { + + // -- only for http://unitsofmeasure.org + if (!UCUM_CODESYSTEM_URL.equals(theSystem) || theValue == null || theCode == null) + return null; + + init(); + Pair theCanonicalPair = null; + + try { + Decimal theDecimal = new Decimal(theValue.toPlainString(), theValue.precision()); + theCanonicalPair = myUcumEssenceService.getCanonicalForm(new Pair(theDecimal, theCode)); + } catch (UcumException e) { + return null; + } + + return theCanonicalPair; + } + +} diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityNormalizedTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityNormalizedTest.java new file mode 100644 index 00000000000..4affba1865c --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityNormalizedTest.java @@ -0,0 +1,82 @@ +package ca.uhn.fhir.jpa.model.entity; + +import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class ResourceIndexedSearchParamQuantityNormalizedTest { + + private ResourceIndexedSearchParamQuantityNormalized createParam(String theParamName, String theValue, String theSystem, String theUnits) { + ResourceIndexedSearchParamQuantityNormalized token = new ResourceIndexedSearchParamQuantityNormalized(new PartitionSettings(), "Observation", theParamName, new BigDecimal(theValue), theSystem, theUnits); + token.setResource(new ResourceTable().setResourceType("Patient")); + return token; + } + + @Test + public void testHashFunctions() { + ResourceIndexedSearchParamQuantityNormalized token = createParam("Quanity", "123.001", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm"); + token.calculateHashes(); + + // Make sure our hashing function gives consistent results + assertEquals(5219730978980909111L, token.getHashIdentity().longValue()); + assertEquals(-2454931617586657338L, token.getHashIdentityAndUnits().longValue()); + assertEquals(878263047209296227L, token.getHashIdentitySystemAndUnits().longValue()); + } + + + @Test + public void testEquals() { + ResourceIndexedSearchParamBaseQuantity val1 = new ResourceIndexedSearchParamQuantityNormalized() + .setValue(Double.parseDouble("123")); + val1.setPartitionSettings(new PartitionSettings()); + val1.calculateHashes(); + ResourceIndexedSearchParamBaseQuantity val2 = new ResourceIndexedSearchParamQuantityNormalized() + .setValue(Double.parseDouble("123")); + val2.setPartitionSettings(new PartitionSettings()); + val2.calculateHashes(); + assertEquals(val1, val1); + assertEquals(val1, val2); + assertNotEquals(val1, null); + assertNotEquals(val1, ""); + } + + @Test + public void testUcum() { + + //-- system is ucum + ResourceIndexedSearchParamQuantityNormalized token1 = createParam("Quanity", "123.001", UcumServiceUtil.UCUM_CODESYSTEM_URL, "cm"); + token1.calculateHashes(); + + assertEquals("m", token1.getUnits()); + assertEquals(Double.parseDouble("1.23001"), token1.getValue()); + + //-- small number + token1 = createParam("Quanity", "0.000001", UcumServiceUtil.UCUM_CODESYSTEM_URL, "mm"); + token1.calculateHashes(); + + assertEquals("m", token1.getUnits()); + assertEquals(Double.parseDouble("0.000000001"), token1.getValue()); + + + // -- non ucum system + ResourceIndexedSearchParamQuantityNormalized token2 = createParam("Quanity", "123.001", "http://abc.org", "cm"); + token2.calculateHashes(); + + assertEquals("cm", token2.getUnits()); + assertEquals(Double.parseDouble("123.001"), token2.getValue()); + + // -- unsupported ucum code + ResourceIndexedSearchParamQuantityNormalized token3 = createParam("Quanity", "123.001", UcumServiceUtil.UCUM_CODESYSTEM_URL, "unknown"); + token3.calculateHashes(); + + assertEquals("unknown", token3.getUnits()); + assertEquals(Double.parseDouble("123.001"), token3.getValue()); + } + +} diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/UcumServiceUtilTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/UcumServiceUtilTest.java new file mode 100644 index 00000000000..2ca1bd1e4d5 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/UcumServiceUtilTest.java @@ -0,0 +1,55 @@ +package ca.uhn.fhir.jpa.model.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.Test; + +public class UcumServiceUtilTest { + + @Test + public void testCanonicalForm() { + + assertEquals(Double.parseDouble("0.000012"), + Double.parseDouble(UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(0.012), "mm").getValue().asDecimal())); + + + assertEquals(Double.parseDouble("149.597870691"), + Double.parseDouble(UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(149597.870691), "mm").getValue().asDecimal())); + + assertEquals("0.0025 m", UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(2.5), "mm").toString()); + assertEquals("0.025 m", UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(2.5), "cm").toString()); + assertEquals("0.25 m", UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(2.5), "dm").toString()); + assertEquals("2.5 m", UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(2.5), "m").toString()); + assertEquals("2500 m", UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(2.5), "km").toString()); + + assertEquals(Double.parseDouble("957.4"), + Double.parseDouble(UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(95.74), "mg/dL").getValue().asDecimal())); + + assertEquals(Double.parseDouble("957400.0"), + Double.parseDouble(UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(95.74), "g/dL").getValue().asDecimal())); + + //-- code g.m-3 + assertEquals(Double.parseDouble("957400000"), + Double.parseDouble(UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(95.74), "kg/dL").getValue().asDecimal())); + } + + @Test + public void testInvalidCanonicalForm() { + + //-- invalid url + assertEquals(null, UcumServiceUtil.getCanonicalForm("url", new BigDecimal(2.5), "cm")); + + //-- missing value + assertEquals(null, UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, null, "dm")); + + //-- missing code + assertEquals(null, UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(2.5), null)); + + //-- invalid codes + assertEquals(null, UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(2.5), "xyz")); + + } + +} diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java index cd0e7ef1a60..25af4ddb6c4 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java @@ -34,6 +34,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; @@ -200,7 +201,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor extractor = createReferenceExtractor(); return extractReferenceParamsAsQueryTokens(theSearchParam, theResource, extractor); case QUANTITY: - extractor = createQuantityExtractor(theResource); + if (myModelConfig.isNormalizedQuantitySearchSupported()) + extractor = createQuantityNormalizedExtractor(theResource); + else + extractor = createQuantityExtractor(theResource); break; case URI: extractor = createUriExtractor(theResource); @@ -373,7 +377,14 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY); } - private IExtractor createQuantityExtractor(IBaseResource theResource) { + + @Override + public SearchParamSet extractSearchParamQuantityNormalized(IBaseResource theResource) { + IExtractor extractor = createQuantityNormalizedExtractor(theResource); + return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY); + } + + private IExtractor createQuantityExtractor(IBaseResource theResource) { return (params, searchParam, value, path) -> { if (value.getClass().equals(myLocationPositionDefinition.getImplementingClass())) { return; @@ -383,7 +394,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor String resourceType = toRootTypeName(theResource); switch (nextType) { case "Quantity": - addQuantity_Quantity(resourceType, params, searchParam, value); + addQuantity_Quantity(resourceType, params, searchParam, value); break; case "Money": addQuantity_Money(resourceType, params, searchParam, value); @@ -395,9 +406,35 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor addUnexpectedDatatypeWarning(params, searchParam, value); break; } + }; + } + + private IExtractor createQuantityNormalizedExtractor(IBaseResource theResource) { + + return (params, searchParam, value, path) -> { + if (value.getClass().equals(myLocationPositionDefinition.getImplementingClass())) { + return; + } + + String nextType = toRootTypeName(value); + String resourceType = toRootTypeName(theResource); + switch (nextType) { + case "Quantity": + addQuantity_QuantityNormalized(resourceType, params, searchParam, value); + break; + case "Money": + addQuantity_MoneyNormalized(resourceType, params, searchParam, value); + break; + case "Range": + addQuantity_RangeNormalized(resourceType, params, searchParam, value); + break; + default: + addUnexpectedDatatypeWarning(params, searchParam, value); + break; + } }; } - + @Override public SearchParamSet extractSearchParamStrings(IBaseResource theResource) { IExtractor extractor = createStringExtractor(theResource); @@ -502,7 +539,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } private void addQuantity_Quantity(String theResourceType, Set theParams, RuntimeSearchParam theSearchParam, IBase theValue) { - Optional> valueField = myQuantityValueValueChild.getAccessor().getFirstValueOrNull(theValue); if (valueField.isPresent() && valueField.get().getValue() != null) { BigDecimal nextValueValue = valueField.get().getValue(); @@ -510,13 +546,25 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor String code = extractValueAsString(myQuantityCodeValueChild, theValue); ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(myPartitionSettings, theResourceType, theSearchParam.getName(), nextValueValue, system, code); + + theParams.add(nextEntity); + } + } + + + private void addQuantity_QuantityNormalized(String theResourceType, Set theParams, RuntimeSearchParam theSearchParam, IBase theValue) { + Optional> valueField = myQuantityValueValueChild.getAccessor().getFirstValueOrNull(theValue); + if (valueField.isPresent() && valueField.get().getValue() != null) { + BigDecimal nextValueValue = valueField.get().getValue(); + String system = extractValueAsString(myQuantitySystemValueChild, theValue); + String code = extractValueAsString(myQuantityCodeValueChild, theValue); + + ResourceIndexedSearchParamQuantityNormalized nextEntity = new ResourceIndexedSearchParamQuantityNormalized(myPartitionSettings, theResourceType, theSearchParam.getName(), nextValueValue, system, code); theParams.add(nextEntity); } - } - + private void addQuantity_Money(String theResourceType, Set theParams, RuntimeSearchParam theSearchParam, IBase theValue) { - Optional> valueField = myMoneyValueChild.getAccessor().getFirstValueOrNull(theValue); if (valueField.isPresent() && valueField.get().getValue() != null) { BigDecimal nextValueValue = valueField.get().getValue(); @@ -524,21 +572,45 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor String nextValueString = "urn:iso:std:iso:4217"; String nextValueCode = extractValueAsString(myMoneyCurrencyChild, theValue); String searchParamName = theSearchParam.getName(); + ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(myPartitionSettings, theResourceType, searchParamName, nextValueValue, nextValueString, nextValueCode); theParams.add(nextEntity); - } - + } } - private void addQuantity_Range(String theResourceType, Set theParams, RuntimeSearchParam theSearchParam, IBase theValue) { + + private void addQuantity_MoneyNormalized(String theResourceType, Set theParams, RuntimeSearchParam theSearchParam, IBase theValue) { + Optional> valueField = myMoneyValueChild.getAccessor().getFirstValueOrNull(theValue); + if (valueField.isPresent() && valueField.get().getValue() != null) { + BigDecimal nextValueValue = valueField.get().getValue(); + String nextValueString = "urn:iso:std:iso:4217"; + String nextValueCode = extractValueAsString(myMoneyCurrencyChild, theValue); + String searchParamName = theSearchParam.getName(); + + ResourceIndexedSearchParamQuantityNormalized nextEntityNormalized = new ResourceIndexedSearchParamQuantityNormalized(myPartitionSettings, theResourceType, searchParamName, nextValueValue, nextValueString, nextValueCode); + theParams.add(nextEntityNormalized); + } + } + + + private void addQuantity_Range(String theResourceType, Set theParams, RuntimeSearchParam theSearchParam, IBase theValue) { Optional low = myRangeLowValueChild.getAccessor().getFirstValueOrNull(theValue); low.ifPresent(theIBase -> addQuantity_Quantity(theResourceType, theParams, theSearchParam, theIBase)); Optional high = myRangeHighValueChild.getAccessor().getFirstValueOrNull(theValue); - high.ifPresent(theIBase -> addQuantity_Quantity(theResourceType, theParams, theSearchParam, theIBase)); + high.ifPresent(theIBase -> addQuantity_Quantity(theResourceType, theParams, theSearchParam, theIBase)); } + + private void addQuantity_RangeNormalized(String theResourceType, Set theParams, RuntimeSearchParam theSearchParam, IBase theValue) { + Optional low = myRangeLowValueChild.getAccessor().getFirstValueOrNull(theValue); + low.ifPresent(theIBase -> addQuantity_QuantityNormalized(theResourceType, theParams, theSearchParam, theIBase)); + + Optional high = myRangeHighValueChild.getAccessor().getFirstValueOrNull(theValue); + high.ifPresent(theIBase -> addQuantity_QuantityNormalized(theResourceType, theParams, theSearchParam, theIBase)); + } + private void addToken_Identifier(String theResourceType, Set theParams, RuntimeSearchParam theSearchParam, IBase theValue) { String system = extractValueAsString(myIdentifierSystemValueChild, theValue); String value = extractValueAsString(myIdentifierValueValueChild, theValue); @@ -553,7 +625,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, text); } } - } protected boolean shouldIndexTextComponentOfToken(RuntimeSearchParam theSearchParam) { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ISearchParamExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ISearchParamExtractor.java index 06e2c018555..891e2dd9202 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ISearchParamExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ISearchParamExtractor.java @@ -1,21 +1,5 @@ package ca.uhn.fhir.jpa.searchparam.extractor; -import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.jpa.model.entity.*; -import org.hl7.fhir.instance.model.api.IBase; -import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; -import org.hl7.fhir.instance.model.api.IBaseResource; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; /* * #%L @@ -37,6 +21,24 @@ import java.util.List; * #L% */ +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.model.entity.*; +import org.hl7.fhir.instance.model.api.IBase; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; + public interface ISearchParamExtractor { // SearchParamSet extractSearchParamCoords(IBaseResource theResource); @@ -47,6 +49,8 @@ public interface ISearchParamExtractor { SearchParamSet extractSearchParamQuantity(IBaseResource theResource); + SearchParamSet extractSearchParamQuantityNormalized(IBaseResource theResource); + SearchParamSet extractSearchParamStrings(IBaseResource theResource); SearchParamSet extractSearchParamTokens(IBaseResource theResource); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java index 7b88c96a9a9..3006df3ddec 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.searchparam.extractor; + /*- * #%L * HAPI FHIR Search Parameters @@ -29,6 +30,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; @@ -58,6 +60,7 @@ public final class ResourceIndexedSearchParams { final public Collection myTokenParams = new HashSet<>(); final public Collection myNumberParams = new ArrayList<>(); final public Collection myQuantityParams = new ArrayList<>(); + final public Collection myQuantityNormalizedParams = new ArrayList<>(); final public Collection myDateParams = new ArrayList<>(); final public Collection myUriParams = new ArrayList<>(); final public Collection myCoordsParams = new ArrayList<>(); @@ -82,6 +85,9 @@ public final class ResourceIndexedSearchParams { if (theEntity.isParamsQuantityPopulated()) { myQuantityParams.addAll(theEntity.getParamsQuantity()); } + if (theEntity.isParamsQuantityNormalizedPopulated()) { + myQuantityNormalizedParams.addAll(theEntity.getParamsQuantityNormalized()); + } if (theEntity.isParamsDatePopulated()) { myDateParams.addAll(theEntity.getParamsDate()); } @@ -110,6 +116,7 @@ public final class ResourceIndexedSearchParams { theEntity.setParamsTokenPopulated(myTokenParams.isEmpty() == false); theEntity.setParamsNumberPopulated(myNumberParams.isEmpty() == false); theEntity.setParamsQuantityPopulated(myQuantityParams.isEmpty() == false); + theEntity.setParamsQuantityNormalizedPopulated(myQuantityNormalizedParams.isEmpty() == false); theEntity.setParamsDatePopulated(myDateParams.isEmpty() == false); theEntity.setParamsUriPopulated(myUriParams.isEmpty() == false); theEntity.setParamsCoordsPopulated(myCoordsParams.isEmpty() == false); @@ -123,6 +130,7 @@ public final class ResourceIndexedSearchParams { theEntity.setParamsToken(myTokenParams); theEntity.setParamsNumber(myNumberParams); theEntity.setParamsQuantity(myQuantityParams); + theEntity.setParamsQuantityNormalized(myQuantityNormalizedParams); theEntity.setParamsDate(myDateParams); theEntity.setParamsUri(myUriParams); theEntity.setParamsCoords(myCoordsParams); @@ -133,6 +141,7 @@ public final class ResourceIndexedSearchParams { setUpdatedTime(myStringParams, theUpdateTime); setUpdatedTime(myNumberParams, theUpdateTime); setUpdatedTime(myQuantityParams, theUpdateTime); + setUpdatedTime(myQuantityNormalizedParams, theUpdateTime); setUpdatedTime(myDateParams, theUpdateTime); setUpdatedTime(myUriParams, theUpdateTime); setUpdatedTime(myCoordsParams, theUpdateTime); @@ -150,6 +159,7 @@ public final class ResourceIndexedSearchParams { } public boolean matchParam(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) { + if (theParamDef == null) { return false; } @@ -159,7 +169,10 @@ public final class ResourceIndexedSearchParams { resourceParams = myTokenParams; break; case QUANTITY: - resourceParams = myQuantityParams; + if (theModelConfig.isNormalizedQuantitySearchSupported()) + resourceParams = myQuantityNormalizedParams; + else + resourceParams = myQuantityParams; break; case STRING: resourceParams = myStringParams; @@ -250,6 +263,7 @@ public final class ResourceIndexedSearchParams { ", tokenParams=" + myTokenParams + ", numberParams=" + myNumberParams + ", quantityParams=" + myQuantityParams + + ", quantityNormalizedParams=" + myQuantityNormalizedParams + ", dateParams=" + myDateParams + ", uriParams=" + myUriParams + ", coordsParams=" + myCoordsParams + @@ -261,7 +275,10 @@ public final class ResourceIndexedSearchParams { public void findMissingSearchParams(PartitionSettings thePartitionSettings, ModelConfig theModelConfig, ResourceTable theEntity, Set> theActiveSearchParams) { findMissingSearchParams(thePartitionSettings, theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.STRING, myStringParams); findMissingSearchParams(thePartitionSettings, theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.NUMBER, myNumberParams); - findMissingSearchParams(thePartitionSettings, theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.QUANTITY, myQuantityParams); + if (theModelConfig.isNormalizedQuantitySearchSupported()) + findMissingSearchParams(thePartitionSettings, theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.QUANTITY, myQuantityNormalizedParams); + else + findMissingSearchParams(thePartitionSettings, theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.QUANTITY, myQuantityParams); findMissingSearchParams(thePartitionSettings, theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.DATE, myDateParams); findMissingSearchParams(thePartitionSettings, theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.URI, myUriParams); findMissingSearchParams(thePartitionSettings, theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.TOKEN, myTokenParams); @@ -292,7 +309,10 @@ public final class ResourceIndexedSearchParams { param = new ResourceIndexedSearchParamNumber(); break; case QUANTITY: - param = new ResourceIndexedSearchParamQuantity(); + if (theModelConfig.isNormalizedQuantitySearchSupported()) + param = new ResourceIndexedSearchParamQuantityNormalized(); + else + param = new ResourceIndexedSearchParamQuantity(); break; case STRING: param = new ResourceIndexedSearchParamString() @@ -382,8 +402,7 @@ public final class ResourceIndexedSearchParams { return queryStringsToPopulate; } - private static void extractCompositeStringUniquesValueChains(String - theResourceType, List> thePartsChoices, List theValues, Set theQueryStringsToPopulate) { + private static void extractCompositeStringUniquesValueChains(String theResourceType, List> thePartsChoices, List theValues, Set theQueryStringsToPopulate) { if (thePartsChoices.size() > 0) { List nextList = thePartsChoices.get(0); Collections.sort(nextList); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java index 725252c78b1..1727f6a1ba4 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java @@ -35,6 +35,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; @@ -103,7 +104,7 @@ public class SearchParamExtractorService { } private void extractSearchIndexParameters(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity) { - + // Strings ISearchParamExtractor.SearchParamSet strings = extractSearchParamStrings(theResource); handleWarnings(theRequestDetails, myInterceptorBroadcaster, strings); @@ -118,7 +119,13 @@ public class SearchParamExtractorService { ISearchParamExtractor.SearchParamSet quantities = extractSearchParamQuantity(theResource); handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantities); theParams.myQuantityParams.addAll(quantities); - + + if (myModelConfig.isNormalizedQuantityStorageSupported()|| myModelConfig.isNormalizedQuantitySearchSupported()) { + ISearchParamExtractor.SearchParamSet quantitiesNormalized = extractSearchParamQuantityNormalized(theResource); + handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantitiesNormalized); + theParams.myQuantityNormalizedParams.addAll(quantitiesNormalized); + } + // Dates ISearchParamExtractor.SearchParamSet dates = extractSearchParamDates(theResource); handleWarnings(theRequestDetails, myInterceptorBroadcaster, dates); @@ -151,6 +158,7 @@ public class SearchParamExtractorService { // Do this after, because we add to strings during both string and token processing populateResourceTable(theParams.myNumberParams, theEntity); populateResourceTable(theParams.myQuantityParams, theEntity); + populateResourceTable(theParams.myQuantityNormalizedParams, theEntity); populateResourceTable(theParams.myDateParams, theEntity); populateResourceTable(theParams.myUriParams, theEntity); populateResourceTable(theParams.myTokenParams, theEntity); @@ -378,6 +386,10 @@ public class SearchParamExtractorService { return mySearchParamExtractor.extractSearchParamQuantity(theResource); } + private ISearchParamExtractor.SearchParamSet extractSearchParamQuantityNormalized(IBaseResource theResource) { + return mySearchParamExtractor.extractSearchParamQuantityNormalized(theResource); + } + private ISearchParamExtractor.SearchParamSet extractSearchParamStrings(IBaseResource theResource) { return mySearchParamExtractor.extractSearchParamStrings(theResource); } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/SubscriptionTestConfig.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/SubscriptionTestConfig.java index 0ea13ede15e..e46ba54cf6f 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/SubscriptionTestConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/SubscriptionTestConfig.java @@ -4,7 +4,7 @@ package ca.uhn.fhir.jpa.subscription.module; * #%L * HAPI FHIR Subscription Server * %% - * Copyright (C) 2014 - 2020 University Health Network + * Copyright (C) 2014 - 2021 Smile CDR, Inc. * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.