From c2a6e78e67731b85627c1d2cd7850992d349e602 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Mon, 23 Feb 2015 18:27:13 -0500 Subject: [PATCH] Add conditional deletes and updates --- .../java/example/GenericClientExample.java | 59 ++++++++- .../RestfulPatientResourceProviderMore.java | 37 ++++++ .../annotation/ConditionalOperationParam.java | 32 +++++ .../ca/uhn/fhir/rest/api/MethodOutcome.java | 4 + .../BaseOutcomeReturningMethodBinding.java | 9 +- .../rest/method/ConditionalParamBinder.java | 57 ++++++++ .../ca/uhn/fhir/rest/method/MethodUtil.java | 7 + .../fhir/rest/method/UpdateMethodBinding.java | 22 +-- .../fhir/rest/client/GenericClientTest.java | 26 ++++ .../ModelInstantiationTest.java | 2 +- .../rest/server/DeleteConditionalTest.java | 125 ++++++++++++++++++ .../rest/server/UpdateConditionalTest.java | 78 +++++++---- src/site/resources/hapi.css | 20 +++ src/site/xdoc/doc_rest_client.xml | 61 ++++++++- src/site/xdoc/doc_rest_operations.xml | 55 +++++++- 15 files changed, 533 insertions(+), 61 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/ConditionalOperationParam.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConditionalParamBinder.java rename hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/model/{dev => dstu2}/ModelInstantiationTest.java (97%) create mode 100644 hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/DeleteConditionalTest.java diff --git a/examples/src/main/java/example/GenericClientExample.java b/examples/src/main/java/example/GenericClientExample.java index e8e9d6e1b72..63c99a6f84d 100644 --- a/examples/src/main/java/example/GenericClientExample.java +++ b/examples/src/main/java/example/GenericClientExample.java @@ -10,6 +10,7 @@ import ca.uhn.fhir.model.base.resource.BaseConformance; import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; import ca.uhn.fhir.model.dstu2.valueset.AdministrativeGenderEnum; import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.model.dstu2.resource.OperationOutcome; import ca.uhn.fhir.model.dstu2.resource.Organization; import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.primitive.IdDt; @@ -68,6 +69,30 @@ public class GenericClientExample { System.out.println("Got ID: " + id.getValue()); // END SNIPPET: create } + { + Patient patient = new Patient(); + // START SNIPPET: createConditional + // One form + MethodOutcome outcome = client.create() + .resource(patient) + .conditionalByUrl("Patient?identifier=system%7C00001") + .execute(); + + // Another form + MethodOutcome outcome2 = client.create() + .resource(patient) + .conditional() + .where(Patient.IDENTIFIER.exactly().systemAndIdentifier("system", "00001")) + .execute(); + + // This will return true if the server responded with an HTTP 201 created, + // otherwise it will return null. + Boolean created = outcome.getCreated(); + + // The ID of the created, or the pre-existing resource + IdDt id = outcome.getId(); + // END SNIPPET: createConditional + } { // START SNIPPET: update Patient patient = new Patient(); @@ -95,6 +120,21 @@ public class GenericClientExample { System.out.println("Got ID: " + id.getValue()); // END SNIPPET: update } + { + Patient patient = new Patient(); + // START SNIPPET: updateConditional + client.update() + .resource(patient) + .conditionalByUrl("Patient?identifier=system%7C00001") + .execute(); + + client.update() + .resource(patient) + .conditional() + .where(Patient.IDENTIFIER.exactly().systemAndIdentifier("system", "00001")) + .execute(); + // END SNIPPET: updateConditional + } { // START SNIPPET: etagupdate // First, let's retrive the latest version of a resource @@ -132,16 +172,27 @@ public class GenericClientExample { } { // START SNIPPET: delete - // Retrieve the server's conformance statement and print its - // description - BaseOperationOutcome outcome = client.delete().resourceById(new IdDt("Patient", "1234")).execute(); + BaseOperationOutcome resp = client.delete().resourceById(new IdDt("Patient", "1234")).execute(); // outcome may be null if the server didn't return one - if (outcome != null) { + if (resp != null) { + OperationOutcome outcome = (OperationOutcome) resp; System.out.println(outcome.getIssueFirstRep().getDetailsElement().getValue()); } // END SNIPPET: delete } + { + // START SNIPPET: deleteConditional + client.delete() + .resourceConditionalByUrl("Patient?identifier=system%7C00001") + .execute(); + + client.delete() + .resourceConditionalByType("Patient") + .where(Patient.IDENTIFIER.exactly().systemAndIdentifier("system", "00001")) + .execute(); + // END SNIPPET: deleteConditional + } { // START SNIPPET: search Bundle response = client.search() diff --git a/examples/src/main/java/example/RestfulPatientResourceProviderMore.java b/examples/src/main/java/example/RestfulPatientResourceProviderMore.java index f53638d4ec6..6dbea80e0fc 100644 --- a/examples/src/main/java/example/RestfulPatientResourceProviderMore.java +++ b/examples/src/main/java/example/RestfulPatientResourceProviderMore.java @@ -34,6 +34,7 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.annotation.AddTags; +import ca.uhn.fhir.rest.annotation.ConditionalOperationParam; import ca.uhn.fhir.rest.annotation.Count; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.DeleteTags; @@ -324,6 +325,23 @@ public void deletePatient(@IdParam IdDt theId) { //END SNIPPET: delete +//START SNIPPET: deleteConditional +@Read() +public void deletePatientConditional(@IdParam IdDt theId, @ConditionalOperationParam String theConditionalUrl) { + // Only one of theId or theConditionalUrl will have a value depending + // on whether the URL receieved was a logical ID, or a conditional + // search string + if (theId != null) { + // do a normal delete + } else { + // do a conditional delete + } + + // otherwise, delete was successful + return; // can also return MethodOutcome +} +//END SNIPPET: deleteConditional + //START SNIPPET: history @History() public List getPatientHistory(@IdParam IdDt theId) { @@ -681,6 +699,25 @@ public MethodOutcome createPatient(@ResourceParam Patient thePatient) { public abstract MethodOutcome createNewPatient(@ResourceParam Patient thePatient); //END SNIPPET: createClient +//START SNIPPET: updateConditional +@Update +public MethodOutcome updatePatientConditional( + @ResourceParam Patient thePatient, + @IdParam IdDt theId, + @ConditionalOperationParam String theConditional) { + + // Only one of theId or theConditional will have a value and the other will be null, + // depending on the URL passed into the server. + if (theConditional != null) { + // Do a conditional update. theConditional will have a value like "Patient?identifier=system%7C00001" + } else { + // Do a normal update. theId will have the identity of the resource to update + } + + return new MethodOutcome(); // populate this +} +//END SNIPPET: updateConditional + //START SNIPPET: update @Update public MethodOutcome updatePatient(@IdParam IdDt theId, @ResourceParam Patient thePatient) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/ConditionalOperationParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/ConditionalOperationParam.java new file mode 100644 index 00000000000..39bf0211ae9 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/ConditionalOperationParam.java @@ -0,0 +1,32 @@ +package ca.uhn.fhir.rest.annotation; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2015 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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ConditionalOperationParam { + // just a marker +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/MethodOutcome.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/MethodOutcome.java index 8f513af3020..4716262608c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/MethodOutcome.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/MethodOutcome.java @@ -131,6 +131,10 @@ public class MethodOutcome { return myVersionId; } + /** + * This will be set to {@link Boolean#TRUE} for instance of MethodOutcome which are + * returned to client instances, if the server has responded with an HTTP 201 Created. + */ public Boolean getCreated() { return myCreated; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java index 3dafdde18fe..f1c5da22ade 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java @@ -273,11 +273,16 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding wantedResourceType = requestContainsResourceType(); + IResource retVal; if (wantedResourceType != null) { - return (IResource) parser.parseResource(wantedResourceType, requestReader); + retVal = (IResource) parser.parseResource(wantedResourceType, requestReader); } else { - return parser.parseResource(requestReader); + retVal = parser.parseResource(requestReader); } + + retVal.setId(theRequest.getId()); + + return retVal; } protected abstract Set provideAllowableRequestTypes(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConditionalParamBinder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConditionalParamBinder.java new file mode 100644 index 00000000000..c5bbdd43964 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConditionalParamBinder.java @@ -0,0 +1,57 @@ +package ca.uhn.fhir.rest.method; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2015 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.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +class ConditionalParamBinder implements IParameter { + + ConditionalParamBinder() { + super(); + } + + @Override + public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map> theTargetQueryArguments) throws InternalErrorException { + throw new UnsupportedOperationException(); + } + + @Override + public Object translateQueryParametersIntoServerArgument(Request theRequest, Object theRequestContents) throws InternalErrorException, InvalidRequestException { + if (theRequest.getId() != null && theRequest.getId().hasIdPart()) { + return null; + } + int questionMarkIndex = theRequest.getCompleteUrl().indexOf('?'); + return theRequest.getResourceName() + theRequest.getCompleteUrl().substring(questionMarkIndex); + } + + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + // nothing + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java index 37c8d260672..c43f5605376 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java @@ -47,6 +47,7 @@ import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.annotation.ConditionalOperationParam; import ca.uhn.fhir.rest.annotation.Count; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IncludeParam; @@ -289,6 +290,10 @@ public class MethodUtil { return MethodUtil.findParamAnnotationIndex(theMethod, TagListParam.class); } + public static Integer findConditionalOperationParameterIndex(Method theMethod) { + return MethodUtil.findParamAnnotationIndex(theMethod, ConditionalOperationParam.class); + } + @SuppressWarnings("deprecation") public static Integer findVersionIdParameterIndex(Method theMethod) { return MethodUtil.findParamAnnotationIndex(theMethod, VersionIdParam.class); @@ -390,6 +395,8 @@ public class MethodUtil { param = new SortParameter(); } else if (nextAnnotation instanceof TransactionParam) { param = new TransactionParamBinder(theContext); + } else if (nextAnnotation instanceof ConditionalOperationParam) { + param = new ConditionalParamBinder(); } else { continue; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java index 3974cfef2bf..965832c2a11 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java @@ -20,19 +20,17 @@ package ca.uhn.fhir.rest.method; * #L% */ -import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.lang.reflect.Method; import java.util.Collections; import java.util.Set; -import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationSystemEnum; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; @@ -43,16 +41,11 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam { private Integer myIdParameterIndex; - private Integer myVersionIdParameterIndex; public UpdateMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { super(theMethod, theContext, Update.class, theProvider); myIdParameterIndex = MethodUtil.findIdParameterIndex(theMethod); - if (myIdParameterIndex == null) { - throw new ConfigurationException("Method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() + "' has no parameter annotated with the @" + IdParam.class.getSimpleName() + " annotation"); - } - myVersionIdParameterIndex = MethodUtil.findVersionIdParameterIndex(theMethod); } @Override @@ -92,7 +85,7 @@ class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceP if (theRequest.getId() != null && theRequest.getId().hasVersionIdPart() == false) { if (id != null && id.hasVersionIdPart()) { - theRequest.setId(id); + theRequest.getId().setValue(id.getValue()); } } @@ -104,9 +97,8 @@ class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceP } } - theParams[myIdParameterIndex] = theRequest.getId(); - if (myVersionIdParameterIndex != null) { - theParams[myVersionIdParameterIndex] = theRequest.getId(); + if (myIdParameterIndex != null) { + theParams[myIdParameterIndex] = theRequest.getId(); } } @@ -117,12 +109,6 @@ class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceP throw new NullPointerException("ID can not be null"); } - if (myVersionIdParameterIndex != null) { - IdDt versionIdDt = (IdDt) theArgs[myVersionIdParameterIndex]; - if (idDt.hasVersionIdPart() == false) { - idDt = idDt.withVersion(versionIdDt.getIdPart()); - } - } FhirContext context = getContext(); HttpPutClientInvocation retVal = MethodUtil.createUpdateInvocation(theResource, null, idDt, context); diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java index 2bf4ef3d39f..e828e14875d 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java @@ -119,6 +119,32 @@ public class GenericClientTest { return msg; } + @Test + public void testCreatePopulatesIsCreated() throws Exception { + + Patient p1 = new Patient(); + p1.addIdentifier("foo:bar", "12345"); + p1.addName().addFamily("Smith").addGiven("John"); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + MethodOutcome resp = client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); + assertTrue(resp.getCreated()); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + resp = client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); + assertNull(resp.getCreated()); + + } + + @Test public void testCreateWithStringAutoDetectsEncoding() throws Exception { diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/model/dev/ModelInstantiationTest.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/model/dstu2/ModelInstantiationTest.java similarity index 97% rename from hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/model/dev/ModelInstantiationTest.java rename to hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/model/dstu2/ModelInstantiationTest.java index 6ca37f3cfa8..a0b2120ddc6 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/model/dev/ModelInstantiationTest.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/model/dstu2/ModelInstantiationTest.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.model.dev; +package ca.uhn.fhir.model.dstu2; import java.util.Properties; diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/DeleteConditionalTest.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/DeleteConditionalTest.java new file mode 100644 index 00000000000..9bdb257e4ec --- /dev/null +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/DeleteConditionalTest.java @@ -0,0 +1,125 @@ +package ca.uhn.fhir.rest.server; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.concurrent.TimeUnit; + +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpDelete; +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.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.annotation.ConditionalOperationParam; +import ca.uhn.fhir.rest.annotation.Delete; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.util.PortUtil; + +/** + * Created by dsotnikov on 2/25/2014. + */ +public class DeleteConditionalTest { + private static CloseableHttpClient ourClient; + private static String ourLastConditionalUrl; + private static int ourPort; + + private static Server ourServer; + private static IdDt ourLastIdParam; + + + + @Before + public void before() { + ourLastConditionalUrl = null; + ourLastIdParam = null; + } + + @Test + public void testUpdateWithConditionalUrl() throws Exception { + Patient patient = new Patient(); + patient.addIdentifier().setValue("002"); + + HttpDelete httpPost = new HttpDelete("http://localhost:" + ourPort + "/Patient?identifier=system%7C001"); + + HttpResponse status = ourClient.execute(httpPost); + + assertEquals(204, status.getStatusLine().getStatusCode()); + + assertNull(ourLastIdParam); + assertEquals("Patient?identifier=system%7C001", ourLastConditionalUrl); + } + + + @Test + public void testUpdateWithoutConditionalUrl() throws Exception { + Patient patient = new Patient(); + patient.addIdentifier().setValue("002"); + + HttpDelete httpPost = new HttpDelete("http://localhost:" + ourPort + "/Patient/2"); + + HttpResponse status = ourClient.execute(httpPost); + + assertEquals(204, status.getStatusLine().getStatusCode()); + + assertEquals("Patient/2", ourLastIdParam.toUnqualified().getValue()); + assertNull(ourLastConditionalUrl); + } + + @AfterClass + public static void afterClass() throws Exception { + ourServer.stop(); + } + + + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + PatientProvider patientProvider = new PatientProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(); + servlet.setResourceProviders(patientProvider); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + + public static class PatientProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + + @Delete() + public MethodOutcome updatePatient(@ConditionalOperationParam String theConditional, @IdParam IdDt theIdParam) { + ourLastConditionalUrl = theConditional; + ourLastIdParam = theIdParam; + return new MethodOutcome(new IdDt("Patient/001/_history/002")); + } + + } + +} diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/UpdateConditionalTest.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/UpdateConditionalTest.java index bdf667faa0f..bb16f19cbbb 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/UpdateConditionalTest.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/UpdateConditionalTest.java @@ -31,6 +31,7 @@ import ca.uhn.fhir.model.dstu2.resource.OperationOutcome; import ca.uhn.fhir.model.dstu2.resource.Organization; import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.annotation.ConditionalOperationParam; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.Update; @@ -42,17 +43,30 @@ import ca.uhn.fhir.util.PortUtil; */ public class UpdateConditionalTest { private static CloseableHttpClient ourClient; + private static String ourLastConditionalUrl; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UpdateConditionalTest.class); private static int ourPort; + private static Server ourServer; + private static IdDt ourLastId; + private static IdDt ourLastIdParam; + + + + @Before + public void before() { + ourLastId = null; + ourLastConditionalUrl = null; + ourLastIdParam = null; + } @Test - public void testUpdate() throws Exception { + public void testUpdateWithConditionalUrl() throws Exception { Patient patient = new Patient(); patient.addIdentifier().setValue("002"); - HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient/001"); + HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient?identifier=system%7C001"); httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); HttpResponse status = ourClient.execute(httpPost); @@ -62,22 +76,48 @@ public class UpdateConditionalTest { ourLog.info("Response was:\n{}", responseContent); - OperationOutcome oo = new FhirContext().newXmlParser().parseResource(OperationOutcome.class, responseContent); - assertEquals("OODETAILS", oo.getIssueFirstRep().getDetails()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("location").getValue()); + assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("content-location").getValue()); + + assertNull(ourLastId.getValue()); + assertNull(ourLastIdParam); + assertEquals("Patient?identifier=system%7C001", ourLastConditionalUrl); + + } + + @Test + public void testUpdateWithoutConditionalUrl() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setValue("002"); + + HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient/2"); + httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + + HttpResponse status = ourClient.execute(httpPost); + + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + + ourLog.info("Response was:\n{}", responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("location").getValue()); assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("content-location").getValue()); + + assertEquals("Patient/2", ourLastId.toUnqualified().getValue()); + assertEquals("Patient/2", ourLastIdParam.toUnqualified().getValue()); + assertNull(ourLastConditionalUrl); } - - - + @AfterClass public static void afterClass() throws Exception { ourServer.stop(); } - + + @BeforeClass public static void beforeClass() throws Exception { ourPort = PortUtil.findFreePort(); @@ -99,14 +139,6 @@ public class UpdateConditionalTest { ourClient = builder.build(); } - - private static String ourLastConditionalUrl; - - - @Before - public void before() { - ourLastConditionalUrl=null; - } public static class PatientProvider implements IResourceProvider { @@ -117,15 +149,11 @@ public class UpdateConditionalTest { @Update() - public MethodOutcome updatePatient(@IdParam IdDt theId, @ResourceParam Patient thePatient) { - IdDt id = theId.withVersion(thePatient.getIdentifierFirstRep().getValue()); - OperationOutcome oo = new OperationOutcome(); - oo.addIssue().setDetails("OODETAILS"); - if (theId.getValueAsString().contains("CREATE")) { - return new MethodOutcome(id,oo, true); - } - - return new MethodOutcome(id,oo); + public MethodOutcome updatePatient(@ResourceParam Patient thePatient, @ConditionalOperationParam String theConditional, @IdParam IdDt theIdParam) { + ourLastConditionalUrl = theConditional; + ourLastId = thePatient.getId(); + ourLastIdParam = theIdParam; + return new MethodOutcome(new IdDt("Patient/001/_history/002")); } } diff --git a/src/site/resources/hapi.css b/src/site/resources/hapi.css index d1a0f120dad..6a8a833122e 100644 --- a/src/site/resources/hapi.css +++ b/src/site/resources/hapi.css @@ -43,6 +43,25 @@ */ +.well { + padding: 5px; +} + +.nav-list { + padding-right: 0px; + font-size: 12px; +} + +.nav-list LI { + line-height: 15px; +} + +/* +.nav-list .divider { + display: none; +} +*/ + body { padding-top: 40px; padding-bottom: 10px; @@ -126,6 +145,7 @@ h4 { font-size: 1.2em; padding: 0px; margin-bottom: 0px; + margin-top: 20px; } li.expanded ul { diff --git a/src/site/xdoc/doc_rest_client.xml b/src/site/xdoc/doc_rest_client.xml index fd645fa897b..3d79cd3b349 100644 --- a/src/site/xdoc/doc_rest_client.xml +++ b/src/site/xdoc/doc_rest_client.xml @@ -12,9 +12,6 @@
- - -

HAPI provides a built-in mechanism for connecting to FHIR RESTful servers. @@ -151,13 +148,23 @@ value="examples/src/main/java/example/GenericClientExample.java" /> -

Search - Using HTTP POST or GET with _search

+

Search - Using HTTP POST

+

+ The FHIR specification allows the use of an HTTP POST to transmit a search to a server instead of using + an HTTP GET. With this style of search, the search parameters are included in the request body instead + of the request URL, which can be useful if you need to transmit a search with a large number + of parameters. +

- The FHIR specification allows several styles of search (HTTP POST, a GET with _search at the end of the URL, etc.) The usingStyle() method controls which style to use. By default, GET style is used unless the client detects that the request would result in a very long URL (over 8000 chars) in which case the client automatically switches to POST.

+

+ An alternate form of the search URL (using a URL ending with _search) was also + supported in FHIR DSTU1. This form is no longer valid in FHIR DSTU2, but HAPI retains support + for using this form in order to interoperate with servers which use it. +

+ +

Conditional Creates

+

+ FHIR also specifies a type of update called "conditional create", where + a set of search parameters are provided and a new resource is only + created if no existing resource matches those parameters. See the + FHIR specification for more information on conditional creation. +

+ + + + @@ -239,7 +259,21 @@ - +

Conditional Deletes

+

+ Conditional deletions are also possible, which is a form where + instead of deleting a resource using its logical ID, you specify + a set of search criteria and a single resource is deleted if + it matches that criteria. Note that this is not a mechanism + for bulk deletion; see the FHIR specification for information + on conditional deletes and how they are used. +

+ + + + +

@@ -257,6 +291,21 @@ value="examples/src/main/java/example/GenericClientExample.java" /> +

Conditional Updates

+

+ FHIR also specifies a type of update called "conditional updates", where + insetad of using the logical ID of a resource to update, a set of + search parameters is provided. If a single resource matches that set of + parameters, that resource is updated. See the FHIR specification for + information on how conditional updates work. +

+ + + + + +

ETags and Resource Contention

See also the page on ETag Support diff --git a/src/site/xdoc/doc_rest_operations.xml b/src/site/xdoc/doc_rest_operations.xml index 6ee614d5a96..629267a2cf1 100644 --- a/src/site/xdoc/doc_rest_operations.xml +++ b/src/site/xdoc/doc_rest_operations.xml @@ -9,6 +9,7 @@

+
- +
+

The following table lists the operations supported by HAPI FHIR RESTful Servers and Clients.

+
@@ -279,10 +283,10 @@ annotation. This parameter contains the resource instance to be created.

- In addition, the method must have a parameter annotated with the + In addition, the method may optionally have a parameter annotated with the @IdParam - annotation, and optionally may have a parameter annotated with the - @VersionIdParam + annotation, or they may obtain the ID of the resource being updated from + the resource itself. Either way, this ID comes from the URL passed in.

Update methods must return an object of type @@ -315,6 +319,27 @@ + +

Conditional Updates

+

+ If you wish to suport conditional updates, you can add a parameter + tagged with a + @ConditionalOperationParam + annotation. If the request URL contains search parameters instead of a + resource ID, then this parameter will be populated. +

+ + + + + + +

+ Example URL to invoke this method (this would be invoked using an HTTP PUT, + with the resource in the PUT body): +
+ http://fhir.example.com/Patient?identifier=system%7C00001 +

@@ -373,6 +398,26 @@ http://fhir.example.com/Patient/111

+

Conditional Deletes

+ +

+ The FHIR specification also allows "conditional deletes". A conditional + delete uses a search style URL instead of a read style URL, and + deletes a single resource if it matches the given search parameters. + The following example shows how to +

+ + + + + + +

+ Example URL to perform a conditional delete (HTTP DELETE): +
+ http://fhir.example.com/Patient?identifier=system%7C0001 +

+