Allow patching in tranactions

This commit is contained in:
James Agnew 2019-07-18 16:41:07 -04:00
parent 87ab796309
commit 5bf4fa22e7
9 changed files with 316 additions and 42 deletions

View File

@ -21,13 +21,16 @@ package ca.uhn.fhir.rest.api;
*/
import ca.uhn.fhir.rest.annotation.Patch;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.UrlUtil;
/**
* Parameter type for methods annotated with {@link Patch}
*/
public enum PatchTypeEnum {
JSON_PATCH(Constants.CT_JSON_PATCH), XML_PATCH(Constants.CT_XML_PATCH);
JSON_PATCH(Constants.CT_JSON_PATCH),
XML_PATCH(Constants.CT_XML_PATCH);
private final String myContentType;
@ -39,4 +42,19 @@ public enum PatchTypeEnum {
return myContentType;
}
public static PatchTypeEnum forContentTypeOrThrowInvalidRequestException(String theContentType) {
String contentType = theContentType;
int semiColonIdx = contentType.indexOf(';');
if (semiColonIdx != -1) {
contentType = theContentType.substring(0, semiColonIdx);
}
contentType = contentType.trim();
if (Constants.CT_JSON_PATCH.equals(contentType)) {
return JSON_PATCH;
} else if (Constants.CT_XML_PATCH.equals(contentType)) {
return XML_PATCH;
} else {
throw new InvalidRequestException("Invalid Content-Type for PATCH operation: " + UrlUtil.sanitizeUrlPart(theContentType));
}
}
}

View File

@ -97,6 +97,8 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulDeletes=Successfully delet
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidSearchParameter=Unknown search parameter "{0}". Value search parameters for this search are: {1}
ca.uhn.fhir.jpa.dao.TransactionProcessor.missingMandatoryResource=Missing required resource in Bundle.entry[{1}].resource for operation {0}
ca.uhn.fhir.jpa.dao.TransactionProcessor.missingPatchContentType=Missing or invalid content type for PATCH operation
ca.uhn.fhir.jpa.dao.TransactionProcessor.missingPatchBody=Unable to determine PATCH body from request
ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor.externalReferenceNotAllowed=Resource contains external reference to URL "{0}" but this server is not configured to allow external references
ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor.failedToExtractPaths=Failed to extract values from resource using FHIRPath "{0}": {1}

View File

