From 794d9145e9197f875b84e72108629668ac5261fc Mon Sep 17 00:00:00 2001 From: James Agnew Date: Wed, 24 Oct 2018 17:54:58 -0300 Subject: [PATCH 1/2] Move query count tests to consolidate them, and avoid an accidental rewrite of existing indexes in some conditions --- .../fhir/rest/gclient/StringClientParam.java | 2 +- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 13 + .../java/ca/uhn/fhir/jpa/dao/DaoConfig.java | 29 + .../ca/uhn/fhir/jpa/dao/SearchBuilder.java | 194 +- .../BaseResourceIndexedSearchParam.java | 6 +- .../ResourceIndexedSearchParamCoords.java | 1 + .../ResourceIndexedSearchParamDate.java | 1 + .../ResourceIndexedSearchParamNumber.java | 1 + .../ResourceIndexedSearchParamQuantity.java | 1 + .../ResourceIndexedSearchParamString.java | 1 + .../ResourceIndexedSearchParamToken.java | 1 + .../entity/ResourceIndexedSearchParamUri.java | 1 + .../SubscriptionTriggeringProvider.java | 20 +- .../search/StaleSearchDeletingSvcImpl.java | 4 +- .../ca/uhn/fhir/jpa/config/TestR4Config.java | 9 +- .../dao/r4/FhirResourceDaoR4CreateTest.java | 38 +- .../r4/FhirResourceDaoR4QueryCountTest.java | 128 +- .../FhirResourceDaoR4SearchNoHashesTest.java | 3343 +++++++++++++++++ .../dao/r4/FhirResourceDaoR4UpdateTest.java | 27 - src/changes/changes.xml | 5 + 20 files changed, 3736 insertions(+), 89 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/StringClientParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/StringClientParam.java index 5d0a463bbfe..c7e023cc56e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/StringClientParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/StringClientParam.java @@ -31,7 +31,7 @@ import java.util.List; * @author james * */ -public class StringClientParam extends BaseClientParam implements IParam { +public class StringClientParam extends BaseClientParam implements IParam { private final String myParamName; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index ea1941a8885..9cab2789e04 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -2086,6 +2086,7 @@ public abstract class BaseHapiFhirDao implements IDao, */ if (thePerformIndexing) { + calculateHashes(stringParams); for (ResourceIndexedSearchParamString next : removeCommon(existingStringParams, stringParams)) { next.setDaoConfig(myConfig); myEntityManager.remove(next); @@ -2095,6 +2096,7 @@ public abstract class BaseHapiFhirDao implements IDao, myEntityManager.persist(next); } + calculateHashes(tokenParams); for (ResourceIndexedSearchParamToken next : removeCommon(existingTokenParams, tokenParams)) { myEntityManager.remove(next); theEntity.getParamsToken().remove(next); @@ -2103,6 +2105,7 @@ public abstract class BaseHapiFhirDao implements IDao, myEntityManager.persist(next); } + calculateHashes(numberParams); for (ResourceIndexedSearchParamNumber next : removeCommon(existingNumberParams, numberParams)) { myEntityManager.remove(next); theEntity.getParamsNumber().remove(next); @@ -2111,6 +2114,7 @@ public abstract class BaseHapiFhirDao implements IDao, myEntityManager.persist(next); } + calculateHashes(quantityParams); for (ResourceIndexedSearchParamQuantity next : removeCommon(existingQuantityParams, quantityParams)) { myEntityManager.remove(next); theEntity.getParamsQuantity().remove(next); @@ -2120,6 +2124,7 @@ public abstract class BaseHapiFhirDao implements IDao, } // Store date SP's + calculateHashes(dateParams); for (ResourceIndexedSearchParamDate next : removeCommon(existingDateParams, dateParams)) { myEntityManager.remove(next); theEntity.getParamsDate().remove(next); @@ -2129,6 +2134,7 @@ public abstract class BaseHapiFhirDao implements IDao, } // Store URI SP's + calculateHashes(uriParams); for (ResourceIndexedSearchParamUri next : removeCommon(existingUriParams, uriParams)) { myEntityManager.remove(next); theEntity.getParamsUri().remove(next); @@ -2138,6 +2144,7 @@ public abstract class BaseHapiFhirDao implements IDao, } // Store Coords SP's + calculateHashes(coordsParams); for (ResourceIndexedSearchParamCoords next : removeCommon(existingCoordsParams, coordsParams)) { myEntityManager.remove(next); theEntity.getParamsCoords().remove(next); @@ -2187,6 +2194,12 @@ public abstract class BaseHapiFhirDao implements IDao, return theEntity; } + private void calculateHashes(Collection theStringParams) { + for (BaseResourceIndexedSearchParam next : theStringParams) { + next.calculateHashes(); + } + } + protected ResourceTable updateEntity(RequestDetails theRequest, IBaseResource theResource, ResourceTable entity, Date theDeletedTimestampOrNull, Date theUpdateTime) { return updateEntity(theRequest, theResource, entity, theDeletedTimestampOrNull, true, true, theUpdateTime, false, true); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java index a455a8dff04..0bd421cc87c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java @@ -155,6 +155,7 @@ public class DaoConfig { private boolean myValidateSearchParameterExpressionsOnSave = true; private List mySearchPreFetchThresholds = Arrays.asList(500, 2000, -1); private List myWarmCacheEntries = new ArrayList<>(); + private boolean myDisableHashBasedSearches; /** * Constructor @@ -1383,6 +1384,34 @@ public class DaoConfig { return mySearchPreFetchThresholds; } + /** + * If set to true (default is false) the server will not use + * hash based searches. These searches were introduced in HAPI FHIR 3.5.0 + * and are the new default way of searching. However they require a very + * large data migration if an existing system has a large amount of data + * so this setting can be used to use the old search mechanism while data + * is migrated. + * + * @since 3.6.0 + */ + public boolean getDisableHashBasedSearches() { + return myDisableHashBasedSearches; + } + + /** + * If set to true (default is false) the server will not use + * hash based searches. These searches were introduced in HAPI FHIR 3.5.0 + * and are the new default way of searching. However they require a very + * large data migration if an existing system has a large amount of data + * so this setting can be used to use the old search mechanism while data + * is migrated. + * + * @since 3.6.0 + */ + public void setDisableHashBasedSearches(boolean theDisableHashBasedSearches) { + myDisableHashBasedSearches = theDisableHashBasedSearches; + } + public enum IndexEnabledEnum { ENABLED, DISABLED diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index 42d9af1f44b..210da7c3d41 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -55,6 +55,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; @@ -94,6 +95,7 @@ public class SearchBuilder implements ISearchBuilder { private static SearchParameterMap ourLastHandlerParamsForUnitTest; private static String ourLastHandlerThreadForUnitTest; private static boolean ourTrackHandlersForUnitTest; + private final boolean myDontUseHashesForSearch; protected IResourceTagDao myResourceTagDao; private IResourceSearchViewDao myResourceSearchViewDao; private List myAlsoIncludePids; @@ -130,6 +132,7 @@ public class SearchBuilder implements ISearchBuilder { myEntityManager = theEntityManager; myFulltextSearchSvc = theFulltextSearchSvc; myCallingDao = theDao; + myDontUseHashesForSearch = theDao.getConfig().getDisableHashBasedSearches(); myResourceIndexedSearchParamUriDao = theResourceIndexedSearchParamUriDao; myForcedIdDao = theForcedIdDao; myTerminologySvc = theTerminologySvc; @@ -304,6 +307,15 @@ public class SearchBuilder implements ISearchBuilder { } private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing) { +// if (myDontUseHashesForSearch) { +// Join paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT); +// Join paramJoin = paramPresentJoin.join("mySearchParam", JoinType.LEFT); +// +// myPredicates.add(myBuilder.equal(paramJoin.get("myResourceName"), theResourceName)); +// myPredicates.add(myBuilder.equal(paramJoin.get("myParamName"), theParamName)); +// myPredicates.add(myBuilder.equal(paramPresentJoin.get("myPresent"), !theMissing)); +// } + Join paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT); Expression hashPresence = paramPresentJoin.get("myHashPresence").as(Long.class); @@ -841,10 +853,18 @@ public class SearchBuilder implements ISearchBuilder { } else { - long hashUri = ResourceIndexedSearchParamUri.calculateHashUri(theResourceName, theParamName, value); - Predicate hashPredicate = myBuilder.equal(join.get("myHashUri"), hashUri); - codePredicates.add(hashPredicate); + if (myDontUseHashesForSearch) { + Predicate predicate = myBuilder.equal(join.get("myUri").as(String.class), value); + codePredicates.add(predicate); + + } else { + + long hashUri = ResourceIndexedSearchParamUri.calculateHashUri(theResourceName, theParamName, value); + Predicate hashPredicate = myBuilder.equal(join.get("myHashUri"), hashUri); + codePredicates.add(hashPredicate); + + } } } else { @@ -868,6 +888,13 @@ public class SearchBuilder implements ISearchBuilder { } private Predicate combineParamIndexPredicateWithParamNamePredicate(String theResourceName, String theParamName, From theFrom, Predicate thePredicate) { + if (myDontUseHashesForSearch) { + Predicate resourceTypePredicate = myBuilder.equal(theFrom.get("myResourceType"), theResourceName); + Predicate paramNamePredicate = myBuilder.equal(theFrom.get("myParamName"), theParamName); + Predicate outerPredicate = myBuilder.and(resourceTypePredicate, paramNamePredicate, thePredicate); + return outerPredicate; + } + long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName); Predicate hashIdentityPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity); return myBuilder.and(hashIdentityPredicate, thePredicate); @@ -1079,6 +1106,37 @@ public class SearchBuilder implements ISearchBuilder { throw new IllegalArgumentException("Invalid quantity type: " + theParam.getClass()); } + if (myDontUseHashesForSearch) { + Predicate system = null; + if (!isBlank(systemValue)) { + system = theBuilder.equal(theFrom.get("mySystem"), systemValue); + } + + Predicate code = null; + if (!isBlank(unitsValue)) { + code = theBuilder.equal(theFrom.get("myUnits"), unitsValue); + } + + cmpValue = ObjectUtils.defaultIfNull(cmpValue, ParamPrefixEnum.EQUAL); + final Expression path = theFrom.get("myValue"); + String invalidMessageName = "invalidQuantityPrefix"; + + Predicate num = createPredicateNumeric(theResourceName, null, theFrom, theBuilder, theParam, cmpValue, valueValue, path, invalidMessageName); + + Predicate singleCode; + if (system == null && code == null) { + singleCode = num; + } else if (system == null) { + singleCode = theBuilder.and(code, num); + } else if (code == null) { + singleCode = theBuilder.and(system, num); + } else { + singleCode = theBuilder.and(system, code, num); + } + + return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + } + Predicate hashPredicate; if (!isBlank(systemValue) && !isBlank(unitsValue)) { long hash = ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(theResourceName, theParamName, systemValue, unitsValue); @@ -1130,6 +1188,31 @@ public class SearchBuilder implements ISearchBuilder { + ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm); } + if (myDontUseHashesForSearch) { + String likeExpression = BaseHapiFhirDao.normalizeString(rawSearchTerm); + if (myCallingDao.getConfig().isAllowContainsSearches()) { + if (theParameter instanceof StringParam) { + if (((StringParam) theParameter).isContains()) { + likeExpression = createLeftAndRightMatchLikeExpression(likeExpression); + } else { + likeExpression = createLeftMatchLikeExpression(likeExpression); + } + } else { + likeExpression = createLeftMatchLikeExpression(likeExpression); + } + } else { + likeExpression = createLeftMatchLikeExpression(likeExpression); + } + + Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression); + if (theParameter instanceof StringParam && ((StringParam) theParameter).isExact()) { + Predicate exactCode = theBuilder.equal(theFrom.get("myValueExact"), rawSearchTerm); + singleCode = theBuilder.and(singleCode, exactCode); + } + + return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + } + boolean exactMatch = theParameter instanceof StringParam && ((StringParam) theParameter).isExact(); if (exactMatch) { @@ -1234,6 +1317,92 @@ public class SearchBuilder implements ISearchBuilder { return new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false); } + if (myDontUseHashesForSearch) { + ArrayList singleCodePredicates = new ArrayList(); + if (codes != null) { + + List orPredicates = new ArrayList(); + Map> map = new HashMap>(); + for (VersionIndependentConcept nextCode : codes) { + List systemCodes = map.get(nextCode.getSystem()); + if (null == systemCodes) { + systemCodes = new ArrayList<>(); + map.put(nextCode.getSystem(), systemCodes); + } + systemCodes.add(nextCode); + } + // Use "in" in case of large numbers of codes due to param modifiers + final Path systemExpression = theFrom.get("mySystem"); + final Path valueExpression = theFrom.get("myValue"); + for (Map.Entry> entry : map.entrySet()) { + CriteriaBuilder.In codePredicate = theBuilder.in(valueExpression); + boolean haveAtLeastOneCode = false; + for (VersionIndependentConcept nextCode : entry.getValue()) { + if (isNotBlank(nextCode.getCode())) { + codePredicate.value(nextCode.getCode()); + haveAtLeastOneCode = true; + } + } + + if (entry.getKey() != null) { + Predicate systemPredicate = theBuilder.equal(systemExpression, entry.getKey()); + if (haveAtLeastOneCode) { + orPredicates.add(theBuilder.and(systemPredicate, codePredicate)); + } else { + orPredicates.add(systemPredicate); + } + } else { + orPredicates.add(codePredicate); + } + } + + Predicate or = theBuilder.or(orPredicates.toArray(new Predicate[0])); + if (modifier == TokenParamModifier.NOT) { + or = theBuilder.not(or); + } + singleCodePredicates.add(or); + + } else { + + /* + * Ok, this is a normal query + */ + + if (StringUtils.isNotBlank(system)) { + if (modifier != null && modifier == TokenParamModifier.NOT) { + singleCodePredicates.add(theBuilder.notEqual(theFrom.get("mySystem"), system)); + } else { + singleCodePredicates.add(theBuilder.equal(theFrom.get("mySystem"), system)); + } + } else if (system == null) { + // don't check the system + } else { + // If the system is "", we only match on null systems + singleCodePredicates.add(theBuilder.isNull(theFrom.get("mySystem"))); + } + + if (StringUtils.isNotBlank(code)) { + if (modifier != null && modifier == TokenParamModifier.NOT) { + singleCodePredicates.add(theBuilder.notEqual(theFrom.get("myValue"), code)); + } else { + singleCodePredicates.add(theBuilder.equal(theFrom.get("myValue"), code)); + } + } else { + /* + * As of HAPI FHIR 1.5, if the client searched for a token with a system but no specified value this means to + * match all tokens with the given value. + * + * I'm not sure I agree with this, but hey.. FHIR-I voted and this was the result :) + */ + // singleCodePredicates.add(theBuilder.isNull(theFrom.get("myValue"))); + } + } + + Predicate singleCode = theBuilder.and(toArray(singleCodePredicates)); + return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + } + + /* * Note: A null system value means "match any system", but * an empty-string system value means "match values that @@ -1607,9 +1776,14 @@ public class SearchBuilder implements ISearchBuilder { if (param.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { thePredicates.add(join.get("mySourcePath").as(String.class).in(param.getPathsSplit())); } else { - Long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myResourceName, theSort.getParamName()); - Predicate joinParam1 = theBuilder.equal(join.get("myHashIdentity"), hashIdentity); - thePredicates.add(joinParam1); + if (myDontUseHashesForSearch) { + Predicate joinParam1 = theBuilder.equal(join.get("myParamName"), theSort.getParamName()); + thePredicates.add(joinParam1); + } else { + Long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myResourceName, theSort.getParamName()); + Predicate joinParam1 = theBuilder.equal(join.get("myHashIdentity"), hashIdentity); + thePredicates.add(joinParam1); + } } } else { ourLog.debug("Reusing join for {}", theSort.getParamName()); @@ -1668,7 +1842,7 @@ public class SearchBuilder implements ISearchBuilder { //-- preload all tags with tag definition if any Map> tagMap = getResourceTagMap(resourceSearchViewList); - Long resourceId = null; + Long resourceId; for (ResourceSearchView next : resourceSearchViewList) { Class resourceType = context.getResourceDefinition(next.getResourceType()).getImplementingClass(); @@ -1706,7 +1880,7 @@ public class SearchBuilder implements ISearchBuilder { private Map> getResourceTagMap(Collection theResourceSearchViewList) { - List idList = new ArrayList(theResourceSearchViewList.size()); + List idList = new ArrayList<>(theResourceSearchViewList.size()); //-- find all resource has tags for (ResourceSearchView resource : theResourceSearchViewList) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java index 82366f7ed15..a51d92819f4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.entity; * 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. @@ -129,6 +129,8 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable { public abstract IQueryParameterType toQueryParameterType(); + public abstract void calculateHashes(); + public static long calculateHashIdentity(String theResourceType, String theParamName) { return hash(theResourceType, theParamName); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java index e21f6bcd9ba..b7503142046 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java @@ -67,6 +67,7 @@ public class ResourceIndexedSearchParamCoords extends BaseResourceIndexedSearchP setLongitude(theLongitude); } + @Override @PrePersist public void calculateHashes() { if (myHashIdentity == null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java index b26ec43f84a..7b43ebf9dce 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java @@ -83,6 +83,7 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar myOriginalValue = theOriginalValue; } + @Override @PrePersist public void calculateHashes() { if (myHashIdentity == null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java index cc28ecbe639..ed5568fbd92 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java @@ -69,6 +69,7 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP setValue(theValue); } + @Override @PrePersist public void calculateHashes() { if (myHashIdentity == null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java index 2e78221cde1..13c7e7fc2ad 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java @@ -95,6 +95,7 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc setUnits(theUnits); } + @Override @PrePersist public void calculateHashes() { if (myHashIdentity == null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java index 9429ad953c8..023199b395f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java @@ -161,6 +161,7 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP myHashIdentity = theHashIdentity; } + @Override @PrePersist public void calculateHashes() { if (myHashNormalizedPrefix == null && myDaoConfig != null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java index 75938fb79e8..74a86253195 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java @@ -108,6 +108,7 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa setValue(theValue); } + @Override @PrePersist public void calculateHashes() { if (myHashSystem == null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java index 6f35d8809e9..b94ee78db6f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java @@ -82,6 +82,7 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara setUri(theUri); } + @Override @PrePersist public void calculateHashes() { if (myHashUri == null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/SubscriptionTriggeringProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/SubscriptionTriggeringProvider.java index 5f8a18d5bbf..2386ea4070a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/SubscriptionTriggeringProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/SubscriptionTriggeringProvider.java @@ -39,6 +39,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.server.IResourceProvider; +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.util.ParametersUtil; @@ -307,7 +308,7 @@ public class SubscriptionTriggeringProvider implements IResourceProvider, Applic ourLog.info("Triggering job[{}] search {} requesting resources {} - {}", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex); List resourceIds = mySearchCoordinatorSvc.getResources(theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex); - ourLog.info("Triggering job[{}] delivering {} resources", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex); + ourLog.info("Triggering job[{}] delivering {} resources", theJobDetails.getJobId(), resourceIds.size()); int highestIndexSubmitted = theJobDetails.getCurrentSearchLastUploadedIndex(); for (Long next : resourceIds) { @@ -374,9 +375,22 @@ public class SubscriptionTriggeringProvider implements IResourceProvider, Applic msg.setNewPayload(myFhirContext, theResourceToTrigger); return myExecutorService.submit(()->{ - for (BaseSubscriptionInterceptor next : mySubscriptionInterceptorList) { - next.submitResourceModified(msg); + for (int i = 0; ; i++) { + try { + for (BaseSubscriptionInterceptor next : mySubscriptionInterceptorList) { + next.submitResourceModified(msg); + } + break; + } catch (Exception e) { + if (i >= 3) { + throw new InternalErrorException(e); + } + + ourLog.warn("Exception while retriggering subscriptions (going to sleep and retry): {}", e.toString()); + Thread.sleep(1000); + } } + return null; }); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/StaleSearchDeletingSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/StaleSearchDeletingSvcImpl.java index 5e90ad0dd27..00a046f9a07 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/StaleSearchDeletingSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/StaleSearchDeletingSvcImpl.java @@ -74,7 +74,6 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc { private void deleteSearch(final Long theSearchPid) { mySearchDao.findById(theSearchPid).ifPresent(searchToDelete -> { - ourLog.info("Deleting search {}/{} - Created[{}] -- Last returned[{}]", searchToDelete.getId(), searchToDelete.getUuid(), new InstantType(searchToDelete.getCreated()), new InstantType(searchToDelete.getSearchLastReturned())); mySearchIncludeDao.deleteForSearch(searchToDelete.getId()); /* @@ -93,7 +92,10 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc { // Only delete if we don't have results left in this search if (resultPids.getNumberOfElements() < max) { + ourLog.info("Deleting search {}/{} - Created[{}] -- Last returned[{}]", searchToDelete.getId(), searchToDelete.getUuid(), new InstantType(searchToDelete.getCreated()), new InstantType(searchToDelete.getSearchLastReturned())); mySearchDao.deleteByPid(searchToDelete.getId()); + } else { + ourLog.info("Purged {} search results for deleted search {}/{}", resultPids.getSize(), searchToDelete.getId(), searchToDelete.getUuid()); } }); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java index d6a0342cacf..1d86d6e2070 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.config; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.validation.ResultSeverityEnum; +import net.ttddyy.dsproxy.listener.SingleQueryCountHolder; import net.ttddyy.dsproxy.listener.ThreadQueryCountHolder; import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; @@ -107,12 +108,18 @@ public class TestR4Config extends BaseJavaConfigR4 { .create(retVal) .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL") .logSlowQueryBySlf4j(10, TimeUnit.SECONDS) - .countQuery(new ThreadQueryCountHolder()) +// .countQuery(new ThreadQueryCountHolder()) + .countQuery(singleQueryCountHolder()) .build(); return dataSource; } + @Bean + public SingleQueryCountHolder singleQueryCountHolder() { + return new SingleQueryCountHolder(); + } + @Override @Bean() public LocalContainerEntityManagerFactoryBean entityManagerFactory() { 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 1a2b7155c1a..4006322d6bc 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 @@ -4,7 +4,6 @@ import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.SearchParameterMap; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.util.TestUtil; -import net.ttddyy.dsproxy.listener.ThreadQueryCountHolder; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.IdType; @@ -18,10 +17,9 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.matchesPattern; -import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4CreateTest.class); @@ -37,22 +35,22 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { Patient p = myFhirCtx.newXmlParser().parseResource(Patient.class, input); String id = myPatientDao.create(p).getId().toUnqualifiedVersionless().getValue(); - SearchParameterMap map= new SearchParameterMap(); + SearchParameterMap map = new SearchParameterMap(); map.setLoadSynchronous(true); map.add(Patient.SP_FAMILY, new StringParam("김")); assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(id)); - map= new SearchParameterMap(); + map = new SearchParameterMap(); map.setLoadSynchronous(true); map.add(Patient.SP_GIVEN, new StringParam("준")); assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(id)); - map= new SearchParameterMap(); + map = new SearchParameterMap(); map.setLoadSynchronous(true); map.add(Patient.SP_GIVEN, new StringParam("준수")); assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(id)); - map= new SearchParameterMap(); + map = new SearchParameterMap(); map.setLoadSynchronous(true); map.add(Patient.SP_GIVEN, new StringParam("수")); // rightmost character only assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), empty()); @@ -60,7 +58,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { } @Test - public void testCreateWithUuidResourceStrategy() throws Exception { + public void testCreateWithUuidResourceStrategy() { myDaoConfig.setResourceServerIdStrategy(DaoConfig.IdStrategyEnum.UUID); Patient p = new Patient(); @@ -110,26 +108,6 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { assertThat(output.getEntry().get(1).getResponse().getLocation(), matchesPattern("Patient/[a-z0-9]{8}-.*")); - } - - @Test - public void testWritesPerformMinimalSqlStatements() { - Patient p = new Patient(); - p.addIdentifier().setSystem("sys1").setValue("val1"); - p.addIdentifier().setSystem("sys2").setValue("val2"); - - ourLog.info("** About to perform write"); - new ThreadQueryCountHolder().getOrCreateQueryCount("").setInsert(0); - new ThreadQueryCountHolder().getOrCreateQueryCount("").setUpdate(0); - - myPatientDao.create(p); - - ourLog.info("** Done performing write"); - - ourLog.info("Inserts: {}", new ThreadQueryCountHolder().getOrCreateQueryCount("").getInsert()); - ourLog.info("Updates: {}", new ThreadQueryCountHolder().getOrCreateQueryCount("").getUpdate()); - - } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index 749492d8def..dfcbc1c61a8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -1,14 +1,19 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.util.TestUtil; +import net.ttddyy.dsproxy.QueryCount; import net.ttddyy.dsproxy.QueryCountHolder; +import net.ttddyy.dsproxy.listener.SingleQueryCountHolder; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.DateTimeType; import org.hl7.fhir.r4.model.Patient; import org.junit.After; import org.junit.AfterClass; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; import static org.junit.Assert.assertEquals; @@ -18,6 +23,8 @@ import static org.junit.Assert.assertEquals; }) public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class); + @Autowired + private SingleQueryCountHolder myCountHolder; @After public void afterResetDao() { @@ -25,22 +32,87 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields()); } + @Test + public void testWritesPerformMinimalSqlStatements() { + Patient p = new Patient(); + p.addIdentifier().setSystem("sys1").setValue("val1"); + p.addIdentifier().setSystem("sys2").setValue("val2"); + + ourLog.info("** About to perform write"); + myCountHolder.clear(); + + IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless(); + + ourLog.info("** Done performing write"); + + assertEquals(6, getQueryCount().getInsert()); + assertEquals(0, getQueryCount().getUpdate()); + + /* + * Not update the value + */ + + p = new Patient(); + p.setId(id); + p.addIdentifier().setSystem("sys1").setValue("val3"); + p.addIdentifier().setSystem("sys2").setValue("val4"); + + ourLog.info("** About to perform write 2"); + myCountHolder.clear(); + + myPatientDao.update(p).getId().toUnqualifiedVersionless(); + + ourLog.info("** Done performing write 2"); + + assertEquals(2, getQueryCount().getInsert()); + assertEquals(1, getQueryCount().getUpdate()); + assertEquals(1, getQueryCount().getDelete()); + } + + @Test + public void testSearch() { + + for (int i = 0; i < 20; i++) { + Patient p = new Patient(); + p.addIdentifier().setSystem("sys1").setValue("val" + i); + myPatientDao.create(p); + } + + myCountHolder.clear(); + + ourLog.info("** About to perform search"); + IBundleProvider search = myPatientDao.search(new SearchParameterMap()); + ourLog.info("** About to retrieve resources"); + search.getResources(0, 20); + ourLog.info("** Done retrieving resources"); + + assertEquals(4, getQueryCount().getSelect()); + assertEquals(2, getQueryCount().getInsert()); + assertEquals(1, getQueryCount().getUpdate()); + assertEquals(0, getQueryCount().getDelete()); + + } + + private QueryCount getQueryCount() { + return myCountHolder.getQueryCountMap().get(""); + } + @Test public void testCreateClientAssignedId() { myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); - QueryCountHolder.clear(); + myCountHolder.clear(); ourLog.info("** Starting Update Non-Existing resource with client assigned ID"); Patient p = new Patient(); p.setId("A"); p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field myPatientDao.update(p).getId().toUnqualifiedVersionless(); - assertEquals(1, QueryCountHolder.getGrandTotal().getSelect()); - assertEquals(4, QueryCountHolder.getGrandTotal().getInsert()); - assertEquals(0, QueryCountHolder.getGrandTotal().getDelete()); + assertEquals(1, getQueryCount().getSelect()); + assertEquals(4, getQueryCount().getInsert()); + assertEquals(0, getQueryCount().getDelete()); // Because of the forced ID's bidirectional link HFJ_RESOURCE <-> HFJ_FORCED_ID - assertEquals(1, QueryCountHolder.getGrandTotal().getUpdate()); + assertEquals(1, getQueryCount().getUpdate()); runInTransaction(() -> { assertEquals(1, myResourceTableDao.count()); assertEquals(1, myResourceHistoryTableDao.count()); @@ -50,17 +122,17 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { // Ok how about an update - QueryCountHolder.clear(); + myCountHolder.clear(); ourLog.info("** Starting Update Existing resource with client assigned ID"); p = new Patient(); p.setId("A"); p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field myPatientDao.update(p).getId().toUnqualifiedVersionless(); - assertEquals(5, QueryCountHolder.getGrandTotal().getSelect()); - assertEquals(1, QueryCountHolder.getGrandTotal().getInsert()); - assertEquals(0, QueryCountHolder.getGrandTotal().getDelete()); - assertEquals(1, QueryCountHolder.getGrandTotal().getUpdate()); + assertEquals(5, getQueryCount().getSelect()); + assertEquals(1, getQueryCount().getInsert()); + assertEquals(0, getQueryCount().getDelete()); + assertEquals(1, getQueryCount().getUpdate()); runInTransaction(() -> { assertEquals(1, myResourceTableDao.count()); assertEquals(2, myResourceHistoryTableDao.count()); @@ -75,24 +147,24 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { public void testOneRowPerUpdate() { myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); - QueryCountHolder.clear(); + myCountHolder.clear(); Patient p = new Patient(); p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless(); - assertEquals(3, QueryCountHolder.getGrandTotal().getInsert()); + assertEquals(3, getQueryCount().getInsert()); runInTransaction(() -> { assertEquals(1, myResourceTableDao.count()); assertEquals(1, myResourceHistoryTableDao.count()); }); - QueryCountHolder.clear(); + myCountHolder.clear(); p = new Patient(); p.setId(id); p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field myPatientDao.update(p).getId().toUnqualifiedVersionless(); - assertEquals(1, QueryCountHolder.getGrandTotal().getInsert()); + assertEquals(1, getQueryCount().getInsert()); runInTransaction(() -> { assertEquals(1, myResourceTableDao.count()); assertEquals(2, myResourceHistoryTableDao.count()); @@ -101,6 +173,34 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { } + @Test + public void testUpdateReusesIndexes() { + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); + + myCountHolder.clear(); + + Patient pt = new Patient(); + pt.setActive(true); + pt.addName().setFamily("FAMILY1").addGiven("GIVEN1A").addGiven("GIVEN1B"); + IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless(); + + ourLog.info("Now have {} deleted", getQueryCount().getDelete()); + ourLog.info("Now have {} inserts", getQueryCount().getInsert()); + myCountHolder.clear(); + + ourLog.info("** About to update"); + + pt.setId(id); + pt.getNameFirstRep().addGiven("GIVEN1C"); + myPatientDao.update(pt); + + ourLog.info("Now have {} deleted", getQueryCount().getDelete()); + ourLog.info("Now have {} inserts", getQueryCount().getInsert()); + assertEquals(0, getQueryCount().getDelete()); + assertEquals(2, getQueryCount().getInsert()); + } + + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); 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 new file mode 100644 index 00000000000..9c3395469fc --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java @@ -0,0 +1,3343 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum; +import ca.uhn.fhir.jpa.entity.*; +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.*; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import ca.uhn.fhir.util.TestUtil; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.BundleType; +import org.hl7.fhir.r4.model.Bundle.HTTPVerb; +import org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem; +import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.r4.model.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType; +import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus; +import org.junit.*; +import org.springframework.transaction.TransactionStatus; +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.*; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +@SuppressWarnings({"unchecked", "Duplicates"}) +public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4SearchNoHashesTest.class); + + @After + public void afterResetSearchSize() { + myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis()); + myDaoConfig.setFetchSizeDefaultMaximum(new DaoConfig().getFetchSizeDefaultMaximum()); + myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches()); + myDaoConfig.setDisableHashBasedSearches(false); + } + + @Before + public void beforeInitialize() { + myDaoConfig.setReuseCachedSearchResultsForMillis(null); + myDaoConfig.setDisableHashBasedSearches(true); + } + + @Test + public void testChainWithMultipleTypePossibilities() { + + Patient sub1 = new Patient(); + sub1.setActive(true); + sub1.addIdentifier().setSystem("foo").setValue("bar"); + String sub1Id = myPatientDao.create(sub1).getId().toUnqualifiedVersionless().getValue(); + + Group sub2 = new Group(); + sub2.setActive(true); + sub2.addIdentifier().setSystem("foo").setValue("bar"); + String sub2Id = myGroupDao.create(sub2).getId().toUnqualifiedVersionless().getValue(); + + Encounter enc1 = new Encounter(); + enc1.getSubject().setReference(sub1Id); + String enc1Id = myEncounterDao.create(enc1).getId().toUnqualifiedVersionless().getValue(); + + Encounter enc2 = new Encounter(); + enc2.getSubject().setReference(sub2Id); + String enc2Id = myEncounterDao.create(enc2).getId().toUnqualifiedVersionless().getValue(); + + List ids; + SearchParameterMap map; + IBundleProvider results; + + map = new SearchParameterMap(); + map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject", "foo|bar").setChain("identifier")); + results = myEncounterDao.search(map); + ids = toUnqualifiedVersionlessIdValues(results); + assertThat(ids, hasItems(enc1Id, enc2Id)); + + map = new SearchParameterMap(); + map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject:Patient", "foo|bar").setChain("identifier")); + results = myEncounterDao.search(map); + ids = toUnqualifiedVersionlessIdValues(results); + assertThat(ids, hasItems(enc1Id)); + + map = new SearchParameterMap(); + map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject:Group", "foo|bar").setChain("identifier")); + results = myEncounterDao.search(map); + ids = toUnqualifiedVersionlessIdValues(results); + assertThat(ids, hasItems(enc2Id)); + + map = new SearchParameterMap(); + map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject", "04823543").setChain("identifier")); + results = myEncounterDao.search(map); + ids = toUnqualifiedVersionlessIdValues(results); + assertThat(ids, empty()); + } + + /** + * See #441 + */ + @Test + public void testChainedMedication() { + Medication medication = new Medication(); + medication.getCode().addCoding().setSystem("SYSTEM").setCode("04823543"); + IIdType medId = myMedicationDao.create(medication).getId().toUnqualifiedVersionless(); + + MedicationAdministration ma = new MedicationAdministration(); + ma.setMedication(new Reference(medId)); + IIdType moId = myMedicationAdministrationDao.create(ma).getId().toUnqualified(); + + SearchParameterMap map = new SearchParameterMap(); + map.add(MedicationAdministration.SP_MEDICATION, new ReferenceAndListParam().addAnd(new ReferenceOrListParam().add(new ReferenceParam("code", "04823543")))); + IBundleProvider results = myMedicationAdministrationDao.search(map); + List ids = toUnqualifiedIdValues(results); + + assertThat(ids, contains(moId.getValue())); + } + + @Test + public void testEmptyChain() { + + SearchParameterMap map = new SearchParameterMap(); + map.add(Encounter.SP_SUBJECT, new ReferenceAndListParam().addAnd(new ReferenceOrListParam().add(new ReferenceParam("subject", "04823543").setChain("identifier")))); + IBundleProvider results = myMedicationAdministrationDao.search(map); + List ids = toUnqualifiedIdValues(results); + + assertThat(ids, empty()); + } + + /** + * See #1053 + */ + @Test + public void testLastUpdateShouldntApplyToIncludes() { + SearchParameterMap map; + List ids; + + Date beforeAll = new Date(); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + + Organization org = new Organization(); + org.setName("O1"); + org.setId("O1"); + myOrganizationDao.update(org); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + + Date beforePatient = new Date(); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + + Patient p = new Patient(); + p.setId("P1"); + p.setActive(true); + p.setManagingOrganization(new Reference("Organization/O1")); + myPatientDao.update(p); + + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + Date afterAll = new Date(); + + // Search with between date (should still return Organization even though + // it was created before that date, since it's an include) + map = new SearchParameterMap(); + map.setLastUpdated(new DateRangeParam().setLowerBoundInclusive(beforePatient)); + map.addInclude(Patient.INCLUDE_ORGANIZATION); + ids = toUnqualifiedVersionlessIdValues(myPatientDao.search(map)); + assertThat(ids, contains("Patient/P1", "Organization/O1")); + + // Search before everything + map = new SearchParameterMap(); + map.setLastUpdated(new DateRangeParam().setLowerBoundInclusive(beforeAll)); + map.addInclude(Patient.INCLUDE_ORGANIZATION); + ids = toUnqualifiedVersionlessIdValues(myPatientDao.search(map)); + assertThat(ids, contains("Patient/P1", "Organization/O1")); + + // Search after everything + map = new SearchParameterMap(); + map.setLastUpdated(new DateRangeParam().setLowerBoundInclusive(afterAll)); + map.addInclude(Patient.INCLUDE_ORGANIZATION); + ids = toUnqualifiedVersionlessIdValues(myPatientDao.search(map)); + assertThat(ids, empty()); + + } + + /** + * See #1053 + *

+ * Note that I don't know that _lastUpdate actually should apply to reverse includes. The + * spec doesn't say one way or ther other, but it seems like sensible behaviour to me. + *

+ * Definitely the $everything operation depends on this behaviour, so if we change it + * we need to account for the everything operation... + */ + @Test + public void testLastUpdateShouldApplyToReverseIncludes() { + SearchParameterMap map; + List ids; + + // This gets updated in a sec.. + Organization org = new Organization(); + org.setActive(false); + org.setId("O1"); + myOrganizationDao.update(org); + + Date beforeAll = new Date(); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + + Patient p = new Patient(); + p.setId("P1"); + p.setActive(true); + p.setManagingOrganization(new Reference("Organization/O1")); + myPatientDao.update(p); + + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + + Date beforeOrg = new Date(); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + + org = new Organization(); + org.setActive(true); + org.setId("O1"); + myOrganizationDao.update(org); + + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + Date afterAll = new Date(); + + // Everything should come back + map = new SearchParameterMap(); + map.setLastUpdated(new DateRangeParam().setLowerBoundInclusive(beforeAll)); + map.addRevInclude(Patient.INCLUDE_ORGANIZATION); + ids = toUnqualifiedVersionlessIdValues(myOrganizationDao.search(map)); + assertThat(ids, contains("Organization/O1", "Patient/P1")); + + // Search before everything + map = new SearchParameterMap(); + map.setLastUpdated(new DateRangeParam().setLowerBoundInclusive(beforeOrg)); + map.addInclude(Patient.INCLUDE_ORGANIZATION); + ids = toUnqualifiedVersionlessIdValues(myOrganizationDao.search(map)); + assertThat(ids, contains("Organization/O1")); + + // Search after everything + map = new SearchParameterMap(); + map.setLastUpdated(new DateRangeParam().setLowerBoundInclusive(afterAll)); + map.addInclude(Patient.INCLUDE_ORGANIZATION); + ids = toUnqualifiedVersionlessIdValues(myOrganizationDao.search(map)); + assertThat(ids, empty()); + + } + + @Test + public void testEverythingTimings() { + String methodName = "testEverythingTimings"; + + Organization org = new Organization(); + org.setName(methodName); + IIdType orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + + Medication med = new Medication(); + med.getCode().setText(methodName); + IIdType medId = myMedicationDao.create(med, mySrd).getId().toUnqualifiedVersionless(); + + Patient pat = new Patient(); + pat.addAddress().addLine(methodName); + pat.getManagingOrganization().setReferenceElement(orgId); + IIdType patId = myPatientDao.create(pat, mySrd).getId().toUnqualifiedVersionless(); + + Patient pat2 = new Patient(); + pat2.addAddress().addLine(methodName + "2"); + pat2.getManagingOrganization().setReferenceElement(orgId); + IIdType patId2 = myPatientDao.create(pat2, mySrd).getId().toUnqualifiedVersionless(); + + MedicationRequest mo = new MedicationRequest(); + mo.getSubject().setReferenceElement(patId); + mo.setMedication(new Reference(medId)); + IIdType moId = myMedicationRequestDao.create(mo, mySrd).getId().toUnqualifiedVersionless(); + + HttpServletRequest request = mock(HttpServletRequest.class); + IBundleProvider resp = myPatientDao.patientTypeEverything(request, null, null, null, null, null, mySrd); + assertThat(toUnqualifiedVersionlessIds(resp), containsInAnyOrder(orgId, medId, patId, moId, patId2)); + + request = mock(HttpServletRequest.class); + resp = myPatientDao.patientInstanceEverything(request, patId, null, null, null, null, null, mySrd); + assertThat(toUnqualifiedVersionlessIds(resp), containsInAnyOrder(orgId, medId, patId, moId)); + } + + /** + * Per message from David Hay on Skype + */ + @Test + @Ignore + public void testEverythingWithLargeSet() throws Exception { + myFhirCtx.setParserErrorHandler(new StrictErrorHandler()); + + String inputString = IOUtils.toString(getClass().getResourceAsStream("/david_big_bundle.json"), StandardCharsets.UTF_8); + Bundle inputBundle = myFhirCtx.newJsonParser().parseResource(Bundle.class, inputString); + inputBundle.setType(BundleType.TRANSACTION); + + Set allIds = new TreeSet(); + for (BundleEntryComponent nextEntry : inputBundle.getEntry()) { + nextEntry.getRequest().setMethod(HTTPVerb.PUT); + nextEntry.getRequest().setUrl(nextEntry.getResource().getId()); + allIds.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue()); + } + + mySystemDao.transaction(mySrd, inputBundle); + + SearchParameterMap map = new SearchParameterMap(); + map.setEverythingMode(EverythingModeEnum.PATIENT_INSTANCE); + IPrimitiveType count = new IntegerType(1000); + IBundleProvider everything = myPatientDao.patientInstanceEverything(mySrd.getServletRequest(), new IdType("Patient/A161443"), count, null, null, null, null, mySrd); + + TreeSet ids = new TreeSet(toUnqualifiedVersionlessIdValues(everything)); + assertThat(ids, hasItem("List/A161444")); + assertThat(ids, hasItem("List/A161468")); + assertThat(ids, hasItem("List/A161500")); + + ourLog.info("Expected {} - {}", allIds.size(), allIds); + ourLog.info("Actual {} - {}", ids.size(), ids); + assertEquals(allIds, ids); + + ids = new TreeSet(); + for (int i = 0; i < everything.size(); i++) { + for (IBaseResource next : everything.getResources(i, i + 1)) { + ids.add(next.getIdElement().toUnqualifiedVersionless().getValue()); + } + } + assertThat(ids, hasItem("List/A161444")); + assertThat(ids, hasItem("List/A161468")); + assertThat(ids, hasItem("List/A161500")); + + ourLog.info("Expected {} - {}", allIds.size(), allIds); + ourLog.info("Actual {} - {}", ids.size(), ids); + assertEquals(allIds, ids); + + } + + @SuppressWarnings("unused") + @Test + public void testHasAndHas() { + Patient p1 = new Patient(); + p1.setActive(true); + IIdType p1id = myPatientDao.create(p1).getId().toUnqualifiedVersionless(); + + Patient p2 = new Patient(); + p2.setActive(true); + IIdType p2id = myPatientDao.create(p2).getId().toUnqualifiedVersionless(); + + Observation p1o1 = new Observation(); + p1o1.setStatus(ObservationStatus.FINAL); + p1o1.getSubject().setReferenceElement(p1id); + IIdType p1o1id = myObservationDao.create(p1o1).getId().toUnqualifiedVersionless(); + + Observation p1o2 = new Observation(); + p1o2.setEffective(new DateTimeType("2001-01-01")); + p1o2.getSubject().setReferenceElement(p1id); + IIdType p1o2id = myObservationDao.create(p1o2).getId().toUnqualifiedVersionless(); + + Observation p2o1 = new Observation(); + p2o1.setStatus(ObservationStatus.FINAL); + p2o1.getSubject().setReferenceElement(p2id); + IIdType p2o1id = myObservationDao.create(p2o1).getId().toUnqualifiedVersionless(); + + SearchParameterMap map = new SearchParameterMap(); + + HasAndListParam hasAnd = new HasAndListParam(); + hasAnd.addValue(new HasOrListParam().add(new HasParam("Observation", "subject", "status", "final"))); + hasAnd.addValue(new HasOrListParam().add(new HasParam("Observation", "subject", "date", "2001-01-01"))); + map.add("_has", hasAnd); + List actual = toUnqualifiedVersionlessIdValues(myPatientDao.search(map)); + assertThat(actual, containsInAnyOrder(p1id.getValue())); + + } + + @Test + public void testHasParameter() { + 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); + myObservationDao.create(obs, mySrd); + } + { + Device device = new Device(); + device.addIdentifier().setValue("DEVICEID"); + IIdType devId = myDeviceDao.create(device, mySrd).getId().toUnqualifiedVersionless(); + + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("NOLINK"); + obs.setDevice(new Reference(devId)); + myObservationDao.create(obs, mySrd); + } + + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|FOO")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), contains(pid0.getValue())); + + // No targets exist + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|UNKNOWN")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), empty()); + + // Target exists but doesn't link to us + params = new SearchParameterMap(); + params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|NOLINK")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), empty()); + } + + @Test + public void testHasParameterChained() { + IIdType pid0; + { + Device device = new Device(); + device.addIdentifier().setSystem("urn:system").setValue("DEVICEID"); + IIdType devId = myDeviceDao.create(device, mySrd).getId().toUnqualifiedVersionless(); + + Patient patient = new Patient(); + patient.setGender(AdministrativeGender.MALE); + pid0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("FOO"); + obs.setDevice(new Reference(devId)); + obs.setSubject(new Reference(pid0)); + myObservationDao.create(obs, mySrd).getId(); + } + + SearchParameterMap params; + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_has", new HasParam("Observation", "subject", "device.identifier", "urn:system|DEVICEID")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), contains(pid0.getValue())); + + // No targets exist + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|UNKNOWN")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), empty()); + + // Target exists but doesn't link to us + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|NOLINK")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), empty()); + } + + @Test + public void testHasParameterInvalidResourceType() { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_has", new HasParam("Observation__", "subject", "identifier", "urn:system|FOO")); + try { + myPatientDao.search(params); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Invalid resource type: Observation__", e.getMessage()); + } + } + + @Test + public void testHasParameterInvalidSearchParam() { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_has", new HasParam("Observation", "subject", "IIIIDENFIEYR", "urn:system|FOO")); + try { + myPatientDao.search(params); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Unknown parameter name: Observation:IIIIDENFIEYR", e.getMessage()); + } + } + + @Test + public void testHasParameterInvalidTargetPath() { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_has", new HasParam("Observation", "soooooobject", "identifier", "urn:system|FOO")); + try { + myPatientDao.search(params); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Unknown parameter name: Observation:soooooobject", e.getMessage()); + } + } + + @Test + public void testIncludeLinkedObservations() { + + DiagnosticReport dr = new DiagnosticReport(); + dr.setId("DiagnosticReport/DR"); + dr.setStatus(DiagnosticReport.DiagnosticReportStatus.FINAL); + + Observation parentObs = new Observation(); + parentObs.setStatus(ObservationStatus.FINAL); + parentObs.setId("Observation/parentObs"); + + Observation childObs = new Observation(); + childObs.setId("Observation/childObs"); + childObs.setStatus(ObservationStatus.FINAL); + + dr.addResult().setReference("Observation/parentObs").setResource(parentObs); + parentObs.addHasMember(new Reference(childObs).setReference("Observation/childObs")); + childObs.addDerivedFrom(new Reference(parentObs).setReference("Observation/parentObs")); + + Bundle input = new Bundle(); + input.setType(BundleType.TRANSACTION); + input.addEntry() + .setResource(dr) + .getRequest().setMethod(HTTPVerb.PUT).setUrl(dr.getId()); + input.addEntry() + .setResource(parentObs) + .getRequest().setMethod(HTTPVerb.PUT).setUrl(parentObs.getId()); + input.addEntry() + .setResource(childObs) + .getRequest().setMethod(HTTPVerb.PUT).setUrl(childObs.getId()); + mySystemDao.transaction(mySrd, input); + + SearchParameterMap params = new SearchParameterMap(); + params.add("_id", new TokenParam(null, "DR")); + params.addInclude(new Include("DiagnosticReport:subject").setRecurse(true)); + params.addInclude(new Include("DiagnosticReport:result").setRecurse(true)); + params.addInclude(Observation.INCLUDE_HAS_MEMBER.setRecurse(true)); + + IBundleProvider result = myDiagnosticReportDao.search(params); + List resultIds = toUnqualifiedVersionlessIdValues(result); + assertThat(resultIds, containsInAnyOrder("DiagnosticReport/DR", "Observation/parentObs", "Observation/childObs")); + + } + + @Test + public void testIndexNoDuplicatesDate() { + Encounter order = new Encounter(); + order.addLocation().getPeriod().setStartElement(new DateTimeType("2011-12-12T11:12:12Z")).setEndElement(new DateTimeType("2011-12-12T11:12:12Z")); + order.addLocation().getPeriod().setStartElement(new DateTimeType("2011-12-12T11:12:12Z")).setEndElement(new DateTimeType("2011-12-12T11:12:12Z")); + order.addLocation().getPeriod().setStartElement(new DateTimeType("2011-12-12T11:12:12Z")).setEndElement(new DateTimeType("2011-12-12T11:12:12Z")); + order.addLocation().getPeriod().setStartElement(new DateTimeType("2011-12-11T11:12:12Z")).setEndElement(new DateTimeType("2011-12-11T11:12:12Z")); + order.addLocation().getPeriod().setStartElement(new DateTimeType("2011-12-11T11:12:12Z")).setEndElement(new DateTimeType("2011-12-11T11:12:12Z")); + order.addLocation().getPeriod().setStartElement(new DateTimeType("2011-12-11T11:12:12Z")).setEndElement(new DateTimeType("2011-12-11T11:12:12Z")); + + IIdType id = myEncounterDao.create(order, mySrd).getId().toUnqualifiedVersionless(); + + List actual = toUnqualifiedVersionlessIds( + myEncounterDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Encounter.SP_LOCATION_PERIOD, new DateParam("2011-12-12T11:12:12Z")))); + assertThat(actual, contains(id)); + + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamDate.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i WHERE i.myMissing = false", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(2, results.size()); + }); + } + + @Test + public void testIndexNoDuplicatesNumber() { + final RiskAssessment res = new RiskAssessment(); + res.addPrediction().setProbability(new DecimalType("1.0")); + res.addPrediction().setProbability(new DecimalType("1.0")); + res.addPrediction().setProbability(new DecimalType("1.0")); + res.addPrediction().setProbability(new DecimalType("2.0")); + res.addPrediction().setProbability(new DecimalType("2.0")); + res.addPrediction().setProbability(new DecimalType("2.0")); + res.addPrediction().setProbability(new DecimalType("2.0")); + + IIdType id = myRiskAssessmentDao.create(res, mySrd).getId().toUnqualifiedVersionless(); + + List actual = toUnqualifiedVersionlessIds(myRiskAssessmentDao.search(new SearchParameterMap().setLoadSynchronous(true).add(RiskAssessment.SP_PROBABILITY, new NumberParam("1.0")))); + assertThat(actual, contains(id)); + actual = toUnqualifiedVersionlessIds(myRiskAssessmentDao.search(new SearchParameterMap().setLoadSynchronous(true).add(RiskAssessment.SP_PROBABILITY, new NumberParam("99.0")))); + assertThat(actual, empty()); + + new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + ResourceTable resource = myResourceTableDao.findAll().iterator().next(); + assertEquals("RiskAssessment", resource.getResourceType()); + + Class type = ResourceIndexedSearchParamNumber.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); + ourLog.info(toStringMultiline(results)); + + ResourceIndexedSearchParamNumber expect0 = new ResourceIndexedSearchParamNumber(RiskAssessment.SP_PROBABILITY, new BigDecimal("1.00")); + expect0.setResource(resource); + expect0.calculateHashes(); + ResourceIndexedSearchParamNumber expect1 = new ResourceIndexedSearchParamNumber(RiskAssessment.SP_PROBABILITY, new BigDecimal("2.00")); + expect1.setResource(resource); + expect1.calculateHashes(); + + assertThat("Got: \"" + results.toString()+"\"", results, containsInAnyOrder(expect0, expect1)); + } + }); + } + + @Test + public void testIndexNoDuplicatesQuantity() { + Substance res = new Substance(); + res.addInstance().getQuantity().setSystem("http://foo").setCode("UNIT").setValue(123); + res.addInstance().getQuantity().setSystem("http://foo").setCode("UNIT").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()); + }); + + 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 testIndexNoDuplicatesReference() { + ServiceRequest pr = new ServiceRequest(); + pr.setId("ServiceRequest/somepract"); + pr.getAuthoredOnElement().setValue(new Date()); + myServiceRequestDao.update(pr, mySrd); + ServiceRequest pr2 = new ServiceRequest(); + pr2.setId("ServiceRequest/somepract2"); + pr2.getAuthoredOnElement().setValue(new Date()); + myServiceRequestDao.update(pr2, mySrd); + + ServiceRequest res = new ServiceRequest(); + res.addReplaces(new Reference("ServiceRequest/somepract")); + res.addReplaces(new Reference("ServiceRequest/somepract")); + res.addReplaces(new Reference("ServiceRequest/somepract2")); + res.addReplaces(new Reference("ServiceRequest/somepract2")); + + final IIdType id = myServiceRequestDao.create(res, mySrd).getId().toUnqualifiedVersionless(); + + TransactionTemplate txTemplate = new TransactionTemplate(myTransactionMgr); + txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); + txTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus theArg0) { + Class type = ResourceLink.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(2, results.size()); + List actual = toUnqualifiedVersionlessIds( + myServiceRequestDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ServiceRequest.SP_REPLACES, new ReferenceParam("ServiceRequest/somepract")))); + assertThat(actual, contains(id)); + } + }); + + } + + @Test + public void testIndexNoDuplicatesString() { + Patient p = new Patient(); + p.addAddress().addLine("123 Fake Street"); + p.addAddress().addLine("123 Fake Street"); + p.addAddress().addLine("123 Fake Street"); + p.addAddress().addLine("456 Fake Street"); + p.addAddress().addLine("456 Fake Street"); + p.addAddress().addLine("456 Fake Street"); + + IIdType id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamString.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i WHERE i.myMissing = false", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(2, results.size()); + }); + + List actual = toUnqualifiedVersionlessIds(myPatientDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Patient.SP_ADDRESS, new StringParam("123 Fake Street")))); + assertThat(actual, contains(id)); + } + + @Test + public void testIndexNoDuplicatesToken() { + Patient res = new Patient(); + res.addIdentifier().setSystem("http://foo1").setValue("123"); + res.addIdentifier().setSystem("http://foo1").setValue("123"); + res.addIdentifier().setSystem("http://foo2").setValue("1234"); + res.addIdentifier().setSystem("http://foo2").setValue("1234"); + + IIdType id = myPatientDao.create(res, mySrd).getId().toUnqualifiedVersionless(); + + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamToken.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i WHERE i.myMissing = false", type).getResultList(); + ourLog.info(toStringMultiline(results)); + // This is 3 for now because the FluentPath for Patient:deceased adds a value.. this should + // be corrected at some point, and we'll then drop back down to 2 + assertEquals(3, results.size()); + }); + + + List actual = toUnqualifiedVersionlessIds(myPatientDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Patient.SP_IDENTIFIER, new TokenParam("http://foo1", "123")))); + assertThat(actual, contains(id)); + } + + @Test + public void testIndexNoDuplicatesUri() { + ValueSet res = new ValueSet(); + res.getCompose().addInclude().setSystem("http://foo"); + res.getCompose().addInclude().setSystem("http://bar"); + res.getCompose().addInclude().setSystem("http://foo"); + res.getCompose().addInclude().setSystem("http://bar"); + res.getCompose().addInclude().setSystem("http://foo"); + res.getCompose().addInclude().setSystem("http://bar"); + + IIdType id = myValueSetDao.create(res, mySrd).getId().toUnqualifiedVersionless(); + + runInTransaction(() -> { + Class type = ResourceIndexedSearchParamUri.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i WHERE i.myMissing = false", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(2, results.size()); + }); + + List actual = toUnqualifiedVersionlessIds(myValueSetDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_REFERENCE, new UriParam("http://foo")))); + assertThat(actual, contains(id)); + } + + /** + * #454 + */ + @Test + public void testIndexWithUtf8Chars() throws IOException { + String input = IOUtils.toString(getClass().getResourceAsStream("/bug454_utf8.json"), StandardCharsets.UTF_8); + + CodeSystem cs = (CodeSystem) myFhirCtx.newJsonParser().parseResource(input); + myCodeSystemDao.create(cs); + } + + @Test + public void testReturnOnlyCorrectResourceType() { + ValueSet vsRes = new ValueSet(); + vsRes.setUrl("http://foo"); + String vsId = myValueSetDao.create(vsRes).getId().toUnqualifiedVersionless().getValue(); + + CodeSystem csRes = new CodeSystem(); + csRes.setUrl("http://bar"); + String csId = myCodeSystemDao.create(csRes).getId().toUnqualifiedVersionless().getValue(); + + SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true); + map.add(ValueSet.SP_URL, new UriParam("http://foo")); + List actual = toUnqualifiedVersionlessIdValues(myValueSetDao.search(map)); + assertThat(actual, contains(vsId)); + + map = new SearchParameterMap().setLoadSynchronous(true); + map.add(ValueSet.SP_URL, new UriParam("http://bar")); + actual = toUnqualifiedVersionlessIdValues(myCodeSystemDao.search(map)); + assertThat(actual, contains(csId)); + } + + @Test + public void testSearchAll() { + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester").addGiven("Joe"); + myPatientDao.create(patient, mySrd); + } + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().setFamily("Tester").addGiven("John"); + myPatientDao.create(patient, mySrd); + } + + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + List patients = toList(myPatientDao.search(params)); + assertTrue(patients.size() >= 2); + } + + @Test + public void testSearchByIdParam() { + String id1; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless().getValue(); + } + String id2; + { + Organization patient = new Organization(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + id2 = myOrganizationDao.create(patient, mySrd).getId().toUnqualifiedVersionless().getValue(); + } + + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), contains(id1)); + + params = new SearchParameterMap(); + params.add("_id", new StringParam(id1)); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), contains(id1)); + + params = new SearchParameterMap(); + params.add("_id", new StringParam("9999999999999999")); + assertEquals(0, toList(myPatientDao.search(params)).size()); + + params = new SearchParameterMap(); + params.add("_id", new StringParam(id2)); + assertEquals(0, toList(myPatientDao.search(params)).size()); + + } + + @Test + public void testSearchByIdParamAnd() { + IIdType id1; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType id2; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + id2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + SearchParameterMap params; + StringAndListParam param; + + params = new SearchParameterMap(); + param = new StringAndListParam(); + param.addAnd(new StringOrListParam().addOr(new StringParam(id1.getIdPart())).addOr(new StringParam(id2.getIdPart()))); + param.addAnd(new StringOrListParam().addOr(new StringParam(id1.getIdPart()))); + params.add("_id", param); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1)); + + params = new SearchParameterMap(); + param = new StringAndListParam(); + param.addAnd(new StringOrListParam().addOr(new StringParam(id2.getIdPart()))); + param.addAnd(new StringOrListParam().addOr(new StringParam(id1.getIdPart()))); + params.add("_id", param); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), empty()); + + params = new SearchParameterMap(); + param = new StringAndListParam(); + param.addAnd(new StringOrListParam().addOr(new StringParam(id2.getIdPart()))); + param.addAnd(new StringOrListParam().addOr(new StringParam("9999999999999"))); + params.add("_id", param); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), empty()); + + params = new SearchParameterMap(); + param = new StringAndListParam(); + param.addAnd(new StringOrListParam().addOr(new StringParam("9999999999999"))); + param.addAnd(new StringOrListParam().addOr(new StringParam(id2.getIdPart()))); + params.add("_id", param); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), empty()); + + } + + @Test + public void testSearchByIdParamOr() { + IIdType id1; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + long betweenTime = System.currentTimeMillis(); + IIdType id2; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + id2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + SearchParameterMap params = new SearchParameterMap(); + params.add("_id", new StringOrListParam().addOr(new StringParam(id1.getIdPart())).addOr(new StringParam(id2.getIdPart()))); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1, id2)); + + params = new SearchParameterMap(); + params.add("_id", new StringOrListParam().addOr(new StringParam(id1.getIdPart())).addOr(new StringParam(id1.getIdPart()))); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1)); + + params = new SearchParameterMap(); + params.add("_id", new StringOrListParam().addOr(new StringParam(id1.getIdPart())).addOr(new StringParam("999999999999"))); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1)); + + // With lastupdated + + params = new SearchParameterMap(); + params.add("_id", new StringOrListParam().addOr(new StringParam(id1.getIdPart())).addOr(new StringParam(id2.getIdPart()))); + params.setLastUpdated(new DateRangeParam(new Date(betweenTime), null)); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id2)); + + } + + @Test + public void testSearchByIdParamWrongType() { + IIdType id1; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType id2; + { + Organization patient = new Organization(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + id2 = myOrganizationDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + SearchParameterMap params = new SearchParameterMap(); + params.add("_id", new StringOrListParam().addOr(new StringParam(id1.getIdPart())).addOr(new StringParam(id2.getIdPart()))); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1)); + + } + + @Test + public void testSearchCode() { + Subscription subs = new Subscription(); + subs.setStatus(SubscriptionStatus.ACTIVE); + subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET); + subs.setCriteria("Observation?"); + IIdType id = mySubscriptionDao.create(subs, mySrd).getId().toUnqualifiedVersionless(); + + SearchParameterMap params = new SearchParameterMap(); + assertThat(toUnqualifiedVersionlessIds(mySubscriptionDao.search(params)), contains(id)); + + params = new SearchParameterMap(); + params.add(Subscription.SP_TYPE, new TokenParam(null, SubscriptionChannelType.WEBSOCKET.toCode())); + params.add(Subscription.SP_STATUS, new TokenParam(null, SubscriptionStatus.ACTIVE.toCode())); + assertThat(toUnqualifiedVersionlessIds(mySubscriptionDao.search(params)), contains(id)); + + params = new SearchParameterMap(); + params.add(Subscription.SP_TYPE, new TokenParam(null, SubscriptionChannelType.WEBSOCKET.toCode())); + params.add(Subscription.SP_STATUS, new TokenParam(null, SubscriptionStatus.ACTIVE.toCode() + "2")); + assertThat(toUnqualifiedVersionlessIds(mySubscriptionDao.search(params)), empty()); + + // Wrong param + params = new SearchParameterMap(); + params.add(Subscription.SP_STATUS, new TokenParam(null, SubscriptionChannelType.WEBSOCKET.toCode())); + assertThat(toUnqualifiedVersionlessIds(mySubscriptionDao.search(params)), empty()); + } + + @Test + public void testSearchCompositeParam() { + Observation o1 = new Observation(); + o1.getCode().addCoding().setSystem("foo").setCode("testSearchCompositeParamN01"); + o1.setValue(new StringType("testSearchCompositeParamS01")); + IIdType id1 = myObservationDao.create(o1, mySrd).getId(); + + Observation o2 = new Observation(); + o2.getCode().addCoding().setSystem("foo").setCode("testSearchCompositeParamN01"); + o2.setValue(new StringType("testSearchCompositeParamS02")); + IIdType id2 = myObservationDao.create(o2, mySrd).getId(); + + { + TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamN01"); + StringParam v1 = new StringParam("testSearchCompositeParamS01"); + CompositeParam val = new CompositeParam(v0, v1); + IBundleProvider result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE_VALUE_STRING, val)); + assertEquals(1, result.size().intValue()); + assertEquals(id1.toUnqualifiedVersionless(), result.getResources(0, 1).get(0).getIdElement().toUnqualifiedVersionless()); + } + { + TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamN01"); + StringParam v1 = new StringParam("testSearchCompositeParamS02"); + CompositeParam val = new CompositeParam(v0, v1); + IBundleProvider result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE_VALUE_STRING, val)); + assertEquals(1, result.size().intValue()); + assertEquals(id2.toUnqualifiedVersionless(), result.getResources(0, 1).get(0).getIdElement().toUnqualifiedVersionless()); + } + } + + @Test + public void testSearchCompositeParamDate() { + Observation o1 = new Observation(); + o1.getCode().addCoding().setSystem("foo").setCode("testSearchCompositeParamDateN01"); + o1.setValue(new Period().setStartElement(new DateTimeType("2001-01-01T11:11:11Z")).setEndElement(new DateTimeType("2001-01-01T12:11:11Z"))); + IIdType id1 = myObservationDao.create(o1, mySrd).getId().toUnqualifiedVersionless(); + + Observation o2 = new Observation(); + o2.getCode().addCoding().setSystem("foo").setCode("testSearchCompositeParamDateN01"); + o2.setValue(new Period().setStartElement(new DateTimeType("2001-01-02T11:11:11Z")).setEndElement(new DateTimeType("2001-01-02T12:11:11Z"))); + IIdType id2 = myObservationDao.create(o2, mySrd).getId().toUnqualifiedVersionless(); + + { + TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamDateN01"); + DateParam v1 = new DateParam("2001-01-01"); + CompositeParam val = new CompositeParam(v0, v1); + IBundleProvider result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE_VALUE_DATE, val)); + assertThat(toUnqualifiedVersionlessIds(result), containsInAnyOrder(id1)); + } + { + TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamDateN01"); + DateParam v1 = new DateParam(">2001-01-01T10:12:12Z"); + CompositeParam val = new CompositeParam(v0, v1); + IBundleProvider result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE_VALUE_DATE, val)); + assertThat(toUnqualifiedVersionlessIds(result), containsInAnyOrder(id1, id2)); + } + { + TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamDateN01"); + DateParam v1 = new DateParam("gt2001-01-01T11:12:12Z"); + CompositeParam val = new CompositeParam(v0, v1); + IBundleProvider result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE_VALUE_DATE, val)); + assertThat(toUnqualifiedVersionlessIds(result), containsInAnyOrder(id1, id2)); + } + { + TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamDateN01"); + DateParam v1 = new DateParam("gt2001-01-01T15:12:12Z"); + CompositeParam val = new CompositeParam(v0, v1); + IBundleProvider result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE_VALUE_DATE, val)); + assertThat(toUnqualifiedVersionlessIds(result), containsInAnyOrder(id2)); + } + + } + + @Test + public void testComponentQuantity() { + Observation o1 = new Observation(); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code1"))) + .setValue(new Quantity().setSystem("http://bar").setCode("code1").setValue(200)); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code2"))) + .setValue(new Quantity().setSystem("http://bar").setCode("code2").setValue(200)); + IIdType id1 = myObservationDao.create(o1, mySrd).getId().toUnqualifiedVersionless(); + + String param = Observation.SP_COMPONENT_VALUE_QUANTITY; + + { + QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 150, "http://bar", "code1"); + 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(); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code1"))) + .setValue(new Quantity().setSystem("http://bar").setCode("code1").setValue(100)); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code2"))) + .setValue(new Quantity().setSystem("http://bar").setCode("code2").setValue(100)); + 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("http://bar").setCode("code1").setValue(200)); + o2.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code3"))) + .setValue(new Quantity().setSystem("http://bar").setCode("code2").setValue(200)); + 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, 150, "http://bar", "code1"); + 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, 50, "http://bar", "code1"); + 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, 50, "http://bar", "code1"); + 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, 50, "http://bar", "code4"); + 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(); + p1.getBirthDateElement().setValueAsString("1980-01-01"); + String id1 = myPatientDao.create(p1).getId().toUnqualifiedVersionless().getValue(); + + Patient p2 = new Patient(); + p2.setDeceased(new DateTimeType("1980-01-01")); + String id2 = myPatientDao.create(p2).getId().toUnqualifiedVersionless().getValue(); + + { + IBundleProvider found = myPatientDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Patient.SP_BIRTHDATE, new DateParam("1980-01-01"))); + assertThat(toUnqualifiedVersionlessIdValues(found), containsInAnyOrder(id1)); + assertEquals(1, found.size().intValue()); + } + { + IBundleProvider found = myPatientDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Patient.SP_DEATH_DATE, new DateParam("1980-01-01"))); + assertThat(toUnqualifiedVersionlessIdValues(found), containsInAnyOrder(id2)); + assertEquals(1, found.size().intValue()); + } + + } + + /** + * #222 + */ + @Test + public void testSearchForDeleted() { + + { + Patient patient = new Patient(); + patient.setId("TEST"); + patient.setLanguageElement(new CodeType("TEST")); + patient.addName().setFamily("TEST"); + patient.addIdentifier().setSystem("TEST").setValue("TEST"); + myPatientDao.update(patient, mySrd); + } + + SearchParameterMap params; + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_id", new StringParam("TEST")); + assertEquals(1, toList(myPatientDao.search(params)).size()); + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_language", new StringParam("TEST")); + assertEquals(1, toList(myPatientDao.search(params)).size()); + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_IDENTIFIER, new TokenParam("TEST", "TEST")); + assertEquals(1, toList(myPatientDao.search(params)).size()); + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_NAME, new StringParam("TEST")); + assertEquals(1, toList(myPatientDao.search(params)).size()); + + myPatientDao.delete(new IdType("Patient/TEST"), mySrd); + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_id", new StringParam("TEST")); + assertEquals(0, toList(myPatientDao.search(params)).size()); + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_language", new StringParam("TEST")); + assertEquals(0, toList(myPatientDao.search(params)).size()); + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_IDENTIFIER, new TokenParam("TEST", "TEST")); + assertEquals(0, toList(myPatientDao.search(params)).size()); + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_NAME, new StringParam("TEST")); + assertEquals(0, toList(myPatientDao.search(params)).size()); + + } + + @Test + public void testSearchForUnknownAlphanumericId() { + { + SearchParameterMap map = new SearchParameterMap(); + map.add("_id", new StringParam("testSearchForUnknownAlphanumericId")); + IBundleProvider retrieved = myPatientDao.search(map); + assertEquals(0, retrieved.size().intValue()); + } + } + + @Test + public void testSearchLanguageParam() { + IIdType id1; + { + Patient patient = new Patient(); + patient.getLanguageElement().setValue("en_CA"); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("testSearchLanguageParam").addGiven("Joe"); + id1 = myPatientDao.create(patient, mySrd).getId(); + } + IIdType id2; + { + Patient patient = new Patient(); + patient.getLanguageElement().setValue("en_US"); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().setFamily("testSearchLanguageParam").addGiven("John"); + id2 = myPatientDao.create(patient, mySrd).getId(); + } + SearchParameterMap params; + { + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + + params.add(IAnyResource.SP_RES_LANGUAGE, new StringParam("en_CA")); + List patients = toList(myPatientDao.search(params)); + assertEquals(1, patients.size()); + assertEquals(id1.toUnqualifiedVersionless(), patients.get(0).getIdElement().toUnqualifiedVersionless()); + } + { + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + + params.add(IAnyResource.SP_RES_LANGUAGE, new StringParam("en_US")); + List patients = toList(myPatientDao.search(params)); + assertEquals(1, patients.size()); + assertEquals(id2.toUnqualifiedVersionless(), patients.get(0).getIdElement().toUnqualifiedVersionless()); + } + { + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + + params.add(IAnyResource.SP_RES_LANGUAGE, new StringParam("en_GB")); + List patients = toList(myPatientDao.search(params)); + assertEquals(0, patients.size()); + } + } + + @Test + public void testSearchLanguageParamAndOr() { + IIdType id1; + { + Patient patient = new Patient(); + patient.getLanguageElement().setValue("en_CA"); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("testSearchLanguageParam").addGiven("Joe"); + id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + Date betweenTime = new Date(); + + IIdType id2; + { + Patient patient = new Patient(); + patient.getLanguageElement().setValue("en_US"); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().setFamily("testSearchLanguageParam").addGiven("John"); + id2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.add(IAnyResource.SP_RES_LANGUAGE, new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("en_US"))); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1, id2)); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.add(IAnyResource.SP_RES_LANGUAGE, new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("en_US"))); + params.setLastUpdated(new DateRangeParam(betweenTime, null)); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id2)); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.add(IAnyResource.SP_RES_LANGUAGE, new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("ZZZZ"))); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1)); + } + { + SearchParameterMap params = new SearchParameterMap(); + StringAndListParam and = new StringAndListParam(); + and.addAnd(new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("ZZZZ"))); + and.addAnd(new StringOrListParam().addOr(new StringParam("en_CA"))); + params.add(IAnyResource.SP_RES_LANGUAGE, and); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1)); + } + { + SearchParameterMap params = new SearchParameterMap(); + StringAndListParam and = new StringAndListParam(); + and.addAnd(new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("ZZZZ"))); + and.addAnd(new StringOrListParam().addOr(new StringParam("ZZZZZ"))); + params.add(IAnyResource.SP_RES_LANGUAGE, and); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), empty()); + } + { + SearchParameterMap params = new SearchParameterMap(); + StringAndListParam and = new StringAndListParam(); + and.addAnd(new StringOrListParam().addOr(new StringParam("ZZZZZ"))); + and.addAnd(new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("ZZZZ"))); + params.add(IAnyResource.SP_RES_LANGUAGE, and); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), empty()); + } + { + SearchParameterMap params = new SearchParameterMap(); + StringAndListParam and = new StringAndListParam(); + and.addAnd(new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("ZZZZ"))); + and.addAnd(new StringOrListParam().addOr(new StringParam("")).addOr(new StringParam(null))); + params.add(IAnyResource.SP_RES_LANGUAGE, and); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1)); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.add("_id", new StringParam(id1.getIdPart())); + StringAndListParam and = new StringAndListParam(); + and.addAnd(new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("ZZZZ"))); + and.addAnd(new StringOrListParam().addOr(new StringParam("")).addOr(new StringParam(null))); + params.add(IAnyResource.SP_RES_LANGUAGE, and); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1)); + } + { + SearchParameterMap params = new SearchParameterMap(); + StringAndListParam and = new StringAndListParam(); + and.addAnd(new StringOrListParam().addOr(new StringParam("en_CA")).addOr(new StringParam("ZZZZ"))); + and.addAnd(new StringOrListParam().addOr(new StringParam("")).addOr(new StringParam(null))); + params.add(IAnyResource.SP_RES_LANGUAGE, and); + params.add("_id", new StringParam(id1.getIdPart())); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(params)), containsInAnyOrder(id1)); + } + + } + + @Test + public void testSearchLastUpdatedParam() throws InterruptedException { + String methodName = "testSearchLastUpdatedParam"; + + int sleep = 100; + Thread.sleep(sleep); + + DateTimeType beforeAny = new DateTimeType(new Date(), TemporalPrecisionEnum.MILLI); + IIdType id1a; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily(methodName).addGiven("Joe"); + id1a = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType id1b; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().setFamily(methodName + "XXXX").addGiven("Joe"); + id1b = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + Thread.sleep(1100); + DateTimeType beforeR2 = new DateTimeType(new Date(), TemporalPrecisionEnum.MILLI); + Thread.sleep(1100); + + IIdType id2; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().setFamily(methodName).addGiven("John"); + id2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + { + SearchParameterMap params = new SearchParameterMap(); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, hasItems(id1a, id1b, id2)); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(beforeAny, null)); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, hasItems(id1a, id1b, id2)); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(beforeR2, null)); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, hasItems(id2)); + assertThat(patients, not(hasItems(id1a, id1b))); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(beforeAny, beforeR2)); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients.toString(), patients, not(hasItems(id2))); + assertThat(patients.toString(), patients, (hasItems(id1a, id1b))); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(null, beforeR2)); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, (hasItems(id1a, id1b))); + assertThat(patients, not(hasItems(id2))); + } + + + { + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, beforeR2))); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, not(hasItems(id1a, id1b))); + assertThat(patients, (hasItems(id2))); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, beforeR2))); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, (hasItems(id1a, id1b))); + assertThat(patients, not(hasItems(id2))); + } + + } + + @SuppressWarnings("deprecation") + @Test + public void testSearchLastUpdatedParamWithComparator() throws InterruptedException { + IIdType id0; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + id0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + int sleep = 100; + + long start = System.currentTimeMillis(); + Thread.sleep(sleep); + + IIdType id1a; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + id1a = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType id1b; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + id1b = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + ourLog.info("Res 1: {}", myPatientDao.read(id0, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); + ourLog.info("Res 2: {}", myPatientDao.read(id1a, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); + ourLog.info("Res 3: {}", myPatientDao.read(id1b, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); + + Thread.sleep(sleep); + long end = System.currentTimeMillis(); + + SearchParameterMap map; + Date startDate = new Date(start); + Date endDate = new Date(end); + DateTimeType startDateTime = new DateTimeType(startDate, TemporalPrecisionEnum.MILLI); + DateTimeType endDateTime = new DateTimeType(endDate, TemporalPrecisionEnum.MILLI); + + map = new SearchParameterMap(); + map.setLastUpdated(new DateRangeParam(startDateTime, endDateTime)); + ourLog.info("Searching: {}", map.getLastUpdated()); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), containsInAnyOrder(id1a, id1b)); + + map = new SearchParameterMap(); + map.setLastUpdated(new DateRangeParam(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, startDateTime), new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, endDateTime))); + ourLog.info("Searching: {}", map.getLastUpdated()); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), containsInAnyOrder(id1a, id1b)); + + map = new SearchParameterMap(); + map.setLastUpdated(new DateRangeParam(new DateParam(ParamPrefixEnum.GREATERTHAN, startDateTime), new DateParam(ParamPrefixEnum.LESSTHAN, endDateTime))); + ourLog.info("Searching: {}", map.getLastUpdated()); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), containsInAnyOrder(id1a, id1b)); + + map = new SearchParameterMap(); + map.setLastUpdated(new DateRangeParam(new DateParam(ParamPrefixEnum.GREATERTHAN, startDateTime.getValue()), + new DateParam(ParamPrefixEnum.LESSTHAN, myPatientDao.read(id1b, mySrd).getMeta().getLastUpdatedElement().getValue()))); + ourLog.info("Searching: {}", map.getLastUpdated()); + assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), containsInAnyOrder(id1a)); + } + + @Test + public void testSearchNameParam() { + IIdType id1; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("testSearchNameParam01Fam").addGiven("testSearchNameParam01Giv"); + id1 = myPatientDao.create(patient, mySrd).getId(); + } + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().setFamily("testSearchNameParam02Fam").addGiven("testSearchNameParam02Giv"); + myPatientDao.create(patient, mySrd); + } + + SearchParameterMap params; + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_FAMILY, new StringParam("testSearchNameParam01Fam")); + List patients = toList(myPatientDao.search(params)); + assertEquals(1, patients.size()); + assertEquals(id1.getIdPart(), patients.get(0).getIdElement().getIdPart()); + + // Given name shouldn't return for family param + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_FAMILY, new StringParam("testSearchNameParam01Giv")); + patients = toList(myPatientDao.search(params)); + assertEquals(0, patients.size()); + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_NAME, new StringParam("testSearchNameParam01Fam")); + patients = toList(myPatientDao.search(params)); + assertEquals(1, patients.size()); + assertEquals(id1.getIdPart(), patients.get(0).getIdElement().getIdPart()); + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_NAME, new StringParam("testSearchNameParam01Giv")); + patients = toList(myPatientDao.search(params)); + assertEquals(1, patients.size()); + assertEquals(id1.getIdPart(), patients.get(0).getIdElement().getIdPart()); + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_FAMILY, new StringParam("testSearchNameParam01Foo")); + patients = toList(myPatientDao.search(params)); + assertEquals(0, patients.size()); + + } + + /** + * TODO: currently this doesn't index, we should get it working + */ + @Test + public void testSearchNearParam() { + { + Location loc = new Location(); + loc.getPosition().setLatitude(43.7); + loc.getPosition().setLatitude(79.4); + myLocationDao.create(loc, mySrd); + } + } + + @Test + public void testSearchNumberParam() { + RiskAssessment e1 = new RiskAssessment(); + e1.addIdentifier().setSystem("foo").setValue("testSearchNumberParam01"); + e1.addPrediction().setProbability(new DecimalType(4 * 24 * 60)); + IIdType id1 = myRiskAssessmentDao.create(e1, mySrd).getId(); + + RiskAssessment e2 = new RiskAssessment(); + e2.addIdentifier().setSystem("foo").setValue("testSearchNumberParam02"); + e2.addPrediction().setProbability(new DecimalType(4)); + IIdType id2 = myRiskAssessmentDao.create(e2, mySrd).getId(); + { + IBundleProvider found = myRiskAssessmentDao.search(new SearchParameterMap().setLoadSynchronous(true).add(RiskAssessment.SP_PROBABILITY, new NumberParam(">2"))); + assertEquals(2, found.size().intValue()); + assertThat(toUnqualifiedVersionlessIds(found), containsInAnyOrder(id1.toUnqualifiedVersionless(), id2.toUnqualifiedVersionless())); + } + { + IBundleProvider found = myRiskAssessmentDao.search(new SearchParameterMap().setLoadSynchronous(true).add(RiskAssessment.SP_PROBABILITY, new NumberParam("<1"))); + assertEquals(0, found.size().intValue()); + } + { + IBundleProvider found = myRiskAssessmentDao.search(new SearchParameterMap().setLoadSynchronous(true).add(RiskAssessment.SP_PROBABILITY, new NumberParam("4"))); + assertEquals(1, found.size().intValue()); + assertThat(toUnqualifiedVersionlessIds(found), containsInAnyOrder(id2.toUnqualifiedVersionless())); + } + } + + @Test + public void testSearchNumberWrongParam() { + ImmunizationRecommendation ir1 = new ImmunizationRecommendation(); + ir1.addRecommendation().setDoseNumber(new PositiveIntType(1)); + String id1 = myImmunizationRecommendationDao.create(ir1).getId().toUnqualifiedVersionless().getValue(); + + ImmunizationRecommendation ir2 = new ImmunizationRecommendation(); + ir2.addRecommendation().setDoseNumber(new PositiveIntType(2)); + String id2 = myImmunizationRecommendationDao.create(ir2).getId().toUnqualifiedVersionless().getValue(); + + { + IBundleProvider found = myImmunizationRecommendationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ImmunizationRecommendation.SP_DOSE_NUMBER, new NumberParam("1"))); + assertThat(toUnqualifiedVersionlessIdValues(found), containsInAnyOrder(id1)); + assertEquals(1, found.size().intValue()); + } + { + IBundleProvider found = myImmunizationRecommendationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ImmunizationRecommendation.SP_DOSE_SEQUENCE, new NumberParam("1"))); + assertThat(toUnqualifiedVersionlessIdValues(found), empty()); + assertEquals(0, found.size().intValue()); + } + + } + + /** + * When a valueset expansion returns no codes + */ + @Test + public void testSearchOnCodesWithNone() { + ValueSet vs = new ValueSet(); + vs.setUrl("urn:testSearchOnCodesWithNone"); + myValueSetDao.create(vs); + + Patient p1 = new Patient(); + p1.setGender(AdministrativeGender.MALE); + String id1 = myPatientDao.create(p1).getId().toUnqualifiedVersionless().getValue(); + + Patient p2 = new Patient(); + p2.setGender(AdministrativeGender.FEMALE); + String id2 = myPatientDao.create(p2).getId().toUnqualifiedVersionless().getValue(); + + { + IBundleProvider found = myPatientDao + .search(new SearchParameterMap().setLoadSynchronous(true).add(Patient.SP_GENDER, new TokenParam().setModifier(TokenParamModifier.IN).setValue("urn:testSearchOnCodesWithNone"))); + assertThat(toUnqualifiedVersionlessIdValues(found), empty()); + assertEquals(0, found.size().intValue()); + } + + } + + @Test + public void testSearchParamChangesType() { + String name = "testSearchParamChangesType"; + IIdType id; + { + Patient patient = new Patient(); + patient.addName().setFamily(name); + id = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + SearchParameterMap params; + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_FAMILY, new StringParam(name)); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, contains(id)); + + Patient patient = new Patient(); + patient.addIdentifier().setSystem(name).setValue(name); + patient.setId(id); + myPatientDao.update(patient, mySrd); + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_FAMILY, new StringParam(name)); + patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, not(contains(id))); + + } + + @Test + public void testSearchPractitionerPhoneAndEmailParam() { + String methodName = "testSearchPractitionerPhoneAndEmailParam"; + IIdType id1; + { + Practitioner patient = new Practitioner(); + patient.addName().setFamily(methodName); + patient.addTelecom().setSystem(ContactPointSystem.PHONE).setValue("123"); + id1 = myPractitionerDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType id2; + { + Practitioner patient = new Practitioner(); + patient.addName().setFamily(methodName); + patient.addTelecom().setSystem(ContactPointSystem.EMAIL).setValue("abc"); + id2 = myPractitionerDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + SearchParameterMap params; + List patients; + + params = new SearchParameterMap(); + params.add(Practitioner.SP_FAMILY, new StringParam(methodName)); + params.add(Practitioner.SP_EMAIL, new TokenParam(null, "123")); + patients = toUnqualifiedVersionlessIds(myPractitionerDao.search(params)); + assertEquals(0, patients.size()); + + params = new SearchParameterMap(); + params.add(Practitioner.SP_FAMILY, new StringParam(methodName)); + patients = toUnqualifiedVersionlessIds(myPractitionerDao.search(params)); + assertEquals(2, patients.size()); + assertThat(patients, containsInAnyOrder(id1, id2)); + + params = new SearchParameterMap(); + params.add(Practitioner.SP_FAMILY, new StringParam(methodName)); + params.add(Practitioner.SP_EMAIL, new TokenParam(null, "abc")); + patients = toUnqualifiedVersionlessIds(myPractitionerDao.search(params)); + assertEquals(1, patients.size()); + assertThat(patients, containsInAnyOrder(id2)); + + params = new SearchParameterMap(); + params.add(Practitioner.SP_FAMILY, new StringParam(methodName)); + params.add(Practitioner.SP_PHONE, new TokenParam(null, "123")); + patients = toUnqualifiedVersionlessIds(myPractitionerDao.search(params)); + assertEquals(1, patients.size()); + assertThat(patients, containsInAnyOrder(id1)); + + } + + @Test + public void testSearchQuantityWrongParam() { + Condition c1 = new Condition(); + c1.setAbatement(new Range().setLow((SimpleQuantity) new SimpleQuantity().setValue(1L)).setHigh((SimpleQuantity) new SimpleQuantity().setValue(1L))); + String id1 = myConditionDao.create(c1).getId().toUnqualifiedVersionless().getValue(); + + Condition c2 = new Condition(); + c2.setOnset(new Range().setLow((SimpleQuantity) new SimpleQuantity().setValue(1L)).setHigh((SimpleQuantity) 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 testSearchResourceLinkWithChain() { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithChainXX"); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithChain01"); + IIdType patientId01 = myPatientDao.create(patient, mySrd).getId(); + + Patient patient02 = new Patient(); + patient02.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithChainXX"); + patient02.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithChain02"); + IIdType patientId02 = myPatientDao.create(patient02, mySrd).getId(); + + Observation obs01 = new Observation(); + obs01.setEffective(new DateTimeType(new Date())); + obs01.setSubject(new Reference(patientId01)); + IIdType obsId01 = myObservationDao.create(obs01, mySrd).getId(); + + Observation obs02 = new Observation(); + obs02.setEffective(new DateTimeType(new Date())); + obs02.setSubject(new Reference(patientId02)); + IIdType obsId02 = myObservationDao.create(obs02, mySrd).getId(); + + // Create another type, that shouldn't be returned + DiagnosticReport dr01 = new DiagnosticReport(); + dr01.setSubject(new Reference(patientId01)); + IIdType drId01 = myDiagnosticReportDao.create(dr01, mySrd).getId(); + + ourLog.info("P1[{}] P2[{}] O1[{}] O2[{}] D1[{}]", patientId01, patientId02, obsId01, obsId02, drId01); + + List result = toList(myObservationDao + .search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam(Patient.SP_IDENTIFIER, "urn:system|testSearchResourceLinkWithChain01")))); + assertEquals(1, result.size()); + assertEquals(obsId01.getIdPart(), result.get(0).getIdElement().getIdPart()); + + result = toList(myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_PATIENT, new ReferenceParam(patientId01.getIdPart())))); + assertEquals(1, result.size()); + + result = toList(myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_PATIENT, new ReferenceParam(patientId01.getIdPart())))); + assertEquals(1, result.size()); + + result = toList(myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam(Patient.SP_IDENTIFIER, "999999999999")))); + assertEquals(0, result.size()); + + result = toList(myObservationDao + .search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam(Patient.SP_IDENTIFIER, "urn:system|testSearchResourceLinkWithChainXX")))); + assertEquals(2, result.size()); + + result = toList( + myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam(Patient.SP_IDENTIFIER, "testSearchResourceLinkWithChainXX")))); + assertEquals(2, result.size()); + + result = toList( + myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam(Patient.SP_IDENTIFIER, "|testSearchResourceLinkWithChainXX")))); + assertEquals(0, result.size()); + + } + + @Test + public void testSearchResourceLinkWithChainDouble() { + String methodName = "testSearchResourceLinkWithChainDouble"; + + Organization org = new Organization(); + org.setName(methodName); + IIdType orgId01 = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + + Location locParent = new Location(); + locParent.setManagingOrganization(new Reference(orgId01)); + IIdType locParentId = myLocationDao.create(locParent, mySrd).getId().toUnqualifiedVersionless(); + + Location locChild = new Location(); + locChild.setPartOf(new Reference(locParentId)); + IIdType locChildId = myLocationDao.create(locChild, mySrd).getId().toUnqualifiedVersionless(); + + Location locGrandchild = new Location(); + locGrandchild.setPartOf(new Reference(locChildId)); + IIdType locGrandchildId = myLocationDao.create(locGrandchild, mySrd).getId().toUnqualifiedVersionless(); + + IBundleProvider found; + ReferenceParam param; + + found = myLocationDao.search(new SearchParameterMap().setLoadSynchronous(true).add("organization", new ReferenceParam(orgId01.getIdPart()))); + assertEquals(1, found.size().intValue()); + assertEquals(locParentId, found.getResources(0, 1).get(0).getIdElement().toUnqualifiedVersionless()); + + param = new ReferenceParam(orgId01.getIdPart()); + param.setChain("organization"); + found = myLocationDao.search(new SearchParameterMap().setLoadSynchronous(true).add("partof", param)); + assertEquals(1, found.size().intValue()); + assertEquals(locChildId, found.getResources(0, 1).get(0).getIdElement().toUnqualifiedVersionless()); + + param = new ReferenceParam(orgId01.getIdPart()); + param.setChain("partof.organization"); + found = myLocationDao.search(new SearchParameterMap().setLoadSynchronous(true).add("partof", param)); + assertEquals(1, found.size().intValue()); + assertEquals(locGrandchildId, found.getResources(0, 1).get(0).getIdElement().toUnqualifiedVersionless()); + + param = new ReferenceParam(methodName); + param.setChain("partof.organization.name"); + found = myLocationDao.search(new SearchParameterMap().setLoadSynchronous(true).add("partof", param)); + assertEquals(1, found.size().intValue()); + assertEquals(locGrandchildId, found.getResources(0, 1).get(0).getIdElement().toUnqualifiedVersionless()); + } + + @Test + public void testSearchResourceLinkWithChainWithMultipleTypes() throws Exception { + Patient patient = new Patient(); + patient.addName().setFamily("testSearchResourceLinkWithChainWithMultipleTypes01"); + patient.addName().setFamily("testSearchResourceLinkWithChainWithMultipleTypesXX"); + IIdType patientId01 = myPatientDao.create(patient, mySrd).getId(); + + Location loc01 = new Location(); + loc01.getNameElement().setValue("testSearchResourceLinkWithChainWithMultipleTypes01"); + IIdType locId01 = myLocationDao.create(loc01, mySrd).getId(); + + Observation obs01 = new Observation(); + obs01.setEffective(new DateTimeType(new Date())); + obs01.setSubject(new Reference(patientId01)); + IIdType obsId01 = myObservationDao.create(obs01, mySrd).getId().toUnqualifiedVersionless(); + + Date between = new Date(); + Thread.sleep(10); + + Observation obs02 = new Observation(); + obs02.setEffective(new DateTimeType(new Date())); + obs02.setSubject(new Reference(locId01)); + IIdType obsId02 = myObservationDao.create(obs02, mySrd).getId().toUnqualifiedVersionless(); + + Thread.sleep(10); + Date after = new Date(); + + ourLog.info("P1[{}] L1[{}] Obs1[{}] Obs2[{}]", patientId01, locId01, obsId01, obsId02); + + List result; + SearchParameterMap params; + + result = toUnqualifiedVersionlessIds(myObservationDao + .search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam(Patient.SP_NAME, "testSearchResourceLinkWithChainWithMultipleTypesXX")))); + assertThat(result, containsInAnyOrder(obsId01)); + assertEquals(1, result.size()); + + result = toUnqualifiedVersionlessIds(myObservationDao.search( + new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("Patient", Patient.SP_NAME, "testSearchResourceLinkWithChainWithMultipleTypes01")))); + assertThat(result, containsInAnyOrder(obsId01)); + assertEquals(1, result.size()); + + params = new SearchParameterMap(); + params.add(Observation.SP_SUBJECT, new ReferenceParam(Patient.SP_NAME, "testSearchResourceLinkWithChainWithMultipleTypes01")); + result = toUnqualifiedVersionlessIds(myObservationDao.search(params)); + assertEquals(2, result.size()); + assertThat(result, containsInAnyOrder(obsId01, obsId02)); + + params = new SearchParameterMap(); + params.add(Observation.SP_SUBJECT, new ReferenceParam(Patient.SP_NAME, "testSearchResourceLinkWithChainWithMultipleTypes01")); + params.setLastUpdated(new DateRangeParam(between, after)); + result = toUnqualifiedVersionlessIds(myObservationDao.search(params)); + assertEquals(1, result.size()); + assertThat(result, containsInAnyOrder(obsId02)); + + result = toUnqualifiedVersionlessIds(myObservationDao + .search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam(Patient.SP_NAME, "testSearchResourceLinkWithChainWithMultipleTypesYY")))); + assertEquals(0, result.size()); + + } + + @Test + public void testSearchResourceLinkWithTextLogicalId() { + Patient patient = new Patient(); + patient.setId("testSearchResourceLinkWithTextLogicalId01"); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithTextLogicalIdXX"); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithTextLogicalId01"); + IIdType patientId01 = myPatientDao.update(patient, mySrd).getId(); + + Patient patient02 = new Patient(); + patient02.setId("testSearchResourceLinkWithTextLogicalId02"); + patient02.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithTextLogicalIdXX"); + patient02.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithTextLogicalId02"); + IIdType patientId02 = myPatientDao.update(patient02, mySrd).getId(); + + Observation obs01 = new Observation(); + obs01.setEffective(new DateTimeType(new Date())); + obs01.setSubject(new Reference(patientId01)); + IIdType obsId01 = myObservationDao.create(obs01, mySrd).getId(); + + Observation obs02 = new Observation(); + obs02.setEffective(new DateTimeType(new Date())); + obs02.setSubject(new Reference(patientId02)); + IIdType obsId02 = myObservationDao.create(obs02, mySrd).getId(); + + // Create another type, that shouldn't be returned + DiagnosticReport dr01 = new DiagnosticReport(); + dr01.setSubject(new Reference(patientId01)); + IIdType drId01 = myDiagnosticReportDao.create(dr01, mySrd).getId(); + + ourLog.info("P1[{}] P2[{}] O1[{}] O2[{}] D1[{}]", patientId01, patientId02, obsId01, obsId02, drId01); + + List result = toList( + myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("testSearchResourceLinkWithTextLogicalId01")))); + assertEquals(1, result.size()); + assertEquals(obsId01.getIdPart(), result.get(0).getIdElement().getIdPart()); + + result = toList(myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("testSearchResourceLinkWithTextLogicalId99")))); + assertEquals(0, result.size()); + + result = toList(myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("999999999999999")))); + assertEquals(0, result.size()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchResourceReferenceOnlyCorrectPath() { + IIdType oid1; + { + Organization org = new Organization(); + org.setActive(true); + oid1 = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType tid1; + { + Task task = new Task(); + task.setRequester(new Reference(oid1)); + tid1 = myTaskDao.create(task, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType tid2; + { + Task task = new Task(); + task.setOwner(new Reference(oid1)); + tid2 = myTaskDao.create(task, mySrd).getId().toUnqualifiedVersionless(); + } + + SearchParameterMap map; + List ids; + + map = new SearchParameterMap(); + map.add(Task.SP_REQUESTER, new ReferenceParam(oid1.getValue())); + ids = toUnqualifiedVersionlessIds(myTaskDao.search(map)); + assertThat(ids, contains(tid1)); // NOT tid2 + + } + + @Test + public void testSearchStringParam() throws Exception { + IIdType pid1; + IIdType pid2; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester_testSearchStringParam").addGiven("Joe"); + pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + Date between = new Date(); + Thread.sleep(10); + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().setFamily("Tester_testSearchStringParam").addGiven("John"); + pid2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + Thread.sleep(10); + Date after = new Date(); + + SearchParameterMap params; + List patients; + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_testSearchStringParam")); + params.setLastUpdated(new DateRangeParam(between, after)); + patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, containsInAnyOrder(pid2)); + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_testSearchStringParam")); + patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, containsInAnyOrder(pid1, pid2)); + assertEquals(2, patients.size()); + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("FOO_testSearchStringParam")); + patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertEquals(0, patients.size()); + + // Try with different casing + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("tester_testsearchstringparam")); + patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, containsInAnyOrder(pid1, pid2)); + assertEquals(2, patients.size()); + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("TESTER_TESTSEARCHSTRINGPARAM")); + patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, containsInAnyOrder(pid1, pid2)); + assertEquals(2, patients.size()); + } + + @Test + public void testSearchStringParamDoesntMatchWrongType() { + IIdType pid1; + IIdType pid2; + { + Patient patient = new Patient(); + patient.addName().setFamily("HELLO"); + pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + { + Practitioner patient = new Practitioner(); + patient.addName().setFamily("HELLO"); + pid2 = myPractitionerDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + SearchParameterMap params; + List patients; + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("HELLO")); + patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, containsInAnyOrder(pid1)); + assertThat(patients, not(containsInAnyOrder(pid2))); + } + + @Test + public void testSearchStringParamReallyLong() { + String methodName = "testSearchStringParamReallyLong"; + String value = StringUtils.rightPad(methodName, 200, 'a'); + + IIdType longId; + IIdType shortId; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily(value); + longId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + shortId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + SearchParameterMap params; + + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + + String substring = value.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); + params.add(Patient.SP_FAMILY, new StringParam(substring)); + IBundleProvider found = myPatientDao.search(params); + assertEquals(1, toList(found).size()); + assertThat(toUnqualifiedVersionlessIds(found), contains(longId)); + assertThat(toUnqualifiedVersionlessIds(found), not(contains(shortId))); + + } + + @Test + public void testSearchStringParamWithNonNormalized() { + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().addGiven("testSearchStringParamWithNonNormalized_h\u00F6ra"); + myPatientDao.create(patient, mySrd); + } + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().addGiven("testSearchStringParamWithNonNormalized_HORA"); + myPatientDao.create(patient, mySrd); + } + + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_GIVEN, new StringParam("testSearchStringParamWithNonNormalized_hora")); + List patients = toList(myPatientDao.search(params)); + assertEquals(2, patients.size()); + + StringParam parameter = new StringParam("testSearchStringParamWithNonNormalized_hora"); + parameter.setExact(true); + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_GIVEN, parameter); + patients = toList(myPatientDao.search(params)); + assertEquals(0, patients.size()); + + } + + @Test + public void testSearchStringWrongParam() { + Patient p1 = new Patient(); + p1.getNameFirstRep().setFamily("AAA"); + String id1 = myPatientDao.create(p1).getId().toUnqualifiedVersionless().getValue(); + + Patient p2 = new Patient(); + p2.getNameFirstRep().addGiven("AAA"); + String id2 = myPatientDao.create(p2).getId().toUnqualifiedVersionless().getValue(); + + { + IBundleProvider found = myPatientDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Patient.SP_FAMILY, new StringParam("AAA"))); + assertThat(toUnqualifiedVersionlessIdValues(found), containsInAnyOrder(id1)); + assertEquals(1, found.size().intValue()); + } + { + IBundleProvider found = myPatientDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Patient.SP_GIVEN, new StringParam("AAA"))); + assertThat(toUnqualifiedVersionlessIdValues(found), containsInAnyOrder(id2)); + assertEquals(1, found.size().intValue()); + } + + } + + @Test + public void testSearchTokenParam() { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam001"); + patient.addName().setFamily("Tester").addGiven("testSearchTokenParam1"); + patient.addCommunication().getLanguage().setText("testSearchTokenParamComText").addCoding().setCode("testSearchTokenParamCode").setSystem("testSearchTokenParamSystem") + .setDisplay("testSearchTokenParamDisplay"); + myPatientDao.create(patient, mySrd); + + patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam002"); + patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2"); + myPatientDao.create(patient, mySrd); + + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", "testSearchTokenParam001")); + IBundleProvider retrieved = myPatientDao.search(map); + assertEquals(1, retrieved.size().intValue()); + } + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_IDENTIFIER, new TokenParam(null, "testSearchTokenParam001")); + IBundleProvider retrieved = myPatientDao.search(map); + assertEquals(1, retrieved.size().intValue()); + } + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_LANGUAGE, new TokenParam("testSearchTokenParamSystem", "testSearchTokenParamCode")); + assertEquals(1, myPatientDao.search(map).size().intValue()); + } + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_LANGUAGE, new TokenParam(null, "testSearchTokenParamCode", true)); + assertEquals(0, myPatientDao.search(map).size().intValue()); + } + { + // Complete match + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_LANGUAGE, new TokenParam(null, "testSearchTokenParamComText", true)); + assertEquals(1, myPatientDao.search(map).size().intValue()); + } + { + // Left match + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_LANGUAGE, new TokenParam(null, "testSearchTokenParamcomtex", true)); + assertEquals(1, myPatientDao.search(map).size().intValue()); + } + { + // Right match + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_LANGUAGE, new TokenParam(null, "testSearchTokenParamComTex", true)); + assertEquals(1, myPatientDao.search(map).size().intValue()); + } + { + SearchParameterMap map = new SearchParameterMap(); + TokenOrListParam listParam = new TokenOrListParam(); + listParam.add("urn:system", "testSearchTokenParam001"); + listParam.add("urn:system", "testSearchTokenParam002"); + map.add(Patient.SP_IDENTIFIER, listParam); + IBundleProvider retrieved = myPatientDao.search(map); + assertEquals(2, retrieved.size().intValue()); + } + { + SearchParameterMap map = new SearchParameterMap(); + TokenOrListParam listParam = new TokenOrListParam(); + listParam.add(null, "testSearchTokenParam001"); + listParam.add("urn:system", "testSearchTokenParam002"); + map.add(Patient.SP_IDENTIFIER, listParam); + IBundleProvider retrieved = myPatientDao.search(map); + assertEquals(2, retrieved.size().intValue()); + } + } + + @Test + public void testSearchTokenParamNoValue() { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam001"); + patient.addName().setFamily("Tester").addGiven("testSearchTokenParam1"); + patient.addCommunication().getLanguage().setText("testSearchTokenParamComText").addCoding().setCode("testSearchTokenParamCode").setSystem("testSearchTokenParamSystem") + .setDisplay("testSearchTokenParamDisplay"); + myPatientDao.create(patient, mySrd); + + patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam002"); + patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2"); + myPatientDao.create(patient, mySrd); + + patient = new Patient(); + patient.addIdentifier().setSystem("urn:system2").setValue("testSearchTokenParam002"); + patient.addName().setFamily("Tester").addGiven("testSearchTokenParam2"); + myPatientDao.create(patient, mySrd); + + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", null)); + IBundleProvider retrieved = myPatientDao.search(map); + assertEquals(2, retrieved.size().intValue()); + } + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", "")); + IBundleProvider retrieved = myPatientDao.search(map); + assertEquals(2, retrieved.size().intValue()); + } + } + + /** + * See #819 + */ + @Test + public void testSearchTokenWithNotModifier() { + String male, female; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester").addGiven("Joe"); + patient.setGender(AdministrativeGender.MALE); + male = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless().getValue(); + } + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().setFamily("Tester").addGiven("Jane"); + patient.setGender(AdministrativeGender.FEMALE); + female = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless().getValue(); + } + + List patients; + SearchParameterMap params; + + params = new SearchParameterMap(); + params.add(Patient.SP_GENDER, new TokenParam(null, "male")); + params.setLoadSynchronous(true); + patients = toUnqualifiedVersionlessIdValues(myPatientDao.search(params)); + assertThat(patients, contains(male)); + + params = new SearchParameterMap(); + params.add(Patient.SP_GENDER, new TokenParam(null, "male").setModifier(TokenParamModifier.NOT)); + params.setLoadSynchronous(true); + patients = toUnqualifiedVersionlessIdValues(myPatientDao.search(params)); + assertThat(patients, contains(female)); + } + + @Test + public void testSearchTokenWrongParam() { + Patient p1 = new Patient(); + p1.setGender(AdministrativeGender.MALE); + String id1 = myPatientDao.create(p1).getId().toUnqualifiedVersionless().getValue(); + + Patient p2 = new Patient(); + p2.addIdentifier().setValue(AdministrativeGender.MALE.toCode()); + String id2 = myPatientDao.create(p2).getId().toUnqualifiedVersionless().getValue(); + + { + IBundleProvider found = myPatientDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Patient.SP_GENDER, new TokenParam(null, "male"))); + assertThat(toUnqualifiedVersionlessIdValues(found), containsInAnyOrder(id1)); + assertEquals(1, found.size().intValue()); + } + { + IBundleProvider found = myPatientDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Patient.SP_IDENTIFIER, new TokenParam(null, "male"))); + assertThat(toUnqualifiedVersionlessIdValues(found), containsInAnyOrder(id2)); + assertEquals(1, found.size().intValue()); + } + + } + + @Test + @Ignore + public void testSearchUnknownContentParam() { + SearchParameterMap params = new SearchParameterMap(); + params.add(Constants.PARAM_CONTENT, new StringParam("fulltext")); + try { + myPatientDao.search(params); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Fulltext search is not enabled on this service, can not process parameter: _content", e.getMessage()); + } + } + + @Test + @Ignore + public void testSearchUnknownTextParam() { + SearchParameterMap params = new SearchParameterMap(); + params.add(Constants.PARAM_TEXT, new StringParam("fulltext")); + try { + myPatientDao.search(params); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Fulltext search is not enabled on this service, can not process parameter: _text", e.getMessage()); + } + } + + @Test + public void testSearchValueQuantity() { + String methodName = "testSearchValueQuantity"; + + String id1; + { + Observation o = new Observation(); + o.getCode().addCoding().setSystem("urn:foo").setCode(methodName + "code"); + Quantity q = new Quantity().setSystem("urn:bar:" + methodName).setCode(methodName + "units").setValue(100); + o.setValue(q); + id1 = myObservationDao.create(o, mySrd).getId().toUnqualifiedVersionless().getValue(); + } + + String id2; + { + Observation o = new Observation(); + o.getCode().addCoding().setSystem("urn:foo").setCode(methodName + "code"); + Quantity q = new Quantity().setSystem("urn:bar:" + methodName).setCode(methodName + "units").setValue(5); + o.setValue(q); + id2 = myObservationDao.create(o, mySrd).getId().toUnqualifiedVersionless().getValue(); + } + + SearchParameterMap map; + IBundleProvider found; + QuantityParam param; + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + param = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, new BigDecimal("10"), null, null); + map.add(Observation.SP_VALUE_QUANTITY, param); + found = myObservationDao.search(map); + assertThat(toUnqualifiedVersionlessIdValues(found), contains(id1)); + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + param = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, new BigDecimal("10"), null, methodName + "units"); + map.add(Observation.SP_VALUE_QUANTITY, param); + found = myObservationDao.search(map); + assertThat(toUnqualifiedVersionlessIdValues(found), contains(id1)); + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + param = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, new BigDecimal("10"), "urn:bar:" + methodName, null); + map.add(Observation.SP_VALUE_QUANTITY, param); + found = myObservationDao.search(map); + assertThat(toUnqualifiedVersionlessIdValues(found), contains(id1)); + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + param = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, new BigDecimal("10"), "urn:bar:" + methodName, methodName + "units"); + map.add(Observation.SP_VALUE_QUANTITY, param); + found = myObservationDao.search(map); + assertThat(toUnqualifiedVersionlessIdValues(found), contains(id1)); + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + param = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, new BigDecimal("1000"), "urn:bar:" + methodName, methodName + "units"); + map.add(Observation.SP_VALUE_QUANTITY, param); + found = myObservationDao.search(map); + assertThat(toUnqualifiedVersionlessIdValues(found), empty()); + + } + + @Test + public void testSearchWithContains() { + myDaoConfig.setAllowContainsSearches(true); + + Patient pt1 = new Patient(); + pt1.addName().setFamily("ABCDEFGHIJK"); + String pt1id = myPatientDao.create(pt1).getId().toUnqualifiedVersionless().getValue(); + + Patient pt2 = new Patient(); + pt2.addName().setFamily("FGHIJK"); + String pt2id = myPatientDao.create(pt2).getId().toUnqualifiedVersionless().getValue(); + + Patient pt3 = new Patient(); + pt3.addName().setFamily("ZZZZZ"); + myPatientDao.create(pt3).getId().toUnqualifiedVersionless().getValue(); + + + List ids; + SearchParameterMap map; + IBundleProvider results; + + // Contains = true + map = new SearchParameterMap(); + map.add(Patient.SP_NAME, new StringParam("FGHIJK").setContains(true)); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIdValues(results); + assertThat(ids, containsInAnyOrder(pt1id, pt2id)); + + // Contains = false + map = new SearchParameterMap(); + map.add(Patient.SP_NAME, new StringParam("FGHIJK").setContains(false)); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIdValues(results); + assertThat(ids, containsInAnyOrder(pt2id)); + + // No contains + map = new SearchParameterMap(); + map.add(Patient.SP_NAME, new StringParam("FGHIJK")); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIdValues(results); + assertThat(ids, containsInAnyOrder(pt2id)); + } + + @Test + public void testSearchWithContainsDisabled() { + myDaoConfig.setAllowContainsSearches(false); + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(Patient.SP_NAME, new StringParam("FGHIJK").setContains(true)); + + try { + myPatientDao.search(map); + fail(); + } catch (MethodNotAllowedException e) { + assertEquals(":contains modifier is disabled on this server", e.getMessage()); + } + } + + @Test + public void testSearchWithDate() { + IIdType orgId = myOrganizationDao.create(new Organization(), mySrd).getId(); + IIdType id2; + IIdType id1; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().setFamily("Tester_testSearchStringParam").addGiven("John"); + patient.setBirthDateElement(new DateType("2011-01-01")); + patient.getManagingOrganization().setReferenceElement(orgId); + id2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_BIRTHDATE, new DateParam("2011-01-01")); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, contains(id2)); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_BIRTHDATE, new DateParam("2011-01-03")); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, empty()); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_BIRTHDATE, new DateParam("2011-01-03").setPrefix(ParamPrefixEnum.LESSTHAN)); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, contains(id2)); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_BIRTHDATE, new DateParam("2010-01-01").setPrefix(ParamPrefixEnum.LESSTHAN)); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, empty()); + } + } + + @Test + public void testSearchWithFetchSizeDefaultMaximum() { + myDaoConfig.setFetchSizeDefaultMaximum(5); + + for (int i = 0; i < 10; i++) { + Patient p = new Patient(); + p.addName().setFamily("PT" + i); + myPatientDao.create(p); + } + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + IBundleProvider values = myPatientDao.search(map); + assertEquals(5, values.size().intValue()); + assertEquals(5, values.getResources(0, 1000).size()); + } + + @Test + public void testSearchWithIncludes() { + String methodName = "testSearchWithIncludes"; + IIdType parentOrgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1Parent"); + parentOrgId = myOrganizationDao.create(org, mySrd).getId(); + } + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1"); + org.setPartOf(new Reference(parentOrgId)); + IIdType orgId = myOrganizationDao.create(org, mySrd).getId(); + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester_" + methodName + "_P1").addGiven("Joe"); + patient.getManagingOrganization().setReferenceElement(orgId); + myPatientDao.create(patient, mySrd); + } + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().setFamily("Tester_" + methodName + "_P2").addGiven("John"); + myPatientDao.create(patient, mySrd); + } + + { + // No includes + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + List patients = toList(myPatientDao.search(params)); + assertEquals(1, patients.size()); + } + { + // Named include + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(Patient.INCLUDE_ORGANIZATION.asNonRecursive()); + IBundleProvider search = myPatientDao.search(params); + List patients = toList(search); + assertEquals(2, patients.size()); + assertEquals(Patient.class, patients.get(0).getClass()); + assertEquals(Organization.class, patients.get(1).getClass()); + } + { + // Named include with parent non-recursive + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(Patient.INCLUDE_ORGANIZATION); + params.addInclude(Organization.INCLUDE_PARTOF.asNonRecursive()); + IBundleProvider search = myPatientDao.search(params); + List patients = toList(search); + assertEquals(2, patients.size()); + assertEquals(Patient.class, patients.get(0).getClass()); + assertEquals(Organization.class, patients.get(1).getClass()); + } + { + // Named include with parent recursive + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(Patient.INCLUDE_ORGANIZATION); + params.addInclude(Organization.INCLUDE_PARTOF.asRecursive()); + IBundleProvider search = myPatientDao.search(params); + List patients = toList(search); + assertEquals(3, patients.size()); + assertEquals(Patient.class, patients.get(0).getClass()); + assertEquals(Organization.class, patients.get(1).getClass()); + assertEquals(Organization.class, patients.get(2).getClass()); + } + { + // * include non recursive + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(IBaseResource.INCLUDE_ALL.asNonRecursive()); + IBundleProvider search = myPatientDao.search(params); + List patients = toList(search); + assertEquals(2, patients.size()); + assertEquals(Patient.class, patients.get(0).getClass()); + assertEquals(Organization.class, patients.get(1).getClass()); + } + { + // * include recursive + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(IBaseResource.INCLUDE_ALL.asRecursive()); + IBundleProvider search = myPatientDao.search(params); + List patients = toList(search); + assertEquals(3, patients.size()); + assertEquals(Patient.class, patients.get(0).getClass()); + assertEquals(Organization.class, patients.get(1).getClass()); + assertEquals(Organization.class, patients.get(2).getClass()); + } + { + // Irrelevant include + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(Encounter.INCLUDE_EPISODE_OF_CARE); + IBundleProvider search = myPatientDao.search(params); + List patients = toList(search); + assertEquals(1, patients.size()); + assertEquals(Patient.class, patients.get(0).getClass()); + } + } + + @SuppressWarnings("unused") + @Test + public void testSearchWithIncludesParameterNoRecurse() { + String methodName = "testSearchWithIncludes"; + IIdType parentParentOrgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1Parent"); + parentParentOrgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType parentOrgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1Parent"); + org.setPartOf(new Reference(parentParentOrgId)); + parentOrgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType orgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1"); + org.setPartOf(new Reference(parentOrgId)); + orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType patientId; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester_" + methodName + "_P1").addGiven("Joe"); + patient.getManagingOrganization().setReferenceElement(orgId); + patientId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + { + SearchParameterMap params = new SearchParameterMap(); + params.add(IAnyResource.SP_RES_ID, new StringParam(orgId.getIdPart())); + params.addInclude(Organization.INCLUDE_PARTOF.asNonRecursive()); + List resources = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + assertThat(resources, contains(orgId, parentOrgId)); + } + } + + @SuppressWarnings("unused") + @Test + public void testSearchWithIncludesParameterRecurse() { + String methodName = "testSearchWithIncludes"; + IIdType parentParentOrgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1Parent"); + parentParentOrgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType parentOrgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1Parent"); + org.setPartOf(new Reference(parentParentOrgId)); + parentOrgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType orgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1"); + org.setPartOf(new Reference(parentOrgId)); + orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType patientId; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester_" + methodName + "_P1").addGiven("Joe"); + patient.getManagingOrganization().setReferenceElement(orgId); + patientId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + { + SearchParameterMap params = new SearchParameterMap(); + params.add(IAnyResource.SP_RES_ID, new StringParam(orgId.getIdPart())); + params.addInclude(Organization.INCLUDE_PARTOF.asRecursive()); + List resources = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + ourLog.info(resources.toString()); + assertThat(resources, contains(orgId, parentOrgId, parentParentOrgId)); + } + } + + @Test + public void testSearchWithIncludesStarNoRecurse() { + String methodName = "testSearchWithIncludes"; + IIdType parentParentOrgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1Parent"); + parentParentOrgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType parentOrgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1Parent"); + org.setPartOf(new Reference(parentParentOrgId)); + parentOrgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType orgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1"); + org.setPartOf(new Reference(parentOrgId)); + orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType patientId; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester_" + methodName + "_P1").addGiven("Joe"); + patient.getManagingOrganization().setReferenceElement(orgId); + patientId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + { + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(new Include("*").asNonRecursive()); + List resources = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(resources, contains(patientId, orgId)); + } + } + + @Test + public void testSearchWithIncludesStarRecurse() { + String methodName = "testSearchWithIncludes"; + IIdType parentParentOrgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1Parent"); + parentParentOrgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType parentOrgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1Parent"); + org.setPartOf(new Reference(parentParentOrgId)); + parentOrgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType orgId; + { + Organization org = new Organization(); + org.getNameElement().setValue(methodName + "_O1"); + org.setPartOf(new Reference(parentOrgId)); + orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType patientId; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester_" + methodName + "_P1").addGiven("Joe"); + patient.getManagingOrganization().setReferenceElement(orgId); + patientId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + { + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(new Include("*").asRecursive()); + List resources = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(resources, contains(patientId, orgId, parentOrgId, parentParentOrgId)); + } + } + + /** + * Test for #62 + */ + @Test + public void testSearchWithIncludesThatHaveTextId() { + { + Organization org = new Organization(); + org.setId("testSearchWithIncludesThatHaveTextIdid1"); + org.getNameElement().setValue("testSearchWithIncludesThatHaveTextId_O1"); + IIdType orgId = myOrganizationDao.update(org, mySrd).getId(); + assertThat(orgId.getValue(), endsWith("Organization/testSearchWithIncludesThatHaveTextIdid1/_history/1")); + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester_testSearchWithIncludesThatHaveTextId_P1").addGiven("Joe"); + patient.getManagingOrganization().setReferenceElement(orgId); + myPatientDao.create(patient, mySrd); + } + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("002"); + patient.addName().setFamily("Tester_testSearchWithIncludesThatHaveTextId_P2").addGiven("John"); + myPatientDao.create(patient, mySrd); + } + + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_testSearchWithIncludesThatHaveTextId_P1")); + params.addInclude(Patient.INCLUDE_ORGANIZATION); + IBundleProvider search = myPatientDao.search(params); + List patients = toList(search); + assertEquals(2, patients.size()); + assertEquals(Patient.class, patients.get(0).getClass()); + assertEquals(Organization.class, patients.get(1).getClass()); + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_testSearchWithIncludesThatHaveTextId_P1")); + patients = toList(myPatientDao.search(params)); + assertEquals(1, patients.size()); + + } + + @Test + public void testSearchWithNoResults() { + Device dev = new Device(); + dev.addIdentifier().setSystem("Foo"); + myDeviceDao.create(dev, mySrd); + + IBundleProvider value = myDeviceDao.search(new SearchParameterMap()); + ourLog.info("Initial size: " + value.size()); + for (IBaseResource next : value.getResources(0, value.size())) { + ourLog.info("Deleting: {}", next.getIdElement()); + myDeviceDao.delete(next.getIdElement(), mySrd); + } + + value = myDeviceDao.search(new SearchParameterMap()); + if (value.size() > 0) { + ourLog.info("Found: " + (value.getResources(0, 1).get(0).getIdElement())); + fail(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(value.getResources(0, 1).get(0))); + } + assertEquals(0, value.size().intValue()); + + List res = value.getResources(0, 0); + assertTrue(res.isEmpty()); + + } + + @Test + public void testSearchWithRevIncludes() { + final String methodName = "testSearchWithRevIncludes"; + TransactionTemplate txTemplate = new TransactionTemplate(myTransactionMgr); + txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); + IIdType pid = txTemplate.execute(new TransactionCallback() { + + @Override + public IIdType doInTransaction(TransactionStatus theStatus) { + Patient p = new Patient(); + p.addName().setFamily(methodName); + IIdType pid = myPatientDao.create(p).getId().toUnqualifiedVersionless(); + + Condition c = new Condition(); + c.getSubject().setReferenceElement(pid); + myConditionDao.create(c); + + return pid; + } + }); + + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_RES_ID, new StringParam(pid.getIdPart())); + map.addRevInclude(Condition.INCLUDE_PATIENT); + IBundleProvider results = myPatientDao.search(map); + List foundResources = results.getResources(0, results.size()); + assertEquals(Patient.class, foundResources.get(0).getClass()); + assertEquals(Condition.class, foundResources.get(1).getClass()); + } + + @Test + public void testSearchWithSecurityAndProfileParams() { + String methodName = "testSearchWithSecurityAndProfileParams"; + + IIdType tag1id; + { + Organization org = new Organization(); + org.getNameElement().setValue("FOO"); + org.getMeta().addSecurity("urn:taglist", methodName + "1a", null); + tag1id = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType tag2id; + { + Organization org = new Organization(); + org.getNameElement().setValue("FOO"); + org.getMeta().addProfile("http://" + methodName); + tag2id = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.add("_security", new TokenParam("urn:taglist", methodName + "1a")); + List patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + assertThat(patients, containsInAnyOrder(tag1id)); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.add("_profile", new UriParam("http://" + methodName)); + List patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + assertThat(patients, containsInAnyOrder(tag2id)); + } + } + + @Test + public void testSearchWithTagParameter() { + String methodName = "testSearchWithTagParameter"; + + IIdType tag1id; + { + Organization org = new Organization(); + org.getNameElement().setValue("FOO"); + org.getMeta().addTag("urn:taglist", methodName + "1a", null); + org.getMeta().addTag("urn:taglist", methodName + "1b", null); + tag1id = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + + Date betweenDate = new Date(); + + IIdType tag2id; + { + Organization org = new Organization(); + org.getNameElement().setValue("FOO"); + org.getMeta().addTag("urn:taglist", methodName + "2a", null); + org.getMeta().addTag("urn:taglist", methodName + "2b", null); + tag2id = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + + { + // One tag + SearchParameterMap params = new SearchParameterMap(); + params.add("_tag", new TokenParam("urn:taglist", methodName + "1a")); + List patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + assertThat(patients, containsInAnyOrder(tag1id)); + } + { + // Code only + SearchParameterMap params = new SearchParameterMap(); + params.add("_tag", new TokenParam(null, methodName + "1a")); + List patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + assertThat(patients, containsInAnyOrder(tag1id)); + } + { + // Or tags + SearchParameterMap params = new SearchParameterMap(); + TokenOrListParam orListParam = new TokenOrListParam(); + orListParam.add(new TokenParam("urn:taglist", methodName + "1a")); + orListParam.add(new TokenParam("urn:taglist", methodName + "2a")); + params.add("_tag", orListParam); + List patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + assertThat(patients, containsInAnyOrder(tag1id, tag2id)); + } + { + // Or tags with lastupdated + SearchParameterMap params = new SearchParameterMap(); + TokenOrListParam orListParam = new TokenOrListParam(); + orListParam.add(new TokenParam("urn:taglist", methodName + "1a")); + orListParam.add(new TokenParam("urn:taglist", methodName + "2a")); + params.add("_tag", orListParam); + params.setLastUpdated(new DateRangeParam(betweenDate, null)); + List patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + assertThat(patients, containsInAnyOrder(tag2id)); + } + // TODO: get multiple/AND working + { + // And tags + SearchParameterMap params = new SearchParameterMap(); + TokenAndListParam andListParam = new TokenAndListParam(); + andListParam.addValue(new TokenOrListParam("urn:taglist", methodName + "1a")); + andListParam.addValue(new TokenOrListParam("urn:taglist", methodName + "2a")); + params.add("_tag", andListParam); + List patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + assertEquals(0, patients.size()); + } + + { + // And tags + SearchParameterMap params = new SearchParameterMap(); + TokenAndListParam andListParam = new TokenAndListParam(); + andListParam.addValue(new TokenOrListParam("urn:taglist", methodName + "1a")); + andListParam.addValue(new TokenOrListParam("urn:taglist", methodName + "1b")); + params.add("_tag", andListParam); + List patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + assertThat(patients, containsInAnyOrder(tag1id)); + } + + } + + @Test + public void testSearchWithTagParameterMissing() { + String methodName = "testSearchWithTagParameterMissing"; + + IIdType tag1id; + { + Organization org = new Organization(); + org.getNameElement().setValue("FOO"); + org.getMeta().addTag("urn:taglist", methodName + "1a", null); + org.getMeta().addTag("urn:taglist", methodName + "1b", null); + tag1id = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + + IIdType tag2id; + { + Organization org = new Organization(); + org.getNameElement().setValue("FOO"); + org.getMeta().addTag("urn:taglist", methodName + "1b", null); + tag2id = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + } + + { + // One tag + SearchParameterMap params = new SearchParameterMap(); + params.add("_tag", new TokenParam("urn:taglist", methodName + "1a").setModifier(TokenParamModifier.NOT)); + List patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + assertThat(patients, containsInAnyOrder(tag2id)); + assertThat(patients, not(containsInAnyOrder(tag1id))); + } + { + // Non existant tag + SearchParameterMap params = new SearchParameterMap(); + params.add("_tag", new TokenParam("urn:taglist", methodName + "FOO").setModifier(TokenParamModifier.NOT)); + List patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + assertThat(patients, containsInAnyOrder(tag1id, tag2id)); + } + { + // Common tag + SearchParameterMap params = new SearchParameterMap(); + params.add("_tag", new TokenParam("urn:taglist", methodName + "1b").setModifier(TokenParamModifier.NOT)); + List patients = toUnqualifiedVersionlessIds(myOrganizationDao.search(params)); + assertThat(patients, empty()); + } + } + + /** + * https://chat.fhir.org/#narrow/stream/implementers/topic/Understanding.20_include + */ + @Test + public void testSearchWithTypedInclude() { + IIdType patId; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + IIdType practId; + { + Practitioner pract = new Practitioner(); + pract.addIdentifier().setSystem("urn:system").setValue("001"); + practId = myPractitionerDao.create(pract, mySrd).getId().toUnqualifiedVersionless(); + } + + Appointment appt = new Appointment(); + appt.addParticipant().getActor().setReference(patId.getValue()); + appt.addParticipant().getActor().setReference(practId.getValue()); + IIdType apptId = myAppointmentDao.create(appt, mySrd).getId().toUnqualifiedVersionless(); + + SearchParameterMap params = new SearchParameterMap(); + params.addInclude(Appointment.INCLUDE_PATIENT); + assertThat(toUnqualifiedVersionlessIds(myAppointmentDao.search(params)), containsInAnyOrder(patId, apptId)); + + } + + @Test + public void testSearchWithUriParam() throws Exception { + Class type = ValueSet.class; + String resourceName = "/valueset-dstu2.json"; + ValueSet vs = loadResourceFromClasspath(type, resourceName); + IIdType id1 = myValueSetDao.update(vs, mySrd).getId().toUnqualifiedVersionless(); + + ValueSet vs2 = new ValueSet(); + vs2.setUrl("http://hl7.org/foo/bar"); + myValueSetDao.create(vs2, mySrd).getId().toUnqualifiedVersionless(); + + IBundleProvider result; + result = myValueSetDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://hl7.org/fhir/ValueSet/basic-resource-type"))); + assertThat(toUnqualifiedVersionlessIds(result), contains(id1)); + + result = myValueSetDao + .search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://hl7.org/fhir/ValueSet/basic-resource-type").setQualifier(UriParamQualifierEnum.BELOW))); + assertThat(toUnqualifiedVersionlessIds(result), contains(id1)); + + result = myValueSetDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://hl7.org/fhir/ValueSet/").setQualifier(UriParamQualifierEnum.BELOW))); + assertThat(toUnqualifiedVersionlessIds(result), contains(id1)); + + result = myValueSetDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://hl7.org/fhir/ValueSet/FOOOOOO"))); + assertThat(toUnqualifiedVersionlessIds(result), empty()); + + } + + @Test + public void testSearchWithUriParamAbove() { + ValueSet vs1 = new ValueSet(); + vs1.setUrl("http://hl7.org/foo/baz"); + myValueSetDao.create(vs1, mySrd).getId().toUnqualifiedVersionless(); + + ValueSet vs2 = new ValueSet(); + vs2.setUrl("http://hl7.org/foo/bar"); + IIdType id2 = myValueSetDao.create(vs2, mySrd).getId().toUnqualifiedVersionless(); + + ValueSet vs3 = new ValueSet(); + vs3.setUrl("http://hl7.org/foo/bar/baz"); + IIdType id3 = myValueSetDao.create(vs3, mySrd).getId().toUnqualifiedVersionless(); + + IBundleProvider result; + result = myValueSetDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://hl7.org/foo/bar/baz/boz").setQualifier(UriParamQualifierEnum.ABOVE))); + assertThat(toUnqualifiedVersionlessIds(result), containsInAnyOrder(id2, id3)); + + result = myValueSetDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://hl7.org/foo/bar/baz").setQualifier(UriParamQualifierEnum.ABOVE))); + assertThat(toUnqualifiedVersionlessIds(result), containsInAnyOrder(id2, id3)); + + result = myValueSetDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://hl7.org/foo/bar").setQualifier(UriParamQualifierEnum.ABOVE))); + assertThat(toUnqualifiedVersionlessIds(result), containsInAnyOrder(id2)); + + result = myValueSetDao + .search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://hl7.org/fhir/ValueSet/basic-resource-type").setQualifier(UriParamQualifierEnum.ABOVE))); + assertThat(toUnqualifiedVersionlessIds(result), empty()); + + result = myValueSetDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://hl7.org").setQualifier(UriParamQualifierEnum.ABOVE))); + assertThat(toUnqualifiedVersionlessIds(result), empty()); + } + + @Test + public void testSearchWithUriParamBelow() throws Exception { + myFhirCtx.setParserErrorHandler(new StrictErrorHandler()); + + Class type = ValueSet.class; + String resourceName = "/valueset-dstu2.json"; + ValueSet vs = loadResourceFromClasspath(type, resourceName); + IIdType id1 = myValueSetDao.update(vs, mySrd).getId().toUnqualifiedVersionless(); + + ValueSet vs2 = new ValueSet(); + vs2.setUrl("http://hl7.org/foo/bar"); + IIdType id2 = myValueSetDao.create(vs2, mySrd).getId().toUnqualifiedVersionless(); + + IBundleProvider result; + + result = myValueSetDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://").setQualifier(UriParamQualifierEnum.BELOW))); + assertThat(toUnqualifiedVersionlessIds(result), containsInAnyOrder(id1, id2)); + + result = myValueSetDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://hl7.org").setQualifier(UriParamQualifierEnum.BELOW))); + assertThat(toUnqualifiedVersionlessIds(result), containsInAnyOrder(id1, id2)); + + result = myValueSetDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://hl7.org/foo").setQualifier(UriParamQualifierEnum.BELOW))); + assertThat(toUnqualifiedVersionlessIds(result), containsInAnyOrder(id2)); + + result = myValueSetDao.search(new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://hl7.org/foo/baz").setQualifier(UriParamQualifierEnum.BELOW))); + assertThat(toUnqualifiedVersionlessIds(result), containsInAnyOrder()); + } + + /** + * See #744 + */ + @Test + public void testSearchWithVeryLongUrlLonger() { + myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis()); + + Patient p = new Patient(); + p.addName().setFamily("A1"); + myPatientDao.create(p); + + assertEquals(0, mySearchEntityDao.count()); + + SearchParameterMap map = new SearchParameterMap(); + StringOrListParam or = new StringOrListParam(); + or.addOr(new StringParam("A1")); + for (int i = 0; i < 50; i++) { + or.addOr(new StringParam(StringUtils.leftPad("", 200, (char) ('A' + i)))); + } + map.add(Patient.SP_NAME, or); + IBundleProvider results = myPatientDao.search(map); + assertEquals(1, results.getResources(0, 10).size()); + assertEquals(1, mySearchEntityDao.count()); + + map = new SearchParameterMap(); + or = new StringOrListParam(); + or.addOr(new StringParam("A1")); + or.addOr(new StringParam("A1")); + for (int i = 0; i < 50; i++) { + or.addOr(new StringParam(StringUtils.leftPad("", 200, (char) ('A' + i)))); + } + map.add(Patient.SP_NAME, or); + results = myPatientDao.search(map); + assertEquals(1, results.getResources(0, 10).size()); + // We expect a new one because we don't cache the search URL for very long search URLs + assertEquals(2, mySearchEntityDao.count()); + + } + + @Test + public void testDateSearchParametersShouldBeTimezoneIndependent() { + + createObservationWithEffective("NO1", "2011-01-02T23:00:00-11:30"); + createObservationWithEffective("NO2", "2011-01-03T00:00:00+01:00"); + + createObservationWithEffective("YES01", "2011-01-02T00:00:00-11:30"); + createObservationWithEffective("YES02", "2011-01-02T00:00:00-10:00"); + createObservationWithEffective("YES03", "2011-01-02T00:00:00-09:00"); + createObservationWithEffective("YES04", "2011-01-02T00:00:00-08:00"); + createObservationWithEffective("YES05", "2011-01-02T00:00:00-07:00"); + createObservationWithEffective("YES06", "2011-01-02T00:00:00-06:00"); + createObservationWithEffective("YES07", "2011-01-02T00:00:00-05:00"); + createObservationWithEffective("YES08", "2011-01-02T00:00:00-04:00"); + createObservationWithEffective("YES09", "2011-01-02T00:00:00-03:00"); + createObservationWithEffective("YES10", "2011-01-02T00:00:00-02:00"); + createObservationWithEffective("YES11", "2011-01-02T00:00:00-01:00"); + createObservationWithEffective("YES12", "2011-01-02T00:00:00Z"); + createObservationWithEffective("YES13", "2011-01-02T00:00:00+01:00"); + createObservationWithEffective("YES14", "2011-01-02T00:00:00+02:00"); + createObservationWithEffective("YES15", "2011-01-02T00:00:00+03:00"); + createObservationWithEffective("YES16", "2011-01-02T00:00:00+04:00"); + createObservationWithEffective("YES17", "2011-01-02T00:00:00+05:00"); + createObservationWithEffective("YES18", "2011-01-02T00:00:00+06:00"); + createObservationWithEffective("YES19", "2011-01-02T00:00:00+07:00"); + createObservationWithEffective("YES20", "2011-01-02T00:00:00+08:00"); + createObservationWithEffective("YES21", "2011-01-02T00:00:00+09:00"); + createObservationWithEffective("YES22", "2011-01-02T00:00:00+10:00"); + createObservationWithEffective("YES23", "2011-01-02T00:00:00+11:00"); + + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(Observation.SP_DATE, new DateParam("2011-01-02")); + IBundleProvider results = myObservationDao.search(map); + List values = toUnqualifiedVersionlessIdValues(results); + Collections.sort(values); + assertThat(values.toString(), values, contains( + "Observation/YES01", + "Observation/YES02", + "Observation/YES03", + "Observation/YES04", + "Observation/YES05", + "Observation/YES06", + "Observation/YES07", + "Observation/YES08", + "Observation/YES09", + "Observation/YES10", + "Observation/YES11", + "Observation/YES12", + "Observation/YES13", + "Observation/YES14", + "Observation/YES15", + "Observation/YES16", + "Observation/YES17", + "Observation/YES18", + "Observation/YES19", + "Observation/YES20", + "Observation/YES21", + "Observation/YES22", + "Observation/YES23" + )); + } + + private void createObservationWithEffective(String theId, String theEffective) { + Observation obs = new Observation(); + obs.setId(theId); + obs.setEffective(new DateTimeType(theEffective)); + myObservationDao.update(obs); + + ourLog.info("Obs {} has time {}", theId, obs.getEffectiveDateTimeType().getValue().toString()); + } + + /** + * See #744 + */ + @Test + public void testSearchWithVeryLongUrlShorter() { + myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis()); + + Patient p = new Patient(); + p.addName().setFamily("A1"); + myPatientDao.create(p); + + assertEquals(0, mySearchEntityDao.count()); + + SearchParameterMap map = new SearchParameterMap(); + StringOrListParam or = new StringOrListParam(); + or.addOr(new StringParam("A1")); + or.addOr(new StringParam(StringUtils.leftPad("", 200, 'A'))); + or.addOr(new StringParam(StringUtils.leftPad("", 200, 'B'))); + or.addOr(new StringParam(StringUtils.leftPad("", 200, 'C'))); + map.add(Patient.SP_NAME, or); + IBundleProvider results = myPatientDao.search(map); + assertEquals(1, results.getResources(0, 10).size()); + assertEquals(1, mySearchEntityDao.count()); + + map = new SearchParameterMap(); + or = new StringOrListParam(); + or.addOr(new StringParam("A1")); + or.addOr(new StringParam(StringUtils.leftPad("", 200, 'A'))); + or.addOr(new StringParam(StringUtils.leftPad("", 200, 'B'))); + or.addOr(new StringParam(StringUtils.leftPad("", 200, 'C'))); + map.add(Patient.SP_NAME, or); + results = myPatientDao.search(map); + assertEquals(1, results.getResources(0, 10).size()); + assertEquals(1, mySearchEntityDao.count()); + + } + + private String toStringMultiline(List theResults) { + StringBuilder b = new StringBuilder(); + for (Object next : theResults) { + b.append('\n'); + b.append(" * ").append(next.toString()); + } + return b.toString(); + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java index 64664d9e64d..728a75f6950 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java @@ -668,33 +668,6 @@ public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test { } - @Test - public void testUpdateReusesIndexes() { - myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); - - QueryCountHolder.clear(); - - Patient pt = new Patient(); - pt.setActive(true); - pt.addName().setFamily("FAMILY1").addGiven("GIVEN1A").addGiven("GIVEN1B"); - IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless(); - - ourLog.info("Now have {} deleted", QueryCountHolder.getGrandTotal().getDelete()); - ourLog.info("Now have {} inserts", QueryCountHolder.getGrandTotal().getInsert()); - QueryCountHolder.clear(); - - ourLog.info("** About to update"); - - pt.setId(id); - pt.getNameFirstRep().addGiven("GIVEN1C"); - myPatientDao.update(pt); - - ourLog.info("Now have {} deleted", QueryCountHolder.getGrandTotal().getDelete()); - ourLog.info("Now have {} inserts", QueryCountHolder.getGrandTotal().getInsert()); - assertEquals(0, QueryCountHolder.getGrandTotal().getDelete()); - assertEquals(4, QueryCountHolder.getGrandTotal().getInsert()); - } - @Test public void testUpdateUnknownNumericIdFails() { Patient p = new Patient(); diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 9a8f49cd6e1..2f7f9661acb 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -116,6 +116,11 @@ the example project]]> if they are not already. + + When updating resources in the JPA server, a bug caused index table entries to be refreshed + sometimes even though the index value hadn't changed. This issue did not cause incorrect search + results but had an effect on write performance. This has been corrected. + From b66e01ce656a39af8b9a5f2596d4a1d1a6674e6c Mon Sep 17 00:00:00 2001 From: James Agnew Date: Mon, 29 Oct 2018 10:36:32 -0400 Subject: [PATCH 2/2] Allow plain server @Operation methods to declare a wildcard so that any opeeration invocations will be direected to them --- .../uhn/fhir/rest/annotation/Operation.java | 33 +- .../main/java/ca/uhn/fhir/util/TestUtil.java | 2 +- .../BaseSubscriptionInterceptor.java | 2 - ...scriptionDeliveringRestHookSubscriber.java | 4 - .../java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java | 2 +- .../FhirResourceDaoDstu3SearchNoFtTest.java | 6 +- .../dao/dstu3/FhirResourceDaoDstu3Test.java | 6 + ...nterceptorRegisteredToDaoConfigR4Test.java | 2 + .../uhn/fhir/rest/server/RestfulServer.java | 91 +- .../server/interceptor/auth/RuleImplOp.java | 4 +- .../rest/server/method/BaseMethodBinding.java | 2 + .../method/ConformanceMethodBinding.java | 3 + .../server/method/CreateMethodBinding.java | 3 + .../server/method/DeleteMethodBinding.java | 3 + .../server/method/GraphQLMethodBinding.java | 2 + .../server/method/HistoryMethodBinding.java | 2 + .../fhir/rest/server/method/MethodUtil.java | 3 +- .../server/method/OperationMethodBinding.java | 103 +- .../rest/server/method/PageMethodBinding.java | 2 + .../server/method/PatchMethodBinding.java | 3 + .../rest/server/method/ReadMethodBinding.java | 3 + .../rest/server/method/ResourceParameter.java | 64 +- .../server/method/SearchMethodBinding.java | 3 + .../method/SearchTotalModeParameter.java | 4 +- .../method/TransactionMethodBinding.java | 3 + .../server/method/UpdateMethodBinding.java | 3 + .../server/OperationGenericServerR4Test.java | 276 +++++ src/changes/changes.xml | 939 +++++++++--------- 28 files changed, 982 insertions(+), 591 deletions(-) create mode 100644 hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationGenericServerR4Test.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java index 7c74c01e379..7fdbdefa417 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.annotation; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,15 +20,14 @@ package ca.uhn.fhir.rest.annotation; * #L% */ +import ca.uhn.fhir.model.valueset.BundleTypeEnum; +import org.hl7.fhir.instance.model.api.IBaseResource; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.hl7.fhir.instance.model.api.IBaseResource; - -import ca.uhn.fhir.model.valueset.BundleTypeEnum; - /** * RESTful method annotation used for a method which provides FHIR "operations". */ @@ -37,10 +36,18 @@ import ca.uhn.fhir.model.valueset.BundleTypeEnum; public @interface Operation { /** - * The name of the operation, e.g. "$everything" - * + * This constant is a special return value for {@link #name()}. If this name is + * used, the given operation method will match all operation calls. This is + * generally not desirable, but can be useful if you have a server that should + * dynamically match any FHIR operations that are requested. + */ + String NAME_MATCH_ALL = "*"; + + /** + * The name of the operation, e.g. "$everything" + * *

- * This may be specified with or without a leading + * This may be specified with or without a leading * '$'. (If the leading '$' is omitted, it will be added internally by the API). *

*/ @@ -61,10 +68,10 @@ public @interface Operation { * (meaning roughly that it does not modify any data or state on the server) * then this flag should be set to true (default is false). *

- * One the server, setting this to true means that the + * One the server, setting this to true means that the * server will allow the operation to be invoked using an HTTP GET * (on top of the standard HTTP POST) - *

+ *

*/ boolean idempotent() default false; @@ -73,9 +80,9 @@ public @interface Operation { * response to this operation. */ OperationParam[] returnParameters() default {}; - + /** - * If this operation returns a bundle, this parameter can be used to specify the + * If this operation returns a bundle, this parameter can be used to specify the * bundle type to set in the bundle. */ BundleTypeEnum bundleType() default BundleTypeEnum.COLLECTION; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TestUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TestUtil.java index 1d1fc8ee42c..1d1dc5ef0ae 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TestUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TestUtil.java @@ -107,7 +107,7 @@ public class TestUtil { * environment */ public static void randomizeLocale() { - Locale[] availableLocales = {Locale.CANADA, Locale.GERMANY, Locale.TAIWAN}; + Locale[] availableLocales = {Locale.CANADA, Locale.GERMANY, Locale.TAIWAN}; Locale.setDefault(availableLocales[(int) (Math.random() * availableLocales.length)]); ourLog.info("Tests are running in locale: " + Locale.getDefault().getDisplayName()); if (Math.random() < 0.5) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java index 8145dff30d1..2d060c56575 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java @@ -478,8 +478,6 @@ public abstract class BaseSubscriptionInterceptor exten TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { - // FIXME: remove - ourLog.info("** Sending processing message " + theMessage + " for: " + theMessage.getNewPayload(myCtx)); ourLog.trace("Sending resource modified message to processing channel"); getProcessingChannel().send(new ResourceModifiedJsonMessage(theMessage)); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionDeliveringRestHookSubscriber.java index 4a1cbd8167a..37ed373598c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionDeliveringRestHookSubscriber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionDeliveringRestHookSubscriber.java @@ -108,10 +108,6 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe operation.encoded(thePayloadType); } - // FIXME: remove - ourLog.info("** This " + this + " Processing delivery message " + theMsg); - - ourLog.info("Delivering {} rest-hook payload {} for {}", theMsg.getOperationType(), thePayloadResource.getIdElement().toUnqualified().getValue(), theSubscription.getIdElement(getContext()).toUnqualifiedVersionless().getValue()); try { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index 6ec7358cd29..5e7d3cc046c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -289,7 +289,7 @@ public abstract class BaseJpaTest { return retVal; } - protected List toUnqualifiedVersionlessIds(List theFound) { + protected List toUnqualifiedVersionlessIds(List theFound) { List retVal = new ArrayList(); for (IBaseResource next : theFound) { retVal.add(next.getIdElement().toUnqualifiedVersionless()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java index d8e7cd2aa30..10953058302 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java @@ -1613,16 +1613,18 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { obs01.setSubject(new Reference(patientId01)); IIdType obsId01 = myObservationDao.create(obs01, mySrd).getId().toUnqualifiedVersionless(); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); Date between = new Date(); - Thread.sleep(10); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); Observation obs02 = new Observation(); obs02.setEffective(new DateTimeType(new Date())); obs02.setSubject(new Reference(locId01)); IIdType obsId02 = myObservationDao.create(obs02, mySrd).getId().toUnqualifiedVersionless(); - Thread.sleep(10); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); Date after = new Date(); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); ourLog.info("P1[{}] L1[{}] Obs1[{}] Obs2[{}]", new Object[] { patientId01, locId01, obsId01, obsId02 }); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java index 81cf41e5427..7bfd175aec8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java @@ -2865,16 +2865,22 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { p.addName().setFamily(methodName); IIdType id1 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); + p = new Patient(); p.addIdentifier().setSystem("urn:system2").setValue(methodName); p.addName().setFamily(methodName); IIdType id2 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); + p = new Patient(); p.addIdentifier().setSystem("urn:system3").setValue(methodName); p.addName().setFamily(methodName); IIdType id3 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); + p = new Patient(); p.addIdentifier().setSystem("urn:system4").setValue(methodName); p.addName().setFamily(methodName); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java index 56560a12a6e..9017744687c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java @@ -250,6 +250,8 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigR4Test extends Base waitForSize(0, ourCreatedObservations); waitForSize(5, ourUpdatedObservations); + ourLog.info("Have observations: {}", toUnqualifiedVersionlessIds(ourUpdatedObservations)); + Assert.assertFalse(subscription1.getId().equals(subscription2.getId())); Assert.assertFalse(observation1.getId().isEmpty()); Assert.assertFalse(observation2.getId().isEmpty()); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index b8d702b8832..d24e27250ff 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -349,7 +349,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer theProviders) { + Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); + + myPlainProviders.clear(); + if (theProviders != null) { + myPlainProviders.addAll(theProviders); + } + } + /** * Sets the non-resource specific providers which implement method calls on this server. * @@ -615,7 +643,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer theProviders) { + Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); + + myResourceProviders.clear(); + if (theProviders != null) { + myResourceProviders.addAll(theProviders); + } + } + /** * Sets the resource providers for this server */ @@ -1521,34 +1561,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer theProviders) { - Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); - - myPlainProviders.clear(); - if (theProviders != null) { - myPlainProviders.addAll(theProviders); - } - } - /** * Sets the non-resource specific providers which implement method calls on this server * @@ -1563,18 +1575,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer theProviders) { - Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); - - myResourceProviders.clear(); - if (theProviders != null) { - myResourceProviders.addAll(theProviders); - } - } - /** * If provided (default is null), the tenant identification * strategy provides a mechanism for a multitenant server to identify which tenant @@ -1585,7 +1585,8 @@ public class RestfulServer extends HttpServlet implements IRestfulServer 0 && (nextString.charAt(0) == '_' || nextString.charAt(0) == '$' || nextString.equals(Constants.URL_TOKEN_METADATA)); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java index fd1bf304169..b0e0c320b75 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java @@ -35,9 +35,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * 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. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 088df536edc..66044200232 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -46,6 +46,7 @@ import org.apache.commons.io.IOUtils; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; +import javax.annotation.Nonnull; import java.io.IOException; import java.io.Reader; import java.lang.reflect.InvocationTargetException; @@ -223,6 +224,7 @@ public abstract class BaseMethodBinding { */ public abstract String getResourceName(); + @Nonnull public abstract RestOperationTypeEnum getRestOperationType(); /** diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ConformanceMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ConformanceMethodBinding.java index 0a61eb2250c..1da2a1b4787 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ConformanceMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ConformanceMethodBinding.java @@ -37,6 +37,8 @@ import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import javax.annotation.Nonnull; + public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding { public ConformanceMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { @@ -86,6 +88,7 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding return false; } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { return RestOperationTypeEnum.METADATA; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/CreateMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/CreateMethodBinding.java index d805e02542e..ac73cfed0b0 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/CreateMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/CreateMethodBinding.java @@ -36,6 +36,8 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import javax.annotation.Nonnull; + public class CreateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam { public CreateMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { @@ -47,6 +49,7 @@ public class CreateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe return null; } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { return RestOperationTypeEnum.CREATE; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/DeleteMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/DeleteMethodBinding.java index 8925671be25..465ea545a1e 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/DeleteMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/DeleteMethodBinding.java @@ -30,12 +30,15 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; +import javax.annotation.Nonnull; + public class DeleteMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody { public DeleteMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { super(theMethod, theContext, theProvider, Delete.class, theMethod.getAnnotation(Delete.class).type()); } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { return RestOperationTypeEnum.DELETE; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java index a9684dac2fc..fffae8ef770 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java @@ -30,6 +30,7 @@ import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServerUtils; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import javax.annotation.Nonnull; import java.io.IOException; import java.io.Writer; import java.lang.reflect.Method; @@ -49,6 +50,7 @@ public class GraphQLMethodBinding extends BaseMethodBinding { return null; } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { return RestOperationTypeEnum.GRAPHQL_REQUEST; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HistoryMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HistoryMethodBinding.java index 929bf8b07ec..df7005ae874 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HistoryMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HistoryMethodBinding.java @@ -39,6 +39,7 @@ import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import javax.annotation.Nonnull; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Date; @@ -91,6 +92,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding { return BundleTypeEnum.HISTORY; } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { return myResourceOperationType; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 8012f47bc1f..fa5a043c3b1 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -193,7 +193,8 @@ public class MethodUtil { b.append(" or String or byte[]"); throw new ConfigurationException(b.toString()); } - param = new ResourceParameter((Class) parameterType, theProvider, mode); + boolean methodIsOperation = theMethod.getAnnotation(Operation.class) != null; + param = new ResourceParameter((Class) parameterType, theProvider, mode, methodIsOperation); } else if (nextAnnotation instanceof IdParam) { param = new NullParameter(); } else if (nextAnnotation instanceof ServerBase) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index a359bede97e..ed9ccaabd1a 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -19,50 +19,58 @@ package ca.uhn.fhir.rest.server.method; * limitations under the License. * #L% */ -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -import java.io.IOException; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.*; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; -import org.hl7.fhir.instance.model.api.IBase; -import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.valueset.BundleTypeEnum; -import ca.uhn.fhir.rest.annotation.*; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; -import ca.uhn.fhir.rest.api.server.*; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.IRestfulServer; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; public class OperationMethodBinding extends BaseResourceReturningMethodBinding { + public static final String WILDCARD_NAME = "$" + Operation.NAME_MATCH_ALL; + private final boolean myIdempotent; + private final Integer myIdParamIndex; + private final String myName; + private final RestOperationTypeEnum myOtherOperatiopnType; + private final ReturnTypeEnum myReturnType; private BundleTypeEnum myBundleType; private boolean myCanOperateAtInstanceLevel; private boolean myCanOperateAtServerLevel; private boolean myCanOperateAtTypeLevel; private String myDescription; - private final boolean myIdempotent; - private final Integer myIdParamIndex; - private final String myName; - private final RestOperationTypeEnum myOtherOperatiopnType; private List myReturnParams; - private final ReturnTypeEnum myReturnType; protected OperationMethodBinding(Class theReturnResourceType, Class theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, - boolean theIdempotent, String theOperationName, Class theOperationType, - OperationParam[] theReturnParams, BundleTypeEnum theBundleType) { + boolean theIdempotent, String theOperationName, Class theOperationType, + OperationParam[] theReturnParams, BundleTypeEnum theBundleType) { super(theReturnResourceType, theMethod, theContext, theProvider); myBundleType = theBundleType; @@ -91,7 +99,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { if (isBlank(theOperationName)) { throw new ConfigurationException("Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName() - + " but this annotation has no name defined"); + + " but this annotation has no name defined"); } if (theOperationName.startsWith("$") == false) { theOperationName = "$" + theOperationName; @@ -152,15 +160,19 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { } public OperationMethodBinding(Class theReturnResourceType, Class theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, - Operation theAnnotation) { + Operation theAnnotation) { this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.returnParameters(), - theAnnotation.bundleType()); + theAnnotation.bundleType()); } public String getDescription() { return myDescription; } + public void setDescription(String theDescription) { + myDescription = theDescription; + } + /** * Returns the name of the operation, starting with "$" */ @@ -173,6 +185,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { return myBundleType; } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { return myOtherOperatiopnType; @@ -189,15 +202,19 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { @Override public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) { + if (!myName.equals(theRequest.getOperation())) { + if (!myName.equals(WILDCARD_NAME)) { + return false; + } + } + if (getResourceName() == null) { if (isNotBlank(theRequest.getResourceName())) { return false; } - } else if (!getResourceName().equals(theRequest.getResourceName())) { - return false; } - if (!myName.equals(theRequest.getOperation())) { + if (getResourceName() != null && !getResourceName().equals(theRequest.getResourceName())) { return false; } @@ -221,7 +238,6 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { return true; } - @Override public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) { RestOperationTypeEnum retVal = super.getRestOperationType(theRequestDetails); @@ -304,11 +320,6 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { theDetails.setResource((IBaseResource) theRequestDetails.getUserData().get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY)); } - public void setDescription(String theDescription) { - myDescription = theDescription; - } - - public static class ReturnType { private int myMax; private int myMin; @@ -322,30 +333,30 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { return myMax; } - public int getMin() { - return myMin; - } - - public String getName() { - return myName; - } - - public String getType() { - return myType; - } - public void setMax(int theMax) { myMax = theMax; } + public int getMin() { + return myMin; + } + public void setMin(int theMin) { myMin = theMin; } + public String getName() { + return myName; + } + public void setName(String theName) { myName = theName; } + public String getType() { + return myType; + } + public void setType(String theType) { myType = theType; } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PageMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PageMethodBinding.java index d7ce7a2b198..68973ce3175 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PageMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PageMethodBinding.java @@ -38,6 +38,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import org.hl7.fhir.instance.model.api.IBaseResource; +import javax.annotation.Nonnull; import java.lang.reflect.Method; import java.util.HashSet; import java.util.Set; @@ -166,6 +167,7 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding { } } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { return RestOperationTypeEnum.GET_PAGE; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PatchMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PatchMethodBinding.java index 9717a3fab7b..97b2dc90201 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PatchMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PatchMethodBinding.java @@ -38,6 +38,8 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; +import javax.annotation.Nonnull; + /** * Base class for an operation that has a resource type but not a resource body in the * request body @@ -86,6 +88,7 @@ public class PatchMethodBinding extends BaseOutcomeReturningMethodBindingWithRes return retVal; } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { return RestOperationTypeEnum.PATCH; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java index 53960db94f1..9d3eb9229b8 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java @@ -44,6 +44,8 @@ import ca.uhn.fhir.rest.server.ETagSupportEnum; import ca.uhn.fhir.rest.server.exceptions.*; import ca.uhn.fhir.util.DateUtils; +import javax.annotation.Nonnull; + public class ReadMethodBinding extends BaseResourceReturningMethodBinding { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReadMethodBinding.class); @@ -91,6 +93,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding { return retVal; } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { return isVread() ? RestOperationTypeEnum.VREAD : RestOperationTypeEnum.READ; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ResourceParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ResourceParameter.java index db1705dcd23..c4f7ebd43fb 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ResourceParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ResourceParameter.java @@ -38,6 +38,7 @@ import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseBinary; import org.hl7.fhir.instance.model.api.IBaseResource; +import javax.annotation.Nonnull; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; @@ -52,15 +53,17 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public class ResourceParameter implements IParameter { + private final boolean myMethodIsOperation; private Mode myMode; private Class myResourceType; - public ResourceParameter(Class theParameterType, Object theProvider, Mode theMode) { + public ResourceParameter(Class theParameterType, Object theProvider, Mode theMode, boolean theMethodIsOperation) { Validate.notNull(theParameterType, "theParameterType can not be null"); Validate.notNull(theMode, "theMode can not be null"); myResourceType = theParameterType; myMode = theMode; + myMethodIsOperation = theMethodIsOperation; Class providerResourceType = null; if (theProvider instanceof IResourceProvider) { @@ -90,24 +93,33 @@ public class ResourceParameter implements IParameter { @Override public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { switch (myMode) { - case BODY: - try { - return IOUtils.toString(createRequestReader(theRequest)); - } catch (IOException e) { - // Shouldn't happen since we're reading from a byte array - throw new InternalErrorException("Failed to load request", e); - } - case BODY_BYTE_ARRAY: - return theRequest.loadRequestContents(); - case ENCODING: - return RestfulServerUtils.determineRequestEncodingNoDefault(theRequest); - case RESOURCE: - default: - return parseResourceFromRequest(theRequest, theMethodBinding, myResourceType); + case BODY: + try { + return IOUtils.toString(createRequestReader(theRequest)); + } catch (IOException e) { + // Shouldn't happen since we're reading from a byte array + throw new InternalErrorException("Failed to load request", e); + } + case BODY_BYTE_ARRAY: + return theRequest.loadRequestContents(); + case ENCODING: + return RestfulServerUtils.determineRequestEncodingNoDefault(theRequest); + case RESOURCE: + default: + Class resourceTypeToParse = myResourceType; + if (myMethodIsOperation) { + // Operations typically have a Parameters resource as the body + resourceTypeToParse = null; + } + return parseResourceFromRequest(theRequest, theMethodBinding, resourceTypeToParse); } // } } + public enum Mode { + BODY, BODY_BYTE_ARRAY, ENCODING, RESOURCE + } + public static Reader createRequestReader(RequestDetails theRequest, Charset charset) { Reader requestReader = new InputStreamReader(new ByteArrayInputStream(theRequest.loadRequestContents()), charset); return requestReader; @@ -118,7 +130,7 @@ public class ResourceParameter implements IParameter { } public static Charset determineRequestCharset(RequestDetails theRequest) { - Charset charset = theRequest.getCharset(); + Charset charset = theRequest.getCharset(); if (charset == null) { charset = Charset.forName("UTF-8"); } @@ -126,7 +138,7 @@ public class ResourceParameter implements IParameter { } @SuppressWarnings("unchecked") - public static T loadResourceFromRequest(RequestDetails theRequest, BaseMethodBinding theMethodBinding, Class theResourceType) { + public static T loadResourceFromRequest(RequestDetails theRequest, @Nonnull BaseMethodBinding theMethodBinding, Class theResourceType) { FhirContext ctx = theRequest.getServer().getFhirContext(); final Charset charset = determineRequestCharset(theRequest); @@ -139,7 +151,6 @@ public class ResourceParameter implements IParameter { String ctValue = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); if (ctValue != null) { if (ctValue.startsWith("application/x-www-form-urlencoded")) { - //FIXME potential null access theMethodBinding String msg = theRequest.getServer().getFhirContext().getLocalizer().getMessage(ResourceParameter.class, "invalidContentTypeInRequest", ctValue, theMethodBinding.getRestOperationType()); throw new InvalidRequestException(msg); } @@ -155,6 +166,9 @@ public class ResourceParameter implements IParameter { // This shouldn't happen since we're reading from a byte array.. throw new InternalErrorException(e); } + if (isBlank(body)) { + return null; + } encoding = EncodingEnum.detectEncodingNoDefault(body); if (encoding == null) { String msg = ctx.getLocalizer().getMessage(ResourceParameter.class, "noContentTypeInRequest", restOperationType); @@ -168,7 +182,7 @@ public class ResourceParameter implements IParameter { } IParser parser = encoding.newParser(ctx); - parser.setServerBaseUrl(theRequest.getFhirServerBase()); + parser.setServerBaseUrl(theRequest.getFhirServerBase()); T retVal; try { if (theResourceType != null) { @@ -180,14 +194,14 @@ public class ResourceParameter implements IParameter { String msg = ctx.getLocalizer().getMessage(ResourceParameter.class, "failedToParseRequest", encoding.name(), e.getMessage()); throw new InvalidRequestException(msg); } - + return retVal; } public static IBaseResource parseResourceFromRequest(RequestDetails theRequest, BaseMethodBinding theMethodBinding, Class theResourceType) { IBaseResource retVal = null; - - if (IBaseBinary.class.isAssignableFrom(theResourceType)) { + + if (theResourceType != null && IBaseBinary.class.isAssignableFrom(theResourceType)) { String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); if (EncodingEnum.forContentTypeStrict(ct) == null) { FhirContext ctx = theRequest.getServer().getFhirContext(); @@ -209,15 +223,11 @@ public class ResourceParameter implements IParameter { } } } - + if (retVal == null) { retVal = loadResourceFromRequest(theRequest, theMethodBinding, theResourceType); } return retVal; } - public enum Mode { - BODY, BODY_BYTE_ARRAY, ENCODING, RESOURCE - } - } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java index 27a59c3afe4..3b06a9b2f19 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java @@ -47,6 +47,8 @@ import ca.uhn.fhir.rest.param.QualifierDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import javax.annotation.Nonnull; + public class SearchMethodBinding extends BaseResourceReturningMethodBinding { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class); @@ -108,6 +110,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { return myDescription; } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { return RestOperationTypeEnum.SEARCH_TYPE; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchTotalModeParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchTotalModeParameter.java index 5f9f0e809c0..2bb73bee73d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchTotalModeParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchTotalModeParameter.java @@ -18,9 +18,9 @@ import java.util.Collection; * 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. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/TransactionMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/TransactionMethodBinding.java index ba3d39d2a37..422ae383c7a 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/TransactionMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/TransactionMethodBinding.java @@ -44,6 +44,8 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.method.TransactionParameter.ParamStyle; +import javax.annotation.Nonnull; + public class TransactionMethodBinding extends BaseResourceReturningMethodBinding { private int myTransactionParamIndex; @@ -73,6 +75,7 @@ public class TransactionMethodBinding extends BaseResourceReturningMethodBinding } } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { return RestOperationTypeEnum.TRANSACTION; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/UpdateMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/UpdateMethodBinding.java index 3b7dca107aa..b728137011e 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/UpdateMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/UpdateMethodBinding.java @@ -39,6 +39,8 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import javax.annotation.Nonnull; + public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam { public UpdateMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { @@ -98,6 +100,7 @@ public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe return null; } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { return RestOperationTypeEnum.UPDATE; diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationGenericServerR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationGenericServerR4Test.java new file mode 100644 index 00000000000..0f0764c421d --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationGenericServerR4Test.java @@ -0,0 +1,276 @@ +package ca.uhn.fhir.rest.server; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StringType; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class OperationGenericServerR4Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationGenericServerR4Test.class); + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx; + private static IdType ourLastId; + private static String ourLastMethod; + private static StringType ourLastParam1; + private static Patient ourLastParam2; + private static int ourPort; + private static Server ourServer; + private static Parameters ourLastResourceParam; + + @Before + public void before() { + ourLastParam1 = null; + ourLastParam2 = null; + ourLastId = null; + ourLastMethod = ""; + ourLastResourceParam = null; + } + + + @Test + public void testOperationOnInstance() throws Exception { + Parameters p = new Parameters(); + p.addParameter().setName("PARAM1").setValue(new StringType("PARAM1val")); + p.addParameter().setName("PARAM2").setResource(new Patient().setActive(true)); + String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/123/$OP_INSTANCE"); + httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + CloseableHttpResponse status = ourClient.execute(httpPost); + try { + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + status.getEntity().getContent().close(); + + assertEquals("PARAM1val", ourLastParam1.getValue()); + assertEquals(true, ourLastParam2.getActive()); + assertEquals("123", ourLastId.getIdPart()); + assertEquals("$OP_INSTANCE", ourLastMethod); + assertEquals("PARAM1", ourLastResourceParam.getParameterFirstRep().getName()); + + Parameters resp = ourCtx.newXmlParser().parseResource(Parameters.class, response); + assertEquals("RET1", resp.getParameter().get(0).getName()); + } finally { + status.getEntity().getContent().close(); + } + + } + + + @Test + public void testOperationOnServer() throws Exception { + Parameters p = new Parameters(); + p.addParameter().setName("PARAM1").setValue(new StringType("PARAM1val")); + p.addParameter().setName("PARAM2").setResource(new Patient().setActive(true)); + String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/$OP_SERVER"); + httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + CloseableHttpResponse status = ourClient.execute(httpPost); + try { + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + + assertEquals("PARAM1", ourLastResourceParam.getParameterFirstRep().getName()); + assertEquals("PARAM1val", ourLastParam1.getValue()); + assertEquals(true, ourLastParam2.getActive()); + assertEquals("$OP_SERVER", ourLastMethod); + + Parameters resp = ourCtx.newXmlParser().parseResource(Parameters.class, response); + assertEquals("RET1", resp.getParameter().get(0).getName()); + } finally { + status.getEntity().getContent().close(); + } + } + + + @Test + public void testOperationOnType() throws Exception { + Parameters p = new Parameters(); + p.addParameter().setName("PARAM1").setValue(new StringType("PARAM1val")); + p.addParameter().setName("PARAM2").setResource(new Patient().setActive(true)); + String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$OP_TYPE"); + httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + CloseableHttpResponse status = ourClient.execute(httpPost); + try { + String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + status.getEntity().getContent().close(); + + assertEquals("PARAM1", ourLastResourceParam.getParameterFirstRep().getName()); + assertEquals("PARAM1val", ourLastParam1.getValue()); + assertEquals(true, ourLastParam2.getActive()); + assertEquals("$OP_TYPE", ourLastMethod); + + Parameters resp = ourCtx.newXmlParser().parseResource(Parameters.class, response); + assertEquals("RET1", resp.getParameter().get(0).getName()); + } finally { + status.getEntity().getContent().close(); + } + } + + + @Test + public void testOperationWithGetUsingParams() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$OP_TYPE?PARAM1=PARAM1val"); + CloseableHttpResponse status = ourClient.execute(httpGet); + try { + String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + status.getEntity().getContent().close(); + + assertNull(ourLastResourceParam); + assertEquals("PARAM1val", ourLastParam1.getValue()); + + assertNull(ourLastParam2); + assertEquals("$OP_TYPE", ourLastMethod); + + Parameters resp = ourCtx.newXmlParser().parseResource(Parameters.class, response); + assertEquals("RET1", resp.getParameter().get(0).getName()); + } finally { + status.getEntity().getContent().close(); + } + } + + @SuppressWarnings("unused") + public static class PatientProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Operation(name = Operation.NAME_MATCH_ALL) + public Parameters opInstance( + @ResourceParam() IBaseResource theResourceParam, + @IdParam IdType theId, + @OperationParam(name = "PARAM1") StringType theParam1, + @OperationParam(name = "PARAM2") Patient theParam2 + ) { + + ourLastMethod = "$OP_INSTANCE"; + ourLastId = theId; + ourLastParam1 = theParam1; + ourLastParam2 = theParam2; + ourLastResourceParam = (Parameters) theResourceParam; + + Parameters retVal = new Parameters(); + retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1")); + return retVal; + } + + @SuppressWarnings("unused") + @Operation(name = Operation.NAME_MATCH_ALL, idempotent = true) + public Parameters opType( + @ResourceParam() IBaseResource theResourceParam, + @OperationParam(name = "PARAM1") StringType theParam1, + @OperationParam(name = "PARAM2") Patient theParam2, + @OperationParam(name = "PARAM3", min = 2, max = 5) List theParam3, + @OperationParam(name = "PARAM4", min = 1) List theParam4 + ) { + + ourLastMethod = "$OP_TYPE"; + ourLastParam1 = theParam1; + ourLastParam2 = theParam2; + ourLastResourceParam = (Parameters) theResourceParam; + + Parameters retVal = new Parameters(); + retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1")); + return retVal; + } + + + } + + @SuppressWarnings("unused") + public static class PlainProvider { + + @Operation(name = Operation.NAME_MATCH_ALL) + public Parameters opServer( + @ResourceParam() IBaseResource theResourceParam, + @OperationParam(name = "PARAM1") StringType theParam1, + @OperationParam(name = "PARAM2") Patient theParam2 + ) { + + ourLastMethod = "$OP_SERVER"; + ourLastParam1 = theParam1; + ourLastParam2 = theParam2; + ourLastResourceParam = (Parameters) theResourceParam; + + Parameters retVal = new Parameters(); + retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1")); + return retVal; + } + + + } + + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourCtx = FhirContext.forR4(); + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(ourCtx); + + servlet.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(2)); + + servlet.setFhirContext(ourCtx); + servlet.setResourceProviders(new PatientProvider()); + servlet.setPlainProviders(new PlainProvider()); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + +} diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 2f7f9661acb..f374f02af8e 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/changes/1.0.0 ./changes.xsd"> James Agnew HAPI FHIR Changelog @@ -23,7 +23,7 @@ version of 3.5.0. - Automatic ID generation for contained resources (in cases where the user hasn't manually specified an ID) + Automatic ID generation for contained resources (in cases where the user hasn't manually specified an ID) has been streamlined to generate more predictable IDs in some cases. @@ -96,12 +96,12 @@ if an unqualified ID is placed in the ID text box. This has been corrected. - AuthorizationInterceptor did not allow FHIR batch operations when the transaction() + AuthorizationInterceptor did not allow FHIR batch operations when the transaction() permission is granted. This has been corrected so that transaction() allows both batch and transaction requests to proceed. - The AuthorizationInterceptor was previously not able to authorize the FHIR + The AuthorizationInterceptor was previously not able to authorize the FHIR batch operation. As of this version, when authorizing a transaction operation (via the transaction() rule), both batch and transaction will be allowed. @@ -110,7 +110,7 @@ settings as long as the JPA EntityManagerFactory was created using HAPI FHIR's built-in method for creating it.
]]> - Existing JPA projects should consider using + Existing JPA projects should consider using super.entityManagerFactory()]]> as shown in the example project]]> @@ -121,6 +121,18 @@ sometimes even though the index value hadn't changed. This issue did not cause incorrect search results but had an effect on write performance. This has been corrected. + + The @Operation annotation used to declare operations on the Plain Server now + has a wildcard constant which may be used for the operation name. This allows + you to create a server that supports operations that are not known to the + server when it starts up. This is generally not advisable but can be useful + for some circumstances. + + + When using an @Operation method in the Plain Server, it is now possible + to use a parameter annotated with @ResourceParam to receive the Parameters + (or other) resource supplied by the client as the request body. + @@ -225,7 +237,7 @@ LOINC uploader has been updated to support the new LOINC filename - scheme introduced in LOINC 2.64. Thanks to Rob Hausam for the + scheme introduced in LOINC 2.64. Thanks to Rob Hausam for the pull request! @@ -246,22 +258,22 @@ create two resources with the same client-assigned ID. - The JPA server + The JPA server $expunge]]> - operation deleted components of an individual resource record in - separate database transactions, meaning that if an operation failed + operation deleted components of an individual resource record in + separate database transactions, meaning that if an operation failed unexpectedly resources could be left in a weird state. This has been corrected. A bug was fixed in the JPA terminology uploader, where it was possible in some cases for some ValueSets and ConceptMaps to not be saved because - of a premature short circuit during deferred uploading. Thanks to + of a premature short circuit during deferred uploading. Thanks to Joel Schneider for the pull request! A bug in the HAPI FHIR CLI was fixed, where uploading terminology for R4 - could cause an error about the incorrect FHIR version. Thanks to + could cause an error about the incorrect FHIR version. Thanks to Rob Hausam for the pull request! @@ -328,7 +340,7 @@ Fixed a bug when creating a custom search parameter in the JPA - server: if the SearchParameter resource contained an invalid + server: if the SearchParameter resource contained an invalid expression, create/update operations for the given resource would fail with a cryptic error. SearchParameter expressions are now validated upon storage, and the SearchParameter will be rejected @@ -395,7 +407,7 @@ when it grows long. - A bug was fixed in JPA server searches: When performing a search with a _lastUpdate + A bug was fixed in JPA server searches: When performing a search with a _lastUpdate filter, the filter was applied to any _include values, which it should not have been. Thanks to Deepak Garg for reporting! @@ -425,7 +437,7 @@ implemented. Thanks to Patrick Werner for the Pull Request! - HAPI FHIR CLI commands that allow Basic Auth credentials or a Bearer Token may now use + HAPI FHIR CLI commands that allow Basic Auth credentials or a Bearer Token may now use a value of "PROMPT" to cause the CLI to prompt the user for credentials using an interactive prompt. @@ -563,7 +575,7 @@ alter table TRM_CODESYSTEM_VER drop constraint IDX_CSV_RESOURCEPID_AND_VER local IDs (e.g. resource.setId("#1")) as well as contained resources with no IDs (meaning HAPI should automatically assign a local ID for these resources) it was possible for HAPI to generate - a local ID that already existed, making the resulting + a local ID that already existed, making the resulting serialization invalid. This has been corrected. @@ -679,7 +691,7 @@ alter table TRM_CODESYSTEM_VER drop constraint IDX_CSV_RESOURCEPID_AND_VER Anthony Sute for identifying this. - A hard-to-understand validation message was fixed in the validator when + A hard-to-understand validation message was fixed in the validator when validating against profiles that declare some elements as mustSupport but have others used but not declared as mustSupport. Thanks to Patrick Werner for the PR! @@ -701,7 +713,7 @@ alter table TRM_CODESYSTEM_VER drop constraint IDX_CSV_RESOURCEPID_AND_VER Thanks to GitHub user @RuthAlk for the pull request! - DateRangeParameter was enhanced to support convenient method chanining, and + DateRangeParameter was enhanced to support convenient method chanining, and the parameter validation was improved to only change state after validating that parameters were valid. Thanks to Gaetano Gallo for the pull request! @@ -746,10 +758,10 @@ alter table TRM_CODESYSTEM_VER drop constraint IDX_CSV_RESOURCEPID_AND_VER FhirInstanceValidator. Thanks to Heinz-Dieter Conradi for the pull request! - The REST server has been modified so that the + The REST server has been modified so that the Location]]> header is no longer returned by the server on read or update responses. - This header was returned in the past, but this header is actually + This header was returned in the past, but this header is actually inappropriate for any response that is not a create operation. The Content-Location]]> @@ -910,12 +922,12 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; no count parameter included. Thanks to Viviana Sanz for reporting! - A bug in the JPA server was fixed where a Subscription incorrectly created - without a status or with invalid criteria would cause a crash during + A bug in the JPA server was fixed where a Subscription incorrectly created + without a status or with invalid criteria would cause a crash during startup. - ResponseHighlightingInterceptor now properly parses _format + ResponseHighlightingInterceptor now properly parses _format parameters that include additional content (e.g. _format=html/json;fhirVersion=1.0]]>) @@ -926,7 +938,7 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; more tolerant of deleting very large search result sets. - Avoid refreshing the search parameter cache from an incoming client + Avoid refreshing the search parameter cache from an incoming client request thread, which caused unneccesary delays for clients. @@ -945,11 +957,11 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; a testcase! - Deleting a resource from the testpage overlay resulted in an error page after + Deleting a resource from the testpage overlay resulted in an error page after clicking "delete", even though the delete succeeded. - A number of info level log lines have been reduced to debug level in the JPA server, in + A number of info level log lines have been reduced to debug level in the JPA server, in order to reduce contention during heavy loads and reduce the amount of noise in log files overall. A typical server should now see far less logging coming from HAPI, at least at the INFO level. @@ -995,7 +1007,7 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; milliseconds spent performing each call that is logged. - ReferenceParam has been enhanced to properly return the resource type to + ReferenceParam has been enhanced to properly return the resource type to user code in a server via the ReferenceType#getResourceType() method if the client has specified a reference parameter with a resource type. Thanks to @CarthageKing for the pull request! @@ -1019,7 +1031,7 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; Jiajing Liang for the pull request! - JAX-RS server now supports R4 and DSTU2_1 FHIR versions, which were + JAX-RS server now supports R4 and DSTU2_1 FHIR versions, which were previously missing. Thanks to Clayton Bodendein for the pull request! @@ -1042,7 +1054,7 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; JPA server sometimes updated resources even though the client - supplied an update with no actual changes in it, due to + supplied an update with no actual changes in it, due to changes in the metadata section being considered content changes. Thanks to Kyle Meadows for the pull request! @@ -1058,11 +1070,11 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; An unneccesary reference to the Javassist library has been - removed from the build. Thanks to Łukasz Dywicki for the + removed from the build. Thanks to Łukasz Dywicki for the pull request! - Support has been added to the JPA server for the :not modifier. Thanks + Support has been added to the JPA server for the :not modifier. Thanks to Łukasz Dywicki for the pull request! @@ -1115,7 +1127,7 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; When encoding URL parameter values, HAPI FHIR would incorrectly escape - a space (" ") as a plus ("+") insetad of as "%20" as required by + a space (" ") as a plus ("+") insetad of as "%20" as required by RFC 3986. This affects client calls, as well as URLs generated by the server (e.g. REST HOOK calls). Thanks to James Daily for reporting! @@ -1136,7 +1148,7 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; from INFO to DEBUG in order to reduce log noise - Fix an issue in JPA server where updating a resource sometimes caused date search indexes to + Fix an issue in JPA server where updating a resource sometimes caused date search indexes to be incorrectly deleted. Thanks to Kyle Meadows for the pull request! @@ -1180,8 +1192,8 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL;
]]> See the HAPI FHIR Android Documentation]]> - for more information. As a part of this fix, all dependencies on - the StAX API have been removed in environments where StAX is not + for more information. As a part of this fix, all dependencies on + the StAX API have been removed in environments where StAX is not present (such as Android). The client will now detect this case, and explicitly request JSON payloads from servers, meaning that Android clients no longer need to include two parser stacks @@ -1223,10 +1235,10 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; for more information.
- In certain cases in the JPA server, if multiple threads all attempted to + In certain cases in the JPA server, if multiple threads all attempted to update the same resource simultaneously, the optimistic lock failure caused a "gap" in the history numbers to occur. This would then cause a mysterious - failure when trying to update this resource further. This has been + failure when trying to update this resource further. This has been resolved. @@ -1240,7 +1252,7 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; The JPA server transaction operation (DSTU3/R4) did not correctly process the - If-Match header when passed in via + If-Match header when passed in via Bundle.entry.request.ifMatch]]> value @@ -1254,7 +1266,7 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; getCountSearchResultsUpTo()]]>. This setting governs how many search results the search coordinator should try to find before returning an initial - search response to the user, which has an effect on whether + search response to the user, which has an effect on whether the Bundle.total]]> field is always populated in search responses. This has now @@ -1276,7 +1288,7 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; as Android phones. - Restored the + Restored the org.hl7.fhir.r4.model.codesystem.*]]> classes (which are Java Enums for the various FHIR codesystems). These were accidentally removed in HAPI FHIR 3.0.0. Thanks to @@ -1295,7 +1307,7 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; Parsing a DSTU3/R4 custom structure which contained a field of - a custom type caused a crash during parsing. Thanks to + a custom type caused a crash during parsing. Thanks to GitHub user @mosaic-hgw for reporting! @@ -1316,12 +1328,13 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; Michael Lawley for the pull request! - Add Prefer and Cache-Control]]> to the list of headers which are declared as + Add Prefer and Cache-Control]]> to the list of headers which are declared + as being acceptable for CORS requests in CorsInterceptor, CLI, and JPA Example. Thanks to Patrick Werner for the pull request! - DSTU2-hl7org and DSTU2.1 structures did not copy resource IDs when invoking + DSTU2-hl7org and DSTU2.1 structures did not copy resource IDs when invoking copyValues(). Thanks to Clayton Bodendein for the pull request! @@ -1339,7 +1352,7 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; the raw binary with the server, as opposed to exchanging a FHIR resource). - When paging through multiple pages of search results, if the + When paging through multiple pages of search results, if the client had requested a subset of resources to be returned using the _elements]]> parameter, the elements list was lost after the first page of results. @@ -1367,8 +1380,8 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; A new client interceptor has been added called - AdditionalRequestHeadersInterceptor, which allows - a developer to add additional custom headers to a + AdditionalRequestHeadersInterceptor, which allows + a developer to add additional custom headers to a client requests. Thanks to Clayton Bodendein for the pull request! @@ -1387,7 +1400,7 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL; When a server method throws a DataFormatException, the error will now be converted into - an HTTP 400 instead of an HTTP 500 when returned to the client (and a stack + an HTTP 400 instead of an HTTP 500 when returned to the client (and a stack trace will now be returned to the client for JAX-RS server if configured to do so). Thanks to Clayton Bodendein for the pull request! @@ -1535,25 +1548,25 @@ Bundle bundle = client.search().forResource(Patient.class) JPA Subscription support has been refactored. A design contributed by Jeff Chung for the REST Hook subscription module has been ported - so that Websocket subscriptions use it too. This design uses an + so that Websocket subscriptions use it too. This design uses an interceptor to scan resources as they are processed to test whether they should be delivered to subscriptions, instead of using a - polling design. + polling design.
]]> In addition, this scanning has been reworked to happen in a separate - thread from the main storage thread, which should improve - performance and scalability of systems with multiple + thread from the main storage thread, which should improve + performance and scalability of systems with multiple subscriptions. Thanks to Jeff for all of his work on this!
hapi-fhir-client-okhttp project POM had dependencies on both - hapi-fhir-structures-dstu2 and hapi-fhir-structures-dstu3, which + hapi-fhir-structures-dstu2 and hapi-fhir-structures-dstu3, which meant that any project using ookhttp would import both structures JARs. This has been removed. JPA server is now able to handle placeholder IDs (e.g. urn:uuid:00....000) - being used in Bundle.entry.request.url as a part of the conditional URL + being used in Bundle.entry.request.url as a part of the conditional URL within transactions. @@ -1568,14 +1581,14 @@ Bundle bundle = client.search().forResource(Patient.class) When using the AuthorizationInterceptor with the JPA server, when a client is updating a resource - from A to B, the user now needs to have write permission for both A and B. This is particularly + from A to B, the user now needs to have write permission for both A and B. This is particularly important for cases where (for example) an Observation is being updated from having a subject of Patient/A to Patient/B. If the user has write permission for Patient/B's compartment, this would previously have been allowed even if the user did not have access to write to Patient/A's compartment. Thanks to Eeva Turkka for reporting! - IServerOperationInterceptor now has a new method + IServerOperationInterceptor now has a new method resourceUpdated(RequestDetails, IBaseResource, IBaseResource)]]> which replaces the previous resourceUpdated(RequestDetails, IBaseResource)]]>. This allows @@ -1592,7 +1605,7 @@ Bundle bundle = client.search().forResource(Patient.class) GET /Observation?value-quantity=]]> - JPA server transaction processing now honours the Prefer header and includes + JPA server transaction processing now honours the Prefer header and includes created and updated resource bodies in the response bundle if it is set appropriately. @@ -1603,12 +1616,12 @@ Bundle bundle = client.search().forResource(Patient.class) Add configuration to JPA server DaoConfig that allows a maximum number of search results to be specified. Queries will never return - more than this number, which can be good for avoiding accidental - performance problems in situations where large queries should not be + more than this number, which can be good for avoiding accidental + performance problems in situations where large queries should not be needed - Prevent duplicates in $everything query response in JPA server. Thanks to @vlad-ignatov + Prevent duplicates in $everything query response in JPA server. Thanks to @vlad-ignatov for reporting! @@ -1631,7 +1644,7 @@ Bundle bundle = client.search().forResource(Patient.class) JPA servers with no paging provider configured, or with a paging provider other than - DatabaseBackedPagingProvider will load all results in a single pass and keep them + DatabaseBackedPagingProvider will load all results in a single pass and keep them in memory. Using this setup is not a good idea unless you know for sure that you will never have very large queries since it means that all results will be loaded into memory, but there are valid reasons to need this and it will perform better than @@ -1649,7 +1662,7 @@ Bundle bundle = client.search().forResource(Patient.class) is 1000. - When executing a search (HTTP GET) as a nested operation in in a transaction or + When executing a search (HTTP GET) as a nested operation in in a transaction or batch operation, the search now returns a normal page of results with a link to the next page, like any other search would. Previously the search would return a small number of results with no paging performed, so this change brings transaction @@ -1680,7 +1693,7 @@ Bundle bundle = client.search().forResource(Patient.class) Sopin for reporting! - JpaConformanceProvider now has a configuration setting to enable and + JpaConformanceProvider now has a configuration setting to enable and disable adding resource counts to the server metadata. @@ -1696,14 +1709,14 @@ Bundle bundle = client.search().forResource(Patient.class) When the server was returning a multi-page search result where the - client did not explicitly request an encoding via the _format + client did not explicitly request an encoding via the _format parameter, a _format parameter was incorrectly added to the paging links in the response Bundle. This would often explicitly request XML encoding because of the browser Accept header even though this was not what the client wanted. - Enhancement to ResponseHighlighterInterceptor where links in the resource + Enhancement to ResponseHighlighterInterceptor where links in the resource body are now converted to actual clickable hyperlinks. Thanks to Eugene Lubarsky for the pull request! @@ -1722,7 +1735,7 @@ Bundle bundle = client.search().forResource(Patient.class) Fix an issue in HapiWorkerContext where structure definitions are - not able to be retrieved if they are referred to by their + not able to be retrieved if they are referred to by their relative or logical ID. This affects profile tooling such as StructureMapUtilities. Thanks to Travis Lukach for reporting and providing a test case! @@ -1741,7 +1754,7 @@ Bundle bundle = client.search().forResource(Patient.class) for the pull request! - Correct an issue with the model classes for STU3 where any classes + Correct an issue with the model classes for STU3 where any classes containing the @ChildOrder annotation (basically the conformance resources) will not correctly set the order if any of the elements are a choice type (i.e. named "foo[x]"). Thanks to @@ -1754,7 +1767,7 @@ Bundle bundle = client.search().forResource(Patient.class) JPA server transaction operations now put OperationOutcome resources resulting - from actions in + from actions in Bundle.entry.response.outcome]]> instead of the previous Bundle.entry.resource]]> @@ -1774,7 +1787,7 @@ Bundle bundle = client.search().forResource(Patient.class) to Sébastien Rivière for the pull request! - Testing UI now has a dropdown for modifiers on token search. Thanks + Testing UI now has a dropdown for modifiers on token search. Thanks to GitHub user @dconlan for the pull request! @@ -1796,7 +1809,7 @@ Bundle bundle = client.search().forResource(Patient.class) Pascal Brandt for the pull request! - Fix incorrect FHIR Version Strings that were being outputted and verified in the + Fix incorrect FHIR Version Strings that were being outputted and verified in the client for some versions of FHIR. Thanks to Clayton Bodendein for the pull request! @@ -1822,11 +1835,12 @@ Bundle bundle = client.search().forResource(Patient.class) Add some browser performance logging to ResponseHighlightingInterceptor. Thanks - to Eugene Lubarsky for the pull request, and for convincing James not to + to Eugene Lubarsky for the pull request, and for convincing James not to optimize something that did not need optimizing! - A new config property has been added to the JPA seerver DaoConfig called "setAutoCreatePlaceholderReferenceTargets". + A new config property has been added to the JPA seerver DaoConfig called + "setAutoCreatePlaceholderReferenceTargets". This property causes references to unknown resources in created/updated resources to have a placeholder target resource automatically created. @@ -1859,12 +1873,12 @@ Bundle bundle = client.search().forResource(Patient.class) with the same URI as the previous one - When uploading a Bundle resource to the server (as a collection or + When uploading a Bundle resource to the server (as a collection or document, not as a transaction) the ID was incorrectly stripped from resources being saved within the Bundle. This has been corrected. - Subscriptions in JPA server now support "email" delivery type through the + Subscriptions in JPA server now support "email" delivery type through the use of a new interceptor which handles that type @@ -1940,7 +1954,7 @@ Bundle bundle = client.search().forResource(Patient.class) AuthorizationInterceptor did not correctly handle paging requests - (e.g. requests for the second page of results for a search operation). + (e.g. requests for the second page of results for a search operation). Thanks to Eeva Turkka for reporting! @@ -1948,7 +1962,7 @@ Bundle bundle = client.search().forResource(Patient.class) allow client code to change unknown extension handling behaviour. - Fix concurrency issues in FhirContext that were causing issues when + Fix concurrency issues in FhirContext that were causing issues when starting a context up on Android. Thanks to GitHub issue @Jaypeg85 for the pull request! @@ -1968,8 +1982,8 @@ Bundle bundle = client.search().forResource(Patient.class) types in the type= property - JSON Parser gave a very unhelpful error message (Unknown attribute 'value' found during parse) - when a scalar value was found in a spot where an object is expected. This has been corrected to + JSON Parser gave a very unhelpful error message (Unknown attribute 'value' found during parse) + when a scalar value was found in a spot where an object is expected. This has been corrected to include much more information. Thanks to GitHub user @jasminas for reporting! @@ -1983,7 +1997,7 @@ Bundle bundle = client.search().forResource(Patient.class) path pointed to an extension, where the client used a chained value. - Fix issue where the JSON parser sometimes did not encode DSTU3 extensions on the root of a + Fix issue where the JSON parser sometimes did not encode DSTU3 extensions on the root of a resource which have a value of type reference. @@ -2019,7 +2033,7 @@ Bundle bundle = client.search().forResource(Patient.class) an older version of commons-codec. - JPA server failed to index search parameters on paths containing a decimal + JPA server failed to index search parameters on paths containing a decimal data type @@ -2037,14 +2051,14 @@ Bundle bundle = client.search().forResource(Patient.class) just wasted space. - Loading the build-in profile structures (StructureDefinition, ValueSet, etc) is now done in + Loading the build-in profile structures (StructureDefinition, ValueSet, etc) is now done in a synchronized block in order to prevent multiple loads happening if the server processes multiple validations in parallel threads right after startup. Previously a heavy load could cause the server to run out of memory and lock up. Thanks to Karl M Davis for analysis and help fixing this! - Fix bad ValueSet URL in DeviceRequest profile definition for STU3 which + Fix bad ValueSet URL in DeviceRequest profile definition for STU3 which was preventing the CLI from uploading definitions correctly. Thanks to Joel Schneider for the Pull Request! @@ -2065,7 +2079,7 @@ Bundle bundle = client.search().forResource(Patient.class) This release brings the DSTU3 structures up to FHIR R3 (FHIR 3.0.1) definitions. Note that there are very few changes between the DSTU3 structures in HAPI FHIR 2.3 and the ones in HAPI FHIR 2.4 since the basis for the DSTU3 structures in HAPI FHIR - 2.3 was the R3 QA FHIR version (1.9.0) but this is the first release of + 2.3 was the R3 QA FHIR version (1.9.0) but this is the first release of HAPI FHIR to support the final/complete R3 release. @@ -2085,13 +2099,13 @@ Bundle bundle = client.search().forResource(Patient.class) ]]> - hapi-fhir-jpaserver-example now includes the - Prefer]]> header in the list of + hapi-fhir-jpaserver-example now includes the + Prefer]]> header in the list of CORS headers. Thanks to GitHub user @elnin0815 for the pull request! - AuthorizationInterceptor can now allow make read or write + AuthorizationInterceptor can now allow make read or write authorization decisions on a resource by instance ID @@ -2125,8 +2139,9 @@ Bundle bundle = client.search().forResource(Patient.class) URLs. Thanks to Eeva Turkka for the suggestion! - Add a utility method to JPA server: - IFhirResourceDao#removeTag(IIdType, TagTypeEnum, String, String)]]>. This allows client code to remove tags + Add a utility method to JPA server: + IFhirResourceDao#removeTag(IIdType, TagTypeEnum, String, String)]]>. This allows + client code to remove tags from a resource without having a servlet request object in context. @@ -2153,7 +2168,7 @@ Bundle bundle = client.search().forResource(Patient.class) In JAX-RS server it is now possible to change the server exception handler at runtime without a server restart. - Thanks to Sebastien Riviere for the + Thanks to Sebastien Riviere for the pull request! @@ -2191,7 +2206,7 @@ Bundle bundle = client.search().forResource(Patient.class) Server interceptor methods were being called twice unnecessarily - by the JPA server, and the DaoConfig interceptor registration + by the JPA server, and the DaoConfig interceptor registration framework was not actually useful. Thanks to GitHub user @mattiuusitalo for reporting! @@ -2203,15 +2218,15 @@ Bundle bundle = client.search().forResource(Patient.class) Eeva Turkka for reporting! - JPA server exported CapabilityStatement includes + JPA server exported CapabilityStatement includes double entries for the _id parameter and uses the - wrong type (string instead of token). Thanks to + wrong type (string instead of token). Thanks to Robert Lichtenberger for reporting! Custom resource types which extend Binary must not - have declared extensions since this is invalid in - FHIR (and HAPI would just ignore them anyhow). Thanks + have declared extensions since this is invalid in + FHIR (and HAPI would just ignore them anyhow). Thanks to Thomas S Berg for reporting! @@ -2220,16 +2235,16 @@ Bundle bundle = client.search().forResource(Patient.class) this out! - Server AuthorizationInterceptor always rejects history operation + Server AuthorizationInterceptor always rejects history operation at the type level even if rules should allow it. - JPA server terminology service was not correctly validating or expanding codes + JPA server terminology service was not correctly validating or expanding codes in SNOMED CT or LOINC code systems. Thanks to David Hay for reporting! - Attempting to search for an invalid resource type (e.g. GET base/FooResource) should - return an HTTP 404 and not a 400, per the HTTP spec. Thanks to + Attempting to search for an invalid resource type (e.g. GET base/FooResource) should + return an HTTP 404 and not a 400, per the HTTP spec. Thanks to GitHub user @CarthageKing for the pull request! @@ -2252,15 +2267,15 @@ Bundle bundle = client.search().forResource(Patient.class) an &rsquot;]]> entity string. - When parsing a quantity parameter on the server with a - value and units but no system (e.g. + When parsing a quantity parameter on the server with a + value and units but no system (e.g. GET [base]/Observation?value=5.4||mg]]>) the unit was incorrectly treated as the system. Thanks to @CarthageKing for the pull request! Correct a typo in the JPA ValueSet ResourceProvider which prevented - successful operation under Spring 4.3. Thanks to + successful operation under Spring 4.3. Thanks to Robbert van Waveren for the pull request! @@ -2281,8 +2296,8 @@ Bundle bundle = client.search().forResource(Patient.class) @sekaijin for the pull request! - When performing a conditional create in a transaction in JPA server, - if a resource already existed matching the conditional expression, the + When performing a conditional create in a transaction in JPA server, + if a resource already existed matching the conditional expression, the server did not change the version of the resource but did update the body with the passed in body. Thanks to Artem Sopin for reporting and providing a test case for this! @@ -2298,23 +2313,23 @@ Bundle bundle = client.search().forResource(Patient.class) Fix an issue in JPA server where _history results were kept in memory instead of being spooled to the database as they should be. Note that as a part of this fix - a new method was added to + a new method was added to IBundleProvider called getUuid()]]>. This method may return null]]> in any current cases. - Expanding a ValueSet in JPA server did not correctly apply + Expanding a ValueSet in JPA server did not correctly apply ?filter=]]> parameter when the ValueSet - being expanded had codes included explicitly (i.e. not by + being expanded had codes included explicitly (i.e. not by is-a relationship). Thanks to David Hay for reporting! JPA validator incorrectly returned an HTTP 400 instead of an HTTP 422 when - the resource ID was not present and required, or vice versa. Thanks to + the resource ID was not present and required, or vice versa. Thanks to Brian Postlethwaite for reporting! - When using an annotation based client, a ClassCastException would + When using an annotation based client, a ClassCastException would occur under certain circumstances when the response contained contained resources @@ -2340,7 +2355,7 @@ Bundle bundle = client.search().forResource(Patient.class) Parser can now be configured when encoding to use a specific - base URL for extensions. Thanks to Sebastien Riviere for the + base URL for extensions. Thanks to Sebastien Riviere for the pull request! @@ -2378,13 +2393,13 @@ Bundle bundle = client.search().forResource(Patient.class) ]]> - + Fix issue in AuthorizationIntetceptor where transactions are blocked even when they should not be - Fix regression in HAPI FHIR 2.1 JPA + Fix regression in HAPI FHIR 2.1 JPA server where some search parameters on metadata resources did not appear (e.g. "StructureDefinition.url"). Thanks @@ -2401,7 +2416,7 @@ Bundle bundle = client.search().forResource(Patient.class) to GitHub user @vijayt27 for reporting! - As the + As the eBay CORS interceptor]]> project has gone dormant, we have introduced a new @@ -2458,7 +2473,7 @@ Bundle bundle = client.search().forResource(Patient.class) When parsing invalid enum values in STU3, report errors through the parserErrorHandler, - not by throwing an exception. Thanks to + not by throwing an exception. Thanks to Michael Lawley for the pull request! @@ -2475,7 +2490,7 @@ Bundle bundle = client.search().forResource(Patient.class) that servers which receive an invalid enum velue will return an HTTP 400 instead of an HTTP 500. Thanks to Jim Steel for reporting! - + DSTU3 context now pulls the FHIR version from the actual model classes. Thanks to Michael Lawley for the pull request! @@ -2495,11 +2510,11 @@ Bundle bundle = client.search().forResource(Patient.class)
  • Normalization of properties across all three generic tasks
  • ]]> -
    - - Fix ordering of validator property handling when an element - has a name that is similar to a shorter name[x] style name. - Thanks to CarthageKing for the pull request! + + + Fix ordering of validator property handling when an element + has a name that is similar to a shorter name[x] style name. + Thanks to CarthageKing for the pull request! Add a docker configuration to the hapi-fhir-jpaservr-example @@ -2516,12 +2531,14 @@ Bundle bundle = client.search().forResource(Patient.class) a test case! - Correct a typo in client + Correct a typo in client IHttpRequest]]> class: "bufferEntitity" should be "bufferEntity". - ErrorHandler is now called (resulting in a warning by default, but can also be an exception) when arsing JSON if - the resource ID is not a JSON string, or an object is found where an array is expected (e.g. repeating field). Thanks + ErrorHandler is now called (resulting in a warning by default, but can also be an exception) when arsing + JSON if + the resource ID is not a JSON string, or an object is found where an array is expected (e.g. repeating + field). Thanks to Jenni Syed of Cerner for providing a test case! @@ -2539,7 +2556,7 @@ Bundle bundle = client.search().forResource(Patient.class) Server interceptor methods were being called twice unnecessarily - by the JPA server, and the DaoConfig interceptor registration + by the JPA server, and the DaoConfig interceptor registration framework was not actually useful. Thanks to GitHub user @mattiuusitalo for reporting! @@ -2551,15 +2568,15 @@ Bundle bundle = client.search().forResource(Patient.class) Eeva Turkka for reporting!
    - JPA server exported CapabilityStatement includes + JPA server exported CapabilityStatement includes double entries for the _id parameter and uses the - wrong type (string instead of token). Thanks to + wrong type (string instead of token). Thanks to Robert Lichtenberger for reporting! Custom resource types which extend Binary must not - have declared extensions since this is invalid in - FHIR (and HAPI would just ignore them anyhow). Thanks + have declared extensions since this is invalid in + FHIR (and HAPI would just ignore them anyhow). Thanks to Thomas S Berg for reporting! @@ -2568,16 +2585,16 @@ Bundle bundle = client.search().forResource(Patient.class) this out! - Server AuthorizationInterceptor always rejects history operation + Server AuthorizationInterceptor always rejects history operation at the type level even if rules should allow it. - JPA server terminology service was not correctly validating or expanding codes + JPA server terminology service was not correctly validating or expanding codes in SNOMED CT or LOINC code systems. Thanks to David Hay for reporting! - Attempting to search for an invalid resource type (e.g. GET base/FooResource) should - return an HTTP 404 and not a 400, per the HTTP spec. Thanks to + Attempting to search for an invalid resource type (e.g. GET base/FooResource) should + return an HTTP 404 and not a 400, per the HTTP spec. Thanks to GitHub user @CarthageKing for the pull request! @@ -2600,15 +2617,15 @@ Bundle bundle = client.search().forResource(Patient.class) an &rsquot;]]> entity string. - When parsing a quantity parameter on the server with a - value and units but no system (e.g. + When parsing a quantity parameter on the server with a + value and units but no system (e.g. GET [base]/Observation?value=5.4||mg]]>) the unit was incorrectly treated as the system. Thanks to @CarthageKing for the pull request! Correct a typo in the JPA ValueSet ResourceProvider which prevented - successful operation under Spring 4.3. Thanks to + successful operation under Spring 4.3. Thanks to Robbert van Waveren for the pull request! @@ -2627,8 +2644,8 @@ Bundle bundle = client.search().forResource(Patient.class)
    - STU3 structure definitions have been updated to the - STU3 latest definitions (1.7.0 - SVN 10129). In + STU3 structure definitions have been updated to the + STU3 latest definitions (1.7.0 - SVN 10129). In particular, this version supports the new CapabilityStatement resource which replaces the previous Conformance resource (in order to reduce upgrade pain, both resource @@ -2644,18 +2661,20 @@ Bundle bundle = client.search().forResource(Patient.class) ]]> - Fix a fairly significant issue in JPA Server when using the DatabaseBackedPagingProvider]]>: When paging over the results - of a search / $everything operation, under certain circumstances resources may be missing from the last page of results + Fix a fairly significant issue in JPA Server when using the + DatabaseBackedPagingProvider]]>: When paging over the results + of a search / $everything operation, under certain circumstances resources may be missing from the last page + of results that is returned. Thanks to David Hay for reporting! Client, Server, and JPA server now support experimental support - for + for using the XML Patch and JSON Patch syntax as explored during the - September 2016 Baltimore Connectathon. See + September 2016 Baltimore Connectathon. See this wiki page]]> - for a description of the syntax. + for a description of the syntax. ]]> Thanks to Pater Girard for all of his help during the connectathon in implementing this feature! @@ -2667,20 +2686,20 @@ Bundle bundle = client.search().forResource(Patient.class) Both client and server now use the new STU3 mime types by default - if running in STU3 mode (in other words, using an STU3 + if running in STU3 mode (in other words, using an STU3 FhirContext). In server, when returning a list of resources, the server sometimes failed to add _include]]> resources to the response bundle if they were - referred to by a contained reosurce. Thanks to Neal Acharya for reporting! + referred to by a contained reosurce. Thanks to Neal Acharya for reporting! Fix regression in web testing UI where "prev" and "next" buttons don't work when showing a result bundle - JPA server should not attempt to resolve built-in FHIR StructureDefinitions from the + JPA server should not attempt to resolve built-in FHIR StructureDefinitions from the database (this causes a significant performance hit when validating) @@ -2707,7 +2726,7 @@ Bundle bundle = client.search().forResource(Patient.class) STU3 servers were adding the old MimeType - strings to the + strings to the Conformance.format]]> part of the generated server conformance statement @@ -2719,23 +2738,23 @@ Bundle bundle = client.search().forResource(Patient.class) to Filip Domazet for reporting! - STU clients now use an Accept header which + STU clients now use an Accept header which indicates support for both the old MimeTypes (e.g. application/xml+fhir]]>) and the new MimeTypes (e.g. application/fhir+xml]]>) - JPA server now sends correct - HTTP 409 Version Conflict]]> - when a + JPA server now sends correct + HTTP 409 Version Conflict]]> + when a DELETE fails because of constraint issues, instead of - HTTP 400 Invalid Request]]> + HTTP 400 Invalid Request]]> Server history operation did not populate the Bundle.entry.request.url field, which is required in order for the bundle to pass validation. - Thanks to Richard Ettema for spotting this! + Thanks to Richard Ettema for spotting this! Add a new method to the server interceptor framework which will be @@ -2773,20 +2792,20 @@ Bundle bundle = client.search().forResource(Patient.class) XhtmlNode.equalsDeep() contained a bug which caused resources containing a narrative to always return - false]]> for STU3 - Resource#equalsDeep()]]>. Thanks to + false]]> for STU3 + Resource#equalsDeep()]]>. Thanks to GitHub user @XcrigX for reporting! JPA server did not correctly process searches for chained parameters where the chain passed across a field that was a choice between a - reference and a non-reference type (e.g. - MedicationAdministration.medication[x]]]>. + reference and a non-reference type (e.g. + MedicationAdministration.medication[x]]]>. Thanks to GitHub user @Crudelus for reporting! Handle parsing an extension without a URL more gracefully. In HAPI FHIR 2.0 this caused - a NullPointerException to be thrown. Now it will trigger a warning, or throw a + a NullPointerException to be thrown. Now it will trigger a warning, or throw a DataFormatException if the StrictErrorHandler is configured on the parser. @@ -2800,7 +2819,8 @@ Bundle bundle = client.search().forResource(Patient.class) Kevin Tallevi for finding this! - Fix #411 - Searching by POST [base]/_search]]> with urlencoded parameters doesn't work correctly if + Fix #411 - Searching by POST [base]/_search]]> with urlencoded parameters doesn't work + correctly if interceptors are accessing the parameters and there is are also parameters on the URL. Thanks to Jim Steel for reporting! @@ -2815,7 +2835,7 @@ Bundle bundle = client.search().forResource(Patient.class) JPA server was not correctly normalizing strings with non-latin characters (e.g. Chinese chars). Thanks to GitHub user @YinAqu for reporting and providing - some great analysis of the issue! + some great analysis of the issue! Add a new method to ReferenceClientParam which allows you to @@ -2824,7 +2844,7 @@ Bundle bundle = client.search().forResource(Patient.class) When encoding a resource in JSON where the resource has - an extension with a value where the value is a reference to a + an extension with a value where the value is a reference to a contained resource, the reference value (e.g. "#1") did not get serialized. Thanks to GitHub user @fw060 for reporting! @@ -2832,7 +2852,7 @@ Bundle bundle = client.search().forResource(Patient.class) ResponseHighlighterInterceptor now pretty-prints responses by default unless the user has explicitly requested a non-pretty-printed response (ie. - using ?_pretty=false]]>. Thanks to + using ?_pretty=false]]>. Thanks to Allan Brohansen and Jens Villadsen for the suggestion! @@ -2862,12 +2882,12 @@ Bundle bundle = client.search().forResource(Patient.class) instance of a given type - STU3 servers were incorrectly returning the + STU3 servers were incorrectly returning the Content-Location]]> header instead of the Content]]> - header. The former has been removed from the - FHIR specification in STU3, but the + header. The former has been removed from the + FHIR specification in STU3, but the latter got removed in HAPI's code base. Thanks to Jim Steel for reporting! @@ -2908,20 +2928,21 @@ Bundle bundle = client.search().forResource(Patient.class) ]]> - STU3 structure definitions have been updated to the + STU3 structure definitions have been updated to the STU3 ballot candidate versions (1.6.0 - SVN 9663) Both client and server now support the new Content Types decided in - FHIR #10199]]>. + FHIR #10199]]> + .
    ]]> - This means that the server now supports + This means that the server now supports application/fhir+xml and application/fhir+json]]> in addition to the older style application/xml+fhir and application/json+fhir]]>. In order to facilitate migration by implementors, the old style remains the default for now, but the server will respond using the new style if the request contains it. The - client now uses an Accept]]> header value which requests both + client now uses an Accept]]> header value which requests both styles with a preference given to the new style when running in DSTU3 mode.
    ]]> As a part of this change, the server has also been enhanced so that if a request @@ -2946,7 +2967,7 @@ Bundle bundle = client.search().forResource(Patient.class) ]]>
    - + Fix issue in DSTU1 Bundle parsing where unexpected elements in the bundle resulted in a failure to parse. @@ -2980,14 +3001,14 @@ Bundle bundle = client.search().forResource(Patient.class) for the pull request! - hapi-fhir-testpage-overlay project contained an unneccesary - dependency on hapi-fhir-jpaserver-base module, which resulted in + hapi-fhir-testpage-overlay project contained an unneccesary + dependency on hapi-fhir-jpaserver-base module, which resulted in projects using the overlay having a large number of unnneded JARs included It is not possible to configure both the parser and the context to - preserve versions in resource references (default behaviour is to + preserve versions in resource references (default behaviour is to strip versions from references). Thanks to GitHub user @cknaap for the suggestion! @@ -2996,7 +3017,7 @@ Bundle bundle = client.search().forResource(Patient.class) set. Thanks to Tim Tschampel for reporting! - JPA server's /Bundle]]> endpoint cleared + JPA server's /Bundle]]> endpoint cleared the Bundle.entry.fullUrl]]> field on stored bundles, resulting in invalid content being saved. Thanks to Mirjam Baltus for reporting! @@ -3011,12 +3032,12 @@ Bundle bundle = client.search().forResource(Patient.class) a custom type did not automatically parse into that type. - Allow servers to specify the authentication realm of their choosing when + Allow servers to specify the authentication realm of their choosing when throwing an AuthenticationException. Thanks to GitHub user @allanbrohansen for the suggestion! - Add a new client implementation which uses the + Add a new client implementation which uses the OkHttp]]> library as the HTTP client implementation (instead of Apache HttpClient). This is particularly useful for Android (where HttpClient is a pain) but @@ -3025,7 +3046,7 @@ Bundle bundle = client.search().forResource(Patient.class) Fix a regression when parsing resources that have contained - resources, where the reference in the outer resource which + resources, where the reference in the outer resource which links to the contained resource sometimes did does not get populated with the actual target resource instance. Thanks to Neal Acharya for reporting! @@ -3050,14 +3071,14 @@ Bundle bundle = client.search().forResource(Patient.class) update]]> operations. This change has been made because the FHIR specification now requires servers to ignore these values. Note that as a result of this change, resources passed - to @Update]]> methods will always have + to @Update]]> methods will always have null]]> ID - Add new methods to + Add new methods to AuthorizationInterceptor]]> which allow user code to declare support for conditional - create, update, and delete. + create, update, and delete. When encoding a resource with a reference to another resource @@ -3065,8 +3086,8 @@ Bundle bundle = client.search().forResource(Patient.class) was incorrectly stripped from the reference. - Servers for STU3 (or newer) will no longer include a - Location:]]> header on responses for + Servers for STU3 (or newer) will no longer include a + Location:]]> header on responses for read]]> operations. This header was required in earlier versions of FHIR but has been removed from the specification. @@ -3086,7 +3107,7 @@ Bundle bundle = client.search().forResource(Patient.class) Parser failed to parse resources containing an extension with a value type of - "id". Thanks to Raphael Mäder for reporting! + "id". Thanks to Raphael Mäder for reporting! When committing a transaction in JPA server @@ -3097,7 +3118,7 @@ Bundle bundle = client.search().forResource(Patient.class) HAPI root pom shouldn't include animal-sniffer plugin, - since that causes any projects which extend this to + since that causes any projects which extend this to be held to Java 6 compliance.
    @@ -3155,11 +3176,11 @@ Bundle bundle = client.search().forResource(Patient.class) REST server now throws an HTTP 400 instead of an HTTP 500 if an operation which takes a FHIR resource in the request body (e.g. create, update) contains invalid content that - the parser is unable to parse. Thanks to Jim Steel for the suggestion! + the parser is unable to parse. Thanks to Jim Steel for the suggestion! Deprecate fluent client search operations without an explicit declaration of the - bundle type being used. This also means that in a client + bundle type being used. This also means that in a client .search()]]> operation, the .returnBundle(Bundle.class)]]> @@ -3169,7 +3190,7 @@ Bundle bundle = client.search().forResource(Patient.class) Server now respects the parameter _format=application/xml+fhir"]]> which is technically invalid since the + should be escaped, but is likely to be used. Also, - a parameter of _format=html]]> can now be used, which + a parameter of _format=html]]> can now be used, which forces SyntaxHighlightingInterceptor to use HTML even if the headers wouldn't otherwise trigger it. Thanks to Jim Steel for reporting! @@ -3197,7 +3218,7 @@ Bundle bundle = client.search().forResource(Patient.class) fixing the choice to a single type, the parser would forget that the field was a choice and would use the wrong name (e.g. "abatement" instead of "abatementDateType"). Thanks to Yaroslav Kovbas for reporting and - providing a unit test! + providing a unit test! JPA server transactions sometimes created an incorrect resource reference @@ -3209,16 +3230,16 @@ Bundle bundle = client.search().forResource(Patient.class) Prefer: return=representation]]> set, if the server does not honour the Prefer header, the client will automatically fetch the resource before returning. Thanks - to Ewout Kramer for the idea! + to Ewout Kramer for the idea! - DSTU3 structures now have + DSTU3 structures now have setFoo(List)]]> - and + and setGetFooFirstRep()]]> methods, bringing them back to parity with the HAPI DSTU2 structures. Thanks to Rahul Somasunderam and - Claude Nanjo for the suggestions! + Claude Nanjo for the suggestions! JPA server has now been refactored to use the @@ -3232,7 +3253,7 @@ Bundle bundle = client.search().forResource(Patient.class) processing to be aborted. - LoggingInterceptor on server has a new parameter + LoggingInterceptor on server has a new parameter ${requestBodyFhir}]]> which logs the entire request body. @@ -3280,7 +3301,7 @@ Bundle bundle = client.search().forResource(Patient.class) When encoding JSON resource, the parser will now always - ensure that XHTML narrative content has an + ensure that XHTML narrative content has an XHTML namespace declaration on the first DIV tag. This was preventing validation for some resources using the official validator @@ -3288,7 +3309,7 @@ Bundle bundle = client.search().forResource(Patient.class) Server failed to invoke operations when the name - was escaped (%24execute instead of $execute). + was escaped (%24execute instead of $execute). Thanks to Michael Lawley for reporting! @@ -3308,7 +3329,7 @@ Bundle bundle = client.search().forResource(Patient.class) When updating a resource via an update operation on the server, if the ID of the resource is not present in the resource body but is present on the URL, this will now be treated as a warning instead of as a failure in order to be a bit more - tolerant of errors. If the ID is present in the body but does not agree with the + tolerant of errors. If the ID is present in the body but does not agree with the ID in the URL this remains an error. @@ -3329,9 +3350,9 @@ Bundle bundle = client.search().forResource(Patient.class) JPA server can now be configured to allow external references (i.e. references that - point to resources on other servers). See + point to resources on other servers). See JPA Documentation]]> for information on - how to use this. Thanks to Naminder Soorma for the suggestion! + how to use this. Thanks to Naminder Soorma for the suggestion! When posting a resource to a server that contains an invalid value in a boolean field @@ -3344,11 +3365,11 @@ Bundle bundle = client.search().forResource(Patient.class) here]]> - JSON parser was incorrectly encoding resource language attribute in JSON as an - array instead of a string. Thanks to David Hay for reporting! + JSON parser was incorrectly encoding resource language attribute in JSON as an + array instead of a string. Thanks to David Hay for reporting! - Sébastien Rivière contributed an excellent pull request which adds a + Sébastien Rivière contributed an excellent pull request which adds a number of enhancements to JAX-RS module: @@ -3360,12 +3381,12 @@ Bundle bundle = client.search().forResource(Patient.class) ]]> - FhirTerser.cloneInto method failed to clone correctly if the source + FhirTerser.cloneInto method failed to clone correctly if the source had any extensions. Thanks to GitHub user @Virdulys for submitting and providing a test case! - Update DSTU2 InstanceValidator to latest version from upstream + Update DSTU2 InstanceValidator to latest version from upstream Web Testing UI was not able to correctly post an STU3 transaction @@ -3390,7 +3411,8 @@ Bundle bundle = client.search().forResource(Patient.class) reporting! - Update STU3 client and server to use the new sort parameter style (param1,-param2,param). Thanks to GitHub user @euz1e4r for + Update STU3 client and server to use the new sort parameter style (param1,-param2,param). Thanks to GitHub + user @euz1e4r for reporting! @@ -3399,7 +3421,7 @@ Bundle bundle = client.search().forResource(Patient.class) Fluent client searches with date parameters were not correctly using - new prefix style (e.g. gt) instead of old one (e.g. >) + new prefix style (e.g. gt) instead of old one (e.g. >) Some built-in v3 code systems for STU3 resources were missing @@ -3416,7 +3438,7 @@ Bundle bundle = client.search().forResource(Patient.class) on Zulip with Grahame that requests that have a parameter of _format=json]]> or _format=xml]]> will output raw FHIR content - instead of HTML highlighting the content as they previously did. + instead of HTML highlighting the content as they previously did. HTML content can now be forced via the (previously existing) _format=html]]> or via the two newly added values @@ -3452,7 +3474,7 @@ Bundle bundle = client.search().forResource(Patient.class) for reporting! - Server parameters annotated with + Server parameters annotated with @Since]]> or @Count]]> @@ -3463,7 +3485,7 @@ Bundle bundle = client.search().forResource(Patient.class) the way other server parameters worked. - Server now supports the _at parameter (including multiple repetitions) + Server now supports the _at parameter (including multiple repetitions) for history operation Add server interceptor framework, and new interceptor for logging incoming - requests. + requests. Add server validation framework for validating resources against the FHIR schemas and schematrons Tester UI created double _format and _pretty param entries in searches. Thanks to Gered King of University - Health Network for reporting! + Health Network for reporting! Create method was incorrectly returning an HTTP 204 on sucessful completion, but - should be returning an HTTP 200 per the FHIR specification. Thanks to wanghaisheng - for reporting! + should be returning an HTTP 200 per the FHIR specification. Thanks to wanghaisheng + for reporting! FHIR Tester UI now correctly sends UTF-8 charset in responses so that message payloads containing @@ -5450,7 +5490,7 @@ Bundle bundle = client.search().forResource(Patient.class) Orion for reporting this! - Contained/included resource instances received by a client are now automatically + Contained/included resource instances received by a client are now automatically added to any ResourceReferenceDt instancea in other resources which reference them. @@ -5465,7 +5505,7 @@ Bundle bundle = client.search().forResource(Patient.class) so the resource language was difficult to access. - JSON Parser now gives a more friendly error message if it tries to parse JSON with invalid use + JSON Parser now gives a more friendly error message if it tries to parse JSON with invalid use of single quotes @@ -5491,11 +5531,11 @@ Bundle bundle = client.search().forResource(Patient.class) Date/time types did not correctly parse values in the format "yyyymmdd" (although the FHIR-defined format is "yyyy-mm-dd" anyhow, and this is correctly handled). Thanks to Jeffrey Ting of Systems Made Simple - for reporting! + for reporting! Server search method for an unnamed query gets called if the client requests a named query - with the same parameter list. Thanks to Neal Acharya of University Health Network for reporting! + with the same parameter list. Thanks to Neal Acharya of University Health Network for reporting! Category header (for tags) is correctly read in client for "read" operation @@ -5517,17 +5557,19 @@ Bundle bundle = client.search().forResource(Patient.class) Rename NotImpementedException to NotImplementedException (to correct typo) - Server setUseBrowserFriendlyContentType setting also respected for errors (e.g. OperationOutcome with 4xx/5xx status) + Server setUseBrowserFriendlyContentType setting also respected for errors (e.g. OperationOutcome with + 4xx/5xx status) Fix performance issue in date/time datatypes where pattern matchers were not static - Server now gives a more helpful error message if a @Read method has a search parameter (which is invalid, but + Server now gives a more helpful error message if a @Read method has a search parameter (which is invalid, + but previously lead to a very unhelpful error message). Thanks to Tahura Chaudhry of UHN for reporting! - Resource of type "List" failed to parse from a bundle correctly. Thanks to David Hay of Orion Health + Resource of type "List" failed to parse from a bundle correctly. Thanks to David Hay of Orion Health for reporting! @@ -5535,18 +5577,18 @@ Bundle bundle = client.search().forResource(Patient.class) If a server defines a method with parameter "_id", incoming search requests for that method may - get delegated to the wrong method. Thanks to Neal Acharya for reporting! + get delegated to the wrong method. Thanks to Neal Acharya for reporting! - SecurityEvent.Object structural element has been renamed to - SecurityEvent.ObjectElement to avoid conflicting names with the + SecurityEvent.Object structural element has been renamed to + SecurityEvent.ObjectElement to avoid conflicting names with the java Object class. Thanks to Laurie Macdougall-Sookraj of UHN for - reporting! + reporting! Text/narrative blocks that were created with a non-empty namespace prefix (e.g. <xhtml:div xmlns:xhtml="...">...</xhtml:div>) - failed to encode correctly (prefix was missing in encoded resource) + failed to encode correctly (prefix was missing in encoded resource) Resource references previously encoded their children (display and reference) @@ -5572,7 +5614,7 @@ Bundle bundle = client.search().forResource(Patient.class) (type)ClientParam, for example: StringClientParam, TokenClientParam, etc.
    ]]> All renamed classes have been retained and deprocated, so this change should not cause any issues - for existing applications but those applications should be refactored to use the + for existing applications but those applications should be refactored to use the new parameters when possible.
    @@ -5596,27 +5638,28 @@ Bundle bundle = client.search().forResource(Patient.class) for configurable logging, capturing requests and responses, and HTTP basic auth. - Transaction client invocations with XML encoding were using the wrong content type ("application/xml+fhir" instead + Transaction client invocations with XML encoding were using the wrong content type ("application/xml+fhir" + instead of the correct "application/atom+xml"). Thanks to David Hay of Orion Health for surfacing this one! Bundle entries now support a link type of "search". Thanks to David Hay for the suggestion! - If a client receives a non 2xx response (e.g. HTTP 500) and the response body is a text/plain message or - an OperationOutcome resource, include the message in the exception message so that it will be - more conveniently displayed in logs and other places. Thanks to Neal Acharya for the suggestion! + If a client receives a non 2xx response (e.g. HTTP 500) and the response body is a text/plain message or + an OperationOutcome resource, include the message in the exception message so that it will be + more conveniently displayed in logs and other places. Thanks to Neal Acharya for the suggestion! - Read invocations in the client now process the "Content-Location" header and use it to - populate the ID of the returned resource. Thanks to Neal Acharya for the suggestion! + Read invocations in the client now process the "Content-Location" header and use it to + populate the ID of the returned resource. Thanks to Neal Acharya for the suggestion! - Fix issue where vread invocations on server incorrectly get routed to instance history method if one is - defined. Thanks to Neal Acharya from UHN for surfacing this one! + Fix issue where vread invocations on server incorrectly get routed to instance history method if one is + defined. Thanks to Neal Acharya from UHN for surfacing this one! - Binary reads on a server not include the Content-Disposition header, to prevent HTML in binary + Binary reads on a server not include the Content-Disposition header, to prevent HTML in binary blobs from being used for nefarious purposes. See FHIR Tracker Bug 3298]]> for more information. @@ -5625,7 +5668,7 @@ Bundle bundle = client.search().forResource(Patient.class) Support has been added for using an HTTP proxy for outgoing requests. - Fix: Primitive extensions declared against custom resource types + Fix: Primitive extensions declared against custom resource types are encoded even if they have no value. Thanks to David Hay of Orion for reporting this! @@ -5634,16 +5677,16 @@ Bundle bundle = client.search().forResource(Patient.class) space (e.g. a WAR file with a space in the name) failed to work correctly. Thanks to David Hay of Orion for reporting this!
    - + - BREAKING CHANGE:]]>: IdDt has been modified so that it + BREAKING CHANGE:]]>: IdDt has been modified so that it contains a partial or complete resource identity. Previously it contained only the simple alphanumeric id of the resource (the part at the end of the "read" URL for that resource) but it can now contain a complete URL or even a partial URL (e.g. "Patient/123") and can optionally contain a version (e.g. "Patient/123/_history/456"). New methods have been added to this datatype which provide just the numeric portion. See the JavaDoc - for more information. + for more information. API CHANGE:]]>: Most elements in the HAPI FHIR model contain @@ -5675,7 +5718,8 @@ Bundle bundle = client.search().forResource(Patient.class) Support for Query resources fixed (in parser) - Nested contained resources (e.g. encoding a resource with a contained resource that itself contains a resource) + Nested contained resources (e.g. encoding a resource with a contained resource that itself contains a + resource) now parse and encode correctly, meaning that all contained resources are placed in the "contained" element of the root resource, and the parser looks in the root resource for all container levels when stitching contained resources back together. @@ -5693,26 +5737,27 @@ Bundle bundle = client.search().forResource(Patient.class) Don't fail on narrative blocks in JSON resources with only an XML declaration but no content (these are - produced by the Health Intersections server) + produced by the Health Intersections server) - Server now automatically compresses responses if the client indicates support + Server now automatically compresses responses if the client indicates support - Server failed to support optional parameters when type is String and :exact qualifier is used + Server failed to support optional parameters when type is String and :exact qualifier is used - Read method in client correctly populated resource ID in returned object + Read method in client correctly populated resource ID in returned object Support added for deleted-entry by/name, by/email, and comment from Tombstones spec - - - + + + - + - +