From 874b16207fcbd7cb3f180f0d91fe4693225493fd Mon Sep 17 00:00:00 2001 From: James Agnew Date: Tue, 19 Jan 2021 11:12:42 -0500 Subject: [PATCH] Bundle.entry.resource.id should not be stripped on POST from client #2297 (#2303) --- .../java/ca/uhn/fhir/parser/BaseParser.java | 2 +- .../main/java/ca/uhn/fhir/parser/IParser.java | 296 ++++++++---------- .../java/ca/uhn/fhir/util/BundleBuilder.java | 18 ++ .../5_3_0/2297-dont-strip-ids-on-create.yaml | 6 + .../ca/uhn/fhir/rest/client/ClientR4Test.java | 48 +++ 5 files changed, 209 insertions(+), 161 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2297-dont-strip-ids-on-create.yaml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java index e2b3d0e282b..3048b98b4b9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java @@ -991,7 +991,7 @@ public abstract class BaseParser implements IParser { protected boolean shouldEncodeResourceId(IBaseResource theResource, EncodeContext theEncodeContext) { boolean retVal = true; - if (isOmitResourceId()) { + if (isOmitResourceId() && theEncodeContext.getPath().size() == 1) { retVal = false; } else { if (myDontEncodeElements != null) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java index 6745516425e..341321723e8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java @@ -57,6 +57,11 @@ public interface IParser { */ IIdType getEncodeForceResourceId(); + /** + * When encoding, force this resource ID to be encoded as the resource ID + */ + IParser setEncodeForceResourceId(IIdType theForceResourceId); + /** * Which encoding does this parser instance produce? */ @@ -64,117 +69,163 @@ public interface IParser { /** * Gets the preferred types, as set using {@link #setPreferTypes(List)} - * + * * @return Returns the preferred types, or null * @see #setPreferTypes(List) */ List> getPreferTypes(); + /** + * If set, when parsing resources the parser will try to use the given types when possible, in + * the order that they are provided (from highest to lowest priority). For example, if a custom + * type which declares to implement the Patient resource is passed in here, and the + * parser is parsing a Bundle containing a Patient resource, the parser will use the given + * custom type. + *

