Clean up handling of searches nested in batch and transaction

This commit is contained in:
James Agnew 2017-06-30 16:20:32 -04:00
parent 28a5b92fe2
commit c9fcef0372
15 changed files with 747 additions and 53 deletions

View File

@ -1744,9 +1744,12 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> 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<IdDt> 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);

View File

@ -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<T extends IBaseResource> 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)) {

View File

@ -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 <code>transaction</code>
* operation. For example, if this value is set to <code>100</code> and a FHIR transaction is processed with a sub-request
* for <code>Patient?gender=male</code>, 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 <code>transaction</code> or
* <code>batch</code> operation. For example, if this value is set to <code>100</code> and
* a FHIR transaction is processed with a sub-request for <code>Patient?gender=male</code>,
* 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
* <p>
* The default value is <code>null</code>, which means that there is no limit.
* </p>
*/
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 <code>transaction</code>
* operation. For example, if this value is set to <code>100</code> and a FHIR transaction is processed with a sub-request
* for <code>Patient?gender=male</code>, 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 <code>transaction</code> or
* <code>batch</code> operation. For example, if this value is set to <code>100</code> and
* a FHIR transaction is processed with a sub-request for <code>Patient?gender=male</code>,
* 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
* <p>
* The default value is <code>null</code>, which means that there is no limit.
* </p>
*/
public void setMaximumSearchResultCountInTransaction(int theMaximumSearchResultCountInTransaction) {
public void setMaximumSearchResultCountInTransaction(Integer theMaximumSearchResultCountInTransaction) {
myMaximumSearchResultCountInTransaction = theMaximumSearchResultCountInTransaction;
}

View File

@ -106,8 +106,6 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
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<Bundle, MetaDt> {
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;
}

View File

@ -114,8 +114,6 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
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<Bundle, Meta> {
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;
}

View File

@ -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;
}

View File

@ -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");

View File

@ -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<Location> myLocationDao;
@Autowired
@Qualifier("myMediaDaoDstu2")
protected IFhirResourceDao<Media> myMediaDao;
@Autowired
@Qualifier("myMediaDaoDstu2")
protected IFhirResourceDao<Media> myMediaDao;
@Qualifier("myMedicationAdministrationDaoDstu2")
protected IFhirResourceDao<MedicationAdministration> myMedicationAdministrationDao;
@Autowired
@Qualifier("myMedicationDaoDstu2")
protected IFhirResourceDao<Medication> myMedicationDao;
@Autowired
@Qualifier("myMedicationAdministrationDaoDstu2")
protected IFhirResourceDao<MedicationAdministration> myMedicationAdministrationDao;
@Autowired
@Qualifier("myMedicationOrderDaoDstu2")
protected IFhirResourceDao<MedicationOrder> myMedicationOrderDao;
@Autowired
@ -135,6 +135,8 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest {
@Qualifier("myOrganizationDaoDstu2")
protected IFhirResourceDao<Organization> myOrganizationDao;
@Autowired
protected DatabaseBackedPagingProvider myPagingProvider;
@Autowired
@Qualifier("myPatientDaoDstu2")
protected IFhirResourceDaoPatient<Patient> 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<StructureDefinition> myStructureDefinitionDao;
@Autowired
@ -171,10 +177,6 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest {
@Autowired
@Qualifier("myValueSetDaoDstu2")
protected IFhirResourceDaoValueSet<ValueSet, CodingDt, CodeableConceptDt> myValueSetDao;
@Autowired
protected ISearchParamPresenceSvc mySearchParamPresenceSvc;
@Autowired
protected ISearchCoordinatorSvc mySearchCoordinatorSvc;
@Before
public void beforeCreateInterceptor() {

View File

@ -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<ValueSet, Coding, CodeableConcept> ourValueSetDao;
// @Autowired
// protected HapiWorkerContext myHapiWorkerContext;
// @Autowired
// protected HapiWorkerContext myHapiWorkerContext;
@Autowired
@Qualifier("myAllergyIntoleranceDaoDstu3")
protected IFhirResourceDao<AllergyIntolerance> myAllergyIntoleranceDao;
@Autowired
protected ApplicationContext myAppCtx;
protected ApplicationContext myAppCtx;
@Autowired
@Qualifier("myAppointmentDaoDstu3")
protected IFhirResourceDao<Appointment> myAppointmentDao;
@ -124,7 +123,7 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
@Autowired
@Qualifier("myBundleDaoDstu3")
protected IFhirResourceDao<Bundle> myBundleDao;
@Autowired
@Autowired
@Qualifier("myCarePlanDaoDstu3")
protected IFhirResourceDao<CarePlan> myCarePlanDao;
@Autowired
@ -195,6 +194,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
@Qualifier("myOrganizationDaoDstu3")
protected IFhirResourceDao<Organization> myOrganizationDao;
@Autowired
protected DatabaseBackedPagingProvider myPagingProvider;
@Autowired
@Qualifier("myPatientDaoDstu3")
protected IFhirResourceDaoPatient<Patient> myPatientDao;
@Autowired

View File

@ -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<String> create20Patients() {
List<String> ids = new ArrayList<String>();
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<String> 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<String> actualIds = toIds(respBundle);
assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0])));
}
@Test
public void testBatchWithGetNormalSearch() throws Exception {
List<String> 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<String> 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<String> 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<String> actualIds = toIds(respBundle);
assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0])));
}
}
@Test
public void testTransactionWithGetHardLimitLargeSynchronous() throws Exception {
List<String> 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<String> actualIds = toIds(respBundle);
assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0])));
}
@Test
public void testTransactionWithGetNormalSearch() throws Exception {
List<String> 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<String> 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<String> 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<String> actualIds = toIds(respBundle);
assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0])));
}
}
private List<String> toIds(Bundle theRespBundle) {
ArrayList<String> retVal = new ArrayList<String>();
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();
}
}

View File

@ -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

View File

@ -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<String> create20Patients() {
List<String> ids = new ArrayList<String>();
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<String> 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<String> actualIds = toIds(respBundle);
assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0])));
}
@Test
public void testBatchWithGetNormalSearch() throws Exception {
List<String> 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<String> 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<String> 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<String> actualIds = toIds(respBundle);
assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0])));
}
}
@Test
public void testTransactionWithGetHardLimitLargeSynchronous() throws Exception {
List<String> 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<String> actualIds = toIds(respBundle);
assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0])));
}
@Test
public void testTransactionWithGetNormalSearch() throws Exception {
List<String> 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<String> 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<String> 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<String> actualIds = toIds(respBundle);
assertThat(actualIds, contains(ids.subList(0, 5).toArray(new String[0])));
}
}
private List<String> toIds(Bundle theRespBundle) {
ArrayList<String> retVal = new ArrayList<String>();
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();
}
}

View File

@ -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<Date> 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<Include> theIncludes) {
myBase = theServerBase;
int numToReturn;
String searchId = null;
List<IBaseResource> 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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -85,6 +85,23 @@
(tags, profiles, and security labels) on an individual resource. The default
is 1000.
</action>
<action type="add">
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.
</action>
<action type="add">
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.
</action>
<action type="fix">
Fix a regression in HAPI FHIR 2.5 JPA server where executing a search in a
transaction or batch operation caused an exception.
</action>
</release>
<release version="2.5" date="2017-06-08">
<action type="fix">