diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/SimpleRequestHeaderInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/SimpleRequestHeaderInterceptor.java new file mode 100644 index 00000000000..63c7a25f90f --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/SimpleRequestHeaderInterceptor.java @@ -0,0 +1,90 @@ +package ca.uhn.fhir.rest.client.interceptor; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2017 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; + +import ca.uhn.fhir.rest.client.IClientInterceptor; +import ca.uhn.fhir.rest.client.api.IHttpRequest; +import ca.uhn.fhir.rest.client.api.IHttpResponse; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; + +/** + * This interceptor adds an arbitrary header to requests made by this client. Both the + * header name and the header value are specified by the calling code. + */ +public class SimpleRequestHeaderInterceptor implements IClientInterceptor { + + private String myHeaderName; + private String myHeaderValue; + + /** + * Constructor + */ + public SimpleRequestHeaderInterceptor() { + this(null, null); + } + + /** + * Constructor + */ + public SimpleRequestHeaderInterceptor(String theHeaderName, String theHeaderValue) { + super(); + myHeaderName = theHeaderName; + myHeaderValue = theHeaderValue; + } + + public String getHeaderName() { + return myHeaderName; + } + + public String getHeaderValue() { + return myHeaderValue; + } + + @Override + public void interceptRequest(IHttpRequest theRequest) { + if (isNotBlank(getHeaderName())) { + theRequest.addHeader(getHeaderName(), getHeaderValue()); + } + } + + @Override + public void interceptResponse(IHttpResponse theResponse) throws IOException { + // nothing + } + + public void setHeaderName(String theHeaderName) { + myHeaderName = theHeaderName; + } + + public void setHeaderValue(String theHeaderValue) { + myHeaderValue = theHeaderValue; + } + +} 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 109a433a52b..6809fa97daf 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 @@ -39,6 +39,7 @@ import java.util.Map.Entry; import java.util.Set; import javax.persistence.TypedQuery; +import javax.servlet.http.HttpServletRequest; import org.apache.http.NameValuePair; import org.hl7.fhir.dstu3.model.Bundle; @@ -79,6 +80,7 @@ import ca.uhn.fhir.jpa.util.DeleteConflict; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.method.BaseMethodBinding; @@ -282,7 +284,7 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { super.clearRequestAsProcessingSubRequest(theRequestDetails); } } - + @SuppressWarnings("unchecked") private Bundle doTransaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) { BundleType transactionType = theRequest.getTypeElement().getValue(); @@ -408,7 +410,7 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { String matchUrl = nextReqEntry.getRequest().getIfNoneExist(); outcome = resourceDao.create(res, matchUrl, false, theRequestDetails); if (nextResourceId != null) { - handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res); + handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); } entriesToProcess.put(nextRespEntry, outcome.getEntity()); if (outcome.getCreated() == false) { @@ -445,12 +447,12 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { if (allDeleted.isEmpty()) { status = Constants.STATUS_HTTP_204_NO_CONTENT; } - + nextRespEntry.getResponse().setOutcome((Resource) deleteOutcome.getOperationOutcome()); } nextRespEntry.getResponse().setStatus(toStatusString(status)); - + break; } case PUT: { @@ -474,7 +476,7 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { } } - handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res); + handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); entriesToProcess.put(nextRespEntry, outcome.getEntity()); break; } @@ -646,10 +648,8 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { return response; } - - private static void handleTransactionCreateOrUpdateOutcome(Map idSubstitutions, Map idToPersistedOutcome, IdType nextResourceId, DaoMethodOutcome outcome, - BundleEntryComponent newEntry, String theResourceType, IBaseResource theRes) { + BundleEntryComponent newEntry, String theResourceType, IBaseResource theRes, ServletRequestDetails theRequestDetails) { IdType newId = (IdType) outcome.getId().toUnqualifiedVersionless(); IdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless(); if (newId.equals(resourceId) == false) { @@ -668,6 +668,19 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK)); } newEntry.getResponse().setLastModified(((Resource) theRes).getMeta().getLastUpdated()); + + if (theRequestDetails != null) { + if (outcome.getResource() != null) { + String prefer = theRequestDetails.getHeader(Constants.HEADER_PREFER); + PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(prefer); + if (preferReturn != null) { + if (preferReturn == PreferReturnEnum.REPRESENTATION) { + newEntry.setResource((Resource) outcome.getResource()); + } + } + } + } + } private static boolean isPlaceholder(IdType theId) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java index 25642e089aa..748901984e5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java @@ -1181,6 +1181,23 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { assertThat(patients, (hasItems(id1a, id1b))); assertThat(patients, not(hasItems(id2))); } + + + { + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, beforeR2))); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, not(hasItems(id1a, id1b))); + assertThat(patients, (hasItems(id2))); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, beforeR2))); + List patients = toUnqualifiedVersionlessIds(myPatientDao.search(params)); + assertThat(patients, (hasItems(id1a, id1b))); + assertThat(patients, not(hasItems(id2))); + } + } @SuppressWarnings("deprecation") diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java index 9a8460e140a..c4a74a2c204 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java @@ -1370,6 +1370,53 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { ourLog.info(ids.toString()); } + @Test + public void testSearchByLastUpdated() throws Exception { + String methodName = "testSearchByLastUpdated"; + + Patient p = new Patient(); + p.addName().setFamily(methodName+"1"); + IIdType pid1 = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless(); + + Thread.sleep(10); + long time1 = System.currentTimeMillis(); + Thread.sleep(10); + + Patient p2 = new Patient(); + p2.addName().setFamily(methodName+"2"); + IIdType pid2 = ourClient.create().resource(p2).execute().getId().toUnqualifiedVersionless(); + + HttpGet get = new HttpGet(ourServerBase + "/Patient?_lastUpdated=lt" + new InstantType(new Date(time1)).getValueAsString()); + CloseableHttpResponse response = ourHttpClient.execute(get); + try { + assertEquals(200, response.getStatusLine().getStatusCode()); + String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + IOUtils.closeQuietly(response.getEntity().getContent()); + ourLog.info(output); + List ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output)); + ourLog.info(ids.toString()); + assertThat(ids, containsInAnyOrder(pid1)); + } finally { + response.close(); + } + + get = new HttpGet(ourServerBase + "/Patient?_lastUpdated=gt" + new InstantType(new Date(time1)).getValueAsString()); + response = ourHttpClient.execute(get); + try { + assertEquals(200, response.getStatusLine().getStatusCode()); + String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + IOUtils.closeQuietly(response.getEntity().getContent()); + ourLog.info(output); + List ids = toUnqualifiedVersionlessIds(myFhirCtx.newXmlParser().parseResource(Bundle.class, output)); + ourLog.info(ids.toString()); + assertThat(ids, containsInAnyOrder(pid2)); + } finally { + response.close(); + } + + } + + @Test public void testEverythingPatientType() throws Exception { String methodName = "testEverythingPatientType"; 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 e4c70769d94..af577fef5db 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 @@ -60,6 +60,7 @@ import ca.uhn.fhir.jpa.rp.dstu3.OrganizationResourceProvider; import ca.uhn.fhir.jpa.rp.dstu3.PatientResourceProvider; 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; @@ -80,6 +81,7 @@ public class SystemProviderDstu3Test extends BaseJpaDstu3Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SystemProviderDstu3Test.class); private static Server ourServer; private static String ourServerBase; + private SimpleRequestHeaderInterceptor mySimpleHeaderInterceptor; @Test public void testTransactionWithInlineConditionalUrl() throws Exception { @@ -237,10 +239,17 @@ public class SystemProviderDstu3Test extends BaseJpaDstu3Test { myRestServer.setDefaultResponseEncoding(EncodingEnum.XML); } + @Before + public void before() { + mySimpleHeaderInterceptor = new SimpleRequestHeaderInterceptor(); + ourClient.registerInterceptor(mySimpleHeaderInterceptor); + } + @SuppressWarnings("deprecation") @After public void after() { myRestServer.setUseBrowserFriendlyContentTypes(true); + ourClient.unregisterInterceptor(mySimpleHeaderInterceptor); } @SuppressWarnings("deprecation") @@ -632,6 +641,44 @@ public class SystemProviderDstu3Test extends BaseJpaDstu3Test { assertEquals(0, respSub.getEntry().size()); } + @Test + public void testTransactionCreateWithPreferHeader() throws Exception { + + Patient p = new Patient(); + p.setActive(true); + + Bundle req; + Bundle resp; + + // No prefer header + req = new Bundle(); + req.setType(BundleType.TRANSACTION); + req.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient"); + resp = ourClient.transaction().withBundle(req).execute(); + assertEquals(null, resp.getEntry().get(0).getResource()); + assertEquals("201 Created", resp.getEntry().get(0).getResponse().getStatus()); + + // Prefer return=minimal + mySimpleHeaderInterceptor.setHeaderName(Constants.HEADER_PREFER); + mySimpleHeaderInterceptor.setHeaderValue(Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_MINIMAL); + req = new Bundle(); + req.setType(BundleType.TRANSACTION); + req.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient"); + resp = ourClient.transaction().withBundle(req).execute(); + assertEquals(null, resp.getEntry().get(0).getResource()); + assertEquals("201 Created", resp.getEntry().get(0).getResponse().getStatus()); + + // Prefer return=representation + mySimpleHeaderInterceptor.setHeaderName(Constants.HEADER_PREFER); + mySimpleHeaderInterceptor.setHeaderValue(Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_REPRESENTATION); + req = new Bundle(); + req.setType(BundleType.TRANSACTION); + req.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient"); + resp = ourClient.transaction().withBundle(req).execute(); + assertEquals(Patient.class, resp.getEntry().get(0).getResource().getClass()); + assertEquals("201 Created", resp.getEntry().get(0).getResponse().getStatus()); + } + @AfterClass public static void afterClassClearContext() throws Exception { ourServer.stop(); diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java index 16674c77d0c..5f7e5b8ff9e 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java @@ -27,6 +27,7 @@ import org.apache.http.ProtocolVersion; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.ContentType; @@ -60,6 +61,7 @@ import ca.uhn.fhir.parser.CustomTypeDstu3Test.MyCustomPatient; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.PreferReturnEnum; +import ca.uhn.fhir.rest.client.SearchClientDstu3Test.ILocationClient; import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; import ca.uhn.fhir.rest.client.interceptor.CookieInterceptor; @@ -177,6 +179,8 @@ public class GenericClientDstu3Test { OperationOutcome oo = (OperationOutcome) outcome.getOperationOutcome(); assertThat(oo.getText().getDivAsString(), containsString("OK!")); } + + @Test public void testPatchJsonByIdType() throws Exception { @@ -1191,6 +1195,36 @@ public class GenericClientDstu3Test { // assertEquals("FAM", resp.getNameFirstRep().getFamilyAsSingleString()); } + @Test + public void testSearchWithNullParameters() throws Exception { + final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).then(new Answer() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + int idx = 0; + + DateTimeDt now = DateTimeDt.withCurrentTime(); + String dateString = now.getValueAsString().substring(0, 10); + + client.search() + .forResource("Patient") + .where(Patient.NAME.matches().value((String)null)) + .returnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(idx).getURI().toString()); + idx++; + } + @Test public void testSearchByDate() throws Exception { final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/SearchClientDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/SearchClientDstu3Test.java index 51a959d7c4f..ddfa5e2849f 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/SearchClientDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/SearchClientDstu3Test.java @@ -43,7 +43,6 @@ import ca.uhn.fhir.rest.annotation.Sort; import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.client.api.IRestfulClient; -import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.util.TestUtil; diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 8cb73b1159d..d0cf8a64659 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -32,6 +32,11 @@ Fix HTTP 500 error in JPA server if a numeric search parameter was supplied with no value, e.g. GET /Observation?value-quantity=]]> + + JPA server transaction processing now honours the Prefer header and includes + created and updated resource bodies in the response bundle if it is set + appropriately. + Optimize queries in JPA server remove a few redundant select columns when performing searches. This provides a slight speed increase in some cases.