+ * This feature is related to, but not the same as the + * {@link FhirContext#setDefaultTypeForProfile(String, Class)} feature. + * setDefaultTypeForProfile is used to specify a type to be used + * when a resource explicitly declares support for a given profile. This + * feature specifies a type to be used irrespective of the profile declaration + * in the metadata statement. + *

+ * + * @param thePreferTypes The preferred types, or null + */ + void setPreferTypes(List> thePreferTypes); + /** * Returns true if resource IDs should be omitted - * + * * @see #setOmitResourceId(boolean) * @since 1.1 */ boolean isOmitResourceId(); + /** + * If set to true (default is false) the ID of any resources being encoded will not be + * included in the output. Note that this does not apply to contained resources, only to root resources. In other + * words, if this is set to true, contained resources will still have local IDs but the outer/containing + * ID will not have an ID. + *

+ * If the resource being encoded is a Bundle or Parameters resource, this setting only applies to the + * outer resource being encoded, not any resources contained wihthin. + *

+ * + * @param theOmitResourceId Should resource IDs be omitted + * @return Returns a reference to this parser so that method calls can be chained together + * @since 1.1 + */ + IParser setOmitResourceId(boolean theOmitResourceId); + /** * If set to true (which is the default), resource references containing a version * will have the version removed when the resource is encoded. This is generally good behaviour because * in most situations, references from one resource to another should be to the resource by ID, not * by ID and version. In some cases though, it may be desirable to preserve the version in resource * links. In that case, this value should be set to false. - * + * * @return Returns the parser instance's configuration setting for stripping versions from resource references when - * encoding. This method will retun null if no value is set, in which case - * the value from the {@link ParserOptions} will be used (default is true) + * encoding. This method will retun null if no value is set, in which case + * the value from the {@link ParserOptions} will be used (default is true) * @see ParserOptions */ Boolean getStripVersionsFromReferences(); + /** + * If set to true (which is the default), resource references containing a version + * will have the version removed when the resource is encoded. This is generally good behaviour because + * in most situations, references from one resource to another should be to the resource by ID, not + * by ID and version. In some cases though, it may be desirable to preserve the version in resource + * links. In that case, this value should be set to false. + *

+ * This method provides the ability to globally disable reference encoding. If finer-grained + * control is needed, use {@link #setDontStripVersionsFromReferencesAtPaths(String...)} + *

+ * + * @param theStripVersionsFromReferences Set this to false to prevent the parser from removing resource versions from references (or null to apply the default setting from the {@link ParserOptions} + * @return Returns a reference to this parser so that method calls can be chained together + * @see #setDontStripVersionsFromReferencesAtPaths(String...) + * @see ParserOptions + */ + IParser setStripVersionsFromReferences(Boolean theStripVersionsFromReferences); + /** * Is the parser in "summary mode"? See {@link #setSummaryMode(boolean)} for information - * + * * @see {@link #setSummaryMode(boolean)} for information */ boolean isSummaryMode(); + /** + * If set to true (default is false) only elements marked by the FHIR specification as + * being "summary elements" will be included. + * + * @return Returns a reference to this parser so that method calls can be chained together + */ + IParser setSummaryMode(boolean theSummaryMode); + /** * Parses a resource - * - * @param theResourceType - * The resource type to use. This can be used to explicitly specify a class which extends a built-in type - * (e.g. a custom type extending the default Patient class) - * @param theReader - * The reader to parse input from. Note that the Reader will not be closed by the parser upon completion. + * + * @param theResourceType The resource type to use. This can be used to explicitly specify a class which extends a built-in type + * (e.g. a custom type extending the default Patient class) + * @param theReader The reader to parse input from. Note that the Reader will not be closed by the parser upon completion. * @return A parsed resource - * @throws DataFormatException - * If the resource can not be parsed because the data is not recognized or invalid for any reason + * @throws DataFormatException If the resource can not be parsed because the data is not recognized or invalid for any reason */ T parseResource(Class theResourceType, Reader theReader) throws DataFormatException; /** * Parses a resource * - * @param theResourceType - * The resource type to use. This can be used to explicitly specify a class which extends a built-in type - * (e.g. a custom type extending the default Patient class) - * @param theInputStream - * The InputStream to parse input from, with an implied charset of UTF-8. Note that the InputStream will not be closed by the parser upon completion. + * @param theResourceType The resource type to use. This can be used to explicitly specify a class which extends a built-in type + * (e.g. a custom type extending the default Patient class) + * @param theInputStream The InputStream to parse input from, with an implied charset of UTF-8. Note that the InputStream will not be closed by the parser upon completion. * @return A parsed resource - * @throws DataFormatException - * If the resource can not be parsed because the data is not recognized or invalid for any reason + * @throws DataFormatException If the resource can not be parsed because the data is not recognized or invalid for any reason */ T parseResource(Class theResourceType, InputStream theInputStream) throws DataFormatException; /** * Parses a resource - * - * @param theResourceType - * The resource type to use. This can be used to explicitly specify a class which extends a built-in type - * (e.g. a custom type extending the default Patient class) - * @param theString - * The string to parse + * + * @param theResourceType The resource type to use. This can be used to explicitly specify a class which extends a built-in type + * (e.g. a custom type extending the default Patient class) + * @param theString The string to parse * @return A parsed resource - * @throws DataFormatException - * If the resource can not be parsed because the data is not recognized or invalid for any reason + * @throws DataFormatException If the resource can not be parsed because the data is not recognized or invalid for any reason */ T parseResource(Class theResourceType, String theString) throws DataFormatException; /** * Parses a resource - * - * @param theReader - * The reader to parse input from. Note that the Reader will not be closed by the parser upon completion. + * + * @param theReader The reader to parse input from. Note that the Reader will not be closed by the parser upon completion. * @return A parsed resource. Note that the returned object will be an instance of {@link IResource} or - * {@link IAnyResource} depending on the specific FhirContext which created this parser. - * @throws DataFormatException - * If the resource can not be parsed because the data is not recognized or invalid for any reason + * {@link IAnyResource} depending on the specific FhirContext which created this parser. + * @throws DataFormatException If the resource can not be parsed because the data is not recognized or invalid for any reason */ IBaseResource parseResource(Reader theReader) throws ConfigurationException, DataFormatException; /** * Parses a resource * - * @param theInputStream - * The InputStream to parse input from (charset is assumed to be UTF-8). - * Note that the stream will not be closed by the parser upon completion. + * @param theInputStream The InputStream to parse input from (charset is assumed to be UTF-8). + * Note that the stream will not be closed by the parser upon completion. * @return A parsed resource. Note that the returned object will be an instance of {@link IResource} or - * {@link IAnyResource} depending on the specific FhirContext which created this parser. - * @throws DataFormatException - * If the resource can not be parsed because the data is not recognized or invalid for any reason + * {@link IAnyResource} depending on the specific FhirContext which created this parser. + * @throws DataFormatException If the resource can not be parsed because the data is not recognized or invalid for any reason */ IBaseResource parseResource(InputStream theInputStream) throws ConfigurationException, DataFormatException; /** * Parses a resource - * - * @param theMessageString - * The string to parse + * + * @param theMessageString The string to parse * @return A parsed resource. Note that the returned object will be an instance of {@link IResource} or - * {@link IAnyResource} depending on the specific FhirContext which created this parser. - * @throws DataFormatException - * If the resource can not be parsed because the data is not recognized or invalid for any reason + * {@link IAnyResource} depending on the specific FhirContext which created this parser. + * @throws DataFormatException If the resource can not be parsed because the data is not recognized or invalid for any reason */ IBaseResource parseResource(String theMessageString) throws ConfigurationException, DataFormatException; @@ -194,9 +245,8 @@ public interface IParser { * as Patient.meta.lastUpdated will only work in * DSTU3+ mode. *

- * - * @param theDontEncodeElements - * The elements to encode + * + * @param theDontEncodeElements The elements to encode * @see #setEncodeElements(Set) */ IParser setDontEncodeElements(Set theDontEncodeElements); @@ -212,9 +262,8 @@ public interface IParser { * wildcard) *
  • *.(mandatory) - This is a special case which causes any mandatory fields (min > 0) to be encoded
  • * - * - * @param theEncodeElements - * The elements to encode + * + * @param theEncodeElements The elements to encode * @see #setDontEncodeElements(Set) */ IParser setEncodeElements(Set theEncodeElements); @@ -225,7 +274,7 @@ public interface IParser { * resource (typically a Bundle), but will be applied to any sub-resources * contained within it (i.e. search result resources in that bundle) */ - void setEncodeElementsAppliesToChildResourcesOnly(boolean theEncodeElementsAppliesToChildResourcesOnly); + boolean isEncodeElementsAppliesToChildResourcesOnly(); /** * If set to true (default is false), the values supplied @@ -233,60 +282,20 @@ public interface IParser { * resource (typically a Bundle), but will be applied to any sub-resources * contained within it (i.e. search result resources in that bundle) */ - boolean isEncodeElementsAppliesToChildResourcesOnly(); - - /** - * When encoding, force this resource ID to be encoded as the resource ID - */ - IParser setEncodeForceResourceId(IIdType theForceResourceId); - - /** - * If set to true (default is false) the ID of any resources being encoded will not be - * included in the output. Note that this does not apply to contained resources, only to root resources. In other - * words, if this is set to true, contained resources will still have local IDs but the outer/containing - * ID will not have an ID. - * - * @param theOmitResourceId - * Should resource IDs be omitted - * @return Returns a reference to this parser so that method calls can be chained together - * @since 1.1 - */ - IParser setOmitResourceId(boolean theOmitResourceId); + void setEncodeElementsAppliesToChildResourcesOnly(boolean theEncodeElementsAppliesToChildResourcesOnly); /** * Registers an error handler which will be invoked when any parse errors are found - * - * @param theErrorHandler - * The error handler to set. Must not be null. + * + * @param theErrorHandler The error handler to set. Must not be null. */ IParser setParserErrorHandler(IParserErrorHandler theErrorHandler); - /** - * If set, when parsing resources the parser will try to use the given types when possible, in - * the order that they are provided (from highest to lowest priority). For example, if a custom - * type which declares to implement the Patient resource is passed in here, and the - * parser is parsing a Bundle containing a Patient resource, the parser will use the given - * custom type. - *

    - * This feature is related to, but not the same as the - * {@link FhirContext#setDefaultTypeForProfile(String, Class)} feature. - * setDefaultTypeForProfile is used to specify a type to be used - * when a resource explicitly declares support for a given profile. This - * feature specifies a type to be used irrespective of the profile declaration - * in the metadata statement. - *

    - * - * @param thePreferTypes - * The preferred types, or null - */ - void setPreferTypes(List> thePreferTypes); - /** * Sets the "pretty print" flag, meaning that the parser will encode resources with human-readable spacing and * newlines between elements instead of condensing output as much as possible. - * - * @param thePrettyPrint - * The flag + * + * @param thePrettyPrint The flag * @return Returns an instance of this parser so that method calls can be chained together */ IParser setPrettyPrint(boolean thePrettyPrint); @@ -294,62 +303,42 @@ public interface IParser { /** * Sets the server's base URL used by this parser. If a value is set, resource references will be turned into * relative references if they are provided as absolute URLs but have a base matching the given base. - * - * @param theUrl - * The base URL, e.g. "http://example.com/base" + * + * @param theUrl The base URL, e.g. "http://example.com/base" * @return Returns an instance of this parser so that method calls can be chained together */ IParser setServerBaseUrl(String theUrl); - /** - * If set to true (which is the default), resource references containing a version - * will have the version removed when the resource is encoded. This is generally good behaviour because - * in most situations, references from one resource to another should be to the resource by ID, not - * by ID and version. In some cases though, it may be desirable to preserve the version in resource - * links. In that case, this value should be set to false. - *

    - * This method provides the ability to globally disable reference encoding. If finer-grained - * control is needed, use {@link #setDontStripVersionsFromReferencesAtPaths(String...)} - *

    - * - * @param theStripVersionsFromReferences - * Set this to false to prevent the parser from removing resource versions from references (or null to apply the default setting from the {@link ParserOptions} - * @see #setDontStripVersionsFromReferencesAtPaths(String...) - * @see ParserOptions - * @return Returns a reference to this parser so that method calls can be chained together - */ - IParser setStripVersionsFromReferences(Boolean theStripVersionsFromReferences); - /** * If set to true (which is the default), the Bundle.entry.fullUrl will override the Bundle.entry.resource's * resource id if the fullUrl is defined. This behavior happens when parsing the source data into a Bundle object. Set this * to false if this is not the desired behavior (e.g. the client code wishes to perform additional * validation checks between the fullUrl and the resource id). * - * @param theOverrideResourceIdWithBundleEntryFullUrl - * Set this to false to prevent the parser from overriding resource ids with the - * Bundle.entry.fullUrl (or null to apply the default setting from the {@link ParserOptions}) - * - * @see ParserOptions - * + * @param theOverrideResourceIdWithBundleEntryFullUrl Set this to false to prevent the parser from overriding resource ids with the + * Bundle.entry.fullUrl (or null to apply the default setting from the {@link ParserOptions}) * @return Returns a reference to this parser so that method calls can be chained together + * @see ParserOptions */ IParser setOverrideResourceIdWithBundleEntryFullUrl(Boolean theOverrideResourceIdWithBundleEntryFullUrl); - /** - * If set to true (default is false) only elements marked by the FHIR specification as - * being "summary elements" will be included. - * - * @return Returns a reference to this parser so that method calls can be chained together - */ - IParser setSummaryMode(boolean theSummaryMode); - /** * If set to true (default is false), narratives will not be included in the encoded * values. */ IParser setSuppressNarratives(boolean theSuppressNarratives); + /** + * Returns the value supplied to {@link IParser#setDontStripVersionsFromReferencesAtPaths(String...)} + * or null if no value has been set for this parser (in which case the default from + * the {@link ParserOptions} will be used} + * + * @see #setDontStripVersionsFromReferencesAtPaths(String...) + * @see #setStripVersionsFromReferences(Boolean) + * @see ParserOptions + */ + Set getDontStripVersionsFromReferencesAtPaths(); + /** * If supplied value(s), any resource references at the specified paths will have their * resource versions encoded instead of being automatically stripped during the encoding @@ -360,15 +349,14 @@ public interface IParser { * has been set to true (which is the default) *

    * - * @param thePaths - * A collection of paths for which the resource versions will not be removed automatically - * when serializing, e.g. "Patient.managingOrganization" or "AuditEvent.object.reference". Note that - * only resource name and field names with dots separating is allowed here (no repetition - * indicators, FluentPath expressions, etc.). Set to null to use the value - * set in the {@link ParserOptions} + * @param thePaths A collection of paths for which the resource versions will not be removed automatically + * when serializing, e.g. "Patient.managingOrganization" or "AuditEvent.object.reference". Note that + * only resource name and field names with dots separating is allowed here (no repetition + * indicators, FluentPath expressions, etc.). Set to null to use the value + * set in the {@link ParserOptions} + * @return Returns a reference to this parser so that method calls can be chained together * @see #setStripVersionsFromReferences(Boolean) * @see ParserOptions - * @return Returns a reference to this parser so that method calls can be chained together */ IParser setDontStripVersionsFromReferencesAtPaths(String... thePaths); @@ -382,27 +370,15 @@ public interface IParser { * has been set to true (which is the default) *

    * - * @param thePaths - * A collection of paths for which the resource versions will not be removed automatically - * when serializing, e.g. "Patient.managingOrganization" or "AuditEvent.object.reference". Note that - * only resource name and field names with dots separating is allowed here (no repetition - * indicators, FluentPath expressions, etc.). Set to null to use the value - * set in the {@link ParserOptions} + * @param thePaths A collection of paths for which the resource versions will not be removed automatically + * when serializing, e.g. "Patient.managingOrganization" or "AuditEvent.object.reference". Note that + * only resource name and field names with dots separating is allowed here (no repetition + * indicators, FluentPath expressions, etc.). Set to null to use the value + * set in the {@link ParserOptions} + * @return Returns a reference to this parser so that method calls can be chained together * @see #setStripVersionsFromReferences(Boolean) * @see ParserOptions - * @return Returns a reference to this parser so that method calls can be chained together */ IParser setDontStripVersionsFromReferencesAtPaths(Collection thePaths); - /** - * Returns the value supplied to {@link IParser#setDontStripVersionsFromReferencesAtPaths(String...)} - * or null if no value has been set for this parser (in which case the default from - * the {@link ParserOptions} will be used} - * - * @see #setDontStripVersionsFromReferencesAtPaths(String...) - * @see #setStripVersionsFromReferences(Boolean) - * @see ParserOptions - */ - Set getDontStripVersionsFromReferencesAtPaths(); - } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java index e2c8658ebd6..3ce27fdc4e0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java @@ -193,6 +193,14 @@ public class BundleBuilder { return new CreateBuilder(request); } + /** + * Adds an entry for a Collection bundle type + */ + public void addCollectionEntry(IBaseResource theResource) { + setType("collection"); + addEntryAndReturnRequest(theResource); + } + /** * Creates new entry and adds it to the bundle * @@ -321,6 +329,16 @@ public class BundleBuilder { return retVal; } + /** + * Sets a value for Bundle.type. That this is a coded field so {@literal theType} + * must be an actual valid value for this field or a {@link ca.uhn.fhir.parser.DataFormatException} + * will be thrown. + */ + public void setType(String theType) { + setBundleField("type", theType); + } + + public static class UpdateBuilder { private final IPrimitiveType myUrl; diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2297-dont-strip-ids-on-create.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2297-dont-strip-ids-on-create.yaml new file mode 100644 index 00000000000..9a424f0da6c --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2297-dont-strip-ids-on-create.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 2297 +title: "When performing a FHIR create using the HAPI FHIR client, if the payload is a Bundle resource + the individual resources in the Bundle had their IDs removed by the client during payload serialization. + This has been corrected." diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientR4Test.java index 5a42f5a03c7..874b56d657c 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientR4Test.java @@ -30,6 +30,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; +import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.UrlUtil; import com.google.common.base.Charsets; @@ -48,6 +49,7 @@ import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicStatusLine; import org.hamcrest.core.StringContains; import org.hamcrest.core.StringEndsWith; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; @@ -73,6 +75,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -206,6 +209,51 @@ public class ClientR4Test { } } + /** + * See #2297 + */ + @Test + public void testCreateBundlePreservesIds() throws Exception { + + BundleBuilder bb = new BundleBuilder(ourCtx); + bb.setType("collection"); + + Patient patient = new Patient(); + patient.setId("Patient/123"); + patient.addIdentifier().setSystem("urn:foo").setValue("bar"); + bb.addCollectionEntry(patient); + + IBaseBundle inputBundle = bb.getBundle(); + inputBundle.setId("ABC"); + + ArgumentCaptor 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.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), StandardCharsets.UTF_8)); + when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://foo"); + client.setEncoding(EncodingEnum.JSON); + CapturingInterceptor interceptor = new CapturingInterceptor(); + client.registerInterceptor(interceptor); + + client.create().resource(inputBundle).execute(); + + assertEquals("http://foo/Bundle?_format=json", ((ApacheHttpRequest) interceptor.getLastRequest()).getApacheRequest().getURI().toASCIIString()); + + assertEquals(HttpPost.class, capt.getValue().getClass()); + HttpPost post = (HttpPost) capt.getValue(); + String requestBody = IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8); + ourLog.info("Request body: {}", requestBody); + assertThat(requestBody, StringContains.containsString("{\"resourceType\":\"Patient\"")); + Bundle requestBundle = ourCtx.newJsonParser().parseResource(Bundle.class, requestBody); + + assertEquals("123", requestBundle.getEntry().get(0).getResource().getIdElement().getIdPart()); + assertThat(requestBody, containsString("\"id\":\"123\"")); + assertThat(requestBody, not(containsString("\"id\":\"ABC\""))); + } + /** * Some servers (older ones?) return the resourcde you created instead of an OperationOutcome. We just need to ignore * it.