diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java index b6e7db23796..ea2bd9131ec 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java @@ -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 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 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; } - 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"; - } 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)"; + // 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) { + resourceIdBasedQuery.append(" AND r.target_resource_type = :target_resource_type "); + } else if(haveTargetTypesDefinedByParam) { + 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> partitions = partition(nextRoundMatches, getMaximumPageSize()); for (Collection 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 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)); } } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderQuestionnaireResponseR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderQuestionnaireResponseR4Test.java index 90ff93727ba..8f09add284c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderQuestionnaireResponseR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderQuestionnaireResponseR4Test.java @@ -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); @@ -100,7 +108,7 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro Questionnaire q1 = new Questionnaire(); q1.addItem().setLinkId("link1").setType(QuestionnaireItemType.STRING); IIdType qId = myQuestionnaireDao.create(q1, mySrd).getId().toUnqualifiedVersionless(); - + QuestionnaireResponse qr1 = new QuestionnaireResponse(); qr1.setQuestionnaire(qId.getValue()); qr1.setStatus(QuestionnaireResponseStatus.COMPLETED); @@ -112,49 +120,99 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro assertThat(myFhirContext.newJsonParser().encodeResourceToString(e.getOperationOutcome()), containsString("Answer value must be of type string")); } } - + + /** + * Test added to verify https://github.com/hapifhir/hapi-fhir/issues/2843 + */ + @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 expectedIds = new ArrayList<>(); + expectedIds.add(qrIdType.getValue()); + expectedIds.add(qIdType.getValue()); + + List 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 = "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - ""; - + String input = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + HttpPost post = new HttpPost(ourServerBase + "/QuestionnaireResponse"); post.setEntity(new StringEntity(input, ContentType.create(ca.uhn.fhir.rest.api.Constants.CT_FHIR_XML, "UTF-8"))); CloseableHttpResponse response = ourHttpClient.execute(post); @@ -179,9 +237,8 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro } finally { IOUtils.closeQuietly(response); } - - - + + } @Test @@ -196,16 +253,16 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro } finally { IOUtils.closeQuietly(response); } - + } - - + + /** * From a Skype message from Brian Postlethwaite */ @Test public void testValidateQuestionnaireResponseWithNoIdForCreate() throws Exception { - + String input = "{\"resourceType\":\"Parameters\",\"parameter\":[{\"name\":\"mode\",\"valueString\":\"create\"},{\"name\":\"resource\",\"resource\":{\"resourceType\":\"QuestionnaireResponse\",\"questionnaire\":\"http://fhirtest.uhn.ca/baseDstu2/Questionnaire/MedsCheckEligibility\",\"text\":{\"status\":\"generated\",\"div\":\"
!-- populated from the rendered HTML below -->
\"},\"status\":\"completed\",\"authored\":\"2017-02-10T00:02:58.098Z\"}}]}"; HttpPost post = new HttpPost(ourServerBase + "/QuestionnaireResponse/$validate?_pretty=true"); post.setEntity(new StringEntity(input, ContentType.APPLICATION_JSON)); @@ -217,15 +274,15 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro } finally { IOUtils.closeQuietly(response); } - + } - + /** * From a Skype message from Brian Postlethwaite */ @Test public void testValidateQuestionnaireResponseWithNoIdForUpdate() throws Exception { - + String input = "{\"resourceType\":\"Parameters\",\"parameter\":[{\"name\":\"mode\",\"valueString\":\"update\"},{\"name\":\"resource\",\"resource\":{\"resourceType\":\"QuestionnaireResponse\",\"questionnaire\":\"http://fhirtest.uhn.ca/baseDstu2/Questionnaire/MedsCheckEligibility\",\"text\":{\"status\":\"generated\",\"div\":\"
!-- populated from the rendered HTML below -->
\"},\"status\":\"completed\",\"authored\":\"2017-02-10T00:02:58.098Z\"}}]}"; HttpPost post = new HttpPost(ourServerBase + "/QuestionnaireResponse/$validate?_pretty=true"); post.setEntity(new StringEntity(input, ContentType.APPLICATION_JSON)); @@ -238,9 +295,8 @@ public class ResourceProviderQuestionnaireResponseR4Test extends BaseResourcePro } finally { IOUtils.closeQuietly(response); } - + } - - + }