headers);
+
+ /**
+ * Patches a Resource in the repository
+ *
+ * @see FHIR patch
+ *
+ * @param an Id type
+ * @param a Parameters type
+ * @param id the id of the Resource to patch
+ * @param patchParameters parameters describing the patches to apply
+ * @return a MethodOutcome with the id of the patched resource
+ */
+ default MethodOutcome patch(I id, P patchParameters) {
+ return this.patch(id, patchParameters, Collections.emptyMap());
+ }
+
+ /**
+ * Patches a Resource in the repository
+ *
+ * @see FHIR patch
+ *
+ * @param an Id type
+ * @param
a Parameters type
+ * @param id the id of the Resource to patch
+ * @param patchParameters parameters describing the patches to apply
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a MethodOutcome with the id of the patched resource
+ */
+ default MethodOutcome patch(
+ I id, P patchParameters, Map headers) {
+ return throwNotImplementedOperationException("patch is not supported by this repository");
+ }
+
+ /**
+ * Updates a Resource in the repository
+ *
+ * @see FHIR update
+ *
+ * @param a Resource type
+ * @param resource the Resource to update
+ * @return a MethodOutcome with the id of the updated Resource
+ */
+ default MethodOutcome update(T resource) {
+ return this.update(resource, Collections.emptyMap());
+ }
+
+ /**
+ * Updates a Resource in the repository
+ *
+ * @see FHIR update
+ *
+ * @param a Resource type
+ * @param resource the Resource to update
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a MethodOutcome with the id of the updated Resource
+ */
+ MethodOutcome update(T resource, Map headers);
+
+ /**
+ * Deletes a Resource in the repository
+ *
+ * @see FHIR delete
+ *
+ * @param a Resource type
+ * @param an Id type
+ * @param resourceType the class of the Resource type to delete
+ * @param id the id of the Resource to delete
+ * @return a MethodOutcome with the id of the deleted resource
+ */
+ default MethodOutcome delete(Class resourceType, I id) {
+ return this.delete(resourceType, id, Collections.emptyMap());
+ }
+
+ /**
+ * Deletes a Resource in the repository
+ *
+ * @see FHIR delete
+ *
+ * @param a Resource type
+ * @param an Id type
+ * @param resourceType the class of the Resource type to delete
+ * @param id the id of the Resource to delete
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a MethodOutcome with the id of the deleted resource
+ */
+ MethodOutcome delete(
+ Class resourceType, I id, Map headers);
+
+ // Querying starts here
+
+ /**
+ * Searches this repository
+ *
+ * @see FHIR search
+ *
+ * @param a Bundle type
+ * @param a Resource type
+ * @param bundleType the class of the Bundle type to return
+ * @param resourceType the class of the Resource type to search
+ * @param searchParameters the searchParameters for this search
+ * @return a Bundle with the results of the search
+ */
+ default B search(
+ Class bundleType, Class resourceType, Map> searchParameters) {
+ return this.search(bundleType, resourceType, searchParameters, Collections.emptyMap());
+ }
+
+ /**
+ * Searches this repository
+ *
+ * @see FHIR search
+ *
+ * @param a Bundle type
+ * @param a Resource type
+ * @param bundleType the class of the Bundle type to return
+ * @param resourceType the class of the Resource type to search
+ * @param searchParameters the searchParameters for this search
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a Bundle with the results of the search
+ */
+ B search(
+ Class bundleType,
+ Class resourceType,
+ Map> searchParameters,
+ Map headers);
+
+ // Paging starts here
+
+ /**
+ * Reads a Bundle from a link on this repository
+ *
+ * This is typically used for paging during searches
+ *
+ * @see FHIR Bundle
+ * link
+ *
+ * @param a Bundle type
+ * @param url the url of the Bundle to load
+ * @return a Bundle
+ */
+ default B link(Class bundleType, String url) {
+ return this.link(bundleType, url, Collections.emptyMap());
+ }
+
+ /**
+ * Reads a Bundle from a link on this repository
+ *
+ * This is typically used for paging during searches
+ *
+ * @see FHIR Bundle
+ * link
+ *
+ * @param a Bundle type
+ * @param url the url of the Bundle to load
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a Bundle
+ */
+ default B link(Class bundleType, String url, Map headers) {
+ return throwNotImplementedOperationException("link is not supported by this repository");
+ }
+
+ // Metadata starts here
+
+ /**
+ * Returns the CapabilityStatement/Conformance metadata for this repository
+ *
+ * @see FHIR capabilities
+ *
+ * @param a CapabilityStatement/Conformance type
+ * @param resourceType the class of the CapabilityStatement/Conformance to return
+ * @return a CapabilityStatement/Conformance with the repository's metadata
+ */
+ default C capabilities(Class resourceType) {
+ return this.capabilities(resourceType, Collections.emptyMap());
+ }
+
+ /**
+ * Returns the CapabilityStatement/Conformance metadata for this repository
+ *
+ * @see FHIR capabilities
+ *
+ * @param a CapabilityStatement/Conformance type
+ * @param resourceType the class of the CapabilityStatement/Conformance to return
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a CapabilityStatement/Conformance with the repository's metadata
+ */
+ default C capabilities(Class resourceType, Map headers) {
+ return throwNotImplementedOperationException("capabilities is not supported by this repository");
+ }
+
+ // Transactions starts here
+
+ /**
+ * Performs a transaction or batch on this repository
+ *
+ * @see FHIR transaction
+ *
+ * @param a Bundle type
+ * @param transaction a Bundle with the transaction/batch
+ * @return a Bundle with the results of the transaction/batch
+ */
+ default B transaction(B transaction) {
+ return this.transaction(transaction, Collections.emptyMap());
+ }
+
+ /**
+ * Performs a transaction or batch on this repository
+ *
+ * @see FHIR transaction
+ *
+ * @param a Bundle type
+ * @param transaction a Bundle with the transaction/batch
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a Bundle with the results of the transaction/batch
+ */
+ default B transaction(B transaction, Map headers) {
+ return throwNotImplementedOperationException("transaction is not supported by this repository");
+ }
+
+ // Operations starts here
+
+ /**
+ * Invokes a server-level operation on this repository that returns a Resource
+ *
+ * @see FHIR operations
+ *
+ * @param a Resource type to return
+ * @param a Parameters type for operation parameters
+ * @param name the name of the operation to invoke
+ * @param parameters the operation parameters
+ * @param returnType the class of the Resource the operation returns
+ * @return the results of the operation
+ */
+ default R invoke(
+ String name, P parameters, Class returnType) {
+ return this.invoke(name, parameters, returnType, Collections.emptyMap());
+ }
+
+ /**
+ * Invokes a server-level operation on this repository that returns a Resource
+ *
+ * @see FHIR operations
+ *
+ * @param a Resource type to return
+ * @param a Parameters type for operation parameters
+ * @param name the name of the operation to invoke
+ * @param parameters the operation parameters
+ * @param returnType the class of the Resource the operation returns
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return the results of the operation
+ */
+ default R invoke(
+ String name, P parameters, Class returnType, Map headers) {
+ return throwNotImplementedOperationException("server-level invoke is not supported by this repository");
+ }
+
+ /**
+ * Invokes a server-level operation on this repository
+ *
+ * @see FHIR operations
+ *
+ * @param a Parameters type for operation parameters
+ * @param name the name of the operation to invoke
+ * @param parameters the operation parameters
+ * @return a MethodOutcome with a status code
+ */
+ default
MethodOutcome invoke(String name, P parameters) {
+ return this.invoke(name, parameters, Collections.emptyMap());
+ }
+
+ /**
+ * Invokes a server-level operation on this repository
+ *
+ * @see FHIR operations
+ *
+ * @param
a Parameters type for operation parameters
+ * @param name the name of the operation to invoke
+ * @param parameters the operation parameters
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a MethodOutcome with a status code
+ */
+ default
MethodOutcome invoke(String name, P parameters, Map headers) {
+ return throwNotImplementedOperationException("server-level invoke is not supported by this repository");
+ }
+
+ /**
+ * Invokes a type-level operation on this repository that returns a Resource
+ *
+ * @see FHIR operations
+ *
+ * @param a Resource type to return
+ * @param a Parameters type for operation parameters
+ * @param a Resource type to do the invocation for
+ * @param resourceType the class of the Resource to do the invocation for
+ * @param name the name of the operation to invoke
+ * @param parameters the operation parameters
+ * @param returnType the class of the Resource the operation returns
+ * @return the results of the operation
+ */
+ default R invoke(
+ Class resourceType, String name, P parameters, Class returnType) {
+ return this.invoke(resourceType, name, parameters, returnType, Collections.emptyMap());
+ }
+
+ /**
+ * Invokes a type-level operation on this repository that returns a Resource
+ *
+ * @see FHIR operations
+ *
+ * @param a Resource type to return
+ * @param a Parameters type for operation parameters
+ * @param a Resource type to do the invocation for
+ * @param resourceType the class of the Resource to do the invocation for
+ * @param name the name of the operation to invoke
+ * @param parameters the operation parameters
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @param returnType the class of the Resource the operation returns
+ * @return the results of the operation
+ */
+ R invoke(
+ Class resourceType, String name, P parameters, Class returnType, Map headers);
+
+ /**
+ * Invokes a type-level operation on this repository
+ *
+ * @see FHIR operations
+ *
+ * @param a Parameters type for operation parameters
+ * @param a Resource type to do the invocation for
+ * @param resourceType the class of the Resource to do the invocation for
+ * @param name the name of the operation to invoke
+ * @param parameters the operation parameters
+ * @return a MethodOutcome with a status code
+ */
+ default MethodOutcome invoke(
+ Class resourceType, String name, P parameters) {
+ return this.invoke(resourceType, name, parameters, Collections.emptyMap());
+ }
+
+ /**
+ * Invokes a type-level operation on this repository
+ *
+ * @see FHIR operations
+ *
+ * @param a Parameters type for operation parameters
+ * @param a Resource type to do the invocation for
+ * @param resourceType the class of the Resource to do the invocation for
+ * @param name the name of the operation to invoke
+ * @param parameters the operation parameters
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a MethodOutcome with a status code
+ */
+ default MethodOutcome invoke(
+ Class resourceType, String name, P parameters, Map headers) {
+ return throwNotImplementedOperationException("type-level invoke is not supported by this repository");
+ }
+
+ /**
+ * Invokes an instance-level operation on this repository that returns a Resource
+ *
+ * @see FHIR operations
+ *
+ * @param a Resource type to return
+ * @param a Parameters type for operation parameters
+ * @param an Id type
+ * @param id the id of the Resource to do the invocation on
+ * @param name the name of the operation to invoke
+ * @param parameters the operation parameters
+ * @param returnType the class of the Resource the operation returns
+ * @return the results of the operation
+ */
+ default R invoke(
+ I id, String name, P parameters, Class returnType) {
+ return this.invoke(id, name, parameters, returnType, Collections.emptyMap());
+ }
+
+ /**
+ * Invokes an instance-level operation on this repository that returns a Resource
+ *
+ * @see FHIR operations
+ *
+ * @param a Resource type to return
+ * @param a Parameters type for operation parameters
+ * @param an Id type
+ * @param id the id of the Resource to do the invocation on
+ * @param name the name of the operation to invoke
+ * @param parameters the operation parameters
+ * @param returnType the class of the Resource the operation returns
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return the results of the operation
+ */
+ R invoke(
+ I id, String name, P parameters, Class returnType, Map headers);
+
+ /**
+ * Invokes an instance-level operation on this repository
+ *
+ * @see FHIR operations
+ *
+ * @param a Parameters type for operation parameters
+ * @param an Id type
+ * @param id the id of the Resource to do the invocation on
+ * @param name the name of the operation to invoke
+ * @param parameters the operation parameters
+ * @return a MethodOutcome with a status code
+ */
+ default
MethodOutcome invoke(I id, String name, P parameters) {
+ return this.invoke(id, name, parameters, Collections.emptyMap());
+ }
+
+ /**
+ * Invokes an instance-level operation on this repository
+ *
+ * @see FHIR operations
+ *
+ * @param
a Parameters type for operation parameters
+ * @param an Id type
+ * @param id the id of the Resource to do the invocation on
+ * @param name the name of the operation to invoke
+ * @param parameters the operation parameters
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a MethodOutcome with a status code
+ */
+ default
MethodOutcome invoke(
+ I id, String name, P parameters, Map headers) {
+ return throwNotImplementedOperationException("instance-level invoke is not supported by this repository");
+ }
+
+ // History starts here
+
+ /**
+ * Returns a Bundle with server-level history for this repository
+ *
+ * @see FHIR history
+ *
+ * @param a Bundle type to return
+ * @param a Parameters type for input parameters
+ * @param parameters the parameters for this history interaction
+ * @param returnType the class of the Bundle type to return
+ * @return a Bundle with the server history
+ */
+ default B history(P parameters, Class returnType) {
+ return this.history(parameters, returnType, Collections.emptyMap());
+ }
+
+ /**
+ * Returns a Bundle with server-level history for this repository
+ *
+ * @see FHIR history
+ *
+ * @param a Bundle type to return
+ * @param
a Parameters type for input parameters
+ * @param parameters the parameters for this history interaction
+ * @param returnType the class of the Bundle type to return
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a Bundle with the server history
+ */
+ default B history(
+ P parameters, Class returnType, Map headers) {
+ return throwNotImplementedOperationException("server-level history is not supported by this repository");
+ }
+
+ /**
+ * Returns a Bundle with type-level history for this repository
+ *
+ * @see FHIR history
+ *
+ * @param a Bundle type to return
+ * @param a Parameters type for input parameters
+ * @param a Resource type to produce history for
+ * @param resourceType the class of the Resource type to produce history for
+ * @param parameters the parameters for this history interaction
+ * @param returnType the class of the Bundle type to return
+ * @return a Bundle with the type history
+ */
+ default B history(
+ Class resourceType, P parameters, Class returnType) {
+ return this.history(resourceType, parameters, returnType, Collections.emptyMap());
+ }
+
+ /**
+ * Returns a Bundle with type-level history for this repository
+ *
+ * @see FHIR history
+ *
+ * @param a Bundle type to return
+ * @param a Parameters type for input parameters
+ * @param a Resource type to produce history for
+ * @param resourceType the class of the Resource type to produce history for
+ * @param parameters the parameters for this history interaction
+ * @param returnType the class of the Bundle type to return
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a Bundle with the type history
+ */
+ default B history(
+ Class resourceType, P parameters, Class returnType, Map headers) {
+ return throwNotImplementedOperationException("type-level history is not supported by this repository");
+ }
+
+ /**
+ * Returns a Bundle with instance-level history
+ *
+ * @see FHIR history
+ *
+ * @param a Bundle type to return
+ * @param a Parameters type for input parameters
+ * @param an Id type for the Resource to produce history for
+ * @param id the id of the Resource type to produce history for
+ * @param parameters the parameters for this history interaction
+ * @param returnType the class of the Bundle type to return
+ * @return a Bundle with the instance history
+ */
+ default B history(
+ I id, P parameters, Class returnType) {
+ return this.history(id, parameters, returnType, Collections.emptyMap());
+ }
+
+ /**
+ * Returns a Bundle with instance-level history
+ *
+ * @see FHIR history
+ *
+ * @param a Bundle type to return
+ * @param
a Parameters type for input parameters
+ * @param an Id type for the Resource to produce history for
+ * @param id the id of the Resource type to produce history for
+ * @param parameters the parameters for this history interaction
+ * @param returnType the class of the Bundle type to return
+ * @param headers headers for this request, typically key-value pairs of HTTP headers
+ * @return a Bundle with the instance history
+ */
+ default B history(
+ I id, P parameters, Class returnType, Map headers) {
+ return throwNotImplementedOperationException("instance-level history is not supported by this repository");
+ }
+
+ /**
+ * Returns the {@link FhirContext} used by the repository
+ *
+ * Practically, implementing FHIR functionality with the HAPI toolset requires a FhirContext. In
+ * particular for things like version independent code. Ideally, a user could which FHIR version a
+ * repository was configured for using things like the CapabilityStatement. In practice, that's
+ * not widely implemented (yet) and it's expensive to create a new context with every call. We
+ * will probably revisit this in the future.
+ *
+ * @return a FhirContext
+ */
+ FhirContext fhirContext();
+
+ private static T throwNotImplementedOperationException(String theMessage) {
+ throw new NotImplementedOperationException(Msg.code(2542) + theMessage);
+ }
+}
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6125-consentinterceptor-dont-call-children-if-parent-authorized-or-rejected.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6125-consentinterceptor-dont-call-children-if-parent-authorized-or-rejected.yaml
new file mode 100644
index 00000000000..951bc31c212
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6125-consentinterceptor-dont-call-children-if-parent-authorized-or-rejected.yaml
@@ -0,0 +1,7 @@
+---
+type: fix
+issue: 6124
+title: "Previously, when retrieving a resource which may contain other resources, such as a document Bundle,
+if a ConsentService's willSeeResource returned AUTHORIZED or REJECT on this parent resource, the willSeeResource was
+still being called for the child resources. This has now been fixed so that if a consent service
+returns AUTHORIZED or REJECT for a parent resource, willSeeResource is not called for the child resources."
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6140-common-data-access-api.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6140-common-data-access-api.yaml
new file mode 100644
index 00000000000..bfca99df2f5
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6140-common-data-access-api.yaml
@@ -0,0 +1,7 @@
+---
+type: change
+issue: 6140
+title: "An prototype interface to abstract data access across different types
+ of FHIR repositories (e.g. remote REST, local JPA) has been added to the `hapi-fhir-base` project.
+ Implementations of this interface will follow in future HAPI releases, and it will continue to evolve
+ as it's validated through implementation."
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6142-fix-hfj-search-migration-task.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6142-fix-hfj-search-migration-task.yaml
new file mode 100644
index 00000000000..0bca96abfa8
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6142-fix-hfj-search-migration-task.yaml
@@ -0,0 +1,6 @@
+---
+type: fix
+issue: 6142
+jira: SMILE-8701
+title: "Previously, if you upgraded from any older HAPI version to 6.6.0 or later, the `SEARCH_UUID` column length still
+showed as 36 despite it being updated to have a length of 48. This has now been fixed."
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6146-mssql-hfj-resource-fhir-id-colllation.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6146-mssql-hfj-resource-fhir-id-colllation.yaml
new file mode 100644
index 00000000000..8aa5528d9e3
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6146-mssql-hfj-resource-fhir-id-colllation.yaml
@@ -0,0 +1,10 @@
+---
+type: fix
+issue: 6146
+jira: SMILE-8191
+title: "Previously, on MSSQL, two resources with IDs that are identical except for case
+ (ex: Patient1 vs. patient1) would be considered to have the same ID because the database collation is
+ case insensitive (SQL_Latin1_General_CP1_CI_AS). Among other things, this would manifest
+ itself when trying to delete and re-create one of the resources.
+ This has been fixed with a migration step that makes the collation on the resource ID case sensitive
+ (SQL_Latin1_General_CP1_CS_AS)."
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java
index 6b54aeee71c..e53fa57ecad 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java
@@ -46,27 +46,29 @@ import java.util.Date;
@SuppressWarnings("SqlDialectInspection")
@Entity
@Immutable
-@Subselect("SELECT h.pid as pid, "
- + " r.res_id as res_id, "
- + " h.res_type as res_type, "
- + " h.res_version as res_version, "
+// Ideally, all tables and columns should be in UPPERCASE if we ever choose to use a case-sensitive collation for MSSQL
+// and there's a risk that queries on lowercase database objects fail.
+@Subselect("SELECT h.PID as PID, "
+ + " r.RES_ID as RES_ID, "
+ + " h.RES_TYPE as RES_TYPE, "
+ + " h.RES_VERSION as RES_VERSION, "
// FHIR version
- + " h.res_ver as res_ver, "
+ + " h.RES_VER as RES_VER, "
// resource version
- + " h.has_tags as has_tags, "
- + " h.res_deleted_at as res_deleted_at, "
- + " h.res_published as res_published, "
- + " h.res_updated as res_updated, "
- + " h.res_text as res_text, "
- + " h.res_text_vc as res_text_vc, "
- + " h.res_encoding as res_encoding, "
+ + " h.HAS_TAGS as HAS_TAGS, "
+ + " h.RES_DELETED_AT as RES_DELETED_AT, "
+ + " h.RES_PUBLISHED as RES_PUBLISHED, "
+ + " h.RES_UPDATED as RES_UPDATED, "
+ + " h.RES_TEXT as RES_TEXT, "
+ + " h.RES_TEXT_VC as RES_TEXT_VC, "
+ + " h.RES_ENCODING as RES_ENCODING, "
+ " h.PARTITION_ID as PARTITION_ID, "
+ " p.SOURCE_URI as PROV_SOURCE_URI,"
+ " p.REQUEST_ID as PROV_REQUEST_ID,"
- + " r.fhir_id as FHIR_ID "
+ + " r.FHIR_ID as FHIR_ID "
+ "FROM HFJ_RESOURCE r "
- + " INNER JOIN HFJ_RES_VER h ON r.res_id = h.res_id and r.res_ver = h.res_ver"
- + " LEFT OUTER JOIN HFJ_RES_VER_PROV p ON p.res_ver_pid = h.pid ")
+ + " INNER JOIN HFJ_RES_VER h ON r.RES_ID = h.RES_ID and r.RES_VER = h.RES_VER"
+ + " LEFT OUTER JOIN HFJ_RES_VER_PROV p ON p.RES_VER_PID = h.PID ")
public class ResourceSearchView implements IBaseResourceEntity, Serializable {
private static final long serialVersionUID = 1L;
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java
index e107d8beb8a..3457d4a622a 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java
@@ -49,6 +49,7 @@ import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.fhir.util.VersionEnum;
import org.apache.commons.lang3.StringUtils;
+import org.intellij.lang.annotations.Language;
import java.util.Arrays;
import java.util.HashMap;
@@ -468,6 +469,59 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks {
.failureAllowed();
}
}
+
+ version.onTable(Search.HFJ_SEARCH)
+ .modifyColumn("20240722.1", Search.SEARCH_UUID)
+ .nullable()
+ .withType(ColumnTypeEnum.STRING, 48);
+
+ {
+ final Builder.BuilderWithTableName hfjResource = version.onTable("HFJ_RESOURCE");
+
+ @Language(("SQL"))
+ final String onlyIfSql = "SELECT CASE CHARINDEX('_CI_', COLLATION_NAME) WHEN 0 THEN 0 ELSE 1 END "
+ + "FROM INFORMATION_SCHEMA.COLUMNS "
+ + "WHERE TABLE_SCHEMA = SCHEMA_NAME() "
+ + "AND TABLE_NAME = 'HFJ_RESOURCE' "
+ + "AND COLUMN_NAME = 'FHIR_ID' ";
+ final String onlyfIReason =
+ "Skipping change to HFJ_RESOURCE.FHIR_ID collation to SQL_Latin1_General_CP1_CS_AS because it is already using it";
+
+ hfjResource
+ .dropIndex("20240724.10", "IDX_RES_FHIR_ID")
+ .onlyAppliesToPlatforms(DriverTypeEnum.MSSQL_2012)
+ .onlyIf(onlyIfSql, onlyfIReason);
+
+ hfjResource
+ .dropIndex("20240724.20", "IDX_RES_TYPE_FHIR_ID")
+ .onlyAppliesToPlatforms(DriverTypeEnum.MSSQL_2012)
+ .onlyIf(onlyIfSql, onlyfIReason);
+
+ version.executeRawSql(
+ "20240724.30",
+ "ALTER TABLE HFJ_RESOURCE ALTER COLUMN FHIR_ID varchar(64) COLLATE SQL_Latin1_General_CP1_CS_AS")
+ .onlyAppliesToPlatforms(DriverTypeEnum.MSSQL_2012)
+ .onlyIf(onlyIfSql, onlyfIReason);
+
+ hfjResource
+ .addIndex("20240724.40", "IDX_RES_FHIR_ID")
+ .unique(false)
+ .online(true)
+ .withColumns("FHIR_ID")
+ .onlyAppliesToPlatforms(DriverTypeEnum.MSSQL_2012)
+ .onlyIf(onlyIfSql, onlyfIReason);
+
+ hfjResource
+ .addIndex("20240724.50", "IDX_RES_TYPE_FHIR_ID")
+ .unique(true)
+ .online(true)
+ // include res_id and our deleted flag so we can satisfy Observation?_sort=_id from the index on
+ // platforms that support it.
+ .includeColumns("RES_ID, RES_DELETED_AT")
+ .withColumns("RES_TYPE", "FHIR_ID")
+ .onlyAppliesToPlatforms(DriverTypeEnum.MSSQL_2012)
+ .onlyIf(onlyIfSql, onlyfIReason);
+ }
}
protected void init720() {
diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ConsentInterceptorResourceProviderR4IT.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ConsentInterceptorResourceProviderR4IT.java
index fe367f7b914..e75e3047b82 100644
--- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ConsentInterceptorResourceProviderR4IT.java
+++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ConsentInterceptorResourceProviderR4IT.java
@@ -1,7 +1,5 @@
package ca.uhn.fhir.jpa.provider.r4;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
@@ -11,6 +9,7 @@ import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
import ca.uhn.fhir.rest.api.Constants;
+import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
@@ -46,6 +45,7 @@ import org.apache.http.entity.StringEntity;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
+import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.IdType;
@@ -71,11 +71,12 @@ import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.leftPad;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.fail;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
-
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -630,6 +631,73 @@ public class ConsentInterceptorResourceProviderR4IT extends BaseResourceProvider
}
+ @Test
+ void testGetBundle_WhenWillSeeReturnsRejectForABundle_ReadingBundleThrowsResourceNotFound() {
+
+ myConsentInterceptor = new ConsentInterceptor(new ConsentSvcWillSeeRejectsBundlesAuthorizesOthers());
+ myServer.getRestfulServer().
+ getInterceptorService().registerInterceptor(myConsentInterceptor);
+
+ // create bundle
+ Bundle bundle = createDocumentBundle();
+ MethodOutcome createOutcome = myClient.create().resource(bundle).execute();
+ IIdType bundleId = createOutcome.getResource().getIdElement();
+
+ // read the created bundle back
+ ResourceNotFoundException ex = assertThrows(ResourceNotFoundException.class,
+ () -> myClient.read().resource(Bundle.class).withId(bundleId).execute());
+
+ assertEquals(404, ex.getStatusCode());
+ }
+
+ @Test
+ void testGetBundle_WhenWillSeeReturnsAuthorizedForABundle_ChildResourcesInTheBundleAreVisible() {
+
+ myConsentInterceptor = new ConsentInterceptor(new ConsentSvcWillSeeAuthorizesBundlesRejectsOthers());
+ myServer.getRestfulServer().
+ getInterceptorService().registerInterceptor(myConsentInterceptor);
+
+ // create bundle
+ Bundle bundle = createDocumentBundle();
+ MethodOutcome createOutcome = myClient.create().resource(bundle).execute();
+ IIdType bundleId = createOutcome.getResource().getIdElement();
+
+ // read the created bundle back
+ Bundle bundleRead = myClient.read().resource(Bundle.class).withId(bundleId).execute();
+
+ // since the consent service AUTHORIZED the bundle, the child resources in the bundle should be visible
+ // because willSeeResource won't be called for the child resources once the bundle is AUTHORIZED
+ assertEquals(2, bundleRead.getEntry().size());
+ Composition compositionEntry = (Composition) bundleRead.getEntry().get(0).getResource();
+ assertEquals("Composition/composition-in-bundle", compositionEntry.getId());
+ Patient patientEntry = (Patient) bundleRead.getEntry().get(1).getResource();
+ assertEquals("Patient/patient-in-bundle", patientEntry.getId());
+ }
+
+
+ @Test
+ void testGetBundle_WhenWillSeeReturnsProceedForABundle_WillSeeIsCalledForChildResourcesInTheBundle() {
+
+ myConsentInterceptor = new ConsentInterceptor(new ConsentSvcWillSeeProceedsBundlesRejectsOthers());
+ myServer.getRestfulServer().
+ getInterceptorService().registerInterceptor(myConsentInterceptor);
+
+
+ // create a bundle
+ Bundle bundle = createDocumentBundle();
+ MethodOutcome createOutcome = myClient.create().resource(bundle).execute();
+ IIdType bundleId = createOutcome.getResource().getIdElement();
+
+ //read the created bundle back
+ Bundle bundleRead = myClient.read().resource(Bundle.class).withId(bundleId).execute();
+
+
+ // since the consent service replies with PROCEED for the bundle in this test case,
+ // willSeeResource should be called for the child resources in the bundle and would be rejected by the
+ // consent service, so the child resources in the bundle should not be visible
+ assertEquals(0, bundleRead.getEntry().size());
+ }
+
/**
* Make sure the default methods all work and allow the response to proceed
*/
@@ -736,7 +804,7 @@ public class ConsentInterceptorResourceProviderR4IT extends BaseResourceProvider
// given
create50Observations();
- myConsentInterceptor = new ConsentInterceptor(new ConsentSvcRejectWillSeeResource());
+ myConsentInterceptor = new ConsentInterceptor(new ConsentSvcWillSeeProceedsBundlesRejectsOthers());
myServer.getRestfulServer().getInterceptorService().registerInterceptor(myConsentInterceptor);
// when
@@ -754,7 +822,7 @@ public class ConsentInterceptorResourceProviderR4IT extends BaseResourceProvider
// given
create50Observations();
- myConsentInterceptor = new ConsentInterceptor(new ConsentSvcRejectWillSeeResource());
+ myConsentInterceptor = new ConsentInterceptor(new ConsentSvcWillSeeProceedsBundlesRejectsOthers());
myServer.getRestfulServer().getInterceptorService().registerInterceptor(myConsentInterceptor);
// when
@@ -767,6 +835,21 @@ public class ConsentInterceptorResourceProviderR4IT extends BaseResourceProvider
}
+ private Bundle createDocumentBundle() {
+ Bundle bundle = new Bundle();
+ bundle.setType(Bundle.BundleType.DOCUMENT);
+
+ Composition composition = new Composition();
+ composition.setId("composition-in-bundle");
+
+ Patient patient = new Patient();
+ patient.setId("patient-in-bundle");
+
+ bundle.addEntry().setResource(composition);
+ bundle.addEntry().setResource(patient);
+ return bundle;
+ }
+
private void createPatientAndOrg() {
myPatientIds = new ArrayList<>();
@@ -1095,7 +1178,7 @@ public class ConsentInterceptorResourceProviderR4IT extends BaseResourceProvider
}
- private static class ConsentSvcRejectWillSeeResource implements IConsentService {
+ private static class ConsentSvcWillSeeProceedsBundlesRejectsOthers implements IConsentService {
@Override
public ConsentOutcome willSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
if("Bundle".equals(theResource.fhirType())){
@@ -1105,5 +1188,27 @@ public class ConsentInterceptorResourceProviderR4IT extends BaseResourceProvider
}
}
+ private static class ConsentSvcWillSeeAuthorizesBundlesRejectsOthers implements IConsentService {
+ @Override
+ public ConsentOutcome willSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
+ if("Bundle".equals(theResource.fhirType())){
+ return new ConsentOutcome(ConsentOperationStatusEnum.AUTHORIZED);
+ }
+ return new ConsentOutcome(ConsentOperationStatusEnum.REJECT);
+ }
+
+ }
+
+ private static class ConsentSvcWillSeeRejectsBundlesAuthorizesOthers implements IConsentService {
+ @Override
+ public ConsentOutcome willSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
+ if("Bundle".equals(theResource.fhirType())){
+ return new ConsentOutcome(ConsentOperationStatusEnum.REJECT);
+ }
+ return new ConsentOutcome(ConsentOperationStatusEnum.AUTHORIZED);
+ }
+
+ }
+
}
diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/embedded/HapiSchemaMigrationTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/embedded/HapiSchemaMigrationTest.java
index 48e264da584..0cc6156dfdd 100644
--- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/embedded/HapiSchemaMigrationTest.java
+++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/embedded/HapiSchemaMigrationTest.java
@@ -13,6 +13,7 @@ import ca.uhn.fhir.util.VersionEnum;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.apache.commons.dbcp2.BasicDataSource;
+import org.intellij.lang.annotations.Language;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -25,7 +26,9 @@ import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
@@ -68,6 +71,8 @@ public class HapiSchemaMigrationTest {
private static final String COLUMN_VAL_VC = "VAL_VC";
private static final String NULL_PLACEHOLDER = "[NULL]";
+ private static final String COLLATION_CASE_INSENSITIVE = "SQL_Latin1_General_CP1_CI_AS";
+ private static final String COLLATION_CASE_SENSITIVE = "SQL_Latin1_General_CP1_CS_AS";
static {
HapiSystemProperties.enableUnitTestMode();
@@ -127,6 +132,8 @@ public class HapiSchemaMigrationTest {
verifyHfjResSearchUrlMigration(database, theDriverType);
verifyTrm_Concept_Desig(database, theDriverType);
+
+ verifyHfjResourceFhirIdCollation(database, theDriverType);
}
/**
@@ -170,7 +177,7 @@ public class HapiSchemaMigrationTest {
try (final Connection connection = theDatabase.getDataSource().getConnection()) {
final DatabaseMetaData tableMetaData = connection.getMetaData();
- final List