From 874b16207fcbd7cb3f180f0d91fe4693225493fd Mon Sep 17 00:00:00 2001
From: James Agnew
+ * This feature is related to, but not the same as the
+ * {@link FhirContext#setDefaultTypeForProfile(String, Class)} feature.
+ *
+ * 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.
+ *
+ * This method provides the ability to globally disable reference encoding. If finer-grained
+ * control is needed, use {@link #setDontStripVersionsFromReferencesAtPaths(String...)}
+ * null
* @see #setPreferTypes(List)
*/
ListsetDefaultTypeForProfile
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.
+ * null
+ */
+ void setPreferTypes(Listtrue
(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.
+ * 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
.
+ * 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
*/
Patient.meta.lastUpdated
will only work in
* DSTU3+ mode.
*
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.
- *
null
- */
- void setPreferTypes(Listthis
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.