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%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.context.ComboSearchParamType;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
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.IInterceptorBroadcaster;
|
||||
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.healthmarketscience.sqlbuilder.Condition;
|
||||
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.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -109,6 +110,9 @@ import javax.annotation.Nonnull;
|
|||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import javax.persistence.PersistenceContextType;
|
||||
import javax.persistence.Query;
|
||||
import javax.persistence.SqlResultSetMapping;
|
||||
import javax.persistence.Tuple;
|
||||
import javax.persistence.TypedQuery;
|
||||
import javax.persistence.criteria.CriteriaBuilder;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
|
@ -148,6 +152,11 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
public static final int MAXIMUM_PAGE_SIZE_FOR_TESTING = 50;
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(SearchBuilder.class);
|
||||
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;
|
||||
private final String myResourceName;
|
||||
private final Class<? extends IBaseResource> myResourceType;
|
||||
|
@ -888,11 +897,11 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
if (theIncludes == null || theIncludes.isEmpty()) {
|
||||
return new HashSet<>();
|
||||
}
|
||||
String searchPidFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid";
|
||||
String findPidFieldName = theReverseMode ? "mySourceResourcePid" : "myTargetResourcePid";
|
||||
String searchPidFieldName = theReverseMode ? MY_TARGET_RESOURCE_PID : MY_SOURCE_RESOURCE_PID;
|
||||
String findPidFieldName = theReverseMode ? MY_SOURCE_RESOURCE_PID : MY_TARGET_RESOURCE_PID;
|
||||
String findVersionFieldName = null;
|
||||
if (!theReverseMode && myModelConfig.isRespectVersionsForSearchIncludes()) {
|
||||
findVersionFieldName = "myTargetResourceVersion";
|
||||
findVersionFieldName = MY_TARGET_RESOURCE_VERSION;
|
||||
}
|
||||
|
||||
List<ResourcePersistentId> nextRoundMatches = new ArrayList<>(theMatches);
|
||||
|
@ -1011,25 +1020,55 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
|
||||
String targetResourceType = defaultString(nextInclude.getParamTargetType(), null);
|
||||
for (String nextPath : paths) {
|
||||
String sql;
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
sql = "SELECT " + fieldsToLoad + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchPidFieldName + " IN (:target_pids) AND r.myTargetResourceType in (:target_resource_types)";
|
||||
} else {
|
||||
sql = "SELECT " + fieldsToLoad + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchPidFieldName + " IN (:target_pids)";
|
||||
resourceIdBasedQuery.append(" AND r.target_resource_type in (:target_resource_types) ");
|
||||
}
|
||||
|
||||
// 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());
|
||||
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("target_pids", ResourcePersistentId.toLongList(nextPartition));
|
||||
if (targetResourceType != null) {
|
||||
|
@ -1037,21 +1076,18 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
} else if (haveTargetTypesDefinedByParam) {
|
||||
q.setParameter("target_resource_types", param.getTargets());
|
||||
}
|
||||
List<?> results = q.getResultList();
|
||||
List<Tuple> results = q.getResultList();
|
||||
if (theMaxCount != null) {
|
||||
q.setMaxResults(theMaxCount);
|
||||
}
|
||||
for (Object resourceLink : results) {
|
||||
if (resourceLink != null) {
|
||||
ResourcePersistentId persistentId;
|
||||
if (findVersionFieldName != null) {
|
||||
persistentId = new ResourcePersistentId(((Object[]) resourceLink)[0]);
|
||||
persistentId.setVersion((Long) ((Object[]) resourceLink)[1]);
|
||||
} else {
|
||||
persistentId = new ResourcePersistentId(resourceLink);
|
||||
for (Tuple result : results) {
|
||||
if (result != null) {
|
||||
Long resourceId = NumberUtils.createLong(String.valueOf(result.get(RESOURCE_ID_ALIAS)));
|
||||
Long resourceVersion = null;
|
||||
if (findVersionFieldName != null && result.get(RESOURCE_VERSION_ALIAS) != null) {
|
||||
resourceVersion = NumberUtils.createLong(String.valueOf(result.get(RESOURCE_VERSION_ALIAS)));
|
||||
}
|
||||
assert persistentId.getId() instanceof Long;
|
||||
pidsToInclude.add(persistentId);
|
||||
pidsToInclude.add(new ResourcePersistentId(resourceId, resourceVersion));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,20 +14,28 @@ import org.apache.http.client.methods.HttpPost;
|
|||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
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.Enumerations;
|
||||
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.Questionnaire;
|
||||
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType;
|
||||
import org.hl7.fhir.r4.model.QuestionnaireResponse;
|
||||
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.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
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.Matchers.containsString;
|
||||
|
@ -71,12 +79,12 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro
|
|||
public void testCreateWithNonLocalReferenceWorksWithIncludes() {
|
||||
String baseUrl = "https://hapi.fhir.org/baseR4/";
|
||||
|
||||
myModelConfig.setTreatBaseUrlsAsLocal(Collections.singleton(baseUrl));
|
||||
Questionnaire questionnaire = new Questionnaire();
|
||||
questionnaire.setUrl(baseUrl + "Questionnaire/my-questionnaire");
|
||||
questionnaire.setId("my-questionnaire");
|
||||
|
||||
QuestionnaireResponse questionnaireResponse = new QuestionnaireResponse();
|
||||
questionnaireResponse.setQuestionnaire(baseUrl + "Questionnaire/my-questionnaire");
|
||||
questionnaireResponse.setQuestionnaire(questionnaire.getUrl());
|
||||
questionnaireResponse.setId("my-questionnaire-response");
|
||||
|
||||
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
|
||||
public void testSaveQuestionnaire() throws Exception {
|
||||
String input = "<QuestionnaireResponse xmlns=\"http://hl7.org/fhir\">\n" +
|
||||
|
@ -181,7 +239,6 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro
|
|||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -242,5 +299,4 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro
|
|||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue