diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java
index b36bc90a43f..db980fb5e9a 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java
@@ -1202,6 +1202,9 @@ public enum Pointcut implements IPointcut {
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
*
+ *
+ * ca.uhn.fhir.jpa.searchparam.SearchParameterMap - Contains the details of the search being checked. This can be modified.
+ *
*
*
* Hooks should return void
.
@@ -1210,7 +1213,8 @@ public enum Pointcut implements IPointcut {
STORAGE_PRESEARCH_REGISTERED(void.class,
"ca.uhn.fhir.rest.server.util.ICachedSearchDetails",
"ca.uhn.fhir.rest.api.server.RequestDetails",
- "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
+ "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
+ "ca.uhn.fhir.jpa.searchparam.SearchParameterMap"
),
/**
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 32701bf0419..659c75b1209 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
@@ -205,6 +205,7 @@ public class Constants {
public static final String PARAMQUALIFIER_STRING_CONTAINS = ":contains";
public static final String PARAMQUALIFIER_STRING_EXACT = ":exact";
public static final String PARAMQUALIFIER_TOKEN_TEXT = ":text";
+ public static final String PARAMQUALIFIER_MDM = ":mdm";
public static final int STATUS_HTTP_200_OK = 200;
public static final int STATUS_HTTP_201_CREATED = 201;
public static final int STATUS_HTTP_204_NO_CONTENT = 204;
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java
index a8c43a708bd..f30036a390b 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java
@@ -22,6 +22,7 @@ package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.primitive.IdDt;
+import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.util.CoverageIgnore;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
@@ -41,6 +42,7 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
private String myBaseUrl;
private String myValue;
private String myIdPart;
+ private Boolean myMdmExpand;
/**
* Constructor
@@ -90,8 +92,8 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
}
}
- @Override
- String doGetQueryParameterQualifier() {
+
+ private String defaultGetQueryParameterQualifier() {
StringBuilder b = new StringBuilder();
if (isNotBlank(myChain)) {
if (isNotBlank(getResourceType())) {
@@ -106,6 +108,10 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
}
return null;
}
+ @Override
+ String doGetQueryParameterQualifier() {
+ return this.myMdmExpand != null ? ":mdm" : defaultGetQueryParameterQualifier();
+ }
@Override
String doGetValueAsQueryToken(FhirContext theContext) {
@@ -121,6 +127,11 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
@Override
void doSetValueAsQueryToken(FhirContext theContext, String theParamName, String theQualifier, String theValue) {
+ if (Constants.PARAMQUALIFIER_MDM.equals(theQualifier)) {
+ myMdmExpand = true;
+ theQualifier = "";
+ }
+
String q = theQualifier;
if (isNotBlank(q)) {
if (q.startsWith(":")) {
@@ -166,6 +177,14 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/
return myBaseUrl;
}
+ public boolean isMdmExpand() {
+ return myMdmExpand != null && myMdmExpand;
+ }
+
+ public ReferenceParam setMdmExpand(boolean theMdmExpand) {
+ myMdmExpand = theMdmExpand;
+ return this;
+ }
public String getChain() {
return myChain;
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2520-support-mdm-expansion-in-search.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2520-support-mdm-expansion-in-search.yaml
new file mode 100644
index 00000000000..8f619d91752
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2520-support-mdm-expansion-in-search.yaml
@@ -0,0 +1,5 @@
+---
+type: add
+issue: 2520
+title: "Add support for `:mdm` search parameter qualifier on reference search parameters. Details about enabling this feature
+can be found [in the documentation](/hapi-fhir/docs/server_jpa_mdm/mdm_expansion.html)."
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties
index 6e4d038a21b..580cda79968 100644
--- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties
@@ -67,6 +67,7 @@ page.server_jpa_mdm.mdm_rules=MDM Rules
page.server_jpa_mdm.mdm_eid=MDM Enterprise Identifiers
page.server_jpa_mdm.mdm_operations=MDM Operations
page.server_jpa_mdm.mdm_details=MDM Technical Details
+page.server_jpa_mdm.mdm_expansion=MDM Search Expansion
section.server_jpa_partitioning.title=JPA Server: Partitioning and Multitenancy
page.server_jpa_partitioning.partitioning=Partitioning and Multitenancy
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md
new file mode 100644
index 00000000000..42302e38332
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md
@@ -0,0 +1,33 @@
+# MDM Expansion
+
+Once you have MDM enabled, and you have many linked resources, it can be useful to search across all linked resources. Let's say you have the following MDM links in your database:
+```bash
+Patient/1 --> Patient/3
+Patient/2 --> Patient/3
+```
+This indicates that both Patient/1 and Patient/2 are MDM-matched to the same golden resource (Patient/3).
+What if you want to get all observations from Patient/1, but also include any observations from all of their linked resources. You could do this by first querying the [$mdm-query-links](/docs/server_jpa_mdm/mdm_operations.html) endpoint, and then making a subsequent call like the following
+```http request
+GET http://example.com:8000/Observation?subject=Patient/1,Patient/2,Patient/3
+```
+
+But HAPI-FHIR allows a shorthand for this, by means of a Search Parameter qualifier, as follows:
+```http request
+GET http://example.com:8000/Observation?subject:mdm=Patient/1
+```
+
+This `:mdm` parameter qualifier instructs an interceptor in HAPI fhir to expand the set of resources included in the search by their MDM-matched resources. The two above HTTP requests will return the same result.
+
+
+
+One important caveat is that chaining is currently not supported when using this prefix.
+
+
+## Enabling MDM Expansion
+
+On top of needing to instantiate an MDM module, you must enable this feature in the [DaoConfig](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html) bean, using the [Allow MDM Expansion](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html#setAllowMdmExpansion(boolean)) property.
+
+
+It is important to note that enabling this functionality can lead to incorrect data being returned by a request, if your MDM links are incorrect. Use with caution.
+
+
diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java
index 8c0f27d6a8b..6353d0a55b9 100644
--- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java
+++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java
@@ -1716,6 +1716,36 @@ public class DaoConfig {
this.myModelConfig.setAllowContainsSearches(theAllowContainsSearches);
}
+ /**
+ * If enabled, the server will support the use of :mdm search parameter qualifier on Reference Search Parameters.
+ * This Parameter Qualifier is HAPI-specific, and not defined anywhere in the FHIR specification. Using this qualifier
+ * will result in an MDM expansion being done on the reference, which will expand the search scope. For example, if Patient/1
+ * is MDM-matched to Patient/2 and you execute the search:
+ * Observation?subject:mdm=Patient/1 , you will receive observations for both Patient/1 and Patient/2.
+ *
+ * Default is false
+ *
+ * @since 5.4.0
+ */
+ public boolean isAllowMdmExpansion() {
+ return myModelConfig.isAllowMdmExpansion();
+ }
+
+ /**
+ * If enabled, the server will support the use of :mdm search parameter qualifier on Reference Search Parameters.
+ * This Parameter Qualifier is HAPI-specific, and not defined anywhere in the FHIR specification. Using this qualifier
+ * will result in an MDM expansion being done on the reference, which will expand the search scope. For example, if Patient/1
+ * is MDM-matched to Patient/2 and you execute the search:
+ * Observation?subject:mdm=Patient/1 , you will receive observations for both Patient/1 and Patient/2.
+ *
+ * Default is false
+ *
+ * @since 5.4.0
+ */
+ public void setAllowMdmExpansion(boolean theAllowMdmExpansion) {
+ myModelConfig.setAllowMdmExpansion(theAllowMdmExpansion);
+ }
+
/**
* This setting may be used to advise the server that any references found in
* resources that have any of the base URLs given here will be replaced with
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java
index 942a5e90405..a625e1d8ed3 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java
@@ -41,6 +41,7 @@ import ca.uhn.fhir.jpa.dao.index.DaoResourceLinkResolver;
import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor;
+import ca.uhn.fhir.jpa.dao.mdm.MdmLinkExpandSvc;
import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilder;
import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderCoords;
import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderDate;
@@ -60,6 +61,7 @@ import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
+import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor;
import ca.uhn.fhir.jpa.interceptor.OverridePathBasedReferentialIntegrityForDeletesInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseTerminologyTranslationInterceptor;
import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder;
@@ -473,6 +475,17 @@ public abstract class BaseConfig {
return new RequestTenantPartitionInterceptor();
}
+ @Bean
+ @Lazy
+ public MdmSearchExpandingInterceptor mdmSearchExpandingInterceptor() {
+ return new MdmSearchExpandingInterceptor();
+ }
+
+ @Bean
+ public MdmLinkExpandSvc myMdmLinkExpandSvc() {
+ return new MdmLinkExpandSvc();
+ }
+
@Bean
@Lazy
public TerminologyUploaderProvider terminologyUploaderProvider() {
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java
index 53814c503b7..16834facbe2 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java
@@ -58,4 +58,13 @@ public interface IMdmLinkDao extends JpaRepository {
Long getSourcePid();
}
+ @Query("SELECT ml.myGoldenResourcePid as goldenPid, ml.mySourcePid as sourcePid " +
+ "FROM MdmLink ml " +
+ "INNER JOIN MdmLink ml2 " +
+ "on ml.myGoldenResourcePid=ml2.myGoldenResourcePid " +
+ "WHERE ml2.mySourcePid=:sourcePid " +
+ "AND ml2.myMatchResult=:matchResult " +
+ "AND ml.myMatchResult=:matchResult")
+ List expandPidsBySourcePidAndMatchResult(@Param("sourcePid") Long theSourcePid, @Param("matchResult") MdmMatchResultEnum theMdmMatchResultEnum);
+
}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java
index a8795a47d4f..d28ba15b5d1 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java
@@ -380,6 +380,29 @@ public class IdHelperService {
}
}
+ /**
+ *
+ * Given a set of PIDs, return a set of public FHIR Resource IDs.
+ * This function will resolve a forced ID if it resolves, and if it fails to resolve to a forced it, will just return the pid
+ * Example:
+ * Let's say we have Patient/1(pid == 1), Patient/pat1 (pid == 2), Patient/3 (pid == 3), their pids would resolve as follows:
+ *
+ * [1,2,3] -> ["1","pat1","3"]
+ *
+ * @param thePids The Set of pids you would like to resolve to external FHIR Resource IDs.
+ * @return A Set of strings representing the FHIR IDs of the pids.
+ */
+ public Set translatePidsToFhirResourceIds(Set thePids) {
+ Map> pidToForcedIdMap = translatePidsToForcedIds(thePids);
+
+ //If the result of the translation is an empty optional, it means there is no forced id, and we can use the PID as the resource ID.
+ Set resolvedResourceIds = pidToForcedIdMap.entrySet().stream()
+ .map(entry -> entry.getValue().isPresent() ? entry.getValue().get() : entry.getKey().toString())
+ .collect(Collectors.toSet());
+
+ return resolvedResourceIds;
+
+ }
public Map> translatePidsToForcedIds(Set thePids) {
Map> retVal = new HashMap<>(myMemoryCacheService.getAllPresent(MemoryCacheService.CacheEnum.FORCED_ID, thePids));
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java
new file mode 100644
index 00000000000..66af7f5a706
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java
@@ -0,0 +1,95 @@
+package ca.uhn.fhir.jpa.dao.mdm;
+
+/*-
+ * #%L
+ * HAPI FHIR JPA Server
+ * %%
+ * Copyright (C) 2014 - 2021 Smile CDR, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao;
+import ca.uhn.fhir.jpa.dao.index.IdHelperService;
+import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
+import ca.uhn.fhir.mdm.log.Logs;
+import ca.uhn.fhir.model.primitive.IdDt;
+import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
+import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.hl7.fhir.instance.model.api.IIdType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Service
+public class MdmLinkExpandSvc {
+ private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
+
+ @Autowired
+ private IMdmLinkDao myMdmLinkDao;
+ @Autowired
+ private IdHelperService myIdHelperService;
+
+ /**
+ * Given a source resource, perform MDM expansion and return all the resource IDs of all resources that are
+ * MDM-Matched to this resource.
+ *
+ * @param theResource The resource to MDM-Expand
+ * @return A set of strings representing the FHIR IDs of the expanded resources.
+ */
+ public Set expandMdmBySourceResource(IBaseResource theResource) {
+ ourLog.debug("About to MDM-expand source resource {}", theResource);
+ return expandMdmBySourceResourceId(theResource.getIdElement());
+ }
+
+ /**
+ * Given a resource ID of a source resource, perform MDM expansion and return all the resource IDs of all resources that are
+ * MDM-Matched to this resource.
+ *
+ * @param theId The Resource ID of the resource to MDM-Expand
+ * @return A set of strings representing the FHIR ids of the expanded resources.
+ */
+ public Set expandMdmBySourceResourceId(IIdType theId) {
+ ourLog.debug("About to expand source resource with resource id {}", theId);
+ Long pidOrThrowException = myIdHelperService.getPidOrThrowException(theId);
+ return expandMdmBySourceResourcePid(pidOrThrowException);
+ }
+
+ /**
+ * Given a PID of a source resource, perform MDM expansion and return all the resource IDs of all resources that are
+ * MDM-Matched to this resource.
+ *
+ * @param theSourceResourcePid The PID of the resource to MDM-Expand
+ * @return A set of strings representing the FHIR ids of the expanded resources.
+ */
+ public Set expandMdmBySourceResourcePid(Long theSourceResourcePid) {
+ ourLog.debug("About to expand source resource with PID {}", theSourceResourcePid);
+ List goldenPidSourcePidTuples = myMdmLinkDao.expandPidsBySourcePidAndMatchResult(theSourceResourcePid, MdmMatchResultEnum.MATCH);
+ Set flattenedPids = new HashSet<>();
+ goldenPidSourcePidTuples.forEach(tuple -> {
+ flattenedPids.add(tuple.getSourcePid());
+ flattenedPids.add(tuple.getGoldenPid());
+ });
+ Set resourceIds = myIdHelperService.translatePidsToFhirResourceIds(flattenedPids);
+ ourLog.debug("Pid {} has been expanded to [{}]", theSourceResourcePid, String.join(",", resourceIds));
+ return resourceIds;
+ }
+
+}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/MdmSearchExpandingInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/MdmSearchExpandingInterceptor.java
new file mode 100644
index 00000000000..63f3d3574bc
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/MdmSearchExpandingInterceptor.java
@@ -0,0 +1,92 @@
+package ca.uhn.fhir.jpa.interceptor;
+
+/*-
+ * #%L
+ * HAPI FHIR - Server Framework
+ * %%
+ * Copyright (C) 2014 - 2021 Smile CDR, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.interceptor.api.Hook;
+import ca.uhn.fhir.interceptor.api.Interceptor;
+import ca.uhn.fhir.interceptor.api.Pointcut;
+import ca.uhn.fhir.jpa.api.config.DaoConfig;
+import ca.uhn.fhir.jpa.dao.mdm.MdmLinkExpandSvc;
+import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
+import ca.uhn.fhir.mdm.log.Logs;
+import ca.uhn.fhir.model.api.IQueryParameterType;
+import ca.uhn.fhir.model.primitive.IdDt;
+import ca.uhn.fhir.rest.api.server.RequestDetails;
+import ca.uhn.fhir.rest.param.ReferenceParam;
+import joptsimple.internal.Strings;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * This interceptor replaces the auto-generated CapabilityStatement that is generated
+ * by the HAPI FHIR Server with a static hard-coded resource.
+ */
+@Interceptor
+public class MdmSearchExpandingInterceptor {
+ private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
+
+ @Autowired
+ private MdmLinkExpandSvc myMdmLinkExpandSvc;
+
+ @Autowired
+ private DaoConfig myDaoConfig;
+
+ @Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED)
+ public void hook(SearchParameterMap theSearchParameterMap) {
+ if (myDaoConfig.isAllowMdmExpansion()) {
+ for (List> andList : theSearchParameterMap.values()) {
+ for (List orList : andList) {
+ expandAnyReferenceParameters(orList);
+ }
+ }
+ }
+ }
+
+ /**
+ * If a Parameter is a reference parameter, and it has been set to expand MDM, perform the expansion.
+ */
+ private void expandAnyReferenceParameters(List orList) {
+ List toRemove = new ArrayList<>();
+ List toAdd = new ArrayList<>();
+ for (IQueryParameterType iQueryParameterType : orList) {
+ if (iQueryParameterType instanceof ReferenceParam) {
+ ReferenceParam refParam = (ReferenceParam) iQueryParameterType;
+ if (refParam.isMdmExpand()) {
+ ourLog.debug("Found a reference parameter to expand: {}", refParam.toString());
+ Set expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(new IdDt(refParam.getValue()));
+ if (!expandedResourceIds.isEmpty()) {
+ ourLog.debug("Parameter has been expanded to: {}", String.join(", ", expandedResourceIds));
+ toRemove.add(refParam);
+ expandedResourceIds.stream().map(resourceId -> new ReferenceParam(refParam.getResourceType() + "/" + resourceId)).forEach(toAdd::add);
+ }
+ }
+ }
+ }
+ orList.removeAll(toRemove);
+ orList.addAll(toAdd);
+ }
+}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java
index 6786f9ff002..aa1d16493f6 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java
@@ -305,8 +305,18 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
public IBundleProvider registerSearch(final IFhirResourceDao> theCallingDao, final SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
final String searchUuid = UUID.randomUUID().toString();
+ final String queryString = theParams.toNormalizedQueryString(myContext);
ourLog.debug("Registering new search {}", searchUuid);
+ Search search = new Search();
+ populateSearchEntity(theParams, theResourceType, searchUuid, queryString, search, theRequestPartitionId);
+ // Interceptor call: STORAGE_PRESEARCH_REGISTERED
+ HookParams params = new HookParams()
+ .add(ICachedSearchDetails.class, search)
+ .add(RequestDetails.class, theRequestDetails)
+ .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
+ .add(SearchParameterMap.class, theParams);
+ JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params);
Class extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(theResourceType).getImplementingClass();
final ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(theCallingDao, theResourceType, resourceTypeClass);
sb.setFetchSize(mySyncSize);
@@ -327,7 +337,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
cacheStatus = SearchCacheStatusEnum.NOT_TRIED;
}
- final String queryString = theParams.toNormalizedQueryString(myContext);
if (cacheStatus != SearchCacheStatusEnum.NOT_TRIED) {
if (theParams.getEverythingMode() == null) {
if (myDaoConfig.getReuseCachedSearchResultsForMillis() != null) {
@@ -340,7 +349,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
}
}
- PersistedJpaSearchFirstPageBundleProvider retVal = submitSearch(theCallingDao, theParams, theResourceType, theRequestDetails, searchUuid, sb, queryString, theRequestPartitionId);
+ PersistedJpaSearchFirstPageBundleProvider retVal = submitSearch(theCallingDao, theParams, theResourceType, theRequestDetails, searchUuid, sb, queryString, theRequestPartitionId, search);
retVal.setCacheStatus(cacheStatus);
return retVal;
@@ -379,23 +388,25 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
}
@NotNull
- private PersistedJpaSearchFirstPageBundleProvider submitSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequestDetails, String theSearchUuid, ISearchBuilder theSb, String theQueryString, RequestPartitionId theRequestPartitionId) {
+ private PersistedJpaSearchFirstPageBundleProvider submitSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequestDetails, String theSearchUuid, ISearchBuilder theSb, String theQueryString, RequestPartitionId theRequestPartitionId, Search theSearch) {
StopWatch w = new StopWatch();
- Search search = new Search();
- populateSearchEntity(theParams, theResourceType, theSearchUuid, theQueryString, search, theRequestPartitionId);
+// Search search = new Search();
+ //TODO GGG MOVE THIS POPULATE AND ALSO THE HOOK CALL HIGHER UP IN THE STACK.
+// populateSearchEntity(theParams, theResourceType, theSearchUuid, theQueryString, search, theRequestPartitionId);
- // Interceptor call: STORAGE_PRESEARCH_REGISTERED
- HookParams params = new HookParams()
- .add(ICachedSearchDetails.class, search)
- .add(RequestDetails.class, theRequestDetails)
- .addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
- JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params);
+// Interceptor call: STORAGE_PRESEARCH_REGISTERED
+// HookParams params = new HookParams()
+// .add(ICachedSearchDetails.class, search)
+// .add(RequestDetails.class, theRequestDetails)
+// .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
+// .add(SearchParameterMap.class, theParams);
+// JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params);
- SearchTask task = new SearchTask(search, theCallingDao, theParams, theResourceType, theRequestDetails, theRequestPartitionId);
- myIdToSearchTask.put(search.getUuid(), task);
+ SearchTask task = new SearchTask(theSearch, theCallingDao, theParams, theResourceType, theRequestDetails, theRequestPartitionId);
+ myIdToSearchTask.put(theSearch.getUuid(), task);
myExecutor.submit(task);
- PersistedJpaSearchFirstPageBundleProvider retVal = myPersistedJpaBundleProviderFactory.newInstanceFirstPage(theRequestDetails, search, task, theSb);
+ PersistedJpaSearchFirstPageBundleProvider retVal = myPersistedJpaBundleProviderFactory.newInstanceFirstPage(theRequestDetails, theSearch, task, theSb);
ourLog.debug("Search initial phase completed in {}ms", w.getMillis());
return retVal;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/helper/SearchParamHelper.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/helper/SearchParamHelper.java
new file mode 100644
index 00000000000..8b18d6faba8
--- /dev/null
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/helper/SearchParamHelper.java
@@ -0,0 +1,60 @@
+package ca.uhn.fhir.jpa.search.helper;
+
+import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.context.RuntimeResourceDefinition;
+import ca.uhn.fhir.context.RuntimeSearchParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class SearchParamHelper {
+
+ @Autowired
+ private FhirContext myFhirContext;
+
+
+ public Collection getPatientSearchParamsForResourceType(String theResourceType) {
+ RuntimeResourceDefinition runtimeResourceDefinition = myFhirContext.getResourceDefinition(theResourceType);
+ Map searchParams = new HashMap<>();
+
+ RuntimeSearchParam patientSearchParam = runtimeResourceDefinition.getSearchParam("patient");
+ if (patientSearchParam != null) {
+ searchParams.put(patientSearchParam.getName(), patientSearchParam);
+
+ }
+ RuntimeSearchParam subjectSearchParam = runtimeResourceDefinition.getSearchParam("subject");
+ if (subjectSearchParam != null) {
+ searchParams.put(subjectSearchParam.getName(), subjectSearchParam);
+ }
+
+ List compartmentSearchParams = getPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition);
+ compartmentSearchParams.forEach(param -> searchParams.put(param.getName(), param));
+
+ return searchParams.values();
+ }
+
+ /**
+ * Search the resource definition for a compartment named 'patient' and return its related Search Parameter.
+ */
+ public List getPatientCompartmentRuntimeSearchParams(RuntimeResourceDefinition runtimeResourceDefinition) {
+ List patientSearchParam = new ArrayList<>();
+ List searchParams = runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient");
+ return searchParams;
+// if (searchParams == null || searchParams.size() == 0) {
+// String errorMessage = String.format("Resource type [%s] is not eligible for this type of export, as it contains no Patient compartment, and no `patient` or `subject` search parameter", myResourceType);
+// throw new IllegalArgumentException(errorMessage);
+// } else if (searchParams.size() == 1) {
+// patientSearchParam = searchParams.get(0);
+// } else {
+// String errorMessage = String.format("Resource type [%s] is not eligible for Group Bulk export, as we are unable to disambiguate which patient search parameter we should be searching by.", myResourceType);
+// throw new IllegalArgumentException(errorMessage);
+// }
+// return patientSearchParam;
+ }
+}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java
index fb0bf5aaa81..b66ec988a8e 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java
@@ -2054,6 +2054,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
@CoverageIgnore
@Override
public IValidationSupport.CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
+ //TODO GGG TRY TO JUST AUTO_PASS HERE AND SEE WHAT HAPPENS.
invokeRunnableForUnitTest();
if (isNotBlank(theValueSetUrl)) {
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java
index a9faee6063e..0aee210356d 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java
@@ -74,6 +74,7 @@ public class JpaValidationSupportChain extends ValidationSupportChain {
public void postConstruct() {
addValidationSupport(myDefaultProfileValidationSupport);
addValidationSupport(myJpaValidationSupport);
+ //TODO MAKE SURE THAT THIS IS BEING CAL
addValidationSupport(myTerminologyService);
addValidationSupport(new SnapshotGeneratingValidationSupport(myFhirContext));
addValidationSupport(new InMemoryTerminologyServerValidationSupport(myFhirContext));
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java
index 5f8e41307e1..bf257e1f5ed 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java
@@ -134,6 +134,7 @@ import org.hl7.fhir.r4.model.DocumentReference;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence;
import org.hl7.fhir.r4.model.EpisodeOfCare;
+import org.hl7.fhir.r4.model.ExplanationOfBenefit;
import org.hl7.fhir.r4.model.Group;
import org.hl7.fhir.r4.model.Immunization;
import org.hl7.fhir.r4.model.ImmunizationRecommendation;
@@ -387,6 +388,9 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil
@Qualifier("myPatientDaoR4")
protected IFhirResourceDaoPatient myPatientDao;
@Autowired
+ @Qualifier("myExplanationOfBenefitDaoR4")
+ protected IFhirResourceDao myExplanationOfBenefitDao;
+ @Autowired
protected IResourceTableDao myResourceTableDao;
@Autowired
protected IResourceHistoryTableDao myResourceHistoryTableDao;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java
index a5fd819663b..e68b4f378ff 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java
@@ -38,6 +38,7 @@ import org.hl7.fhir.r4.model.DiagnosticReport;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
+import org.hl7.fhir.r4.model.ExplanationOfBenefit;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Group;
import org.hl7.fhir.r4.model.IntegerType;
@@ -193,10 +194,8 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam("lt0")));
assertEquals(0, search.size());
-
}
-
/**
* Draft search parameters should be ok even if they aren't completely valid
*/
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java
index ed39b28723b..0fb63d8147d 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java
@@ -13,9 +13,11 @@ import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
+import ca.uhn.fhir.rest.gclient.DateClientParam;
import ca.uhn.fhir.rest.gclient.ReferenceClientParam;
import ca.uhn.fhir.rest.gclient.StringClientParam;
import ca.uhn.fhir.rest.gclient.TokenClientParam;
+import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.BundleUtil;
import org.apache.commons.io.IOUtils;
@@ -332,9 +334,9 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
List foundResources = toUnqualifiedVersionlessIdValues(bundle);
assertThat(foundResources, contains(p1id.getValue()));
-
}
+
@SuppressWarnings("unused")
@Test
public void testSearchQualifiedWithCustomReferenceParam() {
diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java
index 97b2aeefad2..f896e4c4abc 100644
--- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java
+++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java
@@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.mdm.config;
*/
import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor;
import ca.uhn.fhir.jpa.mdm.svc.MdmSurvivorshipSvcImpl;
import ca.uhn.fhir.mdm.api.IMdmControllerSvc;
import ca.uhn.fhir.mdm.api.IMdmExpungeSvc;
@@ -79,6 +80,11 @@ public class MdmConsumerConfig {
return new MdmStorageInterceptor();
}
+ @Bean
+ MdmSearchExpandingInterceptor myMdmSearchExpandingInterceptorInterceptor() {
+ return new MdmSearchExpandingInterceptor();
+ }
+
@Bean
IMdmSurvivorshipService mdmSurvivorshipService() { return new MdmSurvivorshipSvcImpl(); }
diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptor.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptor.java
index 06704d4ff66..14dc07b62db 100644
--- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptor.java
+++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptor.java
@@ -36,6 +36,8 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.hl7.fhir.r4.model.Extension;
+import org.hl7.fhir.r4.model.Patient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -57,13 +59,10 @@ public class MdmStorageInterceptor implements IMdmStorageInterceptor {
private EIDHelper myEIDHelper;
@Autowired
private IMdmSettings myMdmSettings;
- @Autowired
- private GoldenResourceHelper myGoldenResourceHelper;
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
public void blockManualResourceManipulationOnCreate(IBaseResource theBaseResource, RequestDetails theRequestDetails, ServletRequestDetails theServletRequestDetails) {
-
//If running in single EID mode, forbid multiple eids.
if (myMdmSettings.isPreventMultipleEids()) {
forbidIfHasMultipleEids(theBaseResource);
diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java
index b3396377031..912d39331ea 100644
--- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java
+++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java
@@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.mdm.interceptor;
* #L%
*/
+import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
@@ -41,6 +42,8 @@ public class MdmSubmitterInterceptorLoader {
@Autowired
private IMdmStorageInterceptor myIMdmStorageInterceptor;
@Autowired
+ private MdmSearchExpandingInterceptor myMdmSearchExpandingInterceptorInterceptor;
+ @Autowired
private IInterceptorService myInterceptorService;
@Autowired
private SubscriptionSubmitInterceptorLoader mySubscriptionSubmitInterceptorLoader;
@@ -53,6 +56,7 @@ public class MdmSubmitterInterceptorLoader {
myDaoConfig.addSupportedSubscriptionType(Subscription.SubscriptionChannelType.MESSAGE);
myInterceptorService.registerInterceptor(myIMdmStorageInterceptor);
+ myInterceptorService.registerInterceptor(myMdmSearchExpandingInterceptorInterceptor);
ourLog.info("MDM interceptor registered");
// We need to call SubscriptionSubmitInterceptorLoader.start() again in case there were no subscription types the first time it was called.
mySubscriptionSubmitInterceptorLoader.start();
diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java
index 4245c859123..dae0782cccb 100644
--- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java
+++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java
@@ -44,6 +44,7 @@ import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.ContactPoint;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Medication;
+import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Practitioner;
@@ -96,6 +97,8 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test {
@Autowired
protected IFhirResourceDao myPractitionerDao;
@Autowired
+ protected IFhirResourceDao myObservationDao;
+ @Autowired
protected MdmResourceMatcherSvc myMdmResourceMatcherSvc;
@Autowired
protected IMdmLinkDao myMdmLinkDao;
@@ -182,7 +185,6 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test {
Patient patient = (Patient) outcome.getResource();
patient.setId(outcome.getId());
return patient;
-
}
@Nonnull
diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java
index 3d8c392e9a7..ae034af6fa7 100644
--- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java
+++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java
@@ -1,16 +1,28 @@
package ca.uhn.fhir.jpa.mdm.dao;
+import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.IMdmSettings;
+import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.util.TestUtil;
+import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.in;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.isIn;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -48,4 +60,47 @@ public class MdmLinkDaoSvcTest extends BaseMdmR4Test {
assertEquals(rules.getVersion(), newLink.getVersion());
}
+ @Test
+ public void testExpandPidsWorks() {
+
+ Patient golden = createGoldenPatient();
+
+ //Create 10 linked patients.
+ List mdmLinks = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ mdmLinks.add(createPatientAndLinkTo(golden.getIdElement().getIdPartAsLong(), MdmMatchResultEnum.MATCH));
+ }
+
+ //Now lets connect a few as just POSSIBLE_MATCHes and ensure they aren't returned.
+ for (int i = 0 ; i < 5; i++) {
+ createPatientAndLinkTo(golden.getIdElement().getIdPartAsLong(), MdmMatchResultEnum.POSSIBLE_MATCH);
+ }
+
+ List expectedExpandedPids = mdmLinks.stream().map(MdmLink::getSourcePid).collect(Collectors.toList());
+
+ //SUT
+ List lists = myMdmLinkDao.expandPidsBySourcePidAndMatchResult(mdmLinks.get(0).getSourcePid(), MdmMatchResultEnum.MATCH);
+
+ assertThat(lists, hasSize(10));
+
+ lists.stream()
+ .forEach(tuple -> {
+ assertThat(tuple.getGoldenPid(), is(equalTo(golden.getIdElement().getIdPartAsLong())));
+ assertThat(tuple.getSourcePid(), is(in(expectedExpandedPids)));
+ });
+ }
+
+ private MdmLink createPatientAndLinkTo(Long thePatientPid, MdmMatchResultEnum theMdmMatchResultEnum) {
+ Patient patient = createPatient();
+
+ MdmLink mdmLink = myMdmLinkDaoSvc.newMdmLink();
+ mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL);
+ mdmLink.setMatchResult(theMdmMatchResultEnum);
+ mdmLink.setCreated(new Date());
+ mdmLink.setUpdated(new Date());
+ mdmLink.setGoldenResourcePid(thePatientPid);
+ mdmLink.setSourcePid(myIdHelperService.getPidOrNull(patient));
+ MdmLink saved= myMdmLinkDao.save(mdmLink);
+ return saved;
+ }
}
diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmExpungeTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmExpungeTest.java
index a0ca44cc446..659222d3e2a 100644
--- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmExpungeTest.java
+++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmExpungeTest.java
@@ -16,6 +16,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
+import java.util.List;
+
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -49,6 +51,8 @@ public class MdmExpungeTest extends BaseMdmR4Test {
saveLink(mdmLink);
}
+
+
@Test
public void testUninterceptedDeleteRemovesMdmReference() {
assertEquals(1, myMdmLinkDao.count());
diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSearchExpandingInterceptorIT.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSearchExpandingInterceptorIT.java
new file mode 100644
index 00000000000..58d3abf4afa
--- /dev/null
+++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSearchExpandingInterceptorIT.java
@@ -0,0 +1,102 @@
+package ca.uhn.fhir.jpa.mdm.interceptor;
+
+import ca.uhn.fhir.jpa.api.config.DaoConfig;
+import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
+import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
+import ca.uhn.fhir.jpa.mdm.helper.MdmHelperConfig;
+import ca.uhn.fhir.jpa.mdm.helper.MdmHelperR4;
+import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
+import ca.uhn.fhir.mdm.api.MdmConstants;
+import ca.uhn.fhir.rest.api.server.IBundleProvider;
+import ca.uhn.fhir.rest.param.ReferenceOrListParam;
+import ca.uhn.fhir.rest.param.ReferenceParam;
+import org.hl7.fhir.r4.model.CodeableConcept;
+import org.hl7.fhir.r4.model.Observation;
+import org.hl7.fhir.r4.model.Patient;
+import org.hl7.fhir.r4.model.Reference;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ContextConfiguration;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.slf4j.LoggerFactory.getLogger;
+
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
+@ContextConfiguration(classes = {MdmHelperConfig.class})
+public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
+
+ private static final Logger ourLog = getLogger(MdmSearchExpandingInterceptorIT.class);
+
+ @RegisterExtension
+ @Autowired
+ public MdmHelperR4 myMdmHelper;
+ @Autowired
+ private DaoConfig myDaoConfig;
+
+ @Test
+ public void testReferenceExpansionWorks() throws InterruptedException {
+ myDaoConfig.setAllowMdmExpansion(false);
+ MdmHelperR4.OutcomeAndLogMessageWrapper withLatch = myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123"));
+ MdmHelperR4.OutcomeAndLogMessageWrapper withLatch1 = myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123"));
+ MdmHelperR4.OutcomeAndLogMessageWrapper withLatch2 = myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123"));
+ MdmHelperR4.OutcomeAndLogMessageWrapper withLatch3 = myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123"));
+
+ assertLinkCount(4);
+
+ String id = withLatch.getDaoMethodOutcome().getId().getIdPart();
+ String id1 = withLatch1.getDaoMethodOutcome().getId().getIdPart();
+ String id2 = withLatch2.getDaoMethodOutcome().getId().getIdPart();
+ String id3 = withLatch3.getDaoMethodOutcome().getId().getIdPart();
+
+ //Create an Observation for each Patient
+ createObservationWithSubject(id);
+ createObservationWithSubject(id1);
+ createObservationWithSubject(id2);
+ createObservationWithSubject(id3);
+
+ SearchParameterMap searchParameterMap = new SearchParameterMap();
+ searchParameterMap.setLoadSynchronous(true);
+ ReferenceOrListParam referenceOrListParam = new ReferenceOrListParam();
+ referenceOrListParam.addOr(new ReferenceParam("Patient/" + id).setMdmExpand(true));
+ searchParameterMap.add(Observation.SP_SUBJECT, referenceOrListParam);
+
+ //With MDM Expansion disabled, this should return 1 result.
+ IBundleProvider search = myObservationDao.search(searchParameterMap);
+ assertThat(search.size(), is(equalTo(1)));
+
+ //Once MDM Expansion is allowed, this should now return 4 resourecs.
+ myDaoConfig.setAllowMdmExpansion(true);
+ search = myObservationDao.search(searchParameterMap);
+ assertThat(search.size(), is(equalTo(4)));
+ }
+
+ @Test
+ public void testReferenceExpansionQuietlyFailsOnMissingMdmMatches() {
+ myDaoConfig.setAllowMdmExpansion(true);
+ Patient patient = buildJanePatient();
+ patient.getMeta().addTag(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_NO_MDM_MANAGED, "Don't MDM on me!");
+ DaoMethodOutcome daoMethodOutcome = myMdmHelper.doCreateResource(patient, true);
+ String id = daoMethodOutcome.getId().getIdPart();
+ createObservationWithSubject(id);
+
+ //Even though the user has NO mdm links, that should not cause a request failure.
+ SearchParameterMap map = new SearchParameterMap();
+ map.add(Observation.SP_SUBJECT, new ReferenceParam("Patient/" + id).setMdmExpand(true));
+ IBundleProvider search = myObservationDao.search(map);
+ assertThat(search.size(), is(equalTo(1)));
+ }
+
+ private Observation createObservationWithSubject(String thePatientId) {
+ Observation observation = new Observation();
+ observation.setSubject(new Reference("Patient/" + thePatientId));
+ observation.setCode(new CodeableConcept().setText("Made for Patient/" + thePatientId));
+ DaoMethodOutcome daoMethodOutcome = myObservationDao.create(observation);
+ return (Observation) daoMethodOutcome.getResource();
+
+ }
+}
diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java
index 5ad4b9ebe84..c06f0db2c7f 100644
--- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java
+++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java
@@ -6,10 +6,12 @@ import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
import ca.uhn.fhir.jpa.mdm.helper.MdmHelperConfig;
import ca.uhn.fhir.jpa.mdm.helper.MdmHelperR4;
+import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.mdm.model.CanonicalEID;
import ca.uhn.fhir.mdm.rules.config.MdmSettings;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
+import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.server.TransactionLogMessages;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import org.hl7.fhir.instance.model.api.IAnyResource;
@@ -65,6 +67,12 @@ public class MdmStorageInterceptorIT extends BaseMdmR4Test {
assertLinkCount(1);
}
+ @Test
+ public void testSearchExpandingInterceptorWorks() {
+ SearchParameterMap subject = new SearchParameterMap("subject", new ReferenceParam("Patient/123").setMdmExpand(true)).setLoadSynchronous(true);
+ myObservationDao.search(subject);
+ }
+
@Test
public void testDeleteGoldenResourceDeletesLinks() throws InterruptedException {
myMdmHelper.createWithLatch(buildPaulPatient());
diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java
index 9a70e967165..4101d2504b7 100644
--- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java
+++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java
@@ -99,6 +99,7 @@ public class ModelConfig {
private Map> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap();
private boolean myRespectVersionsForSearchIncludes;
private boolean myIndexOnContainedResources = false;
+ private boolean myAllowMdmExpansion = false;
/**
* Constructor
@@ -159,6 +160,36 @@ public class ModelConfig {
return myAllowContainsSearches;
}
+ /**
+ * If enabled, the server will support the use of :mdm search parameter qualifier on Reference Search Parameters.
+ * This Parameter Qualifier is HAPI-specific, and not defined anywhere in the FHIR specification. Using this qualifier
+ * will result in an MDM expansion being done on the reference, which will expand the search scope. For example, if Patient/1
+ * is MDM-matched to Patient/2 and you execute the search:
+ * Observation?subject:mdm=Patient/1 , you will receive observations for both Patient/1 and Patient/2.
+ *
+ * Default is false
+ *
+ * @since 5.4.0
+ */
+ public boolean isAllowMdmExpansion() {
+ return myAllowMdmExpansion;
+ }
+
+ /**
+ * If enabled, the server will support the use of :mdm search parameter qualifier on Reference Search Parameters.
+ * This Parameter Qualifier is HAPI-specific, and not defined anywhere in the FHIR specification. Using this qualifier
+ * will result in an MDM expansion being done on the reference, which will expand the search scope. For example, if Patient/1
+ * is MDM-matched to Patient/2 and you execute the search:
+ * Observation?subject:mdm=Patient/1 , you will receive observations for both Patient/1 and Patient/2.
+ *
+ * Default is false
+ *
+ * @since 5.4.0
+ */
+ public void setAllowMdmExpansion(boolean theAllowMdmExpansion) {
+ myAllowMdmExpansion = theAllowMdmExpansion;
+ }
+
/**
* If enabled, the server will support the use of :contains searches,
* which are helpful but can have adverse effects on performance.