diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java index 0d425b458d2..603483a6593 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java @@ -75,6 +75,7 @@ public enum VersionEnum { V5_4_1, V5_4_2, V5_5_0, + V5_5_1, V5_6_0 ; diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_1/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_1/version.yaml new file mode 100644 index 00000000000..41c2e9375cf --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_1/version.yaml @@ -0,0 +1,3 @@ +--- +release-date: "2021-08-30" +codename: "Quasar" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2793-expunge-everything-purges-caches.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2793-expunge-everything-purges-caches.yaml new file mode 100644 index 00000000000..fc20b9e6c47 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2793-expunge-everything-purges-caches.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 2793 +title: "Previously, when using the Expunge Everything operation, caches could retain old invalid values. This has been corrected. Thanks to [Ben Li-Sauerwine](https://github.com/theGOTOguy) for the fix!" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2932-lookup-display-language-cache-issue.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2932-lookup-display-language-cache-issue.yaml new file mode 100644 index 00000000000..d966668766f --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2932-lookup-display-language-cache-issue.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 2923 +title: "$lookup operation cache was based on system and code, it becomes a defect + after adding displayLanguage support. Problem is now fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2933-fix-double-conditional-create.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2933-fix-double-conditional-create.yaml new file mode 100644 index 00000000000..b43658c202a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2933-fix-double-conditional-create.yaml @@ -0,0 +1,7 @@ +--- +type: add +issue: 2933 +backport: 5.5.1 +jira: SMILE-3056 +title: "Fixed a regression which causes transactions with multiple identical ifNoneExist clauses to create duplicate data." + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2935-Search-with-trailing-percent-sign.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2935-Search-with-trailing-percent-sign.yaml new file mode 100644 index 00000000000..5c99ab10b71 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/2935-Search-with-trailing-percent-sign.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 2935 +jira: SMILE-3022 +title: "No resource returned when search with percent sign. Problem is now fixed" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties index 0a37b3348fe..6e3bb52e90c 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties @@ -38,7 +38,7 @@ section.server_plain.title=Plain Server page.server_plain.server_types=REST Server Types page.server_plain.introduction=Plain Server Introduction page.server_plain.get_started=Get Started ⚡ -page.server_plain.resource_providers=Resource Providers and Plan Providers +page.server_plain.resource_providers=Resource Providers and Plain Providers page.server_plain.rest_operations=REST Operations: Overview page.server_plain.rest_operations_search=REST Operations: Search page.server_plain.rest_operations_operations=REST Operations: Extended Operations diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java index 3bcf236007b..08e6f003a7c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java @@ -94,6 +94,8 @@ import org.hl7.fhir.r4.model.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; @@ -153,8 +155,8 @@ public abstract class BaseTransactionProcessor { @Autowired private InMemoryResourceMatcher myInMemoryResourceMatcher; - private ThreadPoolTaskExecutor myExecutor ; - + private TaskExecutor myExecutor ; + @VisibleForTesting public void setDaoConfig(DaoConfig theDaoConfig) { myDaoConfig = theDaoConfig; @@ -172,16 +174,25 @@ public abstract class BaseTransactionProcessor { @PostConstruct public void start() { ourLog.trace("Starting transaction processor"); - myExecutor = new ThreadPoolTaskExecutor(); - myExecutor.setThreadNamePrefix("bundle_batch_"); - // For single thread set the value to 1 - //myExecutor.setCorePoolSize(1); - //myExecutor.setMaxPoolSize(1); - myExecutor.setCorePoolSize(myDaoConfig.getBundleBatchPoolSize()); - myExecutor.setMaxPoolSize(myDaoConfig.getBundleBatchMaxPoolSize()); - myExecutor.setQueueCapacity(DaoConfig.DEFAULT_BUNDLE_BATCH_QUEUE_CAPACITY); + } - myExecutor.initialize(); + private TaskExecutor getTaskExecutor() { + if (myExecutor == null) { + if (myDaoConfig.getBundleBatchPoolSize() > 1) { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setThreadNamePrefix("bundle_batch_"); + executor.setCorePoolSize(myDaoConfig.getBundleBatchPoolSize()); + executor.setMaxPoolSize(myDaoConfig.getBundleBatchMaxPoolSize()); + executor.setQueueCapacity(DaoConfig.DEFAULT_BUNDLE_BATCH_QUEUE_CAPACITY); + executor.initialize(); + myExecutor = executor; + + } else { + SyncTaskExecutor executor = new SyncTaskExecutor(); + myExecutor = executor; + } + } + return myExecutor; } public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest, boolean theNestedMode) { @@ -349,7 +360,7 @@ public abstract class BaseTransactionProcessor { for (int i=0; i { + public class BundleTask implements Runnable { private CountDownLatch myCompletedLatch; - private ServletRequestDetails myRequestDetails; + private RequestDetails myRequestDetails; private IBase myNextReqEntry; private Map myResponseMap; private int myResponseOrder; @@ -1565,7 +1576,7 @@ public abstract class BaseTransactionProcessor { protected BundleTask(CountDownLatch theCompletedLatch, RequestDetails theRequestDetails, Map theResponseMap, int theResponseOrder, IBase theNextReqEntry, boolean theNestedMode) { this.myCompletedLatch = theCompletedLatch; - this.myRequestDetails = (ServletRequestDetails)theRequestDetails; + this.myRequestDetails = theRequestDetails; this.myNextReqEntry = theNextReqEntry; this.myResponseMap = theResponseMap; this.myResponseOrder = theResponseOrder; @@ -1573,10 +1584,8 @@ public abstract class BaseTransactionProcessor { } @Override - public Void call() { - + public void run() { BaseServerResponseExceptionHolder caughtEx = new BaseServerResponseExceptionHolder(); - try { IBaseBundle subRequestBundle = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode()); myVersionAdapter.addEntry(subRequestBundle, (IBase) myNextReqEntry); @@ -1609,7 +1618,6 @@ public abstract class BaseTransactionProcessor { // checking for the parallelism ourLog.debug("processing bacth for {} is completed", myVersionAdapter.getEntryRequestUrl((IBase)myNextReqEntry)); myCompletedLatch.countDown(); - return null; } } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java index 748ffa6352b..c8e25d517e3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java @@ -244,24 +244,28 @@ public class TransactionProcessor extends BaseTransactionProcessor { if (orPredicates.size() > 1) { cq.where(cb.or(orPredicates.toArray(EMPTY_PREDICATE_ARRAY))); - Map hashToSearchMap = buildHashToSearchMap(searchParameterMapsToResolve); + Map> hashToSearchMap = buildHashToSearchMap(searchParameterMapsToResolve); TypedQuery query = myEntityManager.createQuery(cq); List results = query.getResultList(); for (ResourceIndexedSearchParamToken nextResult : results) { - Optional matchedSearch = Optional.ofNullable(hashToSearchMap.get(nextResult.getHashSystemAndValue())); + Optional> matchedSearch = Optional.ofNullable(hashToSearchMap.get(nextResult.getHashSystemAndValue())); if (!matchedSearch.isPresent()) { matchedSearch = Optional.ofNullable(hashToSearchMap.get(nextResult.getHashValue())); } - matchedSearch.ifPresent(matchUrlToResolve -> setSearchToResolvedAndPrefetchFoundResourcePid(theTransactionDetails, idsToPreFetch, nextResult, matchUrlToResolve)); + matchedSearch.ifPresent(matchUrlsToResolve -> { + matchUrlsToResolve.forEach(matchUrl -> { + setSearchToResolvedAndPrefetchFoundResourcePid(theTransactionDetails, idsToPreFetch, nextResult, matchUrl); + }); + }); } //For each SP Map which did not return a result, tag it as not found. searchParameterMapsToResolve.stream() // No matches .filter(match -> !match.myResolved) .forEach(match -> { - ourLog.warn("Was unable to match url {} from database", match.myRequestUrl); + ourLog.debug("Was unable to match url {} from database", match.myRequestUrl); theTransactionDetails.addResolvedMatchUrl(match.myRequestUrl, TransactionDetails.NOT_FOUND); }); } @@ -322,22 +326,26 @@ public class TransactionProcessor extends BaseTransactionProcessor { return hashPredicate; } - private Map buildHashToSearchMap(List searchParameterMapsToResolve) { - Map hashToSearch = new HashMap<>(); + private Map> buildHashToSearchMap(List searchParameterMapsToResolve) { + Map> hashToSearch = new HashMap<>(); //Build a lookup map so we don't have to iterate over the searches repeatedly. for (MatchUrlToResolve nextSearchParameterMap : searchParameterMapsToResolve) { if (nextSearchParameterMap.myHashSystemAndValue != null) { - hashToSearch.put(nextSearchParameterMap.myHashSystemAndValue, nextSearchParameterMap); + List matchUrlsToResolve = hashToSearch.getOrDefault(nextSearchParameterMap.myHashSystemAndValue, new ArrayList<>()); + matchUrlsToResolve.add(nextSearchParameterMap); + hashToSearch.put(nextSearchParameterMap.myHashSystemAndValue, matchUrlsToResolve); } if (nextSearchParameterMap.myHashValue!= null) { - hashToSearch.put(nextSearchParameterMap.myHashValue, nextSearchParameterMap); + List matchUrlsToResolve = hashToSearch.getOrDefault(nextSearchParameterMap.myHashValue, new ArrayList<>()); + matchUrlsToResolve.add(nextSearchParameterMap); + hashToSearch.put(nextSearchParameterMap.myHashValue, matchUrlsToResolve); } } return hashToSearch; } private void setSearchToResolvedAndPrefetchFoundResourcePid(TransactionDetails theTransactionDetails, List idsToPreFetch, ResourceIndexedSearchParamToken nextResult, MatchUrlToResolve nextSearchParameterMap) { - ourLog.warn("Matched url {} from database", nextSearchParameterMap.myRequestUrl); + ourLog.debug("Matched url {} from database", nextSearchParameterMap.myRequestUrl); idsToPreFetch.add(nextResult.getResourcePid()); myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, nextSearchParameterMap.myResourceDefinition.getName(), nextSearchParameterMap.myRequestUrl, new ResourcePersistentId(nextResult.getResourcePid())); theTransactionDetails.addResolvedMatchUrl(nextSearchParameterMap.myRequestUrl, new ResourcePersistentId(nextResult.getResourcePid())); 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 1fa83140368..fbc828e47c3 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 @@ -65,6 +65,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTag; import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; import ca.uhn.fhir.jpa.model.entity.TagDefinition; +import ca.uhn.fhir.jpa.util.MemoryCacheService; import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; @@ -100,6 +101,9 @@ public class ExpungeEverythingService { private TransactionTemplate myTxTemplate; + @Autowired + private MemoryCacheService myMemoryCacheService; + @PostConstruct public void initTxTemplate() { myTxTemplate = new TransactionTemplate(myPlatformTransactionManager); @@ -122,37 +126,37 @@ public class ExpungeEverythingService { counter.addAndGet(doExpungeEverythingQuery("UPDATE " + TermCodeSystem.class.getSimpleName() + " d SET d.myCurrentVersion = null")); return null; }); - counter.addAndGet(expungeEverythingByType(NpmPackageVersionResourceEntity.class)); - counter.addAndGet(expungeEverythingByType(NpmPackageVersionEntity.class)); - counter.addAndGet(expungeEverythingByType(NpmPackageEntity.class)); - counter.addAndGet(expungeEverythingByType(SearchParamPresent.class)); - counter.addAndGet(expungeEverythingByType(BulkImportJobFileEntity.class)); - counter.addAndGet(expungeEverythingByType(BulkImportJobEntity.class)); - counter.addAndGet(expungeEverythingByType(ForcedId.class)); - 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)); - counter.addAndGet(expungeEverythingByType(ResourceIndexedSearchParamCoords.class)); - counter.addAndGet(expungeEverythingByType(ResourceIndexedComboStringUnique.class)); - counter.addAndGet(expungeEverythingByType(ResourceIndexedComboTokenNonUnique.class)); - counter.addAndGet(expungeEverythingByType(ResourceLink.class)); - counter.addAndGet(expungeEverythingByType(SearchResult.class)); - counter.addAndGet(expungeEverythingByType(SearchInclude.class)); - counter.addAndGet(expungeEverythingByType(TermValueSetConceptDesignation.class)); - counter.addAndGet(expungeEverythingByType(TermValueSetConcept.class)); - counter.addAndGet(expungeEverythingByType(TermValueSet.class)); - counter.addAndGet(expungeEverythingByType(TermConceptParentChildLink.class)); - counter.addAndGet(expungeEverythingByType(TermConceptMapGroupElementTarget.class)); - counter.addAndGet(expungeEverythingByType(TermConceptMapGroupElement.class)); - counter.addAndGet(expungeEverythingByType(TermConceptMapGroup.class)); - counter.addAndGet(expungeEverythingByType(TermConceptMap.class)); - counter.addAndGet(expungeEverythingByType(TermConceptProperty.class)); - counter.addAndGet(expungeEverythingByType(TermConceptDesignation.class)); - counter.addAndGet(expungeEverythingByType(TermConcept.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(NpmPackageVersionResourceEntity.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(NpmPackageVersionEntity.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(NpmPackageEntity.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(SearchParamPresent.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(BulkImportJobFileEntity.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(BulkImportJobEntity.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ForcedId.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceIndexedSearchParamDate.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceIndexedSearchParamNumber.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceIndexedSearchParamQuantity.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceIndexedSearchParamQuantityNormalized.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceIndexedSearchParamString.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceIndexedSearchParamToken.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceIndexedSearchParamUri.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceIndexedSearchParamCoords.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceIndexedComboStringUnique.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceIndexedComboTokenNonUnique.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceLink.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(SearchResult.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(SearchInclude.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermValueSetConceptDesignation.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermValueSetConcept.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermValueSet.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermConceptParentChildLink.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermConceptMapGroupElementTarget.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermConceptMapGroupElement.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermConceptMapGroup.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermConceptMap.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermConceptProperty.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermConceptDesignation.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermConcept.class)); myTxTemplate.execute(t -> { for (TermCodeSystem next : myEntityManager.createQuery("SELECT c FROM " + TermCodeSystem.class.getName() + " c", TermCodeSystem.class).getResultList()) { next.setCurrentVersion(null); @@ -160,52 +164,66 @@ public class ExpungeEverythingService { } return null; }); - counter.addAndGet(expungeEverythingByType(TermCodeSystemVersion.class)); - counter.addAndGet(expungeEverythingByType(TermCodeSystem.class)); - counter.addAndGet(expungeEverythingByType(SubscriptionTable.class)); - counter.addAndGet(expungeEverythingByType(ResourceHistoryTag.class)); - counter.addAndGet(expungeEverythingByType(ResourceTag.class)); - counter.addAndGet(expungeEverythingByType(TagDefinition.class)); - counter.addAndGet(expungeEverythingByType(ResourceHistoryProvenanceEntity.class)); - counter.addAndGet(expungeEverythingByType(ResourceHistoryTable.class)); - counter.addAndGet(expungeEverythingByType(ResourceTable.class)); - counter.addAndGet(expungeEverythingByType(PartitionEntity.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermCodeSystemVersion.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TermCodeSystem.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(SubscriptionTable.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceHistoryTag.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceTag.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(TagDefinition.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceHistoryProvenanceEntity.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceHistoryTable.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(ResourceTable.class)); + counter.addAndGet(expungeEverythingByTypeWithoutPurging(PartitionEntity.class)); myTxTemplate.execute(t -> { counter.addAndGet(doExpungeEverythingQuery("DELETE from " + Search.class.getSimpleName() + " d")); return null; }); + purgeAllCaches(); + ourLog.info("COMPLETED GLOBAL $expunge - Deleted {} rows", counter.get()); } + private void purgeAllCaches() { + myTxTemplate.execute(t -> { + myMemoryCacheService.invalidateAllCaches(); + return null; + }); + } + + private int expungeEverythingByTypeWithoutPurging(Class theEntityType) { + int outcome = 0; + while (true) { + StopWatch sw = new StopWatch(); + + @SuppressWarnings("ConstantConditions") + int count = myTxTemplate.execute(t -> { + CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(theEntityType); + cq.from(theEntityType); + TypedQuery query = myEntityManager.createQuery(cq); + query.setMaxResults(1000); + List results = query.getResultList(); + for (Object result : results) { + myEntityManager.remove(result); + } + return results.size(); + }); + + outcome += count; + if (count == 0) { + break; + } + + ourLog.info("Have deleted {} entities of type {} in {}", outcome, theEntityType.getSimpleName(), sw.toString()); + } + return outcome; + } + public int expungeEverythingByType(Class theEntityType) { - - int outcome = 0; - while (true) { - StopWatch sw = new StopWatch(); - - @SuppressWarnings("ConstantConditions") - int count = myTxTemplate.execute(t -> { - CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); - CriteriaQuery cq = cb.createQuery(theEntityType); - cq.from(theEntityType); - TypedQuery query = myEntityManager.createQuery(cq); - query.setMaxResults(1000); - List results = query.getResultList(); - for (Object result : results) { - myEntityManager.remove(result); - } - return results.size(); - }); - - outcome += count; - if (count == 0) { - break; - } - - ourLog.info("Have deleted {} entities of type {} in {}", outcome, theEntityType.getSimpleName(), sw.toString()); - } - return outcome; + int result = expungeEverythingByTypeWithoutPurging(theEntityType); + purgeAllCaches(); + return result; } private int doExpungeEverythingQuery(String theQuery) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/StringPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/StringPredicateBuilder.java index 88fd7cdae18..bc5b9a4c891 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/StringPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/StringPredicateBuilder.java @@ -225,15 +225,15 @@ public class StringPredicateBuilder extends BaseSearchParamPredicateBuilder { } public static String createLeftAndRightMatchLikeExpression(String likeExpression) { - return "%" + likeExpression.replace("%", "[%]") + "%"; + return "%" + likeExpression.replace("%", "\\%") + "%"; } public static String createLeftMatchLikeExpression(String likeExpression) { - return likeExpression.replace("%", "[%]") + "%"; + return likeExpression.replace("%", "\\%") + "%"; } public static String createRightMatchLikeExpression(String likeExpression) { - return "%" + likeExpression.replace("%", "[%]"); + return "%" + likeExpression.replace("%", "\\%"); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java index 3c79196b2ef..a66bf88efe9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java @@ -2005,9 +2005,9 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { result.setCodeDisplay(code.getDisplay()); for (TermConceptDesignation next : code.getDesignations()) { - IValidationSupport.ConceptDesignation designation = new IValidationSupport.ConceptDesignation(); // filter out the designation based on displayLanguage if any if (isDisplayLanguageMatch(theDisplayLanguage, next.getLanguage())) { + IValidationSupport.ConceptDesignation designation = new IValidationSupport.ConceptDesignation(); designation.setLanguage(next.getLanguage()); designation.setUseSystem(next.getUseSystem()); designation.setUseCode(next.getUseCode()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java index fc330a0e49d..a97890cbe02 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java @@ -116,12 +116,16 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete()); myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields()); myDaoConfig.setMaximumDeleteConflictQueryCount(new DaoConfig().getMaximumDeleteConflictQueryCount()); + myDaoConfig.setBundleBatchPoolSize(new DaoConfig().getBundleBatchPoolSize()); + myDaoConfig.setBundleBatchMaxPoolSize(new DaoConfig().getBundleBatchMaxPoolSize()); } @BeforeEach public void beforeDisableResultReuse() { myDaoConfig.setReuseCachedSearchResultsForMillis(null); + myDaoConfig.setBundleBatchPoolSize(1); + myDaoConfig.setBundleBatchMaxPoolSize(1); } private Bundle createInputTransactionWithPlaceholderIdInMatchUrl(HTTPVerb theVerb) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeHookTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeHookTest.java index a13947beb62..8cd0e76e03c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeHookTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeHookTest.java @@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.test.concurrency.PointcutLatch; import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.Meta; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -40,6 +41,8 @@ public class ExpungeHookTest extends BaseJpaDstu3Test { @BeforeEach public void before() { myDaoConfig.setExpungeEnabled(true); + myDaoConfig.setResourceClientIdStrategy(DaoConfig.ClientIdStrategyEnum.ALPHANUMERIC); + myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true); myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING, myEverythingLatch); myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE, myExpungeResourceLatch); } @@ -65,6 +68,42 @@ public class ExpungeHookTest extends BaseJpaDstu3Test { assertPatientGone(id); } + @Test + public void expungeEverythingAndRecreate() throws InterruptedException { + // Create a patient. + Patient thePatient = new Patient(); + thePatient.setId("ABC123"); + Meta theMeta = new Meta(); + theMeta.addProfile("http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"); + thePatient.setMeta(theMeta); + + IIdType id = myPatientDao.update(thePatient, mySrd).getId(); + assertNotNull(myPatientDao.read(id)); + + // Expunge it directly. + myPatientDao.delete(id); + ExpungeOptions options = new ExpungeOptions(); + options.setExpungeEverything(true); + options.setExpungeDeletedResources(true); + options.setExpungeOldVersions(true); + myPatientDao.expunge(id.toUnqualifiedVersionless(), options, mySrd); + assertPatientGone(id); + + // Create it a second time. + myPatientDao.update(thePatient, mySrd); + assertNotNull(myPatientDao.read(id)); + + // Expunge everything with the service. + myEverythingLatch.setExpectedCount(1); + myExpungeService.expunge(null, null, null, options, mySrd); + myEverythingLatch.awaitExpected(); + assertPatientGone(id); + + // Create it a third time. + myPatientDao.update(thePatient, mySrd); + assertNotNull(myPatientDao.read(id)); + } + private void assertPatientGone(IIdType theId) { try { myPatientDao.read(theId); 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 51a43daf31e..71b3e7bcdc2 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 @@ -20,6 +20,7 @@ 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.model.util.UcumServiceUtil; +import ca.uhn.fhir.jpa.partition.SystemRequestDetails; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; @@ -126,6 +127,7 @@ import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; @@ -150,12 +152,14 @@ import java.util.stream.Collectors; import static ca.uhn.fhir.rest.api.Constants.PARAM_TYPE; import static org.apache.commons.lang3.StringUtils.countMatches; +import static org.hamcrest.CoreMatchers.is; 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.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.hasSize; @@ -1356,6 +1360,34 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { assertThat(actual, contains(id)); } + @Test + @DisplayName("Duplicate Conditional Creates all resolve to the same match") + public void testDuplicateConditionalCreatesOnToken() throws IOException { + String inputString = IOUtils.toString(getClass().getResourceAsStream("/duplicate-conditional-create.json"), StandardCharsets.UTF_8); + Bundle firstBundle = myFhirCtx.newJsonParser().parseResource(Bundle.class, inputString); + + //Before you ask, yes, this has to be separately parsed. The reason for this is that the parameters passed to mySystemDao.transaction are _not_ immutable, so we cannot + //simply reuse the original bundle object. + Bundle duplicateBundle = myFhirCtx.newJsonParser().parseResource(Bundle.class, inputString); + + Bundle bundleResponse = mySystemDao.transaction(new SystemRequestDetails(), firstBundle); + bundleResponse.getEntry() + .forEach( entry -> assertThat(entry.getResponse().getStatus(), is(equalTo("201 Created")))); + + IBundleProvider search = myOrganizationDao.search(new SearchParameterMap().setLoadSynchronous(true)); + assertEquals(1, search.getAllResources().size()); + + //Running the bundle again should just result in 0 new resources created, as the org should already exist, and there is no update to the SR. + bundleResponse= mySystemDao.transaction(new SystemRequestDetails(), duplicateBundle); + bundleResponse.getEntry() + .forEach( entry -> { + assertThat(entry.getResponse().getStatus(), is(equalTo("200 OK"))); + }); + + search = myOrganizationDao.search(new SearchParameterMap().setLoadSynchronous(true), new SystemRequestDetails()); + assertEquals(1, search.getAllResources().size()); + } + @Test public void testIndexNoDuplicatesToken() { Patient res = new Patient(); 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 a1b7bb0ae52..d5428cc9fd1 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 @@ -117,12 +117,16 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { myDaoConfig.setAllowInlineMatchUrlReferences(false); myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete()); myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED); + myDaoConfig.setBundleBatchPoolSize(new DaoConfig().getBundleBatchPoolSize()); + myDaoConfig.setBundleBatchMaxPoolSize(new DaoConfig().getBundleBatchMaxPoolSize()); } @BeforeEach public void beforeDisableResultReuse() { myInterceptorRegistry.registerInterceptor(myInterceptor); myDaoConfig.setReuseCachedSearchResultsForMillis(null); + myDaoConfig.setBundleBatchPoolSize(1); + myDaoConfig.setBundleBatchMaxPoolSize(1); } private Bundle createInputTransactionWithPlaceholderIdInMatchUrl(HTTPVerb theVerb) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CodeSystemDesignationTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CodeSystemDesignationTest.java index 0d9c5eedf27..5f53a3afcd9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CodeSystemDesignationTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CodeSystemDesignationTest.java @@ -45,33 +45,15 @@ public class ResourceProviderR4CodeSystemDesignationTest extends BaseResourcePro String resp = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(respParam); ourLog.info(resp); - + //-- The designations should have de-AT and default language List parameterList = respParam.getParameter(); + + verifyParameterList(parameterList); List designationList = getDesignations(parameterList); - - assertEquals("display", respParam.getParameter().get(0).getName()); - assertEquals(("Systolic blood pressure 12 hour minimum"), ((StringType) respParam.getParameter().get(0).getValue()).getValue()); - - assertEquals("abstract", respParam.getParameter().get(1).getName()); - assertEquals(false, ((BooleanType) respParam.getParameter().get(1).getValue()).getValue()); - - //-- designationList - assertEquals(2, designationList.size()); - - // 1. de-AT:Systolic blood pressure 12 hour minimum - ParametersParameterComponent designation = designationList.get(0); - assertEquals("language", designation.getPart().get(0).getName()); - assertEquals("de-AT", designation.getPart().get(0).getValue().toString()); - assertEquals("value", designation.getPart().get(2).getName()); - assertEquals("de-AT:Systolic blood pressure 12 hour minimum", designation.getPart().get(2).getValue().toString()); - - // 2. Systolic blood pressure 12 hour minimum (no language) - designation = designationList.get(1); - assertEquals("language", designation.getPart().get(0).getName()); - assertNull(designation.getPart().get(0).getValue()); - assertEquals("value", designation.getPart().get(2).getName()); - assertEquals("Systolic blood pressure 12 hour minimum", designation.getPart().get(2).getValue().toString()); - + // should be de-AT and default + assertEquals(2, designationList.size()); + verifyDesignationDeAT(designationList.get(0)); + verifyDesignationNoLanguage(designationList.get(1)); } @@ -89,25 +71,14 @@ public class ResourceProviderR4CodeSystemDesignationTest extends BaseResourcePro String resp = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(respParam); ourLog.info(resp); - + //-- The designations should have default language only List parameterList = respParam.getParameter(); + + verifyParameterList(parameterList); List designationList = getDesignations(parameterList); - - assertEquals("display", respParam.getParameter().get(0).getName()); - assertEquals(("Systolic blood pressure 12 hour minimum"), ((StringType) respParam.getParameter().get(0).getValue()).getValue()); - - assertEquals("abstract", respParam.getParameter().get(1).getName()); - assertEquals(false, ((BooleanType) respParam.getParameter().get(1).getValue()).getValue()); - - //-- designationList - assertEquals(1, designationList.size()); - - // 1. Systolic blood pressure 12 hour minimum (no language) - ParametersParameterComponent designation = designationList.get(0); - assertEquals("language", designation.getPart().get(0).getName()); - assertNull(designation.getPart().get(0).getValue()); - assertEquals("value", designation.getPart().get(2).getName()); - assertEquals("Systolic blood pressure 12 hour minimum", designation.getPart().get(2).getValue().toString()); + // should be default only + assertEquals(1, designationList.size()); + verifyDesignationNoLanguage(designationList.get(0)); } @@ -124,41 +95,145 @@ public class ResourceProviderR4CodeSystemDesignationTest extends BaseResourcePro String resp = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(respParam); ourLog.info(resp); - + //-- The designations should have all languages and the default language List parameterList = respParam.getParameter(); + + verifyParameterList(parameterList); List designationList = getDesignations(parameterList); - - assertEquals("display", respParam.getParameter().get(0).getName()); - assertEquals(("Systolic blood pressure 12 hour minimum"), ((StringType) respParam.getParameter().get(0).getValue()).getValue()); - - assertEquals("abstract", respParam.getParameter().get(1).getName()); - assertEquals(false, ((BooleanType) respParam.getParameter().get(1).getValue()).getValue()); + // designation should be fr-FR, De-AT and default + assertEquals(3, designationList.size()); + verifyDesignationfrFR(designationList.get(0)); + verifyDesignationDeAT(designationList.get(1)); + verifyDesignationNoLanguage(designationList.get(2)); - //-- designationList - assertEquals(3, designationList.size()); + } + + @Test + public void testLookupWithDisplayLanguageCaching() { - // 1. fr-FR:Systolic blood pressure 12 hour minimum - ParametersParameterComponent designation = designationList.get(0); + //-- first call with de-AT + Parameters respParam = myClient + .operation() + .onType(CodeSystem.class) + .named("lookup") + .withParameter(Parameters.class, "code", new CodeType("8494-7")) + .andParameter("system", new UriType(CS_ACME_URL)) + .andParameter("displayLanguage",new CodeType("de-AT")) + .execute(); + + String resp = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(respParam); + ourLog.info(resp); + + //-- The designations should have de-AT and default language + List parameterList = respParam.getParameter(); + + verifyParameterList(parameterList); + List designationList = getDesignations(parameterList); + // should be de-AT and default + assertEquals(2, designationList.size()); + verifyDesignationDeAT(designationList.get(0)); + verifyDesignationNoLanguage(designationList.get(1)); + + //-- second call with zh-CN (not-exist) + respParam = myClient + .operation() + .onType(CodeSystem.class) + .named("lookup") + .withParameter(Parameters.class, "code", new CodeType("8494-7")) + .andParameter("system", new UriType(CS_ACME_URL)) + .andParameter("displayLanguage",new CodeType("zh-CN")) + .execute(); + + resp = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(respParam); + ourLog.info(resp); + + //-- The designations should have default language only + parameterList = respParam.getParameter(); + + verifyParameterList(parameterList); + designationList = getDesignations(parameterList); + // should be default only + assertEquals(1, designationList.size()); + verifyDesignationNoLanguage(designationList.get(0)); + + //-- third call with no language + respParam = myClient + .operation() + .onType(CodeSystem.class) + .named("lookup") + .withParameter(Parameters.class, "code", new CodeType("8494-7")) + .andParameter("system", new UriType(CS_ACME_URL)) + .execute(); + + resp = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(respParam); + ourLog.info(resp); + + //-- The designations should have all languages and the default language + parameterList = respParam.getParameter(); + + verifyParameterList(parameterList); + designationList = getDesignations(parameterList); + // designation should be fr-FR, De-AT and default + assertEquals(3, designationList.size()); + verifyDesignationfrFR(designationList.get(0)); + verifyDesignationDeAT(designationList.get(1)); + verifyDesignationNoLanguage(designationList.get(2)); + + //-- forth call with fr-FR + respParam = myClient + .operation() + .onType(CodeSystem.class) + .named("lookup") + .withParameter(Parameters.class, "code", new CodeType("8494-7")) + .andParameter("system", new UriType(CS_ACME_URL)) + .andParameter("displayLanguage",new CodeType("fr-FR")) + .execute(); + + resp = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(respParam); + ourLog.info(resp); + + //-- The designations should have fr-FR languages and the default language + parameterList = respParam.getParameter(); + + verifyParameterList(parameterList); + designationList = getDesignations(parameterList); + // designation should be fr-FR, default + assertEquals(2, designationList.size()); + verifyDesignationfrFR(designationList.get(0)); + verifyDesignationNoLanguage(designationList.get(1)); + } + + + private void verifyParameterList(List parameterList) { + assertEquals("display", parameterList.get(0).getName()); + assertEquals(("Systolic blood pressure 12 hour minimum"), + ((StringType) parameterList.get(0).getValue()).getValue()); + + assertEquals("abstract", parameterList.get(1).getName()); + assertEquals(false, ((BooleanType) parameterList.get(1).getValue()).getValue()); + } + + private void verifyDesignationfrFR(ParametersParameterComponent designation) { assertEquals("language", designation.getPart().get(0).getName()); assertEquals("fr-FR", designation.getPart().get(0).getValue().toString()); assertEquals("value", designation.getPart().get(2).getName()); assertEquals("fr-FR:Systolic blood pressure 12 hour minimum", designation.getPart().get(2).getValue().toString()); - - // 2. de-AT:Systolic blood pressure 12 hour minimum - designation = designationList.get(1); + } + + private void verifyDesignationDeAT(ParametersParameterComponent designation) { assertEquals("language", designation.getPart().get(0).getName()); assertEquals("de-AT", designation.getPart().get(0).getValue().toString()); assertEquals("value", designation.getPart().get(2).getName()); assertEquals("de-AT:Systolic blood pressure 12 hour minimum", designation.getPart().get(2).getValue().toString()); - - // 3. Systolic blood pressure 12 hour minimum (no language) - designation = designationList.get(2); + } + + private void verifyDesignationNoLanguage(ParametersParameterComponent designation) { assertEquals("language", designation.getPart().get(0).getName()); assertNull(designation.getPart().get(0).getValue()); assertEquals("value", designation.getPart().get(2).getName()); assertEquals("Systolic blood pressure 12 hour minimum", designation.getPart().get(2).getValue().toString()); - } + private List getDesignations(List parameterList) { List designationList = new ArrayList<>(); @@ -168,6 +243,5 @@ public class ResourceProviderR4CodeSystemDesignationTest extends BaseResourcePro designationList.add(parameter); } return designationList; - } } 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 73233bd5e84..b939c4b1536 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 @@ -295,7 +295,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { myCaptureQueriesListener.logSelectQueries(); } - @Test public void testSearchWithContainsLowerCase() { myDaoConfig.setAllowContainsSearches(true); @@ -333,6 +332,37 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { } + @Test + public void testSearchWithPercentSign() { + myDaoConfig.setAllowContainsSearches(true); + + Patient pt1 = new Patient(); + pt1.addName().setFamily("Smith%"); + String pt1id = myPatientDao.create(pt1).getId().toUnqualifiedVersionless().getValue(); + + Bundle output = myClient + .search() + .forResource("Patient") + .where(Patient.NAME.contains().value("Smith%")) + .returnBundle(Bundle.class) + .execute(); + List ids = output.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualifiedVersionless().getValue()).collect(Collectors.toList()); + assertThat(ids, containsInAnyOrder(pt1id)); + + Patient pt2 = new Patient(); + pt2.addName().setFamily("Sm%ith"); + String pt2id = myPatientDao.create(pt2).getId().toUnqualifiedVersionless().getValue(); + + output = myClient + .search() + .forResource("Patient") + .where(Patient.NAME.contains().value("Sm%ith")) + .returnBundle(Bundle.class) + .execute(); + ids = output.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualifiedVersionless().getValue()).collect(Collectors.toList()); + assertThat(ids, containsInAnyOrder(pt2id)); + } + @Test public void testSearchWithDateInvalid() throws IOException { HttpGet get = new HttpGet(ourServerBase + "/Condition?onset-date=junk"); diff --git a/hapi-fhir-jpaserver-base/src/test/resources/duplicate-conditional-create.json b/hapi-fhir-jpaserver-base/src/test/resources/duplicate-conditional-create.json new file mode 100644 index 00000000000..26ea0369f1f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/duplicate-conditional-create.json @@ -0,0 +1,66 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "fullUrl": "urn:uuid:4cd35592-5d4d-462b-8483-e404c023d316", + "resource": { + "resourceType": "Organization", + "identifier": [ + { + "system": "https://fhir.tester.ca/NamingSystem/ca-on-health-care-facility-id", + "value": "3972" + } + ] + }, + "request": { + "method": "POST", + "url": "/Organization", + "ifNoneExist": "Organization?identifier=https://fhir.tester.ca/NamingSystem/ca-on-health-care-facility-id|3972" + } + }, + { + "fullUrl": "urn:uuid:02643c1d-94d1-4991-a063-036fa0f57ec2", + "resource": { + "resourceType": "Organization", + "identifier": [ + { + "system": "https://fhir.tester.ca/NamingSystem/ca-on-health-care-facility-id", + "value": "3972" + } + ] + }, + "request": { + "method": "POST", + "url": "/Organization", + "ifNoneExist": "Organization?identifier=https://fhir.tester.ca/NamingSystem/ca-on-health-care-facility-id|3972" + } + }, + { + "fullUrl": "urn:uuid:8271e94f-e08b-498e-ad6d-751928c3ff99", + "resource": { + "resourceType": "ServiceRequest", + "identifier": [ + { + "system": "https://fhir-tester.ca/NamingSystem/service-request-id", + "value": "1" + } + ], + "performer": [ + { + "reference": "urn:uuid:4cd35592-5d4d-462b-8483-e404c023d316", + "type": "Organization" + }, + { + "reference": "urn:uuid:02643c1d-94d1-4991-a063-036fa0f57ec2", + "type": "Organization" + } + ] + }, + "request": { + "method": "PUT", + "url": "/ServiceRequest?identifier=https://fhir-tester.ca/NamingSystem/service-request-id|1" + } + } + ] +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java index ba5a427ab36..79f5402fb07 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java @@ -112,7 +112,7 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple @Override public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theDisplayLanguage) { - String key = "lookupCode " + theSystem + " " + theCode; + String key = "lookupCode " + theSystem + " " + theCode + " " + defaultIfBlank(theDisplayLanguage, "NO_LANG"); return loadFromCache(myLookupCodeCache, key, t -> super.lookupCode(theValidationSupportContext, theSystem, theCode, theDisplayLanguage)); }