Adjust return type for fluent delete() operation (#1805)

* Add cascading delete to client

* Add changelog

* Client delete should return MethodOutcome

* Rerturn more appropriate type for delete operations

* Refactor cascade detection to be reusable
This commit is contained in:
James Agnew 2020-04-21 15:23:11 -04:00 committed by GitHub
parent f218ade480
commit c716216b34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 195 additions and 133 deletions

View File

@ -21,9 +21,10 @@ package ca.uhn.fhir.rest.gclient;
*/ */
import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum; import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
public interface IDeleteTyped extends IClientExecutable<IDeleteTyped, IBaseOperationOutcome> { public interface IDeleteTyped extends IClientExecutable<IDeleteTyped, MethodOutcome> {
/** /**
* Delete cascade mode - Note that this is a HAPI FHIR specific feature and is not supported on all servers. * Delete cascade mode - Note that this is a HAPI FHIR specific feature and is not supported on all servers.

View File

@ -72,6 +72,7 @@ public interface IQuery<Y> extends IBaseQuery<IQuery<Y>>, IClientExecutable<IQue
* on a single page. * on a single page.
* *
* @deprecated This parameter is badly named, since FHIR calls this parameter "_count" and not "_limit". Use {@link #count(int)} instead (it also sets the _count parameter) * @deprecated This parameter is badly named, since FHIR calls this parameter "_count" and not "_limit". Use {@link #count(int)} instead (it also sets the _count parameter)
* @see #count(int)
*/ */
@Deprecated @Deprecated
IQuery<Y> limitTo(int theLimitTo); IQuery<Y> limitTo(int theLimitTo);

View File

@ -604,7 +604,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
} }
private class DeleteInternal extends BaseSearch<IDeleteTyped, IDeleteWithQueryTyped, IBaseOperationOutcome> implements IDelete, IDeleteTyped, IDeleteWithQuery, IDeleteWithQueryTyped { private class DeleteInternal extends BaseSearch<IDeleteTyped, IDeleteWithQueryTyped, MethodOutcome> implements IDelete, IDeleteTyped, IDeleteWithQuery, IDeleteWithQueryTyped {
private boolean myConditional; private boolean myConditional;
private IIdType myId; private IIdType myId;
@ -613,7 +613,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private DeleteCascadeModeEnum myCascadeMode; private DeleteCascadeModeEnum myCascadeMode;
@Override @Override
public IBaseOperationOutcome execute() { public MethodOutcome execute() {
Map<String, List<String>> additionalParams = new HashMap<>(); Map<String, List<String>> additionalParams = new HashMap<>();
if (myCascadeMode != null) { if (myCascadeMode != null) {
@ -635,7 +635,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
} else { } else {
invocation = DeleteMethodBinding.createDeleteInvocation(getFhirContext(), mySearchUrl, getParamMap()); invocation = DeleteMethodBinding.createDeleteInvocation(getFhirContext(), mySearchUrl, getParamMap());
} }
OperationOutcomeResponseHandler binding = new OperationOutcomeResponseHandler();
OutcomeResponseHandler binding = new OutcomeResponseHandler();
return invoke(additionalParams, binding, invocation); return invoke(additionalParams, binding, invocation);
} }
@ -1389,30 +1390,6 @@ public class GenericClient extends BaseClient implements IGenericClient {
} }
} }
private final class OperationOutcomeResponseHandler implements IClientResponseHandler<IBaseOperationOutcome> {
@Override
public IBaseOperationOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
return null;
}
IParser parser = respType.newParser(myContext);
IBaseOperationOutcome retVal;
try {
// TODO: handle if something else than OO comes back
retVal = (IBaseOperationOutcome) parser.parseResource(theResponseInputStream);
} catch (DataFormatException e) {
ourLog.warn("Failed to parse OperationOutcome response", e);
return null;
}
MethodUtil.parseClientRequestResourceHeaders(null, theHeaders, retVal);
return retVal;
}
}
private final class OutcomeResponseHandler implements IClientResponseHandler<MethodOutcome> { private final class OutcomeResponseHandler implements IClientResponseHandler<MethodOutcome> {
private PreferReturnEnum myPrefer; private PreferReturnEnum myPrefer;

View File

@ -236,21 +236,27 @@ public class GenericClientExample {
// START SNIPPET: conformance // START SNIPPET: conformance
// Retrieve the server's conformance statement and print its // Retrieve the server's conformance statement and print its
// description // description
CapabilityStatement conf = client.capabilities().ofType(CapabilityStatement.class).execute(); CapabilityStatement conf = client
.capabilities()
.ofType(CapabilityStatement.class)
.execute();
System.out.println(conf.getDescriptionElement().getValue()); System.out.println(conf.getDescriptionElement().getValue());
// END SNIPPET: conformance // END SNIPPET: conformance
} }
{ {
// START SNIPPET: delete // START SNIPPET: delete
IBaseOperationOutcome resp = client.delete().resourceById(new IdType("Patient", "1234")).execute(); MethodOutcome response = client
.delete()
.resourceById(new IdType("Patient", "1234"))
.execute();
// outcome may be null if the server didn't return one // outcome may be null if the server didn't return one
if (resp != null) { OperationOutcome outcome = (OperationOutcome) response.getOperationOutcome();
OperationOutcome outcome = (OperationOutcome) resp; if (outcome != null) {
System.out.println(outcome.getIssueFirstRep().getDetails().getCodingFirstRep().getCode()); System.out.println(outcome.getIssueFirstRep().getDetails().getCodingFirstRep().getCode());
} }
// END SNIPPET: delete // END SNIPPET: delete
} }
{ {
// START SNIPPET: deleteConditional // START SNIPPET: deleteConditional
client.delete() client.delete()
@ -356,7 +362,8 @@ public class GenericClientExample {
.revInclude(Provenance.INCLUDE_TARGET) .revInclude(Provenance.INCLUDE_TARGET)
.lastUpdated(new DateRangeParam("2011-01-01", null)) .lastUpdated(new DateRangeParam("2011-01-01", null))
.sort().ascending(Patient.BIRTHDATE) .sort().ascending(Patient.BIRTHDATE)
.sort().descending(Patient.NAME).limitTo(123) .sort().descending(Patient.NAME)
.count(123)
.returnBundle(Bundle.class) .returnBundle(Bundle.class)
.execute(); .execute();
// END SNIPPET: searchAdv // END SNIPPET: searchAdv

View File

@ -33,3 +33,20 @@
These classes have not changed in terms of functionality, but existing projects may need to adjust some These classes have not changed in terms of functionality, but existing projects may need to adjust some
package import statements. package import statements.
" "
- item:
issue: "1804"
type: "change"
title: "**Breaking Change**:
The Generic/Fluent **delete()** operation now returns a [MethodOutcome](/apidocs/hapi-fhir-base/ca/uhn/fhir/rest/api/MethodOutcome.html)
object instead of an OperationOutcome. The OperationOutcomoe is still available direcly by querying
the MethodOutcome object, but this change makes the delete() method more consistent with
other similar methods in the API.
"
- item:
type: "change"
title: "**Breaking Change**:
Some R4 and R5 structure fields containing a `code` value with a **Required (closed) binding**
did not use the java Enum type that was generated for the given field. These have been changed
to use the Enum values where possible. This change does not remove any functionality from the model
but may require a small amount of re-coding to deal with new setter/getter types on a few fields.
"

View File

@ -168,7 +168,7 @@ public class AbstractJaxRsResourceProviderDstu3Test {
@Test @Test
public void testDeletePatient() { public void testDeletePatient() {
when(mock.delete(idCaptor.capture(), conditionalCaptor.capture())).thenReturn(new MethodOutcome()); when(mock.delete(idCaptor.capture(), conditionalCaptor.capture())).thenReturn(new MethodOutcome());
final IBaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute(); final IBaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute().getOperationOutcome();
assertEquals("1", idCaptor.getValue().getIdPart()); assertEquals("1", idCaptor.getValue().getIdPart());
} }

View File

@ -164,7 +164,7 @@ public class AbstractJaxRsResourceProviderTest {
@Test @Test
public void testDeletePatient() { public void testDeletePatient() {
when(mock.delete(idCaptor.capture(), conditionalCaptor.capture())).thenReturn(new MethodOutcome()); when(mock.delete(idCaptor.capture(), conditionalCaptor.capture())).thenReturn(new MethodOutcome());
final IBaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute(); final IBaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute().getOperationOutcome();
assertEquals("1", idCaptor.getValue().getIdPart()); assertEquals("1", idCaptor.getValue().getIdPart());
} }

View File

@ -34,8 +34,10 @@ import ca.uhn.fhir.jpa.delete.DeleteConflictOutcome;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails; import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.OperationOutcomeUtil; import ca.uhn.fhir.util.OperationOutcomeUtil;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
@ -45,6 +47,9 @@ import org.hl7.fhir.r4.model.OperationOutcome;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.validation.constraints.Null;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
@ -92,7 +97,12 @@ public class CascadingDeleteInterceptor {
public DeleteConflictOutcome handleDeleteConflicts(DeleteConflictList theConflictList, RequestDetails theRequest) { public DeleteConflictOutcome handleDeleteConflicts(DeleteConflictList theConflictList, RequestDetails theRequest) {
ourLog.debug("Have delete conflicts: {}", theConflictList); ourLog.debug("Have delete conflicts: {}", theConflictList);
if (!shouldCascade(theRequest)) { if (shouldCascade(theRequest) == DeleteCascadeModeEnum.NONE) {
// Add a message to the response
String message = theRequest.getFhirContext().getLocalizer().getMessage(CascadingDeleteInterceptor.class, "noParam");
theRequest.getUserData().put(CASCADED_DELETES_FAILED_KEY, message);
return null; return null;
} }
@ -180,28 +190,12 @@ public class CascadingDeleteInterceptor {
/** /**
* Subclasses may override * Subclasses may override
* *
* @param theRequest The REST request * @param theRequest The REST request (may be null)
* @return Returns true if cascading delete should be allowed * @return Returns true if cascading delete should be allowed
*/ */
@SuppressWarnings("WeakerAccess") @Nonnull
protected boolean shouldCascade(RequestDetails theRequest) { protected DeleteCascadeModeEnum shouldCascade(@Nullable RequestDetails theRequest) {
if (theRequest != null) { return RestfulServerUtils.extractDeleteCascadeParameter(theRequest);
String[] cascadeParameters = theRequest.getParameters().get(Constants.PARAMETER_CASCADE_DELETE);
if (cascadeParameters != null && Arrays.asList(cascadeParameters).contains(Constants.CASCADE_DELETE)) {
return true;
}
String cascadeHeader = theRequest.getHeader(Constants.HEADER_CASCADE);
if (Constants.CASCADE_DELETE.equals(cascadeHeader)) {
return true;
}
// Add a message to the response
String message = theRequest.getFhirContext().getLocalizer().getMessage(CascadingDeleteInterceptor.class, "noParam");
theRequest.getUserData().put(CASCADED_DELETES_FAILED_KEY, message);
}
return false;
} }

View File

@ -814,15 +814,13 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
myDaoConfig.setAllowMultipleDelete(true); myDaoConfig.setAllowMultipleDelete(true);
//@formatter:off MethodOutcome response = ourClient
IBaseOperationOutcome response = ourClient
.delete() .delete()
.resourceConditionalByType(Patient.class) .resourceConditionalByType(Patient.class)
.where(Patient.IDENTIFIER.exactly().code(methodName)) .where(Patient.IDENTIFIER.exactly().code(methodName))
.execute(); .execute();
//@formatter:on
String encoded = myFhirCtx.newXmlParser().encodeResourceToString(response); String encoded = myFhirCtx.newXmlParser().encodeResourceToString(response.getOperationOutcome());
ourLog.info(encoded); ourLog.info(encoded);
assertThat(encoded, containsString( assertThat(encoded, containsString(
"<issue><severity value=\"information\"/><code value=\"informational\"/><diagnostics value=\"Successfully deleted 2 resource(s) in ")); "<issue><severity value=\"information\"/><code value=\"informational\"/><diagnostics value=\"Successfully deleted 2 resource(s) in "));
@ -1028,8 +1026,8 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
p.addName().setFamily("FAM"); p.addName().setFamily("FAM");
IIdType id = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless(); IIdType id = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless();
IBaseOperationOutcome resp = ourClient.delete().resourceById(id).execute(); MethodOutcome resp = ourClient.delete().resourceById(id).execute();
OperationOutcome oo = (OperationOutcome) resp; OperationOutcome oo = (OperationOutcome) resp.getOperationOutcome();
assertThat(oo.getIssueFirstRep().getDiagnostics(), startsWith("Successfully deleted 1 resource(s) in ")); assertThat(oo.getIssueFirstRep().getDiagnostics(), startsWith("Successfully deleted 1 resource(s) in "));
} }

View File

@ -1257,15 +1257,13 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myDaoConfig.setAllowMultipleDelete(true); myDaoConfig.setAllowMultipleDelete(true);
//@formatter:off MethodOutcome response = ourClient
IBaseOperationOutcome response = ourClient
.delete() .delete()
.resourceConditionalByType(Patient.class) .resourceConditionalByType(Patient.class)
.where(Patient.IDENTIFIER.exactly().code(methodName)) .where(Patient.IDENTIFIER.exactly().code(methodName))
.execute(); .execute();
//@formatter:on
String encoded = myFhirCtx.newXmlParser().encodeResourceToString(response); String encoded = myFhirCtx.newXmlParser().encodeResourceToString(response.getOperationOutcome());
ourLog.info(encoded); ourLog.info(encoded);
assertThat(encoded, containsString( assertThat(encoded, containsString(
"<issue><severity value=\"information\"/><code value=\"informational\"/><diagnostics value=\"Successfully deleted 2 resource(s) in ")); "<issue><severity value=\"information\"/><code value=\"informational\"/><diagnostics value=\"Successfully deleted 2 resource(s) in "));
@ -1478,8 +1476,8 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
p.addName().setFamily("FAM"); p.addName().setFamily("FAM");
IIdType id = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless(); IIdType id = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless();
IBaseOperationOutcome resp = ourClient.delete().resourceById(id).execute(); MethodOutcome resp = ourClient.delete().resourceById(id).execute();
OperationOutcome oo = (OperationOutcome) resp; OperationOutcome oo = (OperationOutcome) resp.getOperationOutcome();
assertThat(oo.getIssueFirstRep().getDiagnostics(), startsWith("Successfully deleted 1 resource(s) in ")); assertThat(oo.getIssueFirstRep().getDiagnostics(), startsWith("Successfully deleted 1 resource(s) in "));
} }

View File

@ -31,6 +31,7 @@ import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.PreferHeader; import ca.uhn.fhir.rest.api.PreferHeader;
import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.PreferReturnEnum;
@ -78,6 +79,7 @@ public class RestfulServerUtils {
private static final HashSet<String> TEXT_ENCODE_ELEMENTS = new HashSet<>(Arrays.asList("*.text", "*.id", "*.meta", "*.(mandatory)")); private static final HashSet<String> TEXT_ENCODE_ELEMENTS = new HashSet<>(Arrays.asList("*.text", "*.id", "*.meta", "*.(mandatory)"));
private static Map<FhirVersionEnum, FhirContext> myFhirContextMap = Collections.synchronizedMap(new HashMap<>()); private static Map<FhirVersionEnum, FhirContext> myFhirContextMap = Collections.synchronizedMap(new HashMap<>());
private static EnumSet<RestOperationTypeEnum> ourOperationsWhichAllowPreferHeader = EnumSet.of(RestOperationTypeEnum.CREATE, RestOperationTypeEnum.UPDATE, RestOperationTypeEnum.PATCH);
private enum NarrativeModeEnum { private enum NarrativeModeEnum {
NORMAL, ONLY, SUPPRESS; NORMAL, ONLY, SUPPRESS;
@ -696,59 +698,55 @@ public class RestfulServerUtils {
return retVal; return retVal;
} }
private static EnumSet<RestOperationTypeEnum> ourOperationsWhichAllowPreferHeader = EnumSet.of(RestOperationTypeEnum.CREATE, RestOperationTypeEnum.UPDATE, RestOperationTypeEnum.PATCH);
public static boolean respectPreferHeader(RestOperationTypeEnum theRestOperationType) { public static boolean respectPreferHeader(RestOperationTypeEnum theRestOperationType) {
return ourOperationsWhichAllowPreferHeader.contains(theRestOperationType); return ourOperationsWhichAllowPreferHeader.contains(theRestOperationType);
} }
@Nonnull @Nonnull
public static PreferHeader parsePreferHeader(IRestfulServer<?> theServer, String theValue) { public static PreferHeader parsePreferHeader(IRestfulServer<?> theServer, String theValue) {
PreferHeader retVal = new PreferHeader(); PreferHeader retVal = new PreferHeader();
if (isNotBlank(theValue)) { if (isNotBlank(theValue)) {
StringTokenizer tok = new StringTokenizer(theValue, ";"); StringTokenizer tok = new StringTokenizer(theValue, ";");
while (tok.hasMoreTokens()) { while (tok.hasMoreTokens()) {
String next = trim(tok.nextToken()); String next = trim(tok.nextToken());
int eqIndex = next.indexOf('='); int eqIndex = next.indexOf('=');
String key; String key;
String value; String value;
if (eqIndex == -1 || eqIndex >= next.length() - 2) { if (eqIndex == -1 || eqIndex >= next.length() - 2) {
key = next; key = next;
value = ""; value = "";
} else { } else {
key = next.substring(0, eqIndex).trim(); key = next.substring(0, eqIndex).trim();
value = next.substring(eqIndex + 1).trim(); value = next.substring(eqIndex + 1).trim();
} }
if (key.equals(Constants.HEADER_PREFER_RETURN)) { if (key.equals(Constants.HEADER_PREFER_RETURN)) {
if (value.length() < 2) { if (value.length() < 2) {
continue; continue;
} }
if ('"' == value.charAt(0) && '"' == value.charAt(value.length() - 1)) { if ('"' == value.charAt(0) && '"' == value.charAt(value.length() - 1)) {
value = value.substring(1, value.length() - 1); value = value.substring(1, value.length() - 1);
} }
retVal.setReturn(PreferReturnEnum.fromHeaderValue(value)); retVal.setReturn(PreferReturnEnum.fromHeaderValue(value));
} else if (key.equals(Constants.HEADER_PREFER_RESPOND_ASYNC)) { } else if (key.equals(Constants.HEADER_PREFER_RESPOND_ASYNC)) {
retVal.setRespondAsync(true); retVal.setRespondAsync(true);
} }
} }
} }
if (retVal.getReturn() == null && theServer != null && theServer.getDefaultPreferReturn() != null) { if (retVal.getReturn() == null && theServer != null && theServer.getDefaultPreferReturn() != null) {
retVal.setReturn(theServer.getDefaultPreferReturn()); retVal.setReturn(theServer.getDefaultPreferReturn());
} }
return retVal; return retVal;
} }
public static boolean prettyPrintResponse(IRestfulServerDefaults theServer, RequestDetails theRequest) { public static boolean prettyPrintResponse(IRestfulServerDefaults theServer, RequestDetails theRequest) {
@ -772,12 +770,12 @@ public class RestfulServerUtils {
} }
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int stausCode, boolean theAddContentLocationHeader, public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int stausCode, boolean theAddContentLocationHeader,
boolean respondGzip, RequestDetails theRequestDetails) throws IOException { boolean respondGzip, RequestDetails theRequestDetails) throws IOException {
return streamResponseAsResource(theServer, theResource, theSummaryMode, stausCode, null, theAddContentLocationHeader, respondGzip, theRequestDetails, null, null); return streamResponseAsResource(theServer, theResource, theSummaryMode, stausCode, null, theAddContentLocationHeader, respondGzip, theRequestDetails, null, null);
} }
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int theStatusCode, String theStatusMessage, public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int theStatusCode, String theStatusMessage,
boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated) boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated)
throws IOException { throws IOException {
IRestfulResponse response = theRequestDetails.getResponse(); IRestfulResponse response = theRequestDetails.getResponse();
@ -954,5 +952,22 @@ public class RestfulServerUtils {
} }
/**
* @since 5.0.0
*/
public static DeleteCascadeModeEnum extractDeleteCascadeParameter(RequestDetails theRequest) {
if (theRequest != null) {
String[] cascadeParameters = theRequest.getParameters().get(Constants.PARAMETER_CASCADE_DELETE);
if (cascadeParameters != null && Arrays.asList(cascadeParameters).contains(Constants.CASCADE_DELETE)) {
return DeleteCascadeModeEnum.DELETE;
}
String cascadeHeader = theRequest.getHeader(Constants.HEADER_CASCADE);
if (Constants.CASCADE_DELETE.equals(cascadeHeader)) {
return DeleteCascadeModeEnum.DELETE;
}
}
return DeleteCascadeModeEnum.NONE;
}
} }

View File

@ -444,7 +444,7 @@ public class GenericClientR4Test {
}); });
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
IBaseOperationOutcome outcome; MethodOutcome outcome;
// Regular delete // Regular delete
outcome = client outcome = client
@ -1707,7 +1707,6 @@ public class GenericClientR4Test {
idx++; idx++;
} }
@SuppressWarnings("deprecation")
@Test @Test
public void testSearchByQuantity() throws Exception { public void testSearchByQuantity() throws Exception {
final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}";

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.*; import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException;
import ca.uhn.fhir.rest.client.impl.BaseClient; import ca.uhn.fhir.rest.client.impl.BaseClient;
import ca.uhn.fhir.rest.client.impl.GenericClient; import ca.uhn.fhir.rest.client.impl.GenericClient;
@ -386,24 +387,82 @@ public class GenericClientTest {
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
OperationOutcome outcome = (OperationOutcome) client.delete().resourceById("Patient", "123") MethodOutcome outcome = client
.withAdditionalHeader("myHeaderName", "myHeaderValue").execute(); .delete()
.resourceById("Patient", "123")
.withAdditionalHeader("myHeaderName", "myHeaderValue")
.execute();
oo = (OperationOutcome) outcome.getOperationOutcome();
assertEquals("http://example.com/fhir/Patient/123", capt.getValue().getURI().toString()); assertEquals("http://example.com/fhir/Patient/123", capt.getValue().getURI().toString());
assertEquals("DELETE", capt.getValue().getMethod()); assertEquals("DELETE", capt.getValue().getMethod());
Assert.assertEquals("testDelete01", outcome.getIssueFirstRep().getLocation().get(0).getValue()); Assert.assertEquals("testDelete01", oo.getIssueFirstRep().getLocation().get(0).getValue());
assertEquals("myHeaderValue", capt.getValue().getFirstHeader("myHeaderName").getValue()); assertEquals("myHeaderValue", capt.getValue().getFirstHeader("myHeaderName").getValue());
}
@Test
public void testDeleteInvalidResponse() throws Exception {
OperationOutcome oo = new OperationOutcome();
oo.addIssue().addLocation("testDelete01");
String ooStr = ourCtx.newXmlParser().encodeResourceToString(oo);
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK"));
when(myHttpResponse.getAllHeaders()).thenReturn(new Header[]{new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("LKJHLKJGLKJKLL"), StandardCharsets.UTF_8)); when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("LKJHLKJGLKJKLL"), StandardCharsets.UTF_8));
outcome = (OperationOutcome) client.delete().resourceById(new IdType("Location", "123", "456")).prettyPrint().encodedJson().execute();
assertEquals("http://example.com/fhir/Location/123?_pretty=true", capt.getAllValues().get(1).getURI().toString()); IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
assertEquals("DELETE", capt.getValue().getMethod());
Assert.assertEquals(null, outcome); // Try with invalid response
try {
client
.delete()
.resourceById(new IdType("Location", "123", "456"))
.prettyPrint()
.encodedJson()
.execute();
} catch (FhirClientConnectionException e) {
assertEquals(0, e.getStatusCode());
assertThat(e.getMessage(), containsString("Failed to parse response from server when performing DELETE to URL"));
}
} }
@Test
public void testDeleteNoResponse() throws Exception {
OperationOutcome oo = new OperationOutcome();
oo.addIssue().addLocation("testDelete01");
String ooStr = ourCtx.newXmlParser().encodeResourceToString(oo);
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK"));
when(myHttpResponse.getAllHeaders()).thenReturn(new Header[]{new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(ooStr), StandardCharsets.UTF_8));
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
MethodOutcome outcome = client
.delete()
.resourceById("Patient", "123")
.withAdditionalHeader("myHeaderName", "myHeaderValue")
.execute();
oo = (OperationOutcome) outcome.getOperationOutcome();
assertEquals("http://example.com/fhir/Patient/123", capt.getValue().getURI().toString());
assertEquals("DELETE", capt.getValue().getMethod());
Assert.assertEquals("testDelete01", oo.getIssueFirstRep().getLocation().get(0).getValue());
assertEquals("myHeaderValue", capt.getValue().getFirstHeader("myHeaderName").getValue());
}
@Test @Test
public void testHistory() throws Exception { public void testHistory() throws Exception {
@ -413,12 +472,8 @@ public class GenericClientTest {
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { when(myHttpResponse.getEntity().getContent()).thenAnswer(t ->
@Override new ReaderInputStream(new StringReader(msg), StandardCharsets.UTF_8));
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(msg), StandardCharsets.UTF_8);
}
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
@ -428,7 +483,7 @@ public class GenericClientTest {
response = client response = client
.history() .history()
.onServer() .onServer()
.andReturnBundle(Bundle.class) .returnBundle(Bundle.class)
.withAdditionalHeader("myHeaderName", "myHeaderValue") .withAdditionalHeader("myHeaderName", "myHeaderValue")
.execute(); .execute();
assertEquals("http://example.com/fhir/_history", capt.getAllValues().get(idx).getURI().toString()); assertEquals("http://example.com/fhir/_history", capt.getAllValues().get(idx).getURI().toString());
@ -439,7 +494,7 @@ public class GenericClientTest {
response = client response = client
.history() .history()
.onType(Patient.class) .onType(Patient.class)
.andReturnBundle(Bundle.class) .returnBundle(Bundle.class)
.withAdditionalHeader("myHeaderName", "myHeaderValue1") .withAdditionalHeader("myHeaderName", "myHeaderValue1")
.withAdditionalHeader("myHeaderName", "myHeaderValue2") .withAdditionalHeader("myHeaderName", "myHeaderValue2")
.execute(); .execute();