Add support for conditional patch (#1348)
* Add support for conditional patch * Add changelog * Test fix
This commit is contained in:
parent
087c53286c
commit
b2e99cf035
|
@ -1,6 +1,7 @@
|
|||
package ca.uhn.fhir.i18n;
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.*;
|
||||
|
@ -92,6 +93,20 @@ public class HapiLocalizer {
|
|||
return getMessage(toKey(theType, theKey), theParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the message and sanitize parameters using {@link }
|
||||
*/
|
||||
public String getMessageSanitized(Class<?> theType, String theKey, Object... theParameters) {
|
||||
if (theParameters != null) {
|
||||
for (int i = 0; i < theParameters.length; i++) {
|
||||
if (theParameters[i] instanceof CharSequence) {
|
||||
theParameters[i] = UrlUtil.sanitizeUrlPart((CharSequence) theParameters[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return getMessage(toKey(theType, theKey), theParameters);
|
||||
}
|
||||
|
||||
public String getMessage(String theQualifiedKey, Object... theParameters) {
|
||||
if (theParameters != null && theParameters.length > 0) {
|
||||
MessageFormat format = myKeyToMessageFormat.get(theQualifiedKey);
|
||||
|
|
|
@ -123,7 +123,7 @@ public class UrlUtil {
|
|||
return value.startsWith("http://") || value.startsWith("https://");
|
||||
}
|
||||
|
||||
public static boolean isNeedsSanitization(String theString) {
|
||||
public static boolean isNeedsSanitization(CharSequence theString) {
|
||||
if (theString != null) {
|
||||
for (int i = 0; i < theString.length(); i++) {
|
||||
char nextChar = theString.charAt(i);
|
||||
|
@ -302,7 +302,7 @@ public class UrlUtil {
|
|||
* This method specifically HTML-encodes the " and
|
||||
* < characters in order to prevent injection attacks
|
||||
*/
|
||||
public static String sanitizeUrlPart(String theString) {
|
||||
public static String sanitizeUrlPart(CharSequence theString) {
|
||||
if (theString == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -316,6 +316,9 @@ public class UrlUtil {
|
|||
|
||||
char nextChar = theString.charAt(j);
|
||||
switch (nextChar) {
|
||||
case '\'':
|
||||
buffer.append("'");
|
||||
break;
|
||||
case '"':
|
||||
buffer.append(""");
|
||||
break;
|
||||
|
@ -332,7 +335,7 @@ public class UrlUtil {
|
|||
return buffer.toString();
|
||||
}
|
||||
|
||||
return theString;
|
||||
return theString.toString();
|
||||
}
|
||||
|
||||
private static Map<String, String[]> toQueryStringMap(HashMap<String, List<String>> map) {
|
||||
|
|
|
@ -75,6 +75,7 @@ import javax.servlet.http.HttpServletResponse;
|
|||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
import static ca.uhn.fhir.util.UrlUtil.sanitizeUrlPart;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
|
@ -151,7 +152,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
|
||||
if (isNotBlank(theResource.getIdElement().getIdPart())) {
|
||||
if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
|
||||
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getIdElement().getIdPart());
|
||||
String message = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getIdElement().getIdPart());
|
||||
throw new InvalidRequestException(message, createErrorOperationOutcome(message, "processing"));
|
||||
} else {
|
||||
// As of DSTU3, ID and version in the body should be ignored for a create/update
|
||||
|
@ -287,7 +288,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
Set<Long> resourceIds = myMatchResourceUrlService.processMatchUrl(theUrl, myResourceType);
|
||||
if (resourceIds.size() > 1) {
|
||||
if (myDaoConfig.isAllowMultipleDelete() == false) {
|
||||
throw new PreconditionFailedException(getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resourceIds.size()));
|
||||
throw new PreconditionFailedException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resourceIds.size()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -335,7 +336,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
IBaseOperationOutcome oo;
|
||||
if (deletedResources.isEmpty()) {
|
||||
oo = OperationOutcomeUtil.newInstance(getContext());
|
||||
String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "unableToDeleteNotFound", theUrl);
|
||||
String message = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "unableToDeleteNotFound", theUrl);
|
||||
String severity = "warning";
|
||||
String code = "not-found";
|
||||
OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
|
||||
|
@ -384,7 +385,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
if (isNotBlank(theIfNoneExist)) {
|
||||
Set<Long> match = myMatchResourceUrlService.processMatchUrl(theIfNoneExist, myResourceType);
|
||||
if (match.size() > 1) {
|
||||
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size());
|
||||
String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size());
|
||||
throw new PreconditionFailedException(msg);
|
||||
} else if (match.size() == 1) {
|
||||
Long pid = match.iterator().next();
|
||||
|
@ -400,11 +401,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
switch (myDaoConfig.getResourceClientIdStrategy()) {
|
||||
case NOT_ALLOWED:
|
||||
throw new ResourceNotFoundException(
|
||||
getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedIdNotAllowed", theResource.getIdElement().getIdPart()));
|
||||
getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedIdNotAllowed", theResource.getIdElement().getIdPart()));
|
||||
case ALPHANUMERIC:
|
||||
if (theResource.getIdElement().isIdPartValidLong()) {
|
||||
throw new InvalidRequestException(
|
||||
getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getIdElement().getIdPart()));
|
||||
getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getIdElement().getIdPart()));
|
||||
}
|
||||
createForcedIdIfNeeded(entity, theResource.getIdElement(), false);
|
||||
break;
|
||||
|
@ -480,7 +481,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
outcome.setId(theResource.getIdElement());
|
||||
}
|
||||
|
||||
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart());
|
||||
String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart());
|
||||
outcome.setOperationOutcome(createInfoOperationOutcome(msg));
|
||||
|
||||
ourLog.debug(msg);
|
||||
|
@ -775,11 +776,29 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
}
|
||||
|
||||
@Override
|
||||
public DaoMethodOutcome patch(IIdType theId, PatchTypeEnum thePatchType, String thePatchBody, RequestDetails theRequestDetails) {
|
||||
ResourceTable entityToUpdate = readEntityLatestVersion(theId);
|
||||
if (theId.hasVersionIdPart()) {
|
||||
if (theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) {
|
||||
throw new ResourceVersionConflictException("Version " + theId.getVersionIdPart() + " is not the most recent version of this resource, unable to apply patch");
|
||||
public DaoMethodOutcome patch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, RequestDetails theRequestDetails) {
|
||||
|
||||
ResourceTable entityToUpdate;
|
||||
if (isNotBlank(theConditionalUrl)) {
|
||||
|
||||
Set<Long> match = myMatchResourceUrlService.processMatchUrl(theConditionalUrl, myResourceType);
|
||||
if (match.size() > 1) {
|
||||
String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "PATCH", theConditionalUrl, match.size());
|
||||
throw new PreconditionFailedException(msg);
|
||||
} else if (match.size() == 1) {
|
||||
Long pid = match.iterator().next();
|
||||
entityToUpdate = myEntityManager.find(ResourceTable.class, pid);
|
||||
} else {
|
||||
String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", theConditionalUrl);
|
||||
throw new ResourceNotFoundException(msg);
|
||||
}
|
||||
|
||||
} else {
|
||||
entityToUpdate = readEntityLatestVersion(theId);
|
||||
if (theId.hasVersionIdPart()) {
|
||||
if (theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) {
|
||||
throw new ResourceVersionConflictException("Version " + theId.getVersionIdPart() + " is not the most recent version of this resource, unable to apply patch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -831,12 +850,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
protected void preProcessResourceForStorage(T theResource) {
|
||||
String type = getContext().getResourceDefinition(theResource).getName();
|
||||
if (!getResourceName().equals(type)) {
|
||||
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "incorrectResourceType", type, getResourceName()));
|
||||
throw new InvalidRequestException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "incorrectResourceType", type, getResourceName()));
|
||||
}
|
||||
|
||||
if (theResource.getIdElement().hasIdPart()) {
|
||||
if (!theResource.getIdElement().isIdPartValid()) {
|
||||
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithInvalidId", theResource.getIdElement().getIdPart()));
|
||||
throw new InvalidRequestException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "failedToCreateWithInvalidId", theResource.getIdElement().getIdPart()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -945,7 +964,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
|
||||
if (theId.hasVersionIdPart()) {
|
||||
if (theId.isVersionIdPartValidLong() == false) {
|
||||
throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless()));
|
||||
throw new ResourceNotFoundException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless()));
|
||||
}
|
||||
if (entity.getVersion() != theId.getVersionIdPartAsLong()) {
|
||||
entity = null;
|
||||
|
@ -961,7 +980,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
try {
|
||||
entity = q.getSingleResult();
|
||||
} catch (NoResultException e) {
|
||||
throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless()));
|
||||
throw new ResourceNotFoundException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1216,7 +1235,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
QualifierDetails qualifiedParamName = SearchMethodBinding.extractQualifiersFromParameterName(nextParamName);
|
||||
RuntimeSearchParam param = searchParams.get(qualifiedParamName.getParamName());
|
||||
if (param == null) {
|
||||
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidSearchParameter", qualifiedParamName.getParamName(), new TreeSet<String>(searchParams.keySet()));
|
||||
String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "invalidSearchParameter", qualifiedParamName.getParamName(), new TreeSet<String>(searchParams.keySet()));
|
||||
throw new InvalidRequestException(msg);
|
||||
}
|
||||
|
||||
|
@ -1268,7 +1287,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
if (isNotBlank(theMatchUrl)) {
|
||||
Set<Long> match = myMatchResourceUrlService.processMatchUrl(theMatchUrl, myResourceType);
|
||||
if (match.size() > 1) {
|
||||
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size());
|
||||
String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size());
|
||||
throw new PreconditionFailedException(msg);
|
||||
} else if (match.size() == 1) {
|
||||
Long pid = match.iterator().next();
|
||||
|
@ -1340,7 +1359,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
outcome.setId(theResource.getIdElement());
|
||||
}
|
||||
|
||||
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulUpdate", outcome.getId(), w.getMillisAndRestart());
|
||||
String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "successfulUpdate", outcome.getId(), w.getMillisAndRestart());
|
||||
outcome.setOperationOutcome(createInfoOperationOutcome(msg));
|
||||
|
||||
ourLog.debug(msg);
|
||||
|
|
|
@ -150,7 +150,7 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
|
|||
*/
|
||||
<MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails);
|
||||
|
||||
DaoMethodOutcome patch(IIdType theId, PatchTypeEnum thePatchType, String thePatchBody, RequestDetails theRequestDetails);
|
||||
DaoMethodOutcome patch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, RequestDetails theRequestDetails);
|
||||
|
||||
Set<Long> processMatchUrl(String theMatchUrl);
|
||||
|
||||
|
|
|
@ -115,10 +115,10 @@ public abstract class BaseJpaResourceProvider<T extends IBaseResource> extends B
|
|||
}
|
||||
|
||||
@Patch
|
||||
public DaoMethodOutcome patch(HttpServletRequest theRequest, @IdParam IIdType theId, RequestDetails theRequestDetails, @ResourceParam String theBody, PatchTypeEnum thePatchType) {
|
||||
public DaoMethodOutcome patch(HttpServletRequest theRequest, @IdParam IIdType theId, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails, @ResourceParam String theBody, PatchTypeEnum thePatchType) {
|
||||
startRequest(theRequest);
|
||||
try {
|
||||
return myDao.patch(theId, thePatchType, theBody, theRequestDetails);
|
||||
return myDao.patch(theId, theConditionalUrl, thePatchType, theBody, theRequestDetails);
|
||||
} finally {
|
||||
endRequest(theRequest);
|
||||
}
|
||||
|
|
|
@ -56,14 +56,11 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
|
|||
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_REPRESENTATION);
|
||||
patch.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_JSON);
|
||||
|
||||
CloseableHttpResponse response = ourHttpClient.execute(patch);
|
||||
try {
|
||||
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
ourLog.info("Response:\n{}", responseString);
|
||||
assertThat(responseString, containsString("\"derivedFrom\":[{\"reference\":\"Media/465eb73a-bce3-423a-b86e-5d0d267638f4\"}]"));
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -84,14 +81,11 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
|
|||
patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
|
||||
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||
|
||||
CloseableHttpResponse response = ourHttpClient.execute(patch);
|
||||
try {
|
||||
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||
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();
|
||||
|
@ -99,6 +93,92 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
|
|||
assertEquals(false, newPt.getActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPatchUsingJsonPatch_Conditional_Success() throws Exception {
|
||||
String methodName = "testPatchUsingJsonPatch";
|
||||
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();
|
||||
}
|
||||
|
||||
HttpPatch patch = new HttpPatch(ourServerBase + "/Patient?_id=" + pid1.getIdPart());
|
||||
patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
|
||||
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||
|
||||
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
assertThat(responseString, containsString("<OperationOutcome"));
|
||||
assertThat(responseString, containsString("INFORMATION"));
|
||||
}
|
||||
|
||||
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_NoMatch() throws Exception {
|
||||
String methodName = "testPatchUsingJsonPatch";
|
||||
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();
|
||||
}
|
||||
|
||||
HttpPatch patch = new HttpPatch(ourServerBase + "/Patient?_id=" + pid1.getIdPart()+"FOO");
|
||||
patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
|
||||
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||
|
||||
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||
assertEquals(404, response.getStatusLine().getStatusCode());
|
||||
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
assertThat(responseString, containsString("<OperationOutcome"));
|
||||
assertThat(responseString, containsString("Invalid match URL "Patient?_id=" + pid1.getIdPart() + "FOO" - No resources match this search"));
|
||||
}
|
||||
|
||||
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
|
||||
assertEquals("1", newPt.getIdElement().getVersionIdPart());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPatchUsingJsonPatch_Conditional_MultipleMatch() throws Exception {
|
||||
String methodName = "testPatchUsingJsonPatch";
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
patient.setActive(true);
|
||||
patient.addIdentifier().setSystem("urn:system").setValue("0");
|
||||
patient.addName().setFamily(methodName).addGiven("Joe");
|
||||
myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
patient.setActive(true);
|
||||
patient.addIdentifier().setSystem("urn:system").setValue("1");
|
||||
patient.addName().setFamily(methodName).addGiven("Joe");
|
||||
myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
HttpPatch patch = new HttpPatch(ourServerBase + "/Patient?active=true");
|
||||
patch.setEntity(new StringEntity("[ { \"op\":\"replace\", \"path\":\"/active\", \"value\":false } ]", ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
|
||||
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||
|
||||
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||
assertEquals(412, response.getStatusLine().getStatusCode());
|
||||
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
assertThat(responseString, containsString("<OperationOutcome"));
|
||||
assertThat(responseString, containsString("Failed to PATCH resource with match URL "Patient?active=true" because this search matched 2 resources"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass in an invalid JSON Patch and make sure the error message
|
||||
* that is returned is useful
|
||||
|
@ -127,14 +207,11 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
|
|||
patch.setEntity(new StringEntity(patchText, ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
|
||||
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||
|
||||
CloseableHttpResponse response = ourHttpClient.execute(patch);
|
||||
try {
|
||||
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
assertThat(responseString, containsString("<OperationOutcome"));
|
||||
assertThat(responseString, containsString("was expecting double-quote to start field name"));
|
||||
assertEquals(400, response.getStatusLine().getStatusCode());
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -155,14 +232,11 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
|
|||
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 {
|
||||
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||
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();
|
||||
|
@ -187,14 +261,11 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
|
|||
patch.addHeader("If-Match", "W/\"1\"");
|
||||
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||
|
||||
CloseableHttpResponse response = ourHttpClient.execute(patch);
|
||||
try {
|
||||
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||
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();
|
||||
|
@ -219,14 +290,11 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
|
|||
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||
patch.setEntity(new StringEntity(patchString, ContentType.parse(Constants.CT_XML_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
|
||||
|
||||
CloseableHttpResponse response = ourHttpClient.execute(patch);
|
||||
try {
|
||||
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||
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();
|
||||
|
|
|
@ -11,7 +11,6 @@ import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu3
|
|||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
||||
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
|
||||
import org.apache.commons.dbcp2.BasicDataSource;
|
||||
import org.hibernate.jpa.HibernatePersistenceProvider;
|
||||
import org.hibernate.search.elasticsearch.cfg.ElasticsearchEnvironment;
|
||||
import org.springframework.beans.factory.annotation.Autowire;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -87,9 +86,10 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 {
|
|||
|
||||
/**
|
||||
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected
|
||||
* @return
|
||||
*/
|
||||
@Bean(autowire = Autowire.BY_TYPE)
|
||||
public IServerInterceptor responseHighlighterInterceptor() {
|
||||
public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
|
||||
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
|
||||
return retVal;
|
||||
}
|
||||
|
|
|
@ -113,29 +113,32 @@ public abstract class RequestDetails {
|
|||
* @return Returns the <b>conditional URL</b> if this request has one, or <code>null</code> otherwise
|
||||
*/
|
||||
public String getConditionalUrl(RestOperationTypeEnum theOperationType) {
|
||||
if (theOperationType == RestOperationTypeEnum.CREATE) {
|
||||
String retVal = this.getHeader(Constants.HEADER_IF_NONE_EXIST);
|
||||
if (isBlank(retVal)) {
|
||||
switch (theOperationType) {
|
||||
case CREATE:
|
||||
String retVal = this.getHeader(Constants.HEADER_IF_NONE_EXIST);
|
||||
if (isBlank(retVal)) {
|
||||
return null;
|
||||
}
|
||||
if (retVal.startsWith(this.getFhirServerBase())) {
|
||||
retVal = retVal.substring(this.getFhirServerBase().length());
|
||||
}
|
||||
return retVal;
|
||||
case DELETE:
|
||||
case UPDATE:
|
||||
case PATCH:
|
||||
if (this.getId() != null && this.getId().hasIdPart()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int questionMarkIndex = this.getCompleteUrl().indexOf('?');
|
||||
if (questionMarkIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getResourceName() + this.getCompleteUrl().substring(questionMarkIndex);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
if (retVal.startsWith(this.getFhirServerBase())) {
|
||||
retVal = retVal.substring(this.getFhirServerBase().length());
|
||||
}
|
||||
return retVal;
|
||||
} else if (theOperationType != RestOperationTypeEnum.DELETE && theOperationType != RestOperationTypeEnum.UPDATE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.getId() != null && this.getId().hasIdPart()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int questionMarkIndex = this.getCompleteUrl().indexOf('?');
|
||||
if (questionMarkIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getResourceName() + this.getCompleteUrl().substring(questionMarkIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
package ca.uhn.fhir.rest.server;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
|
||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||
import ca.uhn.fhir.rest.annotation.Patch;
|
||||
import ca.uhn.fhir.rest.annotation.ResourceParam;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.PatchTypeEnum;
|
||||
import ca.uhn.fhir.test.utilities.JettyUtil;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPatch;
|
||||
|
@ -16,32 +20,39 @@ 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.hl7.fhir.dstu3.model.*;
|
||||
import org.hl7.fhir.dstu3.model.IdType;
|
||||
import org.hl7.fhir.dstu3.model.OperationOutcome;
|
||||
import org.hl7.fhir.dstu3.model.Patient;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.junit.*;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.rest.annotation.*;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.PatchTypeEnum;
|
||||
import ca.uhn.fhir.test.utilities.JettyUtil;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class PatchDstu3Test {
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class PatchServerDstu3Test {
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PatchServerDstu3Test.class);
|
||||
private static CloseableHttpClient ourClient;
|
||||
private static FhirContext ourCtx = FhirContext.forDstu3();
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PatchDstu3Test.class);
|
||||
private static int ourPort;
|
||||
private static Server ourServer;
|
||||
private static String ourLastMethod;
|
||||
private static PatchTypeEnum ourLastPatchType;
|
||||
private static String ourLastBody;
|
||||
private static IdType ourLastId;
|
||||
private static String ourLastConditional;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
ourLastMethod = null;
|
||||
ourLastBody = null;
|
||||
ourLastId = null;
|
||||
ourLastConditional = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -67,6 +78,30 @@ public class PatchDstu3Test {
|
|||
assertEquals(PatchTypeEnum.JSON_PATCH, ourLastPatchType);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPatchUsingConditional() throws Exception {
|
||||
String requestContents = "[ { \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] } ]";
|
||||
HttpPatch httpPatch = new HttpPatch("http://localhost:" + ourPort + "/Patient?_id=123");
|
||||
httpPatch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||
httpPatch.setEntity(new StringEntity(requestContents, ContentType.parse(Constants.CT_JSON_PATCH)));
|
||||
CloseableHttpResponse status = ourClient.execute(httpPatch);
|
||||
|
||||
try {
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
ourLog.info(responseContent);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
assertEquals("<OperationOutcome xmlns=\"http://hl7.org/fhir\"><text><div xmlns=\"http://www.w3.org/1999/xhtml\">OK</div></text></OperationOutcome>", responseContent);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
}
|
||||
|
||||
assertEquals("patientPatch", ourLastMethod);
|
||||
assertEquals("Patient?_id=123", ourLastConditional);
|
||||
assertEquals(null, ourLastId);
|
||||
assertEquals(requestContents, ourLastBody);
|
||||
assertEquals(PatchTypeEnum.JSON_PATCH, ourLastPatchType);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPatchValidXml() throws Exception {
|
||||
String requestContents = "<root/>";
|
||||
|
@ -128,6 +163,32 @@ public class PatchDstu3Test {
|
|||
|
||||
}
|
||||
|
||||
public static class DummyPatientResourceProvider implements IResourceProvider {
|
||||
|
||||
@Override
|
||||
public Class<? extends IBaseResource> getResourceType() {
|
||||
return Patient.class;
|
||||
}
|
||||
|
||||
@Patch
|
||||
public OperationOutcome patientPatch(
|
||||
@IdParam IdType theId,
|
||||
PatchTypeEnum thePatchType,
|
||||
@ResourceParam String theBody,
|
||||
@ConditionalUrlParam String theConditional
|
||||
) {
|
||||
ourLastMethod = "patientPatch";
|
||||
ourLastBody = theBody;
|
||||
ourLastId = theId;
|
||||
ourLastPatchType = thePatchType;
|
||||
ourLastConditional = theConditional;
|
||||
OperationOutcome retVal = new OperationOutcome();
|
||||
retVal.getText().setDivAsString("<div>OK</div>");
|
||||
return retVal;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClassClearContext() throws Exception {
|
||||
JettyUtil.closeServer(ourServer);
|
||||
|
@ -149,7 +210,7 @@ public class PatchDstu3Test {
|
|||
proxyHandler.addServletWithMapping(servletHolder, "/*");
|
||||
ourServer.setHandler(proxyHandler);
|
||||
JettyUtil.startServer(ourServer);
|
||||
ourPort = JettyUtil.getPortForStartedServer(ourServer);
|
||||
ourPort = JettyUtil.getPortForStartedServer(ourServer);
|
||||
|
||||
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
|
||||
HttpClientBuilder builder = HttpClientBuilder.create();
|
||||
|
@ -158,28 +219,4 @@ public class PatchDstu3Test {
|
|||
|
||||
}
|
||||
|
||||
private static String ourLastBody;
|
||||
private static IdType ourLastId;
|
||||
public static class DummyPatientResourceProvider implements IResourceProvider {
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Class<? extends IBaseResource> getResourceType() {
|
||||
return Patient.class;
|
||||
}
|
||||
|
||||
@Patch
|
||||
public OperationOutcome patientPatch(@IdParam IdType theId, PatchTypeEnum thePatchType, @ResourceParam String theBody) {
|
||||
ourLastMethod = "patientPatch";
|
||||
ourLastBody = theBody;
|
||||
ourLastId = theId;
|
||||
ourLastPatchType = thePatchType;
|
||||
OperationOutcome retVal = new OperationOutcome();
|
||||
retVal.getText().setDivAsString("<div>OK</div>");
|
||||
return retVal;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -74,6 +74,11 @@
|
|||
assign a free port. This should theoretically result in fewer failed builds resulting from
|
||||
port conflicts. Thanks to Stig Døssing for the pull request!
|
||||
</action>
|
||||
<action type="add" issue="1348">
|
||||
JPA server now supports conditional PATCH operation (i.e. performing a patch
|
||||
with a syntax such as
|
||||
<![CDATA[<code>/Patient?identifier=sys|val</code>]]>)
|
||||
</action>
|
||||
</release>
|
||||
<release version="3.8.0" date="2019-05-30" description="Hippo">
|
||||
<action type="fix">
|
||||
|
|
Loading…
Reference in New Issue