@ -37,10 +37,7 @@ import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
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.Constants;
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.api.*;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
@ -53,6 +50,7 @@ import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.*;
import com.google.common.base.Charsets;
import com.google.common.collect.ArrayListMultimap;
import org.apache.commons.lang3.Validate;
import org.apache.http.NameValuePair;
@ -755,9 +753,50 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
entriesToProcess.put(nextRespEntry, outcome.getEntity());
break;
}
case "GET":
default:
case "PATCH": {
// PATCH
validateResourcePresent(res, order, verb);
String url = extractTransactionUrlOrThrowException(nextReqEntry, verb);
UrlUtil.UrlParts parts = UrlUtil.parseUrl(url);
String matchUrl = toMatchUrl(nextReqEntry);
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
String patchBody = null;
String contentType = null;
if (res instanceof IBaseBinary) {
IBaseBinary binary = (IBaseBinary) res;
if (binary.getContent() != null && binary.getContent().length > 0) {
patchBody = new String(binary.getContent(), Charsets.UTF_8);
}
contentType = binary.getContentType();
}
if (isBlank(patchBody)) {
String msg = myContext.getLocalizer().getMessage(TransactionProcessor.class, "missingPatchBody");
throw new InvalidRequestException(msg);
}
if (isBlank(contentType)) {
String msg = myContext.getLocalizer().getMessage(TransactionProcessor.class, "missingPatchContentType");
throw new InvalidRequestException(msg);
}
ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb, url);
PatchTypeEnum patchType = PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(contentType);
IIdType patchId = myContext.getVersion().newIdType().setValue(parts.getResourceId());
DaoMethodOutcome outcome = dao.patch(patchId, matchUrl, patchType, patchBody, theRequest);
updatedEntities.add(outcome.getEntity());
if (outcome.getResource() != null) {
updatedResources.add(outcome.getResource());
}
break;
}
case "GET":
break;
default:
throw new InvalidRequestException("Unable to handle verb in transaction: " + verb);
}
@ -1051,6 +1090,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
* Process any DELETE interactions
* Process any POST interactions
* Process any PUT interactions
* Process any PATCH interactions
* Process any GET interactions
*/
//@formatter:off
@ -1107,21 +1147,6 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
return o1 - o2;
}
private String toMatchUrl(BUNDLEENTRY theEntry) {
String verb = myVersionAdapter.getEntryRequestVerb(theEntry);
if (verb.equals("POST")) {
return myVersionAdapter.getEntryIfNoneExist(theEntry);
}
if (verb.equals("PUT") || verb.equals("DELETE")) {
String url = extractTransactionUrlOrThrowException(theEntry, verb);
UrlUtil.UrlParts parts = UrlUtil.parseUrl(url);
if (isBlank(parts.getResourceId())) {
return parts.getResourceType() + '?' + parts.getParams();
}
}
return null;
}
private int toOrder(BUNDLEENTRY theO1) {
int o1 = 0;
if (myVersionAdapter.getEntryRequestVerb(theO1) != null) {
@ -1135,9 +1160,12 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
case "PUT":
o1 = 3;
break;
case "GET":
case "PATCH":
o1 = 4;
break;
case "GET":
o1 = 5;
break;
default:
o1 = 0;
break;
@ -1171,4 +1199,22 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
}
private String toMatchUrl(BUNDLEENTRY theEntry) {
String verb = myVersionAdapter.getEntryRequestVerb(theEntry);
if (verb.equals("POST")) {
return myVersionAdapter.getEntryIfNoneExist(theEntry);
}
if (verb.equals("PATCH")) {
return myVersionAdapter.getEntryRequestIfMatch(theEntry);
}
if (verb.equals("PUT") || verb.equals("DELETE")) {
String url = extractTransactionUrlOrThrowException(theEntry, verb);
UrlUtil.UrlParts parts = UrlUtil.parseUrl(url);
if (isBlank(parts.getResourceId())) {
return parts.getResourceType() + '?' + parts.getParams();
}
}
return null;
}
}

View File

@ -130,9 +130,11 @@ public class JpaValidationSupportR4 implements IJpaValidationSupportR4, Applicat
}
} else if ("StructureDefinition".equals(resourceName)) {
// Don't allow the core FHIR definitions to be overwritten
String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length());
if (myR4Ctx.getElementDefinition(typeName) != null) {
return null;
if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) {
String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length());
if (myR4Ctx.getElementDefinition(typeName) != null) {
return null;
}
}
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);

View File

@ -308,6 +308,9 @@ public class BinaryAccessProviderR4Test extends BaseResourceProviderR4Test {
if (theSetData) {
attachment.setData(SOME_BYTES_2);
}
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(documentReference));
return ourClient.create().resource(documentReference).execute().getId().toUnqualifiedVersionless();
}

View File

