Fix '_include' to work for canonical urls (#3440)
* Added test to verify BUG: #2843 (_include doesn't return canonically referred resource) * Fix '_include' query search by native query to support canonical url references * Fixed code review comments
This commit is contained in:
parent
20e092ba6d
commit
ecf782d59e
|
@ -20,12 +20,12 @@ package ca.uhn.fhir.jpa.search.builder;
|
||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ca.uhn.fhir.i18n.Msg;
|
|
||||||
import ca.uhn.fhir.context.ComboSearchParamType;
|
import ca.uhn.fhir.context.ComboSearchParamType;
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||||
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||||
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
|
@ -96,6 +96,7 @@ import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.healthmarketscience.sqlbuilder.Condition;
|
import com.healthmarketscience.sqlbuilder.Condition;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
|
import org.apache.commons.lang3.math.NumberUtils;
|
||||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -109,6 +110,9 @@ import javax.annotation.Nonnull;
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.PersistenceContext;
|
import javax.persistence.PersistenceContext;
|
||||||
import javax.persistence.PersistenceContextType;
|
import javax.persistence.PersistenceContextType;
|
||||||
|
import javax.persistence.Query;
|
||||||
|
import javax.persistence.SqlResultSetMapping;
|
||||||
|
import javax.persistence.Tuple;
|
||||||
import javax.persistence.TypedQuery;
|
import javax.persistence.TypedQuery;
|
||||||
import javax.persistence.criteria.CriteriaBuilder;
|
import javax.persistence.criteria.CriteriaBuilder;
|
||||||
import javax.persistence.criteria.CriteriaQuery;
|
import javax.persistence.criteria.CriteriaQuery;
|
||||||
|
@ -148,6 +152,11 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
public static final int MAXIMUM_PAGE_SIZE_FOR_TESTING = 50;
|
public static final int MAXIMUM_PAGE_SIZE_FOR_TESTING = 50;
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(SearchBuilder.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(SearchBuilder.class);
|
||||||
private static final ResourcePersistentId NO_MORE = new ResourcePersistentId(-1L);
|
private static final ResourcePersistentId NO_MORE = new ResourcePersistentId(-1L);
|
||||||
|
private static final String MY_TARGET_RESOURCE_PID = "myTargetResourcePid";
|
||||||
|
private static final String MY_SOURCE_RESOURCE_PID = "mySourceResourcePid";
|
||||||
|
private static final String MY_TARGET_RESOURCE_VERSION = "myTargetResourceVersion";
|
||||||
|
public static final String RESOURCE_ID_ALIAS = "resource_id";
|
||||||
|
public static final String RESOURCE_VERSION_ALIAS = "resource_version";
|
||||||
public static boolean myUseMaxPageSize50ForTest = false;
|
public static boolean myUseMaxPageSize50ForTest = false;
|
||||||
private final String myResourceName;
|
private final String myResourceName;
|
||||||
private final Class<? extends IBaseResource> myResourceType;
|
private final Class<? extends IBaseResource> myResourceType;
|
||||||
|
@ -888,11 +897,11 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
if (theIncludes == null || theIncludes.isEmpty()) {
|
if (theIncludes == null || theIncludes.isEmpty()) {
|
||||||
return new HashSet<>();
|
return new HashSet<>();
|
||||||
}
|
}
|
||||||
String searchPidFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid";
|
String searchPidFieldName = theReverseMode ? MY_TARGET_RESOURCE_PID : MY_SOURCE_RESOURCE_PID;
|
||||||
String findPidFieldName = theReverseMode ? "mySourceResourcePid" : "myTargetResourcePid";
|
String findPidFieldName = theReverseMode ? MY_SOURCE_RESOURCE_PID : MY_TARGET_RESOURCE_PID;
|
||||||
String findVersionFieldName = null;
|
String findVersionFieldName = null;
|
||||||
if (!theReverseMode && myModelConfig.isRespectVersionsForSearchIncludes()) {
|
if (!theReverseMode && myModelConfig.isRespectVersionsForSearchIncludes()) {
|
||||||
findVersionFieldName = "myTargetResourceVersion";
|
findVersionFieldName = MY_TARGET_RESOURCE_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ResourcePersistentId> nextRoundMatches = new ArrayList<>(theMatches);
|
List<ResourcePersistentId> nextRoundMatches = new ArrayList<>(theMatches);
|
||||||
|
@ -1011,25 +1020,55 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
|
|
||||||
String targetResourceType = defaultString(nextInclude.getParamTargetType(), null);
|
String targetResourceType = defaultString(nextInclude.getParamTargetType(), null);
|
||||||
for (String nextPath : paths) {
|
for (String nextPath : paths) {
|
||||||
String sql;
|
|
||||||
|
|
||||||
boolean haveTargetTypesDefinedByParam = param.hasTargets();
|
boolean haveTargetTypesDefinedByParam = param.hasTargets();
|
||||||
String fieldsToLoad = "r." + findPidFieldName;
|
String findPidFieldSqlColumn = findPidFieldName.equals(MY_SOURCE_RESOURCE_PID) ? "src_resource_id" : "target_resource_id";
|
||||||
|
String fieldsToLoad = "r." + findPidFieldSqlColumn + " AS " + RESOURCE_ID_ALIAS;
|
||||||
if (findVersionFieldName != null) {
|
if (findVersionFieldName != null) {
|
||||||
fieldsToLoad += ", r." + findVersionFieldName;
|
fieldsToLoad += ", r.target_resource_version AS " + RESOURCE_VERSION_ALIAS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Query for includes lookup has consider 2 cases
|
||||||
|
// Case 1: Where target_resource_id is available in hfj_res_link table for local references
|
||||||
|
// Case 2: Where target_resource_id is null in hfj_res_link table and referred by a canonical url in target_resource_url
|
||||||
|
|
||||||
|
// Case 1:
|
||||||
|
String searchPidFieldSqlColumn = searchPidFieldName.equals(MY_TARGET_RESOURCE_PID) ? "target_resource_id" : "src_resource_id";
|
||||||
|
StringBuilder resourceIdBasedQuery = new StringBuilder("SELECT " + fieldsToLoad +
|
||||||
|
" FROM hfj_res_link r " +
|
||||||
|
" WHERE r.src_path = :src_path AND " +
|
||||||
|
" r.target_resource_id IS NOT NULL AND " +
|
||||||
|
" r." + searchPidFieldSqlColumn + " IN (:target_pids) ");
|
||||||
if(targetResourceType != null) {
|
if(targetResourceType != null) {
|
||||||
sql = "SELECT " + fieldsToLoad + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchPidFieldName + " IN (:target_pids) AND r.myTargetResourceType = :target_resource_type";
|
resourceIdBasedQuery.append(" AND r.target_resource_type = :target_resource_type ");
|
||||||
} else if(haveTargetTypesDefinedByParam) {
|
} else if(haveTargetTypesDefinedByParam) {
|
||||||
sql = "SELECT " + fieldsToLoad + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchPidFieldName + " IN (:target_pids) AND r.myTargetResourceType in (:target_resource_types)";
|
resourceIdBasedQuery.append(" AND r.target_resource_type in (:target_resource_types) ");
|
||||||
} else {
|
|
||||||
sql = "SELECT " + fieldsToLoad + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchPidFieldName + " IN (:target_pids)";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Case 2:
|
||||||
|
String fieldsToLoadFromSpidxUriTable = "rUri.res_id";
|
||||||
|
// to match the fields loaded in union
|
||||||
|
if(fieldsToLoad.split(",").length > 1) {
|
||||||
|
for (int i = 0; i < fieldsToLoad.split(",").length - 1; i++) {
|
||||||
|
fieldsToLoadFromSpidxUriTable += ", NULL";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//@formatter:off
|
||||||
|
StringBuilder resourceUrlBasedQuery = new StringBuilder("SELECT " + fieldsToLoadFromSpidxUriTable +
|
||||||
|
" FROM hfj_res_link r " +
|
||||||
|
" JOIN hfj_spidx_uri rUri ON ( " +
|
||||||
|
" r.target_resource_url = rUri.sp_uri AND " +
|
||||||
|
" rUri.sp_name = 'url' " +
|
||||||
|
" ) " +
|
||||||
|
" WHERE r.src_path = :src_path AND " +
|
||||||
|
" r.target_resource_id IS NULL AND " +
|
||||||
|
" r." + searchPidFieldSqlColumn + " IN (:target_pids) ");
|
||||||
|
//@formatter:on
|
||||||
|
|
||||||
|
String sql = resourceIdBasedQuery + " UNION " + resourceUrlBasedQuery;
|
||||||
|
|
||||||
List<Collection<ResourcePersistentId>> partitions = partition(nextRoundMatches, getMaximumPageSize());
|
List<Collection<ResourcePersistentId>> partitions = partition(nextRoundMatches, getMaximumPageSize());
|
||||||
for (Collection<ResourcePersistentId> nextPartition : partitions) {
|
for (Collection<ResourcePersistentId> nextPartition : partitions) {
|
||||||
TypedQuery<?> q = theEntityManager.createQuery(sql, Object[].class);
|
Query q = theEntityManager.createNativeQuery(sql, Tuple.class);
|
||||||
q.setParameter("src_path", nextPath);
|
q.setParameter("src_path", nextPath);
|
||||||
q.setParameter("target_pids", ResourcePersistentId.toLongList(nextPartition));
|
q.setParameter("target_pids", ResourcePersistentId.toLongList(nextPartition));
|
||||||
if (targetResourceType != null) {
|
if (targetResourceType != null) {
|
||||||
|
@ -1037,21 +1076,18 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
} else if (haveTargetTypesDefinedByParam) {
|
} else if (haveTargetTypesDefinedByParam) {
|
||||||
q.setParameter("target_resource_types", param.getTargets());
|
q.setParameter("target_resource_types", param.getTargets());
|
||||||
}
|
}
|
||||||
List<?> results = q.getResultList();
|
List<Tuple> results = q.getResultList();
|
||||||
if (theMaxCount != null) {
|
if (theMaxCount != null) {
|
||||||
q.setMaxResults(theMaxCount);
|
q.setMaxResults(theMaxCount);
|
||||||
}
|
}
|
||||||
for (Object resourceLink : results) {
|
for (Tuple result : results) {
|
||||||
if (resourceLink != null) {
|
if (result != null) {
|
||||||
ResourcePersistentId persistentId;
|
Long resourceId = NumberUtils.createLong(String.valueOf(result.get(RESOURCE_ID_ALIAS)));
|
||||||
if (findVersionFieldName != null) {
|
Long resourceVersion = null;
|
||||||
persistentId = new ResourcePersistentId(((Object[]) resourceLink)[0]);
|
if (findVersionFieldName != null && result.get(RESOURCE_VERSION_ALIAS) != null) {
|
||||||
persistentId.setVersion((Long) ((Object[]) resourceLink)[1]);
|
resourceVersion = NumberUtils.createLong(String.valueOf(result.get(RESOURCE_VERSION_ALIAS)));
|
||||||
} else {
|
|
||||||
persistentId = new ResourcePersistentId(resourceLink);
|
|
||||||
}
|
}
|
||||||
assert persistentId.getId() instanceof Long;
|
pidsToInclude.add(new ResourcePersistentId(resourceId, resourceVersion));
|
||||||
pidsToInclude.add(persistentId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,20 +14,28 @@ import org.apache.http.client.methods.HttpPost;
|
||||||
import org.apache.http.entity.ContentType;
|
import org.apache.http.entity.ContentType;
|
||||||
import org.apache.http.entity.StringEntity;
|
import org.apache.http.entity.StringEntity;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
|
import org.hl7.fhir.r4.model.Coding;
|
||||||
|
import org.hl7.fhir.r4.model.DateType;
|
||||||
import org.hl7.fhir.r4.model.DecimalType;
|
import org.hl7.fhir.r4.model.DecimalType;
|
||||||
|
import org.hl7.fhir.r4.model.Enumerations;
|
||||||
import org.hl7.fhir.r4.model.IdType;
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
|
import org.hl7.fhir.r4.model.Identifier;
|
||||||
import org.hl7.fhir.r4.model.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
import org.hl7.fhir.r4.model.Questionnaire;
|
import org.hl7.fhir.r4.model.Questionnaire;
|
||||||
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType;
|
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType;
|
||||||
import org.hl7.fhir.r4.model.QuestionnaireResponse;
|
import org.hl7.fhir.r4.model.QuestionnaireResponse;
|
||||||
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus;
|
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus;
|
||||||
|
import org.hl7.fhir.r4.model.StringType;
|
||||||
import org.junit.jupiter.api.AfterAll;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
@ -71,12 +79,12 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro
|
||||||
public void testCreateWithNonLocalReferenceWorksWithIncludes() {
|
public void testCreateWithNonLocalReferenceWorksWithIncludes() {
|
||||||
String baseUrl = "https://hapi.fhir.org/baseR4/";
|
String baseUrl = "https://hapi.fhir.org/baseR4/";
|
||||||
|
|
||||||
myModelConfig.setTreatBaseUrlsAsLocal(Collections.singleton(baseUrl));
|
|
||||||
Questionnaire questionnaire = new Questionnaire();
|
Questionnaire questionnaire = new Questionnaire();
|
||||||
|
questionnaire.setUrl(baseUrl + "Questionnaire/my-questionnaire");
|
||||||
questionnaire.setId("my-questionnaire");
|
questionnaire.setId("my-questionnaire");
|
||||||
|
|
||||||
QuestionnaireResponse questionnaireResponse = new QuestionnaireResponse();
|
QuestionnaireResponse questionnaireResponse = new QuestionnaireResponse();
|
||||||
questionnaireResponse.setQuestionnaire(baseUrl + "Questionnaire/my-questionnaire");
|
questionnaireResponse.setQuestionnaire(questionnaire.getUrl());
|
||||||
questionnaireResponse.setId("my-questionnaire-response");
|
questionnaireResponse.setId("my-questionnaire-response");
|
||||||
|
|
||||||
myQuestionnaireDao.update(questionnaire);
|
myQuestionnaireDao.update(questionnaire);
|
||||||
|
@ -113,6 +121,56 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test added to verify <a href="https://github.com/hapifhir/hapi-fhir/issues/2843">https://github.com/hapifhir/hapi-fhir/issues/2843</a>
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testSearch_withIncludeQuestionnaire_shouldReturnWithCanonicalReferencedQuestionnaire() {
|
||||||
|
String questionnaireCanonicalUrl = "https://hapi.fhir.org/baseR4/Questionnaire/xl-54127-6-hapi";
|
||||||
|
Questionnaire questionnaire = new Questionnaire();
|
||||||
|
questionnaire.setId("xl-54127-6-hapi");
|
||||||
|
questionnaire.setUrl(questionnaireCanonicalUrl);
|
||||||
|
questionnaire.addIdentifier(new Identifier().setSystem("http://loinc.org").setValue("54127-6"));
|
||||||
|
questionnaire.setName("US Surgeon General family health portrait");
|
||||||
|
questionnaire.setTitle(questionnaire.getName());
|
||||||
|
questionnaire.setStatus(Enumerations.PublicationStatus.DRAFT);
|
||||||
|
questionnaire.addSubjectType("Patient").addSubjectType("Person");
|
||||||
|
questionnaire.addCode(new Coding("http://loinc.org", "54127-6", questionnaire.getName()));
|
||||||
|
questionnaire.addItem().setLinkId("/54126-8")
|
||||||
|
.setType(QuestionnaireItemType.GROUP)
|
||||||
|
.setRequired(false)
|
||||||
|
.setText("My health history")
|
||||||
|
.addCode(new Coding("http://loinc.org", "54126-8", "My health history"));
|
||||||
|
IIdType qIdType = myQuestionnaireDao.create(questionnaire, mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
|
QuestionnaireResponse questionnaireResponse = new QuestionnaireResponse();
|
||||||
|
questionnaireResponse.setId("xl-54127-6-hapi-response");
|
||||||
|
questionnaireResponse.setQuestionnaire(questionnaireCanonicalUrl);
|
||||||
|
questionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED);
|
||||||
|
questionnaireResponse.addItem().setLinkId("/54126-8")
|
||||||
|
.setText("My health history")
|
||||||
|
.addItem().setLinkId("/54126-8/54125-0").setText("Name").addAnswer().setValue(new StringType("TAMBRA AGARWAL"))
|
||||||
|
.addItem().setLinkId("/54126-8/21112-8").setText("Birth Date").addAnswer().setValue(new DateType("1994-01-01"));
|
||||||
|
IIdType qrIdType = myQuestionnaireResponseDao.create(questionnaireResponse, mySrd).getId().toUnqualifiedVersionless();
|
||||||
|
|
||||||
|
// Search
|
||||||
|
Bundle results = myClient.search()
|
||||||
|
.byUrl("QuestionnaireResponse?_id=" + qrIdType.toUnqualifiedVersionless() + "&_include=QuestionnaireResponse:questionnaire")
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
assertThat(results.getEntry().size(), is(equalTo(2)));
|
||||||
|
|
||||||
|
List<String> expectedIds = new ArrayList<>();
|
||||||
|
expectedIds.add(qrIdType.getValue());
|
||||||
|
expectedIds.add(qIdType.getValue());
|
||||||
|
|
||||||
|
List<String> actualIds = results.getEntry().stream()
|
||||||
|
.map(Bundle.BundleEntryComponent::getResource)
|
||||||
|
.map(resource -> resource.getIdElement().toUnqualifiedVersionless().toString())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertEquals(expectedIds, actualIds);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSaveQuestionnaire() throws Exception {
|
public void testSaveQuestionnaire() throws Exception {
|
||||||
String input = "<QuestionnaireResponse xmlns=\"http://hl7.org/fhir\">\n" +
|
String input = "<QuestionnaireResponse xmlns=\"http://hl7.org/fhir\">\n" +
|
||||||
|
@ -181,7 +239,6 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -242,5 +299,4 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue