From c9fcef0372750542c7e92398fa4e660d55e1b3c3 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Fri, 30 Jun 2017 16:20:32 -0400 Subject: [PATCH] Clean up handling of searches nested in batch and transaction --- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 7 +- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 9 +- .../java/ca/uhn/fhir/jpa/dao/DaoConfig.java | 34 +- .../uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java | 3 - .../jpa/dao/dstu3/FhirSystemDaoDstu3.java | 3 - .../search/DatabaseBackedPagingProvider.java | 1 - .../uhn/fhir/jpa/config/TestDstu3Config.java | 3 +- .../fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java | 22 +- .../fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java | 13 +- ...temProviderTransactionSearchDstu2Test.java | 304 +++++++++++++++ .../dstu3/SystemProviderDstu3Test.java | 3 +- ...temProviderTransactionSearchDstu3Test.java | 358 ++++++++++++++++++ .../provider/dstu2/Dstu2BundleFactory.java | 16 +- .../hapi/rest/server/Dstu3BundleFactory.java | 7 +- src/changes/changes.xml | 17 + 15 files changed, 747 insertions(+), 53 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderTransactionSearchDstu2Test.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/SystemProviderTransactionSearchDstu3Test.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 191fcf87046..8c44dd49f88 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -1744,9 +1744,12 @@ public abstract class BaseHapiFhirDao implements IDao { TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res); if (tagList != null) { tag = tagList.getTag(Constants.TAG_SUBSETTED_SYSTEM, Constants.TAG_SUBSETTED_CODE); + totalMetaCount += tagList.size(); + } + List profileList = ResourceMetadataKeyEnum.PROFILES.get(res); + if (profileList != null) { + totalMetaCount += profileList.size(); } - totalMetaCount += tagList.size(); - totalMetaCount += ResourceMetadataKeyEnum.PROFILES.get(res).size(); } else { IAnyResource res = (IAnyResource) theResource; tag = res.getMeta().getTag(Constants.TAG_SUBSETTED_SYSTEM, Constants.TAG_SUBSETTED_CODE); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index d60d372d957..6fc9a7f7b87 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -29,6 +29,7 @@ import javax.annotation.PostConstruct; import javax.persistence.NoResultException; import javax.persistence.TypedQuery; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.dstu3.model.IdType; import org.hl7.fhir.instance.model.api.*; import org.springframework.beans.factory.annotation.Autowired; @@ -926,9 +927,11 @@ public abstract class BaseHapiFhirResourceDao extends B notifyInterceptors(RestOperationTypeEnum.SEARCH_TYPE, requestDetails); if (theRequestDetails.isSubRequest()) { - theParams.setLoadSynchronous(true); - int max = myDaoConfig.getMaximumSearchResultCountInTransaction(); - theParams.setLoadSynchronousUpTo(myDaoConfig.getMaximumSearchResultCountInTransaction()); + Integer max = myDaoConfig.getMaximumSearchResultCountInTransaction(); + if (max != null) { + Validate.inclusiveBetween(1, Integer.MAX_VALUE, max.intValue(), "Maximum search result count in transaction ust be a positive integer"); + theParams.setLoadSynchronousUpTo(myDaoConfig.getMaximumSearchResultCountInTransaction()); + } } if (!isPagingProviderDatabaseBacked(theRequestDetails)) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java index c12ead7edf7..99f9ae23518 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java @@ -57,7 +57,7 @@ public class DaoConfig { * * @see #setMaximumSearchResultCountInTransaction(int) */ - private static final int DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION = 500; + private static final Integer DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION = null; /** * Default value for {@link #setReuseCachedSearchResultsForMillis(Long)}: 60000ms (one minute) @@ -110,7 +110,7 @@ public class DaoConfig { * update setter javadoc if default changes */ private int myMaximumExpansionSize = 5000; - private int myMaximumSearchResultCountInTransaction = DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION; + private Integer myMaximumSearchResultCountInTransaction = DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION; private ResourceEncodingEnum myResourceEncoding = ResourceEncodingEnum.JSONC; /** * update setter javadoc if default changes @@ -233,14 +233,17 @@ public class DaoConfig { } /** - * Provides the maximum number of results which may be returned by a search within a FHIR transaction - * operation. For example, if this value is set to 100 and a FHIR transaction is processed with a sub-request - * for Patient?gender=male, the server will throw an error (and the transaction will fail) if there are more than + * Provides the maximum number of results which may be returned by a search (HTTP GET) which + * is executed as a sub-operation within within a FHIR transaction or + * batch operation. For example, if this value is set to 100 and + * a FHIR transaction is processed with a sub-request for Patient?gender=male, + * the server will throw an error (and the transaction will fail) if there are more than * 100 resources on the server which match this query. - * - * @see #DEFAULT_LOGICAL_BASE_URLS The default value for this setting + *

+ * The default value is null, which means that there is no limit. + *

*/ - public int getMaximumSearchResultCountInTransaction() { + public Integer getMaximumSearchResultCountInTransaction() { return myMaximumSearchResultCountInTransaction; } @@ -699,14 +702,17 @@ public class DaoConfig { } /** - * Provides the maximum number of results which may be returned by a search within a FHIR transaction - * operation. For example, if this value is set to 100 and a FHIR transaction is processed with a sub-request - * for Patient?gender=male, the server will throw an error (and the transaction will fail) if there are more than + * Provides the maximum number of results which may be returned by a search (HTTP GET) which + * is executed as a sub-operation within within a FHIR transaction or + * batch operation. For example, if this value is set to 100 and + * a FHIR transaction is processed with a sub-request for Patient?gender=male, + * the server will throw an error (and the transaction will fail) if there are more than * 100 resources on the server which match this query. - * - * @see #DEFAULT_LOGICAL_BASE_URLS The default value for this setting + *

+ * The default value is null, which means that there is no limit. + *

*/ - public void setMaximumSearchResultCountInTransaction(int theMaximumSearchResultCountInTransaction) { + public void setMaximumSearchResultCountInTransaction(Integer theMaximumSearchResultCountInTransaction) { myMaximumSearchResultCountInTransaction = theMaximumSearchResultCountInTransaction; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java index f2031d0eedf..3f503c089e4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java @@ -106,8 +106,6 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { Bundle resp = new Bundle(); resp.setType(BundleTypeEnum.BATCH_RESPONSE); - OperationOutcome ooResp = new OperationOutcome(); - resp.addEntry().setResource(ooResp); /* * For batch, we handle each entry as a mini-transaction in its own database transaction so that if one fails, it doesn't prevent others @@ -163,7 +161,6 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { long delay = System.currentTimeMillis() - start; ourLog.info("Batch completed in {}ms", new Object[] { delay }); - ooResp.addIssue().setSeverity(IssueSeverityEnum.INFORMATION).setDiagnostics("Batch completed in " + delay + "ms"); return resp; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java index 6809fa97daf..79a3d3a965f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java @@ -114,8 +114,6 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { Bundle resp = new Bundle(); resp.setType(BundleType.BATCHRESPONSE); - OperationOutcome ooResp = new OperationOutcome(); - resp.addEntry().setResource(ooResp); /* * For batch, we handle each entry as a mini-transaction in its own database transaction so that if one fails, it doesn't prevent others @@ -171,7 +169,6 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { long delay = System.currentTimeMillis() - start; ourLog.info("Batch completed in {}ms", new Object[] { delay }); - ooResp.addIssue().setSeverity(IssueSeverity.INFORMATION).setDiagnostics("Batch completed in " + delay + "ms"); return resp; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java index da3ca84ee2d..2e4a9ea0da8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java @@ -77,7 +77,6 @@ public class DatabaseBackedPagingProvider extends BasePagingProvider implements @Override public synchronized String storeResultList(IBundleProvider theList) { String uuid = theList.getUuid(); - Validate.notNull(uuid); return uuid; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java index 4b418e87de0..89366320cb7 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java @@ -40,7 +40,6 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { DataSource dataSource = ProxyDataSourceBuilder .create(retVal) - .multiline() .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL") .logSlowQueryBySlf4j(10, TimeUnit.SECONDS) .countQuery() @@ -70,7 +69,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { private Properties jpaProperties() { Properties extraProperties = new Properties(); extraProperties.put("hibernate.jdbc.batch_size", "50"); - extraProperties.put("hibernate.format_sql", "true"); + extraProperties.put("hibernate.format_sql", "false"); extraProperties.put("hibernate.show_sql", "false"); extraProperties.put("hibernate.hbm2ddl.auto", "update"); extraProperties.put("hibernate.dialect", "org.hibernate.dialect.DerbyTenSevenDialect"); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java index 7537c60e7f1..3adbb5fd8ea 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java @@ -37,6 +37,7 @@ import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; +import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; @@ -75,7 +76,6 @@ import ca.uhn.fhir.util.TestUtil; @ContextConfiguration(classes= {TestDstu2Config.class, ca.uhn.fhir.jpa.config.WebsocketDstu2DispatcherConfig.class}) //@formatter:on public abstract class BaseJpaDstu2Test extends BaseJpaTest { - @Autowired protected ApplicationContext myAppCtx; @Autowired @@ -114,18 +114,18 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { @Autowired @Qualifier("myLocationDaoDstu2") protected IFhirResourceDao myLocationDao; + @Autowired + @Qualifier("myMediaDaoDstu2") + protected IFhirResourceDao myMediaDao; @Autowired - @Qualifier("myMediaDaoDstu2") - protected IFhirResourceDao myMediaDao; +@Qualifier("myMedicationAdministrationDaoDstu2") +protected IFhirResourceDao myMedicationAdministrationDao; @Autowired @Qualifier("myMedicationDaoDstu2") protected IFhirResourceDao myMedicationDao; @Autowired - @Qualifier("myMedicationAdministrationDaoDstu2") - protected IFhirResourceDao myMedicationAdministrationDao; - @Autowired @Qualifier("myMedicationOrderDaoDstu2") protected IFhirResourceDao myMedicationOrderDao; @Autowired @@ -135,6 +135,8 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { @Qualifier("myOrganizationDaoDstu2") protected IFhirResourceDao myOrganizationDao; @Autowired + protected DatabaseBackedPagingProvider myPagingProvider; + @Autowired @Qualifier("myPatientDaoDstu2") protected IFhirResourceDaoPatient myPatientDao; @Autowired @@ -150,8 +152,12 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { @Qualifier("myResourceProvidersDstu2") protected Object myResourceProviders; @Autowired + protected ISearchCoordinatorSvc mySearchCoordinatorSvc; + @Autowired protected IFulltextSearchSvc mySearchDao; @Autowired + protected ISearchParamPresenceSvc mySearchParamPresenceSvc; + @Autowired @Qualifier("myStructureDefinitionDaoDstu2") protected IFhirResourceDao myStructureDefinitionDao; @Autowired @@ -171,10 +177,6 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { @Autowired @Qualifier("myValueSetDaoDstu2") protected IFhirResourceDaoValueSet myValueSetDao; - @Autowired - protected ISearchParamPresenceSvc mySearchParamPresenceSvc; - @Autowired - protected ISearchCoordinatorSvc mySearchCoordinatorSvc; @Before public void beforeCreateInterceptor() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java index 38d94f9af7f..8520877dff2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java @@ -86,8 +86,7 @@ import ca.uhn.fhir.jpa.dao.dstu2.FhirResourceDaoDstu2SearchNoFtTest; import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; -import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; -import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; +import ca.uhn.fhir.jpa.search.*; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu3; @@ -108,13 +107,13 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { private static JpaValidationSupportChainDstu3 ourJpaValidationSupportChainDstu3; private static IFhirResourceDaoValueSet ourValueSetDao; - // @Autowired -// protected HapiWorkerContext myHapiWorkerContext; + // @Autowired + // protected HapiWorkerContext myHapiWorkerContext; @Autowired @Qualifier("myAllergyIntoleranceDaoDstu3") protected IFhirResourceDao myAllergyIntoleranceDao; @Autowired - protected ApplicationContext myAppCtx; + protected ApplicationContext myAppCtx; @Autowired @Qualifier("myAppointmentDaoDstu3") protected IFhirResourceDao myAppointmentDao; @@ -124,7 +123,7 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { @Autowired @Qualifier("myBundleDaoDstu3") protected IFhirResourceDao myBundleDao; -@Autowired + @Autowired @Qualifier("myCarePlanDaoDstu3") protected IFhirResourceDao myCarePlanDao; @Autowired @@ -195,6 +194,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { @Qualifier("myOrganizationDaoDstu3") protected IFhirResourceDao myOrganizationDao; @Autowired + protected DatabaseBackedPagingProvider myPagingProvider; + @Autowired @Qualifier("myPatientDaoDstu3") protected IFhirResourceDaoPatient myPatientDao; @Autowired diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderTransactionSearchDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderTransactionSearchDstu2Test.java new file mode 100644 index 00000000000..92df70236e4 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderTransactionSearchDstu2Test.java @@ -0,0 +1,304 @@ +package ca.uhn.fhir.jpa.provider; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.dstu2.BaseJpaDstu2Test; +import ca.uhn.fhir.jpa.rp.dstu2.*; +import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; +import ca.uhn.fhir.model.dstu2.resource.Bundle; +import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.dstu2.valueset.*; +import ca.uhn.fhir.rest.client.IGenericClient; +import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor; +import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.util.TestUtil; + +public class SystemProviderTransactionSearchDstu2Test extends BaseJpaDstu2Test { + + private static RestfulServer myRestServer; + private static IGenericClient ourClient; + private static FhirContext ourCtx; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SystemProviderTransactionSearchDstu2Test.class); + private static Server ourServer; + private static String ourServerBase; + private SimpleRequestHeaderInterceptor mySimpleHeaderInterceptor; + + + + + @SuppressWarnings("deprecation") + @After + public void after() { + myRestServer.setUseBrowserFriendlyContentTypes(true); + ourClient.unregisterInterceptor(mySimpleHeaderInterceptor); + myDaoConfig.setMaximumSearchResultCountInTransaction(new DaoConfig().getMaximumSearchResultCountInTransaction()); + } + + @Before + public void before() { + mySimpleHeaderInterceptor = new SimpleRequestHeaderInterceptor(); + ourClient.registerInterceptor(mySimpleHeaderInterceptor); + } + + @Before + public void beforeStartServer() throws Exception { + if (myRestServer == null) { + PatientResourceProvider patientRp = new PatientResourceProvider(); + patientRp.setDao(myPatientDao); + + QuestionnaireResourceProviderDstu2 questionnaireRp = new QuestionnaireResourceProviderDstu2(); + questionnaireRp.setDao(myQuestionnaireDao); + + ObservationResourceProvider observationRp = new ObservationResourceProvider(); + observationRp.setDao(myObservationDao); + + OrganizationResourceProvider organizationRp = new OrganizationResourceProvider(); + organizationRp.setDao(myOrganizationDao); + + RestfulServer restServer = new RestfulServer(ourCtx); + restServer.setResourceProviders(patientRp, questionnaireRp, observationRp, organizationRp); + + restServer.setPlainProviders(mySystemProvider); + + int myPort = RandomServerPortProvider.findFreePort(); + ourServer = new Server(myPort); + + ServletContextHandler proxyHandler = new ServletContextHandler(); + proxyHandler.setContextPath("/"); + + ourServerBase = "http://localhost:" + myPort + "/fhir/context"; + + ServletHolder servletHolder = new ServletHolder(); + servletHolder.setServlet(restServer); + proxyHandler.addServlet(servletHolder, "/fhir/context/*"); + + ourCtx = FhirContext.forDstu2(); + restServer.setFhirContext(ourCtx); + + ourServer.setHandler(proxyHandler); + ourServer.start(); + + ourCtx.getRestfulClientFactory().setSocketTimeout(600 * 1000); + ourClient = ourCtx.newRestfulGenericClient(ourServerBase); + myRestServer = restServer; + } + + myRestServer.setDefaultResponseEncoding(EncodingEnum.XML); + myRestServer.setPagingProvider(myPagingProvider); + } + + + private List create20Patients() { + List ids = new ArrayList(); + for (int i = 0; i < 20; i++) { + Patient patient = new Patient(); + patient.setGender(AdministrativeGenderEnum.MALE); + patient.addIdentifier().setSystem("urn:foo").setValue("A"); + patient.addName().addFamily("abcdefghijklmnopqrstuvwxyz".substring(i, i+1)); + String id = myPatientDao.create(patient).getId().toUnqualifiedVersionless().getValue(); + ids.add(id); + } + return ids; + } + + @Test + public void testBatchWithGetHardLimitLargeSynchronous() throws Exception { + List ids = create20Patients(); + + Bundle input = new Bundle(); + input.setType(BundleTypeEnum.BATCH); + input + .addEntry() + .getRequest() + .setMethod(HTTPVerbEnum.GET) + .setUrl("Patient?_count=5"); + + myDaoConfig.setMaximumSearchResultCountInTransaction(100); + + Bundle output = ourClient.transaction().withBundle(input).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(1, output.getEntry().size()); + Bundle respBundle = (Bundle) output.getEntry().get(0).getResource(); + assertEquals(5, respBundle.getEntry().size()); + assertEquals(null, respBundle.getLink("next")); + List actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0]))); + } + + @Test + public void testBatchWithGetNormalSearch() throws Exception { + List ids = create20Patients(); + + Bundle input = new Bundle(); + input.setType(BundleTypeEnum.BATCH); + input + .addEntry() + .getRequest() + .setMethod(HTTPVerbEnum.GET) + .setUrl("Patient?_count=5&_sort=name"); + + Bundle output = ourClient.transaction().withBundle(input).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(1, output.getEntry().size()); + Bundle respBundle = (Bundle) output.getEntry().get(0).getResource(); + assertEquals(5, respBundle.getEntry().size()); + List actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0]))); + + String nextPageLink = respBundle.getLink("next").getUrl(); + output = ourClient.loadPage().byUrl(nextPageLink).andReturnBundle(Bundle.class).execute(); + respBundle = output; + assertEquals(5, respBundle.getEntry().size()); + actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(5, 10).toArray(new String[0]))); + } + + /** + * 30 searches in one batch! Whoa! + */ + @Test + public void testBatchWithManyGets() throws Exception { + List ids = create20Patients(); + + + Bundle input = new Bundle(); + input.setType(BundleTypeEnum.BATCH); + for (int i = 0; i < 30; i++) { + input + .addEntry() + .getRequest() + .setMethod(HTTPVerbEnum.GET) + .setUrl("Patient?_count=5&identifier=urn:foo|A,AAAAA" + i); + } + + Bundle output = ourClient.transaction().withBundle(input).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(30, output.getEntry().size()); + for (int i = 0; i < 30; i++) { + Bundle respBundle = (Bundle) output.getEntry().get(i).getResource(); + assertEquals(5, respBundle.getEntry().size()); + assertThat(respBundle.getLink("next").getUrl(), not(nullValue())); + List actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0]))); + } + } + + @Test + public void testTransactionWithGetHardLimitLargeSynchronous() throws Exception { + List ids = create20Patients(); + + Bundle input = new Bundle(); + input.setType(BundleTypeEnum.TRANSACTION); + input + .addEntry() + .getRequest() + .setMethod(HTTPVerbEnum.GET) + .setUrl("Patient?_count=5"); + + myDaoConfig.setMaximumSearchResultCountInTransaction(100); + + Bundle output = ourClient.transaction().withBundle(input).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(1, output.getEntry().size()); + Bundle respBundle = (Bundle) output.getEntry().get(0).getResource(); + assertEquals(5, respBundle.getEntry().size()); + assertEquals(null, respBundle.getLink("next")); + List actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0]))); + } + + @Test + public void testTransactionWithGetNormalSearch() throws Exception { + List ids = create20Patients(); + + Bundle input = new Bundle(); + input.setType(BundleTypeEnum.TRANSACTION); + input + .addEntry() + .getRequest() + .setMethod(HTTPVerbEnum.GET) + .setUrl("Patient?_count=5&_sort=name"); + + Bundle output = ourClient.transaction().withBundle(input).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(1, output.getEntry().size()); + Bundle respBundle = (Bundle) output.getEntry().get(0).getResource(); + assertEquals(5, respBundle.getEntry().size()); + List actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0]))); + + String nextPageLink = respBundle.getLink("next").getUrl(); + output = ourClient.loadPage().byUrl(nextPageLink).andReturnBundle(Bundle.class).execute(); + respBundle = output; + assertEquals(5, respBundle.getEntry().size()); + actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(5, 10).toArray(new String[0]))); + } + + /** + * 30 searches in one Transaction! Whoa! + */ + @Test + public void testTransactionWithManyGets() throws Exception { + List ids = create20Patients(); + + + Bundle input = new Bundle(); + input.setType(BundleTypeEnum.TRANSACTION); + for (int i = 0; i < 30; i++) { + input + .addEntry() + .getRequest() + .setMethod(HTTPVerbEnum.GET) + .setUrl("Patient?_count=5&identifier=urn:foo|A,AAAAA" + i); + } + + Bundle output = ourClient.transaction().withBundle(input).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(30, output.getEntry().size()); + for (int i = 0; i < 30; i++) { + Bundle respBundle = (Bundle) output.getEntry().get(i).getResource(); + assertEquals(5, respBundle.getEntry().size()); + assertThat(respBundle.getLink("next").getUrl(), not(nullValue())); + List actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0]))); + } + } + + private List toIds(Bundle theRespBundle) { + ArrayList retVal = new ArrayList(); + for (Entry next : theRespBundle.getEntry()) { + retVal.add(next.getResource().getIdElement().toUnqualifiedVersionless().getValue()); + } + return retVal; + } + + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/SystemProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/SystemProviderDstu3Test.java index af577fef5db..1e0d52bba4f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/SystemProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/SystemProviderDstu3Test.java @@ -58,6 +58,7 @@ import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; import ca.uhn.fhir.jpa.rp.dstu3.ObservationResourceProvider; import ca.uhn.fhir.jpa.rp.dstu3.OrganizationResourceProvider; import ca.uhn.fhir.jpa.rp.dstu3.PatientResourceProvider; +import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor; @@ -202,7 +203,6 @@ public class SystemProviderDstu3Test extends BaseJpaDstu3Test { organizationRp.setDao(myOrganizationDao); RestfulServer restServer = new RestfulServer(ourCtx); - restServer.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(10)); restServer.setResourceProviders(patientRp, questionnaireRp, observationRp, organizationRp); restServer.setPlainProviders(mySystemProvider); @@ -237,6 +237,7 @@ public class SystemProviderDstu3Test extends BaseJpaDstu3Test { } myRestServer.setDefaultResponseEncoding(EncodingEnum.XML); + myRestServer.setPagingProvider(myPagingProvider); } @Before diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/SystemProviderTransactionSearchDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/SystemProviderTransactionSearchDstu3Test.java new file mode 100644 index 00000000000..2ae03965067 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/SystemProviderTransactionSearchDstu3Test.java @@ -0,0 +1,358 @@ +package ca.uhn.fhir.jpa.provider.dstu3; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.Bundle.*; +import org.hl7.fhir.dstu3.model.DecimalType; +import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.Observation; +import org.hl7.fhir.dstu3.model.OperationDefinition; +import org.hl7.fhir.dstu3.model.OperationOutcome; +import org.hl7.fhir.dstu3.model.Organization; +import org.hl7.fhir.dstu3.model.Parameters; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.DaoMethodOutcome; +import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test; +import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; +import ca.uhn.fhir.jpa.rp.dstu3.ObservationResourceProvider; +import ca.uhn.fhir.jpa.rp.dstu3.OrganizationResourceProvider; +import ca.uhn.fhir.jpa.rp.dstu3.PatientResourceProvider; +import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; +import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; +import ca.uhn.fhir.rest.client.IGenericClient; +import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; +import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; +import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.validation.ResultSeverityEnum; + +public class SystemProviderTransactionSearchDstu3Test extends BaseJpaDstu3Test { + + private static RestfulServer myRestServer; + private static IGenericClient ourClient; + private static FhirContext ourCtx; + private static CloseableHttpClient ourHttpClient; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SystemProviderTransactionSearchDstu3Test.class); + private static Server ourServer; + private static String ourServerBase; + private SimpleRequestHeaderInterceptor mySimpleHeaderInterceptor; + + + + @SuppressWarnings("deprecation") + @After + public void after() { + myRestServer.setUseBrowserFriendlyContentTypes(true); + ourClient.unregisterInterceptor(mySimpleHeaderInterceptor); + myDaoConfig.setMaximumSearchResultCountInTransaction(new DaoConfig().getMaximumSearchResultCountInTransaction()); + } + + @Before + public void before() { + mySimpleHeaderInterceptor = new SimpleRequestHeaderInterceptor(); + ourClient.registerInterceptor(mySimpleHeaderInterceptor); + } + + @Before + public void beforeStartServer() throws Exception { + if (myRestServer == null) { + PatientResourceProvider patientRp = new PatientResourceProvider(); + patientRp.setDao(myPatientDao); + + QuestionnaireResourceProviderDstu3 questionnaireRp = new QuestionnaireResourceProviderDstu3(); + questionnaireRp.setDao(myQuestionnaireDao); + + ObservationResourceProvider observationRp = new ObservationResourceProvider(); + observationRp.setDao(myObservationDao); + + OrganizationResourceProvider organizationRp = new OrganizationResourceProvider(); + organizationRp.setDao(myOrganizationDao); + + RestfulServer restServer = new RestfulServer(ourCtx); + restServer.setResourceProviders(patientRp, questionnaireRp, observationRp, organizationRp); + + restServer.setPlainProviders(mySystemProvider); + + int myPort = RandomServerPortProvider.findFreePort(); + ourServer = new Server(myPort); + + ServletContextHandler proxyHandler = new ServletContextHandler(); + proxyHandler.setContextPath("/"); + + ourServerBase = "http://localhost:" + myPort + "/fhir/context"; + + ServletHolder servletHolder = new ServletHolder(); + servletHolder.setServlet(restServer); + proxyHandler.addServlet(servletHolder, "/fhir/context/*"); + + ourCtx = FhirContext.forDstu3(); + restServer.setFhirContext(ourCtx); + + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourHttpClient = builder.build(); + + ourCtx.getRestfulClientFactory().setSocketTimeout(600 * 1000); + ourClient = ourCtx.newRestfulGenericClient(ourServerBase); + ourClient.setLogRequestAndResponse(true); + myRestServer = restServer; + } + + myRestServer.setDefaultResponseEncoding(EncodingEnum.XML); + myRestServer.setPagingProvider(myPagingProvider); + } + + + private List create20Patients() { + List ids = new ArrayList(); + for (int i = 0; i < 20; i++) { + Patient patient = new Patient(); + patient.setGender(AdministrativeGender.MALE); + patient.addIdentifier().setSystem("urn:foo").setValue("A"); + patient.addName().setFamily("abcdefghijklmnopqrstuvwxyz".substring(i, i+1)); + String id = myPatientDao.create(patient).getId().toUnqualifiedVersionless().getValue(); + ids.add(id); + } + return ids; + } + + @Test + public void testBatchWithGetHardLimitLargeSynchronous() throws Exception { + List ids = create20Patients(); + + Bundle input = new Bundle(); + input.setType(BundleType.BATCH); + input + .addEntry() + .getRequest() + .setMethod(HTTPVerb.GET) + .setUrl("Patient?_count=5"); + + myDaoConfig.setMaximumSearchResultCountInTransaction(100); + + Bundle output = ourClient.transaction().withBundle(input).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(1, output.getEntry().size()); + Bundle respBundle = (Bundle) output.getEntry().get(0).getResource(); + assertEquals(5, respBundle.getEntry().size()); + assertEquals(null, respBundle.getLink("next")); + List actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0]))); + } + + @Test + public void testBatchWithGetNormalSearch() throws Exception { + List ids = create20Patients(); + + Bundle input = new Bundle(); + input.setType(BundleType.BATCH); + input + .addEntry() + .getRequest() + .setMethod(HTTPVerb.GET) + .setUrl("Patient?_count=5&_sort=name"); + + Bundle output = ourClient.transaction().withBundle(input).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(1, output.getEntry().size()); + Bundle respBundle = (Bundle) output.getEntry().get(0).getResource(); + assertEquals(5, respBundle.getEntry().size()); + List actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0]))); + + String nextPageLink = respBundle.getLink("next").getUrl(); + output = ourClient.loadPage().byUrl(nextPageLink).andReturnBundle(Bundle.class).execute(); + respBundle = output; + assertEquals(5, respBundle.getEntry().size()); + actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(5, 10).toArray(new String[0]))); + } + + /** + * 30 searches in one batch! Whoa! + */ + @Test + public void testBatchWithManyGets() throws Exception { + List ids = create20Patients(); + + + Bundle input = new Bundle(); + input.setType(BundleType.BATCH); + for (int i = 0; i < 30; i++) { + input + .addEntry() + .getRequest() + .setMethod(HTTPVerb.GET) + .setUrl("Patient?_count=5&identifier=urn:foo|A,AAAAA" + i); + } + + Bundle output = ourClient.transaction().withBundle(input).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(30, output.getEntry().size()); + for (int i = 0; i < 30; i++) { + Bundle respBundle = (Bundle) output.getEntry().get(i).getResource(); + assertEquals(5, respBundle.getEntry().size()); + assertThat(respBundle.getLink("next").getUrl(), not(nullValue())); + List actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0]))); + } + } + + @Test + public void testTransactionWithGetHardLimitLargeSynchronous() throws Exception { + List ids = create20Patients(); + + Bundle input = new Bundle(); + input.setType(BundleType.TRANSACTION); + input + .addEntry() + .getRequest() + .setMethod(HTTPVerb.GET) + .setUrl("Patient?_count=5"); + + myDaoConfig.setMaximumSearchResultCountInTransaction(100); + + Bundle output = ourClient.transaction().withBundle(input).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(1, output.getEntry().size()); + Bundle respBundle = (Bundle) output.getEntry().get(0).getResource(); + assertEquals(5, respBundle.getEntry().size()); + assertEquals(null, respBundle.getLink("next")); + List actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0]))); + } + + @Test + public void testTransactionWithGetNormalSearch() throws Exception { + List ids = create20Patients(); + + Bundle input = new Bundle(); + input.setType(BundleType.TRANSACTION); + input + .addEntry() + .getRequest() + .setMethod(HTTPVerb.GET) + .setUrl("Patient?_count=5&_sort=name"); + + Bundle output = ourClient.transaction().withBundle(input).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(1, output.getEntry().size()); + Bundle respBundle = (Bundle) output.getEntry().get(0).getResource(); + assertEquals(5, respBundle.getEntry().size()); + List actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0]))); + + String nextPageLink = respBundle.getLink("next").getUrl(); + output = ourClient.loadPage().byUrl(nextPageLink).andReturnBundle(Bundle.class).execute(); + respBundle = output; + assertEquals(5, respBundle.getEntry().size()); + actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(5, 10).toArray(new String[0]))); + } + + /** + * 30 searches in one Transaction! Whoa! + */ + @Test + public void testTransactionWithManyGets() throws Exception { + List ids = create20Patients(); + + + Bundle input = new Bundle(); + input.setType(BundleType.TRANSACTION); + for (int i = 0; i < 30; i++) { + input + .addEntry() + .getRequest() + .setMethod(HTTPVerb.GET) + .setUrl("Patient?_count=5&identifier=urn:foo|A,AAAAA" + i); + } + + Bundle output = ourClient.transaction().withBundle(input).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(30, output.getEntry().size()); + for (int i = 0; i < 30; i++) { + Bundle respBundle = (Bundle) output.getEntry().get(i).getResource(); + assertEquals(5, respBundle.getEntry().size()); + assertThat(respBundle.getLink("next").getUrl(), not(nullValue())); + List actualIds = toIds(respBundle); + assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0]))); + } + } + + private List toIds(Bundle theRespBundle) { + ArrayList retVal = new ArrayList(); + for (BundleEntryComponent next : theRespBundle.getEntry()) { + retVal.add(next.getResource().getIdElement().toUnqualifiedVersionless().getValue()); + } + return retVal; + } + + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + +} diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java index 95280e5d37e..6a4373c0884 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.rest.server.provider.dstu2; +import static org.apache.commons.lang3.StringUtils.isBlank; /* * #%L * HAPI FHIR Structures - DSTU2 (FHIR v1.0.0) @@ -10,7 +11,7 @@ package ca.uhn.fhir.rest.server.provider.dstu2; * 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 + * 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, @@ -62,6 +63,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.ResourceReferenceInfo; public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(Dstu2BundleFactory.class); private Bundle myBundle; private FhirContext myContext; @@ -223,7 +225,7 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { entry.getRequest().getMethodElement().setValueAsString(httpVerb.getCode()); } populateBundleEntryFullUrl(next, entry); - + BundleEntrySearchModeEnum searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(next); if (searchMode != null) { entry.getSearch().getModeElement().setValue(searchMode.getCode()); @@ -257,7 +259,7 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { public void addRootPropertiesToBundle(String theAuthor, String theServerBase, String theCompleteUrl, Integer theTotalResults, BundleTypeEnum theBundleType, IPrimitiveType theLastUpdated) { myBase = theServerBase; - + if (myBundle.getId().isEmpty()) { myBundle.setId(UUID.randomUUID().toString()); } @@ -302,7 +304,7 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { public void initializeBundleFromBundleProvider(IRestfulServer theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, boolean thePrettyPrint, int theOffset, Integer theLimit, String theSearchId, BundleTypeEnum theBundleType, Set theIncludes) { myBase = theServerBase; - + int numToReturn; String searchId = null; List resourceList; @@ -327,7 +329,7 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { if (numTotalResults != null) { numToReturn = Math.min(numToReturn, numTotalResults - theOffset); } - + if (numToReturn > 0) { resourceList = theResult.getResources(theOffset, numToReturn + theOffset); } else { @@ -340,7 +342,9 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { } else { if (numTotalResults == null || numTotalResults > numToReturn) { searchId = pagingProvider.storeResultList(theResult); - Validate.notNull(searchId, "Paging provider returned null searchId"); + if (isBlank(searchId)) { + ourLog.info("Found {} results but paging provider did not provide an ID to use for paging", numTotalResults); + } } } } diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/Dstu3BundleFactory.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/Dstu3BundleFactory.java index e4a57134cbc..6ad431c94ca 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/Dstu3BundleFactory.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/Dstu3BundleFactory.java @@ -1,5 +1,6 @@ package org.hl7.fhir.dstu3.hapi.rest.server; +import static org.apache.commons.lang3.StringUtils.isBlank; /* * #%L * HAPI FHIR Structures - DSTU2 (FHIR v1.0.0) @@ -44,7 +45,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.ResourceReferenceInfo; public class Dstu3BundleFactory implements IVersionSpecificBundleFactory { - + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(Dstu3BundleFactory.class); private Bundle myBundle; private FhirContext myContext; private String myBase; @@ -336,7 +337,9 @@ public class Dstu3BundleFactory implements IVersionSpecificBundleFactory { } else { if (numTotalResults == null || numTotalResults > numToReturn) { searchId = pagingProvider.storeResultList(theResult); - Validate.notNull(searchId, "Paging provider returned null searchId"); + if (isBlank(searchId)) { + ourLog.info("Found {} results but paging provider did not provide an ID to use for paging", numTotalResults); + } } } } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 35ea92cb170..19b00caebb7 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -85,6 +85,23 @@ (tags, profiles, and security labels) on an individual resource. The default is 1000. + + When executing a search (HTTP GET) as a nested operation in in a transaction or + batch operation, the search now returns a normal page of results with a link to + the next page, like any other search would. Previously the search would return + a small number of results with no paging performed, so this change brings transaction + and batch processing in line with other types of search. + + + JPA server no longer returns an OperationOutcome resource as the first resource + in the Bundle for a response to a batch operation. This behaviour was previously + present, but was not specified in the FHIR specification so it caused confusion and + was inconsistent with behaviour in other servers. + + + Fix a regression in HAPI FHIR 2.5 JPA server where executing a search in a + transaction or batch operation caused an exception. +