@ -1,15 +1,15 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.rest.api.Constants;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Media;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.*;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -93,6 +93,48 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(false, newPt.getActive());
}
@Test
public void testPatchUsingJsonPatch_Transaction() throws Exception {
String methodName = "testPatchUsingJsonPatch_Transaction";
IIdType pid1;
{
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier().setSystem("urn:system").setValue("0");
patient.addName().setFamily(methodName).addGiven("Joe");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
String patchString = "[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]";
Binary patch = new Binary();
patch.setContentType(Constants.CT_JSON_PATCH);
patch.setContent(patchString.getBytes(Charsets.UTF_8));
Bundle input = new Bundle();
input.setType(Bundle.BundleType.TRANSACTION);
input.addEntry()
.setFullUrl(pid1.getValue())
.setResource(patch)
.getRequest().setUrl(pid1.getValue())
.setMethod(Bundle.HTTPVerb.PATCH);
HttpPost post = new HttpPost(ourServerBase);
String encodedRequest = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input);
ourLog.info("Requet:\n{}", encodedRequest);
post.setEntity(new StringEntity(encodedRequest, ContentType.parse(Constants.CT_FHIR_JSON_NEW+ Constants.CHARSET_UTF8_CTSUFFIX)));
try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("\"resourceType\":\"Bundle\""));
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("2", newPt.getIdElement().getVersionIdPart());
assertEquals(false, newPt.getActive());
}
@Test
public void testPatchUsingJsonPatch_Conditional_Success() throws Exception {
String methodName = "testPatchUsingJsonPatch";
@ -303,4 +345,158 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
}
@Test
public void testPatchUsingXmlPatch_Transaction() throws Exception {
String methodName = "testPatchUsingXmlPatch_Transaction";
IIdType pid1;
{
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier().setSystem("urn:system").setValue("0");
patient.addName().setFamily(methodName).addGiven("Joe");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
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>";
Binary patch = new Binary();
patch.setContentType(Constants.CT_XML_PATCH);
patch.setContent(patchString.getBytes(Charsets.UTF_8));
Bundle input = new Bundle();
input.setType(Bundle.BundleType.TRANSACTION);
input.addEntry()
.setFullUrl(pid1.getValue())
.setResource(patch)
.getRequest().setUrl(pid1.getValue())
.setMethod(Bundle.HTTPVerb.PATCH);
HttpPost post = new HttpPost(ourServerBase);
post.setEntity(new StringEntity(myFhirCtx.newJsonParser().encodeResourceToString(input), ContentType.parse(Constants.CT_FHIR_JSON_NEW+ Constants.CHARSET_UTF8_CTSUFFIX)));
try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("\"resourceType\":\"Bundle\""));
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("2", newPt.getIdElement().getVersionIdPart());
assertEquals(false, newPt.getActive());
}
@Test
public void testPatchInTransaction_MissingContentType() throws Exception {
String methodName = "testPatchUsingJsonPatch_Transaction";
IIdType pid1;
{
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier().setSystem("urn:system").setValue("0");
patient.addName().setFamily(methodName).addGiven("Joe");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
String patchString = "[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]";
Binary patch = new Binary();
patch.setContent(patchString.getBytes(Charsets.UTF_8));
Bundle input = new Bundle();
input.setType(Bundle.BundleType.TRANSACTION);
input.addEntry()
.setFullUrl(pid1.getValue())
.setResource(patch)
.getRequest().setUrl(pid1.getValue())
.setMethod(Bundle.HTTPVerb.PATCH);
HttpPost post = new HttpPost(ourServerBase);
post.setEntity(new StringEntity(myFhirCtx.newJsonParser().encodeResourceToString(input), ContentType.parse(Constants.CT_FHIR_JSON_NEW+ Constants.CHARSET_UTF8_CTSUFFIX)));
try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
assertEquals(400, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("Missing or invalid content type for PATCH operation"));
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("1", newPt.getIdElement().getVersionIdPart());
assertEquals(true, newPt.getActive());
}
@Test
public void testPatchInTransaction_MissingBody() throws Exception {
String methodName = "testPatchUsingJsonPatch_Transaction";
IIdType pid1;
{
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier().setSystem("urn:system").setValue("0");
patient.addName().setFamily(methodName).addGiven("Joe");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
String patchString = "[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]";
Binary patch = new Binary();
patch.setContentType(Constants.CT_JSON_PATCH);
Bundle input = new Bundle();
input.setType(Bundle.BundleType.TRANSACTION);
input.addEntry()
.setFullUrl(pid1.getValue())
.setResource(patch)
.getRequest().setUrl(pid1.getValue())
.setMethod(Bundle.HTTPVerb.PATCH);
HttpPost post = new HttpPost(ourServerBase);
post.setEntity(new StringEntity(myFhirCtx.newJsonParser().encodeResourceToString(input), ContentType.parse(Constants.CT_FHIR_JSON_NEW+ Constants.CHARSET_UTF8_CTSUFFIX)));
try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
assertEquals(400, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("Unable to determine PATCH body from request"));
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("1", newPt.getIdElement().getVersionIdPart());
assertEquals(true, newPt.getActive());
}
@Test
public void testPatchInTransaction_InvalidContentType() throws Exception {
String methodName = "testPatchUsingJsonPatch_Transaction";
IIdType pid1;
{
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier().setSystem("urn:system").setValue("0");
patient.addName().setFamily(methodName).addGiven("Joe");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
String patchString = "[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]";
Binary patch = new Binary();
patch.setContentType(Constants.CT_FHIR_JSON_NEW);
patch.setContent(patchString.getBytes(Charsets.UTF_8));
Bundle input = new Bundle();
input.setType(Bundle.BundleType.TRANSACTION);
input.addEntry()
.setFullUrl(pid1.getValue())
.setResource(patch)
.getRequest().setUrl(pid1.getValue())
.setMethod(Bundle.HTTPVerb.PATCH);
HttpPost post = new HttpPost(ourServerBase);
post.setEntity(new StringEntity(myFhirCtx.newJsonParser().encodeResourceToString(input), ContentType.parse(Constants.CT_FHIR_JSON_NEW+ Constants.CHARSET_UTF8_CTSUFFIX)));
try (CloseableHttpResponse response = ourHttpClient.execute(post)) {
assertEquals(400, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseString, containsString("Invalid Content-Type for PATCH operation: application/fhir+json"));
}
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
assertEquals("1", newPt.getIdElement().getVersionIdPart());
assertEquals(true, newPt.getActive());
}
}

View File

@ -19,6 +19,7 @@ import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.gclient.StringClientParam;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -3184,6 +3185,19 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
// }
}
@Test
public void testEncounterWithReason() {
Encounter enc = new Encounter();
enc.addReasonCode()
.addCoding().setSystem("http://myorg").setCode("hugs").setDisplay("Hugs for better wellness");
enc.getPeriod().setStartElement(new DateTimeType("2012"));
IIdType id = ourClient.create().resource(enc).execute().getId().toUnqualifiedVersionless();
enc = ourClient.read().resource(Encounter.class).withId(id).execute();
assertEquals("hugs", enc.getReasonCodeFirstRep().getCodingFirstRep().getCode());
}
@Test
public void testTerminologyWithCompleteCs_SearchForConceptIn() throws Exception {

View File

@ -45,19 +45,7 @@ class PatchTypeParameter implements IParameter {
public static PatchTypeEnum getTypeForRequestOrThrowInvalidRequestException(RequestDetails theRequest) {
String contentTypeAll = defaultString(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE));
String contentType = contentTypeAll;
int semiColonIdx = contentType.indexOf(';');
if (semiColonIdx != -1) {
contentType = contentTypeAll.substring(0, semiColonIdx);
}
contentType = contentType.trim();
if (Constants.CT_JSON_PATCH.equals(contentType)) {
return PatchTypeEnum.JSON_PATCH;
} else if (Constants.CT_XML_PATCH.equals(contentType)) {
return PatchTypeEnum.XML_PATCH;
} else {
throw new InvalidRequestException("Invalid Content-Type for PATCH operation: " + contentTypeAll);
}
return PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(contentTypeAll);
}
}

View File

@ -322,6 +322,11 @@
words in MySQL. The database migrator tool has been updated to handle this
change.
</action>
<action type="add">
Support for PATCH operations performed within a transaction (using a Binary
resource as the resource type in order to hold a JSONPatch or XMLPatch body)
has been added to the JPA server.
</action>
</release>
<release version="3.8.0" date="2019-05-30" description="Hippo">
<action type="fix">