Patch modifications

This commit is contained in:
James Agnew 2016-09-18 08:35:54 -04:00
parent 40286f49c2
commit d8c99363db
8 changed files with 133 additions and 50 deletions

View File

@ -10,6 +10,8 @@ import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.hl7.fhir.dstu3.model.IdType;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.BundleEntry;
@ -108,6 +110,23 @@ public List<Organization> getAllOrganizations() {
}
//END SNIPPET: searchAll
//START SNIPPET: updateEtag
@Update
public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient) {
String resourceId = theId.getIdPart();
String versionId = theId.getVersionIdPart(); // this will contain the ETag
String currentVersion = "1"; // populate this with the current version
if (!versionId.equals(currentVersion)) {
throw new ResourceVersionConflictException("Expected version " + currentVersion);
}
// ... perform the update ...
return new MethodOutcome();
}
//END SNIPPET: updateEtag
//START SNIPPET: summaryAndElements
@Search

View File

@ -180,7 +180,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
final ResourceTable entity = readEntityLatestVersion(theId);
if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) {
throw new InvalidRequestException("Trying to delete " + theId + " but this is not the current version");
throw new ResourceVersionConflictException("Trying to delete " + theId + " but this is not the current version");
}
validateOkToDelete(deleteConflicts, entity);
@ -1038,7 +1038,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
if (resourceId.hasVersionIdPart() && Long.parseLong(resourceId.getVersionIdPart()) != entity.getVersion()) {
throw new InvalidRequestException("Trying to update " + resourceId + " but this is not the current version");
throw new ResourceVersionConflictException("Trying to update " + resourceId + " but this is not the current version");
}
if (resourceId.hasResourceType() && !resourceId.getResourceType().equals(getResourceName())) {
@ -1080,7 +1080,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
ResourceTable entityToUpdate = readEntityLatestVersion(theId);
if (theId.hasVersionIdPart()) {
if (theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) {
throw new PreconditionFailedException("Version " + theId.getVersionIdPart() + " is not the most recent version of this resource, unable to apply patch");
throw new ResourceVersionConflictException("Version " + theId.getVersionIdPart() + " is not the most recent version of this resource, unable to apply patch");
}
}

View File

@ -3,13 +3,13 @@ package ca.uhn.fhir.jpa.util.xmlpatch;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.hl7.fhir.instance.model.api.IBaseResource;
import com.github.dnault.xmlpatch.Patcher;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
public class XmlPatchUtils {
@ -23,12 +23,12 @@ public class XmlPatchUtils {
ByteArrayOutputStream result = new ByteArrayOutputStream();
try {
Patcher.patch(new ByteArrayInputStream(inputResource.getBytes(StandardCharsets.UTF_8)), new ByteArrayInputStream(thePatchBody.getBytes(StandardCharsets.UTF_8)), result);
Patcher.patch(new ByteArrayInputStream(inputResource.getBytes(Constants.CHARSET_UTF8)), new ByteArrayInputStream(thePatchBody.getBytes(Constants.CHARSET_UTF8)), result);
} catch (IOException e) {
throw new InternalErrorException(e);
}
String resultString = new String(result.toByteArray(), StandardCharsets.UTF_8);
String resultString = new String(result.toByteArray(), Constants.CHARSET_UTF8);
T retVal = theCtx.newXmlParser().parseResource(clazz, resultString);
return retVal;

View File

@ -880,7 +880,7 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test {
try {
myPatientDao.delete(id2, mySrd);
fail();
} catch (InvalidRequestException e) {
} catch (ResourceVersionConflictException e) {
// good
}

View File

@ -1046,7 +1046,7 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
try {
myPatientDao.delete(id2, mySrd);
fail();
} catch (InvalidRequestException e) {
} catch (ResourceVersionConflictException e) {
// good
}

View File

@ -85,9 +85,6 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@Test
public void testSearchPagingKeepsOldSearches() throws Exception {
String methodName = "testSearchPagingKeepsOldSearches";
@ -105,16 +102,10 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
patient.addName().addFamily(methodName).addGiven("Joe");
myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
List<String> linkNext = Lists.newArrayList();
for (int i = 0 ; i < 100; i++) {
Bundle bundle = ourClient
.search()
.forResource(Patient.class)
.where(Patient.NAME.matches().value("testSearchPagingKeepsOldSearches"))
.count(5)
.returnBundle(Bundle.class)
.execute();
List<String> linkNext = Lists.newArrayList();
for (int i = 0; i < 100; i++) {
Bundle bundle = ourClient.search().forResource(Patient.class).where(Patient.NAME.matches().value("testSearchPagingKeepsOldSearches")).count(5).returnBundle(Bundle.class).execute();
assertTrue(isNotBlank(bundle.getLink("next").getUrl()));
assertEquals(5, bundle.getEntry().size());
linkNext.add(bundle.getLink("next").getUrl());
@ -142,22 +133,22 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
HttpPatch patch = new HttpPatch(ourServerBase + "/Patient/" + pid1.getIdPart());
patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
CloseableHttpResponse response = ourHttpClient.execute(patch);
try {
assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(),StandardCharsets.UTF_8);
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("INFORMATION"));
} finally {
response.close();
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("2", newPt.getIdElement().getVersionIdPart());
assertEquals(false, newPt.getActive());
}
@Test
public void testPatchUsingJsonPatchWithContentionCheckGood() throws Exception {
String methodName = "testPatchUsingJsonPatchWithContentionCheckGood";
@ -173,17 +164,17 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
HttpPatch patch = new HttpPatch(ourServerBase + "/Patient/" + pid1.getIdPart());
patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
patch.addHeader("If-Match", "W/\"1\"");
CloseableHttpResponse response = ourHttpClient.execute(patch);
try {
assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(),StandardCharsets.UTF_8);
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("INFORMATION"));
} finally {
response.close();
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("2", newPt.getIdElement().getVersionIdPart());
assertEquals(false, newPt.getActive());
@ -204,17 +195,17 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
HttpPatch patch = new HttpPatch(ourServerBase + "/Patient/" + pid1.getIdPart());
patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
patch.addHeader("If-Match", "W/\"9\"");
CloseableHttpResponse response = ourHttpClient.execute(patch);
try {
assertEquals(412, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(),StandardCharsets.UTF_8);
assertEquals(409, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("<diagnostics value=\"Version 9 is not the most recent version of this resource, unable to apply patch\"/>"));
} finally {
response.close();
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("1", newPt.getIdElement().getVersionIdPart());
assertEquals(true, newPt.getActive());
@ -235,17 +226,17 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
HttpPatch patch = new HttpPatch(ourServerBase + "/Patient/" + pid1.getIdPart());
String patchString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><diff xmlns:fhir=\"http://hl7.org/fhir\"><replace sel=\"fhir:Patient/fhir:active/@value\">false</replace></diff>";
patch.setEntity(new StringEntity(patchString, ContentType.parse(Constants.CT_XML_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
CloseableHttpResponse response = ourHttpClient.execute(patch);
try {
assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(),StandardCharsets.UTF_8);
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("<OperationOutcome"));
assertThat(responseString, containsString("INFORMATION"));
} finally {
response.close();
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("2", newPt.getIdElement().getVersionIdPart());
assertEquals(false, newPt.getActive());
@ -306,7 +297,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
patient.getName().get(0).getFamily().get(0).setValue(methodName + "_i");
ids.add(myPatientDao.update(patient, mySrd).getId().toUnqualified().getValue());
}
List<String> idValues;
idValues = searchAndReturnUnqualifiedIdValues(ourServerBase + "/Patient/" + id.getIdPart() + "/_history?_at=gt" + toStr(preDates.get(0)) + "&_at=lt" + toStr(preDates.get(3)));
@ -317,7 +308,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
idValues = searchAndReturnUnqualifiedIdValues(ourServerBase + "/_history?_at=gt" + toStr(preDates.get(0)) + "&_at=lt" + toStr(preDates.get(3)));
assertThat(idValues.toString(), idValues, contains(ids.get(2), ids.get(1), ids.get(0)));
idValues = searchAndReturnUnqualifiedIdValues(ourServerBase + "/_history?_at=gt2060");
assertThat(idValues.toString(), idValues, empty());
@ -572,13 +563,13 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
org.setName("ORG");
IIdType orgId = ourClient.create().resource(org).execute().getId();
assertEquals("1", orgId.getVersionIdPart());
Patient patient = new Patient();
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.getManagingOrganization().setReference(orgId.toUnqualified().getValue());
IIdType patientId = ourClient.create().resource(patient).execute().getId();
assertEquals("1", patientId.getVersionIdPart());
AuditEvent ae = new org.hl7.fhir.dstu3.model.AuditEvent();
ae.addEntity().getReference().setReference(patientId.toUnqualified().getValue());
IIdType aeId = ourClient.create().resource(ae).execute().getId();
@ -587,11 +578,11 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
patient = ourClient.read().resource(Patient.class).withId(patientId).execute();
assertTrue(patient.getManagingOrganization().getReferenceElement().hasIdPart());
assertFalse(patient.getManagingOrganization().getReferenceElement().hasVersionIdPart());
ae = ourClient.read().resource(AuditEvent.class).withId(aeId).execute();
assertTrue(ae.getEntityFirstRep().getReference().getReferenceElement().hasIdPart());
assertTrue(ae.getEntityFirstRep().getReference().getReferenceElement().hasVersionIdPart());
}
// private void delete(String theResourceType, String theParamName, String theParamValue) {
@ -703,7 +694,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
String resource = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(pt);
ourLog.info("Input: {}", resource);
HttpPost post = new HttpPost(ourServerBase + "/Patient");
post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post);
@ -721,7 +712,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
assertEquals("1", id.getVersionIdPart());
assertNotEquals("AAA", id.getIdPart());
HttpGet get = new HttpGet(ourServerBase + "/Patient/" + id.getIdPart());
response = ourHttpClient.execute(get);
try {
@ -745,7 +736,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
String resource = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(pt);
ourLog.info("Input: {}", resource);
HttpPost post = new HttpPost(ourServerBase + "/Patient");
post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post);
@ -763,7 +754,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
assertEquals("1", id.getVersionIdPart());
assertNotEquals("AAA", id.getIdPart());
HttpPut put = new HttpPut(ourServerBase + "/Patient/" + id.getIdPart() + "/_history/1");
put.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
response = ourHttpClient.execute(put);
@ -780,7 +771,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
assertEquals("2", id.getVersionIdPart());
assertNotEquals("AAA", id.getIdPart());
HttpGet get = new HttpGet(ourServerBase + "/Patient/" + id.getIdPart());
response = ourHttpClient.execute(get);
try {
@ -973,7 +964,8 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
String encoded = myFhirCtx.newXmlParser().encodeResourceToString(response);
ourLog.info(encoded);
assertThat(encoded, containsString("<issue><severity value=\"information\"/><code value=\"informational\"/><diagnostics value=\"Successfully deleted Patient?identifier=testDeleteConditionalMultiple resource(s) in 2ms\"/></issue>"));
assertThat(encoded, containsString(
"<issue><severity value=\"information\"/><code value=\"informational\"/><diagnostics value=\"Successfully deleted Patient?identifier=testDeleteConditionalMultiple resource(s) in 2ms\"/></issue>"));
try {
ourClient.read().resource("Patient").withId(id1).execute();
fail();
@ -991,7 +983,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
@Test
public void testDeleteConditionalNoMatches() throws Exception {
String methodName = "testDeleteConditionalNoMatches";
HttpDelete delete = new HttpDelete(ourServerBase + "/Patient?identifier=" + methodName);
CloseableHttpResponse resp = ourHttpClient.execute(delete);
try {
@ -999,7 +991,8 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
String response = IOUtils.toString(resp.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(response);
assertEquals(200, resp.getStatusLine().getStatusCode());
assertThat(response, containsString("<issue><severity value=\"warning\"/><code value=\"not-found\"/><diagnostics value=\"Unable to find resource matching URL &quot;Patient?identifier=testDeleteConditionalNoMatches&quot;. Deletion failed.\"/></issue>"));
assertThat(response, containsString(
"<issue><severity value=\"warning\"/><code value=\"not-found\"/><diagnostics value=\"Unable to find resource matching URL &quot;Patient?identifier=testDeleteConditionalNoMatches&quot;. Deletion failed.\"/></issue>"));
} finally {
IOUtils.closeQuietly(resp);
}
@ -2648,6 +2641,47 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
}
}
@Test
public void testUpdateWithETag() throws IOException, Exception {
String methodName = "testUpdateWithETag";
Patient pt = new Patient();
pt.addName().addFamily(methodName);
IIdType id = ourClient.create().resource(pt).execute().getId().toUnqualifiedVersionless();
pt.addName().addFamily("FAM2");
String resource = myFhirCtx.newXmlParser().encodeResourceToString(pt);
HttpPut put = new HttpPut(ourServerBase + "/Patient/" + id.getIdPart());
put.addHeader(Constants.HEADER_IF_MATCH, "W/\"44\"");
put.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(put);
try {
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseString);
assertEquals(409, response.getStatusLine().getStatusCode());
OperationOutcome oo = myFhirCtx.newXmlParser().parseResource(OperationOutcome.class, responseString);
assertThat(oo.getIssue().get(0).getDiagnostics(), containsString("Trying to update Patient/" + id.getIdPart() + "/_history/44 but this is not the current version"));
} finally {
response.close();
}
// Now a good one
put = new HttpPut(ourServerBase + "/Patient/" + id.getIdPart());
put.addHeader(Constants.HEADER_IF_MATCH, "W/\"1\"");
put.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
response = ourHttpClient.execute(put);
try {
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseString);
assertEquals(200, response.getStatusLine().getStatusCode());
} finally {
response.close();
}
}
@Test
public void testUpdateInvalidReference2() throws IOException, Exception {
String methodName = "testUpdateInvalidReference2";
@ -2994,7 +3028,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
patient.addCommunication().setPreferred(true); // missing language
IIdType id = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
HttpGet get = new HttpGet(ourServerBase + "/Patient/" + id.getIdPart() + "/$validate");
CloseableHttpResponse response = ourHttpClient.execute(get);
try {

View File

@ -7,6 +7,18 @@
</properties>
<body>
<release version="2.1" date="TBD">
<action type="add">
Client, Server, and JPA server now support experimental support
for
<![CDATA[HTTP PATCH]]>
using the XML Patch and JSON Patch syntax as explored during the
September 2016 Baltimore Connectathon. See
<![CDATA[<a href="http://wiki.hl7.org/index.php?title=201609_PATCH_Connectathon_Track_Proposal">this wiki page</a>]]>
for a description of the syntax.
<![CDATA[<br/>]]>
Thanks to Pater Girard for all of his help during the connectathon
in implementing this feature!
</action>
<action type="fix">
In server, when returning a list of resources, the server sometimes failed to add
<![CDATA[<code>_include</code>]]> resources to the response bundle if they were
@ -62,6 +74,13 @@
and the new MimeTypes
(e.g. <![CDATA[<code>application/fhir+xml</code>]]>)
</action>
<action type="fix">
JPA server now sends correct
<![CDATA[<code>HTTP 409 Version Conflict</code>]]>
when a
DELETE fails because of constraint issues, instead of
<![CDATA[<code>HTTP 400 Invalid Request</code>]]>
</action>
</release>
<release version="2.0" date="2016-08-30">
<action type="fix">

View File

@ -236,6 +236,17 @@
<pre><![CDATA[PUT [serverBase]/Patient/123
If-Match: W/"3"]]></pre>
<p>
If a client performs a contention aware update, the ETag version will be
placed in the version part of the IdDt/IdType that is passed into the
method. For example:
</p>
<macro name="snippet">
<param name="id" value="updateEtag" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<a name="instance_delete" />
</section>