mirror of
https://github.com/hapifhir/hapi-fhir.git
synced 2025-02-08 14:05:02 +00:00
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;
|
package ca.uhn.fhir.i18n;
|
||||||
|
|
||||||
import ca.uhn.fhir.context.ConfigurationException;
|
import ca.uhn.fhir.context.ConfigurationException;
|
||||||
|
import ca.uhn.fhir.util.UrlUtil;
|
||||||
|
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@ -92,6 +93,20 @@ public class HapiLocalizer {
|
|||||||
return getMessage(toKey(theType, theKey), theParameters);
|
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) {
|
public String getMessage(String theQualifiedKey, Object... theParameters) {
|
||||||
if (theParameters != null && theParameters.length > 0) {
|
if (theParameters != null && theParameters.length > 0) {
|
||||||
MessageFormat format = myKeyToMessageFormat.get(theQualifiedKey);
|
MessageFormat format = myKeyToMessageFormat.get(theQualifiedKey);
|
||||||
|
@ -123,7 +123,7 @@ public class UrlUtil {
|
|||||||
return value.startsWith("http://") || value.startsWith("https://");
|
return value.startsWith("http://") || value.startsWith("https://");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isNeedsSanitization(String theString) {
|
public static boolean isNeedsSanitization(CharSequence theString) {
|
||||||
if (theString != null) {
|
if (theString != null) {
|
||||||
for (int i = 0; i < theString.length(); i++) {
|
for (int i = 0; i < theString.length(); i++) {
|
||||||
char nextChar = theString.charAt(i);
|
char nextChar = theString.charAt(i);
|
||||||
@ -302,7 +302,7 @@ public class UrlUtil {
|
|||||||
* This method specifically HTML-encodes the " and
|
* This method specifically HTML-encodes the " and
|
||||||
* < characters in order to prevent injection attacks
|
* < characters in order to prevent injection attacks
|
||||||
*/
|
*/
|
||||||
public static String sanitizeUrlPart(String theString) {
|
public static String sanitizeUrlPart(CharSequence theString) {
|
||||||
if (theString == null) {
|
if (theString == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -316,6 +316,9 @@ public class UrlUtil {
|
|||||||
|
|
||||||
char nextChar = theString.charAt(j);
|
char nextChar = theString.charAt(j);
|
||||||
switch (nextChar) {
|
switch (nextChar) {
|
||||||
|
case '\'':
|
||||||
|
buffer.append("'");
|
||||||
|
break;
|
||||||
case '"':
|
case '"':
|
||||||
buffer.append(""");
|
buffer.append(""");
|
||||||
break;
|
break;
|
||||||
@ -332,7 +335,7 @@ public class UrlUtil {
|
|||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return theString;
|
return theString.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<String, String[]> toQueryStringMap(HashMap<String, List<String>> map) {
|
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.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import static ca.uhn.fhir.util.UrlUtil.sanitizeUrlPart;
|
||||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
|
|
||||||
@Transactional(propagation = Propagation.REQUIRED)
|
@Transactional(propagation = Propagation.REQUIRED)
|
||||||
@ -151,7 +152,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||||||
|
|
||||||
if (isNotBlank(theResource.getIdElement().getIdPart())) {
|
if (isNotBlank(theResource.getIdElement().getIdPart())) {
|
||||||
if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
|
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"));
|
throw new InvalidRequestException(message, createErrorOperationOutcome(message, "processing"));
|
||||||
} else {
|
} else {
|
||||||
// As of DSTU3, ID and version in the body should be ignored for a create/update
|
// 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);
|
Set<Long> resourceIds = myMatchResourceUrlService.processMatchUrl(theUrl, myResourceType);
|
||||||
if (resourceIds.size() > 1) {
|
if (resourceIds.size() > 1) {
|
||||||
if (myDaoConfig.isAllowMultipleDelete() == false) {
|
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;
|
IBaseOperationOutcome oo;
|
||||||
if (deletedResources.isEmpty()) {
|
if (deletedResources.isEmpty()) {
|
||||||
oo = OperationOutcomeUtil.newInstance(getContext());
|
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 severity = "warning";
|
||||||
String code = "not-found";
|
String code = "not-found";
|
||||||
OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
|
OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
|
||||||
@ -384,7 +385,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||||||
if (isNotBlank(theIfNoneExist)) {
|
if (isNotBlank(theIfNoneExist)) {
|
||||||
Set<Long> match = myMatchResourceUrlService.processMatchUrl(theIfNoneExist, myResourceType);
|
Set<Long> match = myMatchResourceUrlService.processMatchUrl(theIfNoneExist, myResourceType);
|
||||||
if (match.size() > 1) {
|
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);
|
throw new PreconditionFailedException(msg);
|
||||||
} else if (match.size() == 1) {
|
} else if (match.size() == 1) {
|
||||||
Long pid = match.iterator().next();
|
Long pid = match.iterator().next();
|
||||||
@ -400,11 +401,11 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||||||
switch (myDaoConfig.getResourceClientIdStrategy()) {
|
switch (myDaoConfig.getResourceClientIdStrategy()) {
|
||||||
case NOT_ALLOWED:
|
case NOT_ALLOWED:
|
||||||
throw new ResourceNotFoundException(
|
throw new ResourceNotFoundException(
|
||||||
getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedIdNotAllowed", theResource.getIdElement().getIdPart()));
|
getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedIdNotAllowed", theResource.getIdElement().getIdPart()));
|
||||||
case ALPHANUMERIC:
|
case ALPHANUMERIC:
|
||||||
if (theResource.getIdElement().isIdPartValidLong()) {
|
if (theResource.getIdElement().isIdPartValidLong()) {
|
||||||
throw new InvalidRequestException(
|
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);
|
createForcedIdIfNeeded(entity, theResource.getIdElement(), false);
|
||||||
break;
|
break;
|
||||||
@ -480,7 +481,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||||||
outcome.setId(theResource.getIdElement());
|
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));
|
outcome.setOperationOutcome(createInfoOperationOutcome(msg));
|
||||||
|
|
||||||
ourLog.debug(msg);
|
ourLog.debug(msg);
|
||||||
@ -775,13 +776,31 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DaoMethodOutcome patch(IIdType theId, PatchTypeEnum thePatchType, String thePatchBody, RequestDetails theRequestDetails) {
|
public DaoMethodOutcome patch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, RequestDetails theRequestDetails) {
|
||||||
ResourceTable entityToUpdate = readEntityLatestVersion(theId);
|
|
||||||
|
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.hasVersionIdPart()) {
|
||||||
if (theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) {
|
if (theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) {
|
||||||
throw new ResourceVersionConflictException("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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
validateResourceType(entityToUpdate);
|
validateResourceType(entityToUpdate);
|
||||||
|
|
||||||
@ -831,12 +850,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||||||
protected void preProcessResourceForStorage(T theResource) {
|
protected void preProcessResourceForStorage(T theResource) {
|
||||||
String type = getContext().getResourceDefinition(theResource).getName();
|
String type = getContext().getResourceDefinition(theResource).getName();
|
||||||
if (!getResourceName().equals(type)) {
|
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().hasIdPart()) {
|
||||||
if (!theResource.getIdElement().isIdPartValid()) {
|
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.hasVersionIdPart()) {
|
||||||
if (theId.isVersionIdPartValidLong() == false) {
|
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()) {
|
if (entity.getVersion() != theId.getVersionIdPartAsLong()) {
|
||||||
entity = null;
|
entity = null;
|
||||||
@ -961,7 +980,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||||||
try {
|
try {
|
||||||
entity = q.getSingleResult();
|
entity = q.getSingleResult();
|
||||||
} catch (NoResultException e) {
|
} 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);
|
QualifierDetails qualifiedParamName = SearchMethodBinding.extractQualifiersFromParameterName(nextParamName);
|
||||||
RuntimeSearchParam param = searchParams.get(qualifiedParamName.getParamName());
|
RuntimeSearchParam param = searchParams.get(qualifiedParamName.getParamName());
|
||||||
if (param == null) {
|
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);
|
throw new InvalidRequestException(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1268,7 +1287,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||||||
if (isNotBlank(theMatchUrl)) {
|
if (isNotBlank(theMatchUrl)) {
|
||||||
Set<Long> match = myMatchResourceUrlService.processMatchUrl(theMatchUrl, myResourceType);
|
Set<Long> match = myMatchResourceUrlService.processMatchUrl(theMatchUrl, myResourceType);
|
||||||
if (match.size() > 1) {
|
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);
|
throw new PreconditionFailedException(msg);
|
||||||
} else if (match.size() == 1) {
|
} else if (match.size() == 1) {
|
||||||
Long pid = match.iterator().next();
|
Long pid = match.iterator().next();
|
||||||
@ -1340,7 +1359,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||||||
outcome.setId(theResource.getIdElement());
|
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));
|
outcome.setOperationOutcome(createInfoOperationOutcome(msg));
|
||||||
|
|
||||||
ourLog.debug(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);
|
<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);
|
Set<Long> processMatchUrl(String theMatchUrl);
|
||||||
|
|
||||||
|
@ -115,10 +115,10 @@ public abstract class BaseJpaResourceProvider<T extends IBaseResource> extends B
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch
|
@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);
|
startRequest(theRequest);
|
||||||
try {
|
try {
|
||||||
return myDao.patch(theId, thePatchType, theBody, theRequestDetails);
|
return myDao.patch(theId, theConditionalUrl, thePatchType, theBody, theRequestDetails);
|
||||||
} finally {
|
} finally {
|
||||||
endRequest(theRequest);
|
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_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_REPRESENTATION);
|
||||||
patch.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_JSON);
|
patch.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_JSON);
|
||||||
|
|
||||||
CloseableHttpResponse response = ourHttpClient.execute(patch);
|
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||||
try {
|
|
||||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
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);
|
||||||
ourLog.info("Response:\n{}", responseString);
|
ourLog.info("Response:\n{}", responseString);
|
||||||
assertThat(responseString, containsString("\"derivedFrom\":[{\"reference\":\"Media/465eb73a-bce3-423a-b86e-5d0d267638f4\"}]"));
|
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.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);
|
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||||
|
|
||||||
CloseableHttpResponse response = ourHttpClient.execute(patch);
|
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||||
try {
|
|
||||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
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("<OperationOutcome"));
|
||||||
assertThat(responseString, containsString("INFORMATION"));
|
assertThat(responseString, containsString("INFORMATION"));
|
||||||
} finally {
|
|
||||||
response.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
|
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
|
||||||
@ -99,6 +93,92 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
|
|||||||
assertEquals(false, newPt.getActive());
|
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
|
* Pass in an invalid JSON Patch and make sure the error message
|
||||||
* that is returned is useful
|
* 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.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);
|
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||||
|
|
||||||
CloseableHttpResponse response = ourHttpClient.execute(patch);
|
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||||
try {
|
|
||||||
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("<OperationOutcome"));
|
||||||
assertThat(responseString, containsString("was expecting double-quote to start field name"));
|
assertThat(responseString, containsString("was expecting double-quote to start field name"));
|
||||||
assertEquals(400, response.getStatusLine().getStatusCode());
|
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.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\"");
|
patch.addHeader("If-Match", "W/\"9\"");
|
||||||
|
|
||||||
CloseableHttpResponse response = ourHttpClient.execute(patch);
|
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||||
try {
|
|
||||||
assertEquals(409, response.getStatusLine().getStatusCode());
|
assertEquals(409, 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("<OperationOutcome"));
|
||||||
assertThat(responseString, containsString("<diagnostics value=\"Version 9 is not the most recent version of this resource, unable to apply patch\"/>"));
|
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();
|
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("If-Match", "W/\"1\"");
|
||||||
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
patch.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + "=" + Constants.HEADER_PREFER_RETURN_OPERATION_OUTCOME);
|
||||||
|
|
||||||
CloseableHttpResponse response = ourHttpClient.execute(patch);
|
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||||
try {
|
|
||||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
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("<OperationOutcome"));
|
||||||
assertThat(responseString, containsString("INFORMATION"));
|
assertThat(responseString, containsString("INFORMATION"));
|
||||||
} finally {
|
|
||||||
response.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
|
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.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)));
|
patch.setEntity(new StringEntity(patchString, ContentType.parse(Constants.CT_XML_PATCH + Constants.CHARSET_UTF8_CTSUFFIX)));
|
||||||
|
|
||||||
CloseableHttpResponse response = ourHttpClient.execute(patch);
|
try (CloseableHttpResponse response = ourHttpClient.execute(patch)) {
|
||||||
try {
|
|
||||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
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("<OperationOutcome"));
|
||||||
assertThat(responseString, containsString("INFORMATION"));
|
assertThat(responseString, containsString("INFORMATION"));
|
||||||
} finally {
|
|
||||||
response.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Patient newPt = ourClient.read().resource(Patient.class).withId(pid1.getIdPart()).execute();
|
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.IServerInterceptor;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
|
||||||
import org.apache.commons.dbcp2.BasicDataSource;
|
import org.apache.commons.dbcp2.BasicDataSource;
|
||||||
import org.hibernate.jpa.HibernatePersistenceProvider;
|
|
||||||
import org.hibernate.search.elasticsearch.cfg.ElasticsearchEnvironment;
|
import org.hibernate.search.elasticsearch.cfg.ElasticsearchEnvironment;
|
||||||
import org.springframework.beans.factory.annotation.Autowire;
|
import org.springframework.beans.factory.annotation.Autowire;
|
||||||
import org.springframework.context.annotation.Bean;
|
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
|
* This interceptor adds some pretty syntax highlighting in responses when a browser is detected
|
||||||
|
* @return
|
||||||
*/
|
*/
|
||||||
@Bean(autowire = Autowire.BY_TYPE)
|
@Bean(autowire = Autowire.BY_TYPE)
|
||||||
public IServerInterceptor responseHighlighterInterceptor() {
|
public ResponseHighlighterInterceptor responseHighlighterInterceptor() {
|
||||||
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
|
ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor();
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,8 @@ public abstract class RequestDetails {
|
|||||||
* @return Returns the <b>conditional URL</b> if this request has one, or <code>null</code> otherwise
|
* @return Returns the <b>conditional URL</b> if this request has one, or <code>null</code> otherwise
|
||||||
*/
|
*/
|
||||||
public String getConditionalUrl(RestOperationTypeEnum theOperationType) {
|
public String getConditionalUrl(RestOperationTypeEnum theOperationType) {
|
||||||
if (theOperationType == RestOperationTypeEnum.CREATE) {
|
switch (theOperationType) {
|
||||||
|
case CREATE:
|
||||||
String retVal = this.getHeader(Constants.HEADER_IF_NONE_EXIST);
|
String retVal = this.getHeader(Constants.HEADER_IF_NONE_EXIST);
|
||||||
if (isBlank(retVal)) {
|
if (isBlank(retVal)) {
|
||||||
return null;
|
return null;
|
||||||
@ -122,10 +123,9 @@ public abstract class RequestDetails {
|
|||||||
retVal = retVal.substring(this.getFhirServerBase().length());
|
retVal = retVal.substring(this.getFhirServerBase().length());
|
||||||
}
|
}
|
||||||
return retVal;
|
return retVal;
|
||||||
} else if (theOperationType != RestOperationTypeEnum.DELETE && theOperationType != RestOperationTypeEnum.UPDATE) {
|
case DELETE:
|
||||||
return null;
|
case UPDATE:
|
||||||
}
|
case PATCH:
|
||||||
|
|
||||||
if (this.getId() != null && this.getId().hasIdPart()) {
|
if (this.getId() != null && this.getId().hasIdPart()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -136,6 +136,9 @@ public abstract class RequestDetails {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.getResourceName() + this.getCompleteUrl().substring(questionMarkIndex);
|
return this.getResourceName() + this.getCompleteUrl().substring(questionMarkIndex);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
package ca.uhn.fhir.rest.server;
|
package ca.uhn.fhir.rest.server;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
|
||||||
import java.nio.charset.StandardCharsets;
|
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||||
import java.util.concurrent.TimeUnit;
|
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.commons.io.IOUtils;
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
import org.apache.http.client.methods.HttpPatch;
|
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.server.Server;
|
||||||
import org.eclipse.jetty.servlet.ServletHandler;
|
import org.eclipse.jetty.servlet.ServletHandler;
|
||||||
import org.eclipse.jetty.servlet.ServletHolder;
|
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.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 java.nio.charset.StandardCharsets;
|
||||||
import ca.uhn.fhir.rest.annotation.*;
|
import java.util.concurrent.TimeUnit;
|
||||||
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;
|
|
||||||
|
|
||||||
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 CloseableHttpClient ourClient;
|
||||||
private static FhirContext ourCtx = FhirContext.forDstu3();
|
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 int ourPort;
|
||||||
private static Server ourServer;
|
private static Server ourServer;
|
||||||
private static String ourLastMethod;
|
private static String ourLastMethod;
|
||||||
private static PatchTypeEnum ourLastPatchType;
|
private static PatchTypeEnum ourLastPatchType;
|
||||||
|
private static String ourLastBody;
|
||||||
|
private static IdType ourLastId;
|
||||||
|
private static String ourLastConditional;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void before() {
|
public void before() {
|
||||||
ourLastMethod = null;
|
ourLastMethod = null;
|
||||||
ourLastBody = null;
|
ourLastBody = null;
|
||||||
ourLastId = null;
|
ourLastId = null;
|
||||||
|
ourLastConditional = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -67,6 +78,30 @@ public class PatchDstu3Test {
|
|||||||
assertEquals(PatchTypeEnum.JSON_PATCH, ourLastPatchType);
|
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
|
@Test
|
||||||
public void testPatchValidXml() throws Exception {
|
public void testPatchValidXml() throws Exception {
|
||||||
String requestContents = "<root/>";
|
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
|
@AfterClass
|
||||||
public static void afterClassClearContext() throws Exception {
|
public static void afterClassClearContext() throws Exception {
|
||||||
JettyUtil.closeServer(ourServer);
|
JettyUtil.closeServer(ourServer);
|
||||||
@ -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
|
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!
|
port conflicts. Thanks to Stig Døssing for the pull request!
|
||||||
</action>
|
</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>
|
||||||
<release version="3.8.0" date="2019-05-30" description="Hippo">
|
<release version="3.8.0" date="2019-05-30" description="Hippo">
|
||||||
<action type="fix">
|
<action type="fix">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user