diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index 8678d76cae8..09dfbae3d4f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -260,6 +260,7 @@ public class Constants { */ public static final String EXT_META_SOURCE = "http://hapifhir.io/fhir/StructureDefinition/resource-meta-source"; public static final String PARAM_FHIRPATH = "_fhirpath"; + public static final String PARAM_TYPE = "_type"; static { CHARSET_UTF8 = StandardCharsets.UTF_8; diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index c5b3696974e..bc15d09dc64 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -135,3 +135,6 @@ ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.cannotCreateDuplicateValueSetUrl=Can no ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.expansionTooLarge=Expansion of ValueSet produced too many codes (maximum {0}) - Operation aborted! ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils.failedToApplyPatch=Failed to apply JSON patch to {0}: {1} + +ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderReference.invalidTargetTypeForChain=Resource type "{0}" is not a valid target type for reference search parameter: {1} +ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderReference.invalidResourceType=Invalid/unsupported resource type: "{0}" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1772-allow-chain-on-type.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1772-allow-chain-on-type.yaml new file mode 100644 index 00000000000..61454172e40 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1772-allow-chain-on-type.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 1772 +title: "The JPA server now allows chained searches on the `_type` parameter. For example, the following + could be used to find all Encounters with a context of type Group: `Encounter?subject._type=Group`." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java index 6306487cabf..1a71242f249 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java @@ -20,14 +20,31 @@ package ca.uhn.fhir.jpa.dao.predicate; * #L% */ -import ca.uhn.fhir.context.*; +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; +import ca.uhn.fhir.context.BaseRuntimeElementDefinition; +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeChildChoiceDefinition; +import ca.uhn.fhir.context.RuntimeChildResourceDefinition; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.dao.*; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.DaoRegistry; +import ca.uhn.fhir.jpa.dao.IDao; +import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; -import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; @@ -48,21 +65,34 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import com.google.common.collect.Lists; -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.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; -import javax.persistence.criteria.*; -import java.util.*; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.persistence.criteria.Subquery; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; import java.util.stream.Collectors; -import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.trim; @Component @Scope("prototype") @@ -273,20 +303,41 @@ class PredicateBuilderReference extends BasePredicateBuilder { } } else { + try { RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theReferenceParam.getResourceType()); resourceTypes = new ArrayList<>(1); resourceTypes.add(resDef.getImplementingClass()); } catch (DataFormatException e) { - throw new InvalidRequestException("Invalid resource type: " + theReferenceParam.getResourceType()); + throw newInvalidResourceTypeException(theReferenceParam.getResourceType()); } + + } + + // Handle chain on _type + if (Constants.PARAM_TYPE.equals(theReferenceParam.getChain())) { + String typeValue = theReferenceParam.getValue(); + + Class wantedType; + try { + wantedType = myContext.getResourceDefinition(typeValue).getImplementingClass(); + } catch (DataFormatException e) { + throw newInvalidResourceTypeException(typeValue); + } + if (!resourceTypes.contains(wantedType)) { + throw newInvalidTargetTypeForChainException(theResourceName, theParamName, typeValue); + } + + Predicate targetTypeParameter = myCriteriaBuilder.equal(theJoin.get("myTargetResourceType"), typeValue); + myQueryRoot.addPredicate(targetTypeParameter); + return targetTypeParameter; } boolean foundChainMatch = false; List> candidateTargetTypes = new ArrayList<>(); for (Class nextType : resourceTypes) { - String chain = theReferenceParam.getChain(); + String remainingChain = null; int chainDotIndex = chain.indexOf('.'); if (chainDotIndex != -1) { @@ -936,4 +987,18 @@ class PredicateBuilderReference extends BasePredicateBuilder { return retVal; } + + @NotNull + private InvalidRequestException newInvalidTargetTypeForChainException(String theResourceName, String theParamName, String theTypeValue) { + String searchParamName = theResourceName + ":" + theParamName; + String msg = myContext.getLocalizer().getMessage(PredicateBuilderReference.class, "invalidTargetTypeForChain", theTypeValue, searchParamName); + return new InvalidRequestException(msg); + } + + @NotNull + private InvalidRequestException newInvalidResourceTypeException(String theResourceType) { + String msg = myContext.getLocalizer().getMessageSanitized(PredicateBuilderReference.class, "invalidResourceType", theResourceType); + throw new InvalidRequestException(msg); + } + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index e465c6d03a6..9d7d26bfada 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java @@ -316,6 +316,67 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { assertThat(ids, empty()); } + @Test + public void testChainOnType() { + + 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.setLoadSynchronous(true); + map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject", "Patient").setChain("_type")); + results = myEncounterDao.search(map); + ids = toUnqualifiedVersionlessIdValues(results); + assertThat(ids, hasItems(enc1Id)); + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject", "Group").setChain("_type")); + results = myEncounterDao.search(map); + ids = toUnqualifiedVersionlessIdValues(results); + assertThat(ids, hasItems(enc2Id)); + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject", "Organization").setChain("_type")); + try { + myEncounterDao.search(map); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Resource type \"Organization\" is not a valid target type for reference search parameter: Encounter:subject", e.getMessage()); + } + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject", "HelpImABug").setChain("_type")); + try { + myEncounterDao.search(map); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Invalid/unsupported resource type: \"HelpImABug\"", e.getMessage()); + } + + } + /** * See #441 */ diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java index f03f54eefee..acaeccee165 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java @@ -346,7 +346,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { .returnBundle(Bundle.class) .execute(); } catch (InvalidRequestException e) { - assertEquals("HTTP 400 Bad Request: Invalid resource type: FOO", e.getMessage()); + assertEquals("HTTP 400 Bad Request: Invalid/unsupported resource type: \"FOO\"", e.getMessage()); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index 56c98b94cfd..9fe504d4752 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -405,15 +405,15 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { assertThat(idValues, contains(pid)); // Search param on extension - myCaptureQueriesListener.clear(); idValues = searchAndReturnUnqualifiedIdValues(ourServerBase + "/Patient?extpatorg=" + orgId.getValue()); - myCaptureQueriesListener.logSelectQueries(); assertThat(idValues, contains(pid)); idValues = searchAndReturnUnqualifiedIdValues(ourServerBase + "/Patient?extpatorg.name=ORGANIZATION"); assertThat(idValues, contains(pid)); + myCaptureQueriesListener.clear(); idValues = searchAndReturnUnqualifiedIdValues(ourServerBase + "/Patient?extpatorg.extorgorg.name=PARENT"); + myCaptureQueriesListener.logSelectQueries(); assertThat(idValues, contains(pid)); idValues = searchAndReturnUnqualifiedIdValues(ourServerBase + "/Patient?extpatorg.extorgorg.extorgorg.name=GRANDPARENT"); diff --git a/pom.xml b/pom.xml index f3c0bdfb4e7..b2c8d47459d 100644 --- a/pom.xml +++ b/pom.xml @@ -1516,6 +1516,7 @@ +