diff --git a/.editorconfig b/.editorconfig index 499513576f9..f19de7e2a01 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,12 +5,7 @@ end_of_line = lf insert_final_newline = true tab_width = 3 indent_size = 3 - -[*.java] charset = utf-8 -indent_style = tab -tab_width = 3 -indent_size = 3 [*.xml] charset = utf-8 @@ -30,3 +25,259 @@ indent_style = tab tab_width = 3 indent_size = 3 + +[*.java] +charset = utf-8 +indent_style = tab +tab_width = 3 +indent_size = 3 +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_at_first_column = true +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = off +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_class_names_in_javadoc = 1 +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_while_brace_force = never +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_pk_class = java.lang.String +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_field_name_prefix = my +ij_java_finally_on_new_line = false +ij_java_for_brace_force = never +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = never +ij_java_imports_layout = *,|,javax.**,java.**,|,$* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_at_first_column = true +ij_java_message_dd_suffix = EJB +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_new_line_after_lparen_in_record_header = false +ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* +ij_java_parameter_annotation_wrap = off +ij_java_parameter_name_prefix = the +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_record_header = false +ij_java_session_dd_suffix = EJB +ij_java_session_eb_suffix = Bean +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_static_field_name_prefix = our +ij_java_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = never +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false + diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java index aa5b7b08d6f..f8a1330a3c8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java @@ -425,7 +425,7 @@ public enum Pointcut { "java.io.Writer", "ca.uhn.fhir.rest.api.server.RequestDetails", "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails" - ), + ), /** @@ -1401,7 +1401,7 @@ public enum Pointcut { * * *

- * Hooks should return an instance of ca.uhn.fhir.jpa.api.model.RequestPartitionId or null. + * Hooks must return an instance of ca.uhn.fhir.interceptor.model.RequestPartitionId. *

*/ STORAGE_PARTITION_IDENTIFY_CREATE( @@ -1440,7 +1440,7 @@ public enum Pointcut { * * *

- * Hooks should return an instance of ca.uhn.fhir.jpa.api.model.RequestPartitionId or null. + * Hooks must return an instance of ca.uhn.fhir.interceptor.model.RequestPartitionId. *

*/ STORAGE_PARTITION_IDENTIFY_READ( @@ -1451,6 +1451,49 @@ public enum Pointcut { "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails" ), + /** + * Storage Hook: + * Invoked before any partition aware FHIR operation, when the selected partition has been identified (ie. after the + * {@link #STORAGE_PARTITION_IDENTIFY_CREATE} or {@link #STORAGE_PARTITION_IDENTIFY_READ} hook was called. This allows + * a separate hook to register, and potentially make decisions about whether the request should be allowed to proceed. + *

+ * This hook will only be called if + * partitioning is enabled in the JPA server. + *

+ *

+ * Hooks may accept the following parameters: + *

+ * + *

+ * Hooks must return void. + *

+ */ + STORAGE_PARTITION_SELECTED( + // Return type + void.class, + // Params + "ca.uhn.fhir.interceptor.model.RequestPartitionId", + "ca.uhn.fhir.rest.api.server.RequestDetails", + "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails" + ), + /** * Performance Tracing Hook: * This hook is invoked when any informational messages generated by the diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java index c1d0f8291c1..d66e74208d5 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/RequestPartitionId.java @@ -20,27 +20,55 @@ package ca.uhn.fhir.interceptor.model; * #L% */ +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.time.LocalDate; -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; - +/** + * @since 5.0.0 + */ public class RequestPartitionId { - private final Integer myPartitionId; + private static final RequestPartitionId ALL_PARTITIONS = new RequestPartitionId(); private final LocalDate myPartitionDate; + private final boolean myAllPartitions; + private final Integer myPartitionId; private final String myPartitionName; /** - * Constructor + * Constructor for a single partition */ private RequestPartitionId(@Nullable String thePartitionName, @Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) { - myPartitionName = thePartitionName; myPartitionId = thePartitionId; + myPartitionName = thePartitionName; myPartitionDate = thePartitionDate; + myAllPartitions = false; } + /** + * Constructor for all partitions + */ + private RequestPartitionId() { + super(); + myPartitionDate = null; + myPartitionName = null; + myPartitionId = null; + myAllPartitions = true; + } + + public boolean isAllPartitions() { + return myAllPartitions; + } + + @Nullable + public LocalDate getPartitionDate() { + return myPartitionDate; + } + + @Nullable public String getPartitionName() { return myPartitionName; } @@ -50,32 +78,59 @@ public class RequestPartitionId { return myPartitionId; } - @Nullable - public LocalDate getPartitionDate() { - return myPartitionDate; - } - @Override public String toString() { - return getPartitionIdStringOrNullString(); + return "RequestPartitionId[id=" + getPartitionId() + ", name=" + getPartitionName() + "]"; } /** * Returns the partition ID (numeric) as a string, or the string "null" */ public String getPartitionIdStringOrNullString() { - return defaultIfNull(myPartitionId, "null").toString(); + if (myPartitionId == null) { + return "null"; + } + return myPartitionId.toString(); } - /** - * Create a string representation suitable for use as a cache key. Null aware. - */ - public static String stringifyForKey(RequestPartitionId theRequestPartitionId) { - String retVal = "(null)"; - if (theRequestPartitionId != null) { - retVal = theRequestPartitionId.getPartitionIdStringOrNullString(); + @Override + public boolean equals(Object theO) { + if (this == theO) { + return true; } - return retVal; + + if (theO == null || getClass() != theO.getClass()) { + return false; + } + + RequestPartitionId that = (RequestPartitionId) theO; + + return new EqualsBuilder() + .append(myAllPartitions, that.myAllPartitions) + .append(myPartitionDate, that.myPartitionDate) + .append(myPartitionId, that.myPartitionId) + .append(myPartitionName, that.myPartitionName) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(myPartitionDate) + .append(myAllPartitions) + .append(myPartitionId) + .append(myPartitionName) + .toHashCode(); + } + + @Nonnull + public static RequestPartitionId allPartitions() { + return ALL_PARTITIONS; + } + + @Nonnull + public static RequestPartitionId defaultPartition() { + return fromPartitionId(null); } @Nonnull @@ -99,8 +154,23 @@ public class RequestPartitionId { } @Nonnull - public static RequestPartitionId forPartitionNameAndId(@Nullable String thePartitionName, @Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) { + public static RequestPartitionId fromPartitionIdAndName(@Nullable Integer thePartitionId, @Nullable String thePartitionName) { + return new RequestPartitionId(thePartitionName, thePartitionId, null); + } + + @Nonnull + public static RequestPartitionId forPartitionIdAndName(@Nullable Integer thePartitionId, @Nullable String thePartitionName, @Nullable LocalDate thePartitionDate) { return new RequestPartitionId(thePartitionName, thePartitionId, thePartitionDate); } + /** + * Create a string representation suitable for use as a cache key. Null aware. + */ + public static String stringifyForKey(RequestPartitionId theRequestPartitionId) { + String retVal = "(null)"; + if (theRequestPartitionId != null) { + retVal = theRequestPartitionId.getPartitionIdStringOrNullString(); + } + return retVal; + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index 9628034b3f9..b8d66cac6ce 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -264,6 +264,13 @@ public class Constants { public static final String PARAM_FHIRPATH = "_fhirpath"; public static final String PARAM_TYPE = "_type"; + /** + * {@link org.hl7.fhir.instance.model.api.IBaseResource#getUserData(String) User metadata key} used + * to store the partition ID (if any) associated with the given resource. Value for this + * key will be of type {@link ca.uhn.fhir.interceptor.model.RequestPartitionId}. + */ + public static final String RESOURCE_PARTITION_ID = Constants.class.getName() + "_RESOURCE_PARTITION_ID"; + static { CHARSET_UTF8 = StandardCharsets.UTF_8; CHARSET_US_ASCII = StandardCharsets.ISO_8859_1; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/BaseHttpRequest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/BaseHttpRequest.java new file mode 100644 index 00000000000..2ced28af0fe --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/BaseHttpRequest.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.rest.client.api; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public abstract class BaseHttpRequest implements IHttpRequest { + + private UrlSourceEnum myUrlSource; + + @Override + public UrlSourceEnum getUrlSource() { + return myUrlSource; + } + + @Override + public void setUrlSource(UrlSourceEnum theUrlSource) { + myUrlSource = theUrlSource; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IHttpRequest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IHttpRequest.java index 62389f6c5b3..a2c7445849f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IHttpRequest.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IHttpRequest.java @@ -60,11 +60,15 @@ public interface IHttpRequest { /** * Return the request URI, or null + * + * @see #getUri() */ String getUri(); /** * Modify the request URI, or null + * + * @see #setUrlSource(UrlSourceEnum) */ void setUri(String theUrl); @@ -79,4 +83,19 @@ public interface IHttpRequest { * @param theHeaderName The header name, e.g. "Accept" (must not be null or blank) */ void removeHeaders(String theHeaderName); + + /** + * Where was the URL from? + * + * @since 5.0.0 + */ + UrlSourceEnum getUrlSource(); + + /** + * Where was the URL from? + * + * @since 5.0.0 + */ + void setUrlSource(UrlSourceEnum theUrlSource); + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/UrlSourceEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/UrlSourceEnum.java new file mode 100644 index 00000000000..26230194c0a --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/UrlSourceEnum.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.rest.client.api; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public enum UrlSourceEnum { + + /** + * URL was generated (typically by adding the base URL + other things) + */ + GENERATED, + + /** + * URL was supplied (i.e. it came from a paging link in a bundle) + */ + EXPLICIT + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java index 609a49ebd06..89cb4343942 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java @@ -9,9 +9,16 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.util.DateUtils; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import java.util.*; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.TimeZone; -import static ca.uhn.fhir.rest.param.ParamPrefixEnum.*; +import static ca.uhn.fhir.rest.param.ParamPrefixEnum.EQUAL; +import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS; +import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS; import static java.lang.String.format; import static org.apache.commons.lang3.StringUtils.isNotBlank; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/DateUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/DateUtils.java index d208e3fe599..58b83b7b630 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/DateUtils.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/DateUtils.java @@ -23,7 +23,12 @@ package ca.uhn.fhir.util; import java.lang.ref.SoftReference; import java.text.ParsePosition; import java.text.SimpleDateFormat; -import java.util.*; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; /** * A utility class for parsing and formatting HTTP dates as used in cookies and diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/MetaUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/MetaUtil.java index cd6c9384906..775c274e929 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/MetaUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/MetaUtil.java @@ -96,7 +96,7 @@ public class MetaUtil { value.setValue(theValue); sourceExtension.setValue(value); } else { - ourLog.error(MetaUtil.class.getSimpleName() + ".setSource() not supported on FHIR Version " + theContext.getVersion().getVersion()); + ourLog.debug(MetaUtil.class.getSimpleName() + ".setSource() not supported on FHIR Version " + theContext.getVersion().getVersion()); } } diff --git a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IPrimitiveType.java b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IPrimitiveType.java index 089aba760e7..8a782d961ec 100644 --- a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IPrimitiveType.java +++ b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IPrimitiveType.java @@ -22,16 +22,26 @@ package org.hl7.fhir.instance.model.api; */ -public interface IPrimitiveType extends IBaseDatatype { +import javax.annotation.Nullable; - void setValueAsString(String theValue) throws IllegalArgumentException; +public interface IPrimitiveType extends IBaseDatatype { String getValueAsString(); + void setValueAsString(String theValue) throws IllegalArgumentException; + T getValue(); - boolean hasValue(); - IPrimitiveType setValue(T theValue) throws IllegalArgumentException; - + + boolean hasValue(); + + /** + * If the supplied argument is non-null, returns the results of {@link #getValue()}. If the supplied argument is null, returns null. + */ + @Nullable + static T toValueOrNull(@Nullable IPrimitiveType thePrimitiveType) { + return thePrimitiveType != null ? thePrimitiveType.getValue() : null; + } + } diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index 9a1b0c23eb4..ac017230a12 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -83,7 +83,6 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao.transactionContainsMultipleWithDuplica ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao.transactionEntryHasInvalidVerb=Transaction bundle entry has missing or invalid HTTP Verb specified in Bundle.entry({1}).request.method. Found value: "{0}" ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao.transactionMissingUrl=Unable to perform {0}, no URL provided. ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao.transactionInvalidUrl=Unable to perform {0}, URL provided is invalid: {1} -ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao.noSystemOrTypeHistoryForPartitionAwareServer=Type- and Server- level history operation not supported on partitioned server ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.cantValidateWithNoResource=No resource supplied for $validate operation (resource is required unless mode is \"delete\") ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.deleteBlockedBecauseDisabled=Resource deletion is not permitted on this server @@ -144,9 +143,9 @@ ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils.failedToApplyPatch=Failed to apply ca.uhn.fhir.jpa.graphql.JpaStorageServices.invalidGraphqlArgument=Unknown GraphQL argument "{0}". Value GraphQL argument for this type are: {1} -ca.uhn.fhir.jpa.partition.RequestPartitionHelperService.blacklistedResourceTypeForPartitioning=Resource type {0} can not be partitioned -ca.uhn.fhir.jpa.partition.RequestPartitionHelperService.unknownPartitionId=Unknown partition ID: {0} -ca.uhn.fhir.jpa.partition.RequestPartitionHelperService.unknownPartitionName=Unknown partition name: {0} +ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.blacklistedResourceTypeForPartitioning=Resource type {0} can not be partitioned +ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.unknownPartitionId=Unknown partition ID: {0} +ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.unknownPartitionName=Unknown partition name: {0} ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderReference.invalidTargetTypeForChain=Resource type "{0}" is not a valid target type for reference search parameter: {1} @@ -154,6 +153,7 @@ ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderReference.invalidResourceType=Inva ca.uhn.fhir.jpa.dao.index.IdHelperService.nonUniqueForcedId=Non-unique ID specified, can not process request +ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.noIdSupplied=No Partition ID supplied ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.missingPartitionIdOrName=Partition must have an ID and a Name ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantCreatePartition0=Can not create a partition with ID 0 (this is a reserved value) ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.unknownPartitionId=No partition exists with ID {0} @@ -163,3 +163,5 @@ ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantDeleteDefaultPartition=Can ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.cantRenameDefaultPartition=Can not rename default partition ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor.unknownTenantName=Unknown tenant: {0} + +ca.uhn.fhir.jpa.dao.HistoryBuilder.noSystemOrTypeHistoryForPartitionAwareServer=Type- and Server- level history operation not supported across partitions on partitioned server diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/interceptor/model/RequestPartitionIdTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/interceptor/model/RequestPartitionIdTest.java new file mode 100644 index 00000000000..4664c991f11 --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/interceptor/model/RequestPartitionIdTest.java @@ -0,0 +1,24 @@ +package ca.uhn.fhir.interceptor.model; + +import org.junit.Test; + +import java.time.LocalDate; + +import static org.junit.Assert.*; + +public class RequestPartitionIdTest { + + @Test + public void testHashCode() { + assertEquals(31860737, RequestPartitionId.allPartitions().hashCode()); + } + + @Test + public void testEquals() { + assertEquals(RequestPartitionId.fromPartitionId(123, LocalDate.of(2020,1,1)), RequestPartitionId.fromPartitionId(123, LocalDate.of(2020,1,1))); + assertNotEquals(RequestPartitionId.fromPartitionId(123, LocalDate.of(2020,1,1)), null); + assertNotEquals(RequestPartitionId.fromPartitionId(123, LocalDate.of(2020,1,1)), "123"); + } + + +} diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/resources/logback-cli-on-debug.xml b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/resources/logback-cli-on-debug.xml index fa16cfb7451..d5fa11fe0f9 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/resources/logback-cli-on-debug.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/resources/logback-cli-on-debug.xml @@ -16,6 +16,12 @@ utf-8 + + + + + + diff --git a/hapi-fhir-client-okhttp/src/main/java/ca/uhn/fhir/okhttp/client/OkHttpRestfulRequest.java b/hapi-fhir-client-okhttp/src/main/java/ca/uhn/fhir/okhttp/client/OkHttpRestfulRequest.java index ce2c7e7d347..9f8a393e0e1 100644 --- a/hapi-fhir-client-okhttp/src/main/java/ca/uhn/fhir/okhttp/client/OkHttpRestfulRequest.java +++ b/hapi-fhir-client-okhttp/src/main/java/ca/uhn/fhir/okhttp/client/OkHttpRestfulRequest.java @@ -26,6 +26,7 @@ import java.util.Map; */ import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.client.api.BaseHttpRequest; import ca.uhn.fhir.rest.client.api.IHttpRequest; import ca.uhn.fhir.rest.client.api.IHttpResponse; import ca.uhn.fhir.util.StopWatch; @@ -39,7 +40,7 @@ import okhttp3.RequestBody; * * @author Matthew Clarke | matthew.clarke@orionhealth.com | Orion Health */ -public class OkHttpRestfulRequest implements IHttpRequest { +public class OkHttpRestfulRequest extends BaseHttpRequest implements IHttpRequest { private final Request.Builder myRequestBuilder; private Factory myClient; diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttpRequest.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttpRequest.java index 5f9a2334a27..63ea8dee384 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttpRequest.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttpRequest.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.rest.client.apache; * #L% */ +import ca.uhn.fhir.rest.client.api.BaseHttpRequest; import ca.uhn.fhir.rest.client.api.IHttpRequest; import ca.uhn.fhir.rest.client.api.IHttpResponse; import ca.uhn.fhir.util.StopWatch; @@ -36,7 +37,11 @@ import org.apache.http.entity.ContentType; import java.io.IOException; import java.net.URI; import java.nio.charset.Charset; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; /** * A Http Request based on Apache. This is an adapter around the class @@ -44,7 +49,7 @@ import java.util.*; * * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare */ -public class ApacheHttpRequest implements IHttpRequest { +public class ApacheHttpRequest extends BaseHttpRequest implements IHttpRequest { private HttpClient myClient; private HttpRequestBase myRequest; @@ -112,13 +117,13 @@ public class ApacheHttpRequest implements IHttpRequest { } @Override - public void setUri(String theUrl) { - myRequest.setURI(URI.create(theUrl)); + public String getUri() { + return myRequest.getURI().toString(); } @Override - public String getUri() { - return myRequest.getURI().toString(); + public void setUri(String theUrl) { + myRequest.setURI(URI.create(theUrl)); } @Override diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java index 45918f97c00..63e92b431a0 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java @@ -20,7 +20,13 @@ package ca.uhn.fhir.rest.client.impl; * #L% */ -import ca.uhn.fhir.context.*; +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; +import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; +import ca.uhn.fhir.context.BaseRuntimeElementDefinition; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.IRuntimeDatatypeDefinition; +import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.Include; @@ -30,16 +36,92 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.UriDt; import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; -import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.rest.api.*; +import ca.uhn.fhir.rest.api.CacheControlDirective; +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.IVersionSpecificBundleFactory; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.rest.api.PreferReturnEnum; +import ca.uhn.fhir.rest.api.SearchStyleEnum; +import ca.uhn.fhir.rest.api.SearchTotalModeEnum; +import ca.uhn.fhir.rest.api.SortOrderEnum; +import ca.uhn.fhir.rest.api.SortSpec; +import ca.uhn.fhir.rest.api.SummaryEnum; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.IHttpClient; import ca.uhn.fhir.rest.client.api.IHttpRequest; +import ca.uhn.fhir.rest.client.api.UrlSourceEnum; import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; -import ca.uhn.fhir.rest.client.method.*; -import ca.uhn.fhir.rest.gclient.*; +import ca.uhn.fhir.rest.client.method.DeleteMethodBinding; +import ca.uhn.fhir.rest.client.method.HistoryMethodBinding; +import ca.uhn.fhir.rest.client.method.HttpDeleteClientInvocation; +import ca.uhn.fhir.rest.client.method.HttpGetClientInvocation; +import ca.uhn.fhir.rest.client.method.HttpSimpleGetClientInvocation; +import ca.uhn.fhir.rest.client.method.IClientResponseHandler; +import ca.uhn.fhir.rest.client.method.MethodUtil; +import ca.uhn.fhir.rest.client.method.OperationMethodBinding; +import ca.uhn.fhir.rest.client.method.ReadMethodBinding; +import ca.uhn.fhir.rest.client.method.SearchMethodBinding; +import ca.uhn.fhir.rest.client.method.SortParameter; +import ca.uhn.fhir.rest.client.method.TransactionMethodBinding; +import ca.uhn.fhir.rest.client.method.ValidateMethodBindingDstu2Plus; +import ca.uhn.fhir.rest.gclient.IBaseQuery; +import ca.uhn.fhir.rest.gclient.IClientExecutable; +import ca.uhn.fhir.rest.gclient.ICreate; +import ca.uhn.fhir.rest.gclient.ICreateTyped; +import ca.uhn.fhir.rest.gclient.ICreateWithQuery; +import ca.uhn.fhir.rest.gclient.ICreateWithQueryTyped; +import ca.uhn.fhir.rest.gclient.ICriterion; +import ca.uhn.fhir.rest.gclient.ICriterionInternal; +import ca.uhn.fhir.rest.gclient.IDelete; +import ca.uhn.fhir.rest.gclient.IDeleteTyped; +import ca.uhn.fhir.rest.gclient.IDeleteWithQuery; +import ca.uhn.fhir.rest.gclient.IDeleteWithQueryTyped; +import ca.uhn.fhir.rest.gclient.IFetchConformanceTyped; +import ca.uhn.fhir.rest.gclient.IFetchConformanceUntyped; +import ca.uhn.fhir.rest.gclient.IGetPage; +import ca.uhn.fhir.rest.gclient.IGetPageTyped; +import ca.uhn.fhir.rest.gclient.IGetPageUntyped; +import ca.uhn.fhir.rest.gclient.IHistory; +import ca.uhn.fhir.rest.gclient.IHistoryTyped; +import ca.uhn.fhir.rest.gclient.IHistoryUntyped; +import ca.uhn.fhir.rest.gclient.IMeta; +import ca.uhn.fhir.rest.gclient.IMetaAddOrDeleteSourced; +import ca.uhn.fhir.rest.gclient.IMetaAddOrDeleteUnsourced; +import ca.uhn.fhir.rest.gclient.IMetaGetUnsourced; +import ca.uhn.fhir.rest.gclient.IOperation; +import ca.uhn.fhir.rest.gclient.IOperationProcessMsg; +import ca.uhn.fhir.rest.gclient.IOperationProcessMsgMode; +import ca.uhn.fhir.rest.gclient.IOperationUnnamed; +import ca.uhn.fhir.rest.gclient.IOperationUntyped; +import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput; +import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInputAndPartialOutput; +import ca.uhn.fhir.rest.gclient.IParam; +import ca.uhn.fhir.rest.gclient.IPatch; +import ca.uhn.fhir.rest.gclient.IPatchExecutable; +import ca.uhn.fhir.rest.gclient.IPatchWithBody; +import ca.uhn.fhir.rest.gclient.IPatchWithQuery; +import ca.uhn.fhir.rest.gclient.IPatchWithQueryTyped; +import ca.uhn.fhir.rest.gclient.IQuery; +import ca.uhn.fhir.rest.gclient.IRead; +import ca.uhn.fhir.rest.gclient.IReadExecutable; +import ca.uhn.fhir.rest.gclient.IReadIfNoneMatch; +import ca.uhn.fhir.rest.gclient.IReadTyped; +import ca.uhn.fhir.rest.gclient.ISort; +import ca.uhn.fhir.rest.gclient.ITransaction; +import ca.uhn.fhir.rest.gclient.ITransactionTyped; +import ca.uhn.fhir.rest.gclient.IUntypedQuery; +import ca.uhn.fhir.rest.gclient.IUpdate; +import ca.uhn.fhir.rest.gclient.IUpdateExecutable; +import ca.uhn.fhir.rest.gclient.IUpdateTyped; +import ca.uhn.fhir.rest.gclient.IUpdateWithQuery; +import ca.uhn.fhir.rest.gclient.IUpdateWithQueryTyped; +import ca.uhn.fhir.rest.gclient.IValidate; +import ca.uhn.fhir.rest.gclient.IValidateUntyped; import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.TokenParam; @@ -53,14 +135,38 @@ import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; -import org.hl7.fhir.instance.model.api.*; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseConformance; +import org.hl7.fhir.instance.model.api.IBaseDatatype; +import org.hl7.fhir.instance.model.api.IBaseMetaType; +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import java.io.IOException; import java.io.InputStream; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; -import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; /** * @author James Agnew @@ -749,6 +855,7 @@ public class GenericClient extends BaseClient implements IGenericClient { IClientResponseHandler binding; binding = new ResourceResponseHandler(myBundleType, getPreferResponseTypes()); HttpSimpleGetClientInvocation invocation = new HttpSimpleGetClientInvocation(myContext, myUrl); + invocation.setUrlSource(UrlSourceEnum.EXPLICIT); Map> params = null; return invoke(params, binding, invocation); @@ -1838,7 +1945,7 @@ public class GenericClient extends BaseClient implements IGenericClient { BaseHttpClientInvocation invocation; if (mySearchUrl != null) { - invocation = SearchMethodBinding.createSearchInvocation(myContext, mySearchUrl, params); + invocation = SearchMethodBinding.createSearchInvocation(myContext, mySearchUrl, UrlSourceEnum.EXPLICIT, params); } else { invocation = SearchMethodBinding.createSearchInvocation(myContext, myResourceName, params, resourceId, myCompartmentName, mySearchStyle); } diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/interceptor/UrlTenantSelectionInterceptor.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/interceptor/UrlTenantSelectionInterceptor.java index 26e0bf5c36c..90d419a552d 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/interceptor/UrlTenantSelectionInterceptor.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/interceptor/UrlTenantSelectionInterceptor.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.rest.client.api.IHttpRequest; import ca.uhn.fhir.rest.client.api.IRestfulClient; +import ca.uhn.fhir.rest.client.api.UrlSourceEnum; import org.apache.commons.lang3.Validate; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -81,6 +82,10 @@ public class UrlTenantSelectionInterceptor { Validate.isTrue(requestUri.startsWith(serverBase), "Request URI %s does not start with server base %s", requestUri, serverBase); + if (theRequest.getUrlSource() == UrlSourceEnum.EXPLICIT) { + return; + } + String newUri = serverBase + "/" + tenantId + requestUri.substring(serverBase.length()); theRequest.setUri(newUri); } diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/HttpGetClientInvocation.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/HttpGetClientInvocation.java index 5e3a5b71114..eddf2375ffc 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/HttpGetClientInvocation.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/HttpGetClientInvocation.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.IHttpRequest; +import ca.uhn.fhir.rest.client.api.UrlSourceEnum; import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation; import ca.uhn.fhir.util.UrlUtil; import org.apache.commons.lang3.StringUtils; @@ -41,17 +42,24 @@ public class HttpGetClientInvocation extends BaseHttpClientInvocation { private final Map> myParameters; private final String myUrlPath; + private final UrlSourceEnum myUrlSource; public HttpGetClientInvocation(FhirContext theContext, Map> theParameters, String... theUrlFragments) { + this(theContext, theParameters, UrlSourceEnum.GENERATED, theUrlFragments); + } + + public HttpGetClientInvocation(FhirContext theContext, Map> theParameters, UrlSourceEnum theUrlSource, String... theUrlFragments) { super(theContext); myParameters = theParameters; myUrlPath = StringUtils.join(theUrlFragments, '/'); + myUrlSource = theUrlSource; } public HttpGetClientInvocation(FhirContext theContext, String theUrlPath) { super(theContext); myParameters = new HashMap<>(); myUrlPath = theUrlPath; + myUrlSource = UrlSourceEnum.GENERATED; } @@ -95,7 +103,10 @@ public class HttpGetClientInvocation extends BaseHttpClientInvocation { appendExtraParamsWithQuestionMark(theExtraParams, b, first); - return super.createHttpRequest(b.toString(), theEncoding, RequestTypeEnum.GET); + IHttpRequest retVal = super.createHttpRequest(b.toString(), theEncoding, RequestTypeEnum.GET); + retVal.setUrlSource(myUrlSource); + + return retVal; } public Map> getParameters() { diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/HttpSimpleGetClientInvocation.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/HttpSimpleGetClientInvocation.java index 2908f612ded..6dde247b5fc 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/HttpSimpleGetClientInvocation.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/HttpSimpleGetClientInvocation.java @@ -27,11 +27,13 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.IHttpRequest; +import ca.uhn.fhir.rest.client.api.UrlSourceEnum; import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation; public class HttpSimpleGetClientInvocation extends BaseHttpClientInvocation { private final String myUrl; + private UrlSourceEnum myUrlSource = UrlSourceEnum.GENERATED; public HttpSimpleGetClientInvocation(FhirContext theContext, String theUrlPath) { super(theContext); @@ -40,7 +42,12 @@ public class HttpSimpleGetClientInvocation extends BaseHttpClientInvocation { @Override public IHttpRequest asHttpRequest(String theUrlBase, Map> theExtraParams, EncodingEnum theEncoding, Boolean thePrettyPrint) { - return createHttpRequest(myUrl, theEncoding, RequestTypeEnum.GET); + IHttpRequest retVal = createHttpRequest(myUrl, theEncoding, RequestTypeEnum.GET); + retVal.setUrlSource(myUrlSource); + return retVal; } + public void setUrlSource(UrlSourceEnum theUrlSource) { + myUrlSource = theUrlSource; + } } diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchMethodBinding.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchMethodBinding.java index 2649d487655..4194493df37 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchMethodBinding.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchMethodBinding.java @@ -19,27 +19,33 @@ package ca.uhn.fhir.rest.client.method; * limitations under the License. * #L% */ -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -import java.lang.reflect.Method; -import java.util.*; -import java.util.Map.Entry; - -import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.rest.annotation.Search; -import ca.uhn.fhir.rest.api.*; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.SearchStyleEnum; +import ca.uhn.fhir.rest.client.api.UrlSourceEnum; import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation; import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; public class SearchMethodBinding extends BaseResourceReturningMethodBinding { private String myCompartmentName; @@ -157,8 +163,8 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { return getMethod().toString(); } - public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theSearchUrl, Map> theParams) { - return new HttpGetClientInvocation(theContext, theParams, theSearchUrl); + public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theSearchUrl, UrlSourceEnum theUrlSource, Map> theParams) { + return new HttpGetClientInvocation(theContext, theParams, theUrlSource, theSearchUrl); } diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ClientExamples.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ClientExamples.java index 15a2def45ba..e8a7a512b3a 100644 --- a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ClientExamples.java +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ClientExamples.java @@ -34,7 +34,8 @@ import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor; import ca.uhn.fhir.rest.client.interceptor.BearerTokenAuthInterceptor; import ca.uhn.fhir.rest.client.interceptor.CookieInterceptor; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; -import org.hl7.fhir.r4.model.*; +import ca.uhn.fhir.rest.client.interceptor.UrlTenantSelectionInterceptor; +import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Patient; public class ClientExamples { @@ -60,6 +61,29 @@ public class ClientExamples { // END SNIPPET: proxy } + + public void tenantId() { + // START SNIPPET: tenantId + FhirContext ctx = FhirContext.forR4(); + + // Create the client + IGenericClient genericClient = ctx.newRestfulGenericClient("http://localhost:9999/fhir"); + + // Register the interceptor + UrlTenantSelectionInterceptor tenantSelection = new UrlTenantSelectionInterceptor(); + genericClient.registerInterceptor(tenantSelection); + + // Read from tenant A + tenantSelection.setTenantId("TENANT-A"); + Patient patientA = genericClient.read().resource(Patient.class).withId("123").execute(); + + // Read from tenant B + tenantSelection.setTenantId("TENANT-B"); + Patient patientB = genericClient.read().resource(Patient.class).withId("456").execute(); + // END SNIPPET: tenantId + } + + @SuppressWarnings("unused") public void processMessage() { // START SNIPPET: processMessage diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1813-reduce-history-sql-operations.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1813-reduce-history-sql-operations.yaml new file mode 100644 index 00000000000..2a2cf44ad88 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1813-reduce-history-sql-operations.yaml @@ -0,0 +1,7 @@ +--- +type: perf +issue: 1813 +title: History operations in the JPA server have been significantly optimized to remove the number of SQL SELECT statements, + and to completely eliminate any INSERT statements. This should have a positive effect on heavy users of history + operations. In addition, history operations will no longer write an entry in the query cache (HFJ_SEARCH) table which + should further improve performance of this operation. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml index 4d12c1b1907..8ee1344115c 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml @@ -58,7 +58,7 @@ issue: "1807" type: "change" title: "**New Feature**: - A new feature has been added to the JPA server called **[Partitioning](/hapi-fhir/docs/server_jpa/partitioning.html). This + A new feature has been added to the JPA server called **[Partitioning](/hapi-fhir/docs/server_jpa_partitioning/partitioning.html). This feature allows data to be segregated using a user defined partitioning strategy. This can be leveraged to take advantags of native RDBMS partition strategies, and also to implement **multitenant servers**. " diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/client/examples.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/client/examples.md index f79d97cbfff..e3267ffed28 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/client/examples.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/client/examples.md @@ -2,79 +2,63 @@ This page contains examples of how to use the client to perform complete tasks. If you have an example you could contribute, we'd love to hear from you! -# Transaction With Placeholder IDs +# Transaction With Conditional Create -The following example shows how to post a transaction with two resources, where one resource contains a reference to the other. A temporary ID (a UUID) is used as an ID to refer to, and this ID will be replaced by the server by a permanent ID. +The following example demonstrates a common scenario: How to create a new piece of data for a Patient (in this case, an Observation) where the identifier of the Patient is known, but the ID is not. -```java -{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ClientTransactionExamples.java|conditional}} -``` +In this scenario, we want to look up the Patient record and reference it from the newly created Observation. In the event that no Patient record already exists with the given identifier, a new one will be created and the Observation will reference it. This is known in FHIR as a [Conditional Create](http://hl7.org/fhir/http.html#ccreate). -This code creates the following transaction bundle: - **JSON**: ```json { "resourceType": "Bundle", "type": "transaction", - "entry": [ - { - "fullUrl": "urn:uuid:3bc44de3-069d-442d-829b-f3ef68cae371", - "resource": { - "resourceType": "Patient", - "identifier": [ - { - "system": "http://acme.org/mrns", - "value": "12345" - } - ], - "name": [ - { - "family": "Jameson", - "given": [ - "J", - "Jonah" - ] - } - ], - "gender": "male" + "entry": [ { + "fullUrl": "urn:uuid:3bc44de3-069d-442d-829b-f3ef68cae371", + "resource": { + "resourceType": "Patient", + "identifier": [ { + "system": "http://acme.org/mrns", + "value": "12345" + } ], + "name": [ { + "family": "Jameson", + "given": [ "J", "Jonah" ] + } ], + "gender": "male" + }, + "request": { + "method": "POST", + "url": "Patient", + "ifNoneExist": "identifier=http://acme.org/mrns|12345" + } + }, { + "resource": { + "resourceType": "Observation", + "status": "final", + "code": { + "coding": [ { + "system": "http://loinc.org", + "code": "789-8", + "display": "Erythrocytes [#/volume] in Blood by Automated count" + } ] }, - "request": { - "method": "POST", - "url": "Patient", - "ifNoneExist": "identifier=http://acme.org/mrns|12345" + "subject": { + "reference": "urn:uuid:3bc44de3-069d-442d-829b-f3ef68cae371" + }, + "valueQuantity": { + "value": 4.12, + "unit": "10 trillion/L", + "system": "http://unitsofmeasure.org", + "code": "10*12/L" } }, - { - "resource": { - "resourceType": "Observation", - "status": "final", - "code": { - "coding": [ - { - "system": "http://loinc.org", - "code": "789-8", - "display": "Erythrocytes [#/volume] in Blood by Automated count" - } - ] - }, - "subject": { - "reference": "urn:uuid:3bc44de3-069d-442d-829b-f3ef68cae371" - }, - "valueQuantity": { - "value": 4.12, - "unit": "10 trillion/L", - "system": "http://unitsofmeasure.org", - "code": "10*12/L" - } - }, - "request": { - "method": "POST", - "url": "Observation" - } + "request": { + "method": "POST", + "url": "Observation" } - ] + } ] } ``` @@ -164,6 +148,12 @@ The server responds with the following response. Note that the ID of the already ``` +To produce this transaction in Java code: + +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ClientTransactionExamples.java|conditional}} +``` + # Fetch all Pages of a Bundle This following example shows how to load all pages of a bundle by fetching each page one-after-the-other and then joining the results. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties index b766e3fa56f..899241f5e0e 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties @@ -45,9 +45,14 @@ page.server_jpa.schema=Database Schema page.server_jpa.configuration=Configuration page.server_jpa.search=Search page.server_jpa.performance=Performance -page.server_jpa.partitioning=Partitioning and Multitenancy page.server_jpa.upgrading=Upgrade Guide +section.server_jpa_partitioning.title=JPA Server: Partitioning and Multitenancy +page.server_jpa_partitioning.partitioning=Partitioning and Multitenancy +page.server_jpa_partitioning.partitioning_management_operations=Partitioning Management Operations +page.server_jpa_partitioning.enabling_in_hapi_fhir=Enabling Partitioning in HAPI FHIR + + section.interceptors.title=Interceptors page.interceptors.interceptors=Interceptors Overview page.interceptors.client_interceptors=Client Interceptors diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_client_interceptors.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_client_interceptors.md index 0d83f189397..9df29a8335f 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_client_interceptors.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_client_interceptors.md @@ -82,6 +82,9 @@ When communicating with a server that supports [URL Base Multitenancy](/docs/ser * [UrlTenantSelectionInterceptor JavaDoc](/apidocs/hapi-fhir-client/ca/uhn/fhir/rest/client/interceptor/UrlTenantSelectionInterceptor.html) * [UrlTenantSelectionInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/interceptor/UrlTenantSelectionInterceptor.java) +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ClientExamples.java|tenantId}} +``` # Performance: GZip Outgoing Request Bodies diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md index eaf3690558c..bb6fccad007 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md @@ -28,7 +28,7 @@ This interceptor will then produce output similar to the following: # Partitioning: Multitenant Request Partition -If the JPA server has [partitioning](/docs/server_jpa/partitioning.html) enabled, the RequestTenantPartitionInterceptor can be used in combination with a [Tenant Identification Strategy](/docs/server_plain/multitenancy.html) in order to achieve a multitenant solution. See [JPA Server Partitioning](/docs/server_jpa/partitioning.html) for more information on partitioning. +If the JPA server has [partitioning](/docs/server_jpa_partitioning/partitioning.html) enabled, the RequestTenantPartitionInterceptor can be used in combination with a [Tenant Identification Strategy](/docs/server_plain/multitenancy.html) in order to achieve a multitenant solution. See [JPA Server Partitioning](/docs/server_jpa_partitioning/partitioning.html) for more information on partitioning. * [RequestTenantPartitionInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.html) * [RequestTenantPartitionInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.java) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/schema.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/schema.md index bdcd28b8976..3514d0fbd0d 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/schema.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/schema.md @@ -39,7 +39,7 @@ The HFJ_RESOURCE table indicates a single resource of any type in the database. Integer Nullable - This is the optional partition ID, if the resource is in a partition. See Partitioning. + This is the optional partition ID, if the resource is in a partition. See Partitioning. @@ -48,7 +48,7 @@ The HFJ_RESOURCE table indicates a single resource of any type in the database. Timestamp Nullable - This is the optional partition date, if the resource is in a partition. See Partitioning. + This is the optional partition date, if the resource is in a partition. See Partitioning. @@ -154,7 +154,7 @@ The complete raw contents of the resource is stored in the `RES_TEXT` column, us Integer Nullable - This is the optional partition ID, if the resource is in a partition. See Partitioning. + This is the optional partition ID, if the resource is in a partition. See Partitioning. @@ -163,7 +163,7 @@ The complete raw contents of the resource is stored in the `RES_TEXT` column, us Timestamp Nullable - This is the optional partition date, if the resource is in a partition. See Partitioning. + This is the optional partition date, if the resource is in a partition. See Partitioning. @@ -263,7 +263,7 @@ If the server has been configured with a [Resource Server ID Strategy](/apidocs/ Integer Nullable - This is the optional partition ID, if the resource is in a partition. See Partitioning. + This is the optional partition ID, if the resource is in a partition. See Partitioning. @@ -272,7 +272,7 @@ If the server has been configured with a [Resource Server ID Strategy](/apidocs/ Timestamp Nullable - This is the optional partition date, if the resource is in a partition. See Partitioning. + This is the optional partition date, if the resource is in a partition. See Partitioning. @@ -332,7 +332,7 @@ When a resource is created or updated, it is indexed for searching. Any search p Integer Nullable - This is the optional partition ID, if the resource is in a partition. See Partitioning. + This is the optional partition ID, if the resource is in a partition. See Partitioning. Note that the partition indicated by the PARTITION_ID and PARTITION_DATE columns refers to the partition of the SOURCE resource, and not necessarily the TARGET. @@ -343,7 +343,7 @@ When a resource is created or updated, it is indexed for searching. Any search p Timestamp Nullable - This is the optional partition date, if the resource is in a partition. See Partitioning. + This is the optional partition date, if the resource is in a partition. See Partitioning. Note that the partition indicated by the PARTITION_ID and PARTITION_DATE columns refers to the partition of the SOURCE resource, and not necessarily the TARGET. @@ -448,7 +448,7 @@ The following columns are common to **all HFJ_SPIDX_xxx tables**. Integer Nullable - This is the optional partition ID, if the resource is in a partition. See Partitioning. + This is the optional partition ID, if the resource is in a partition. See Partitioning. Note that the partition indicated by the PARTITION_ID and PARTITION_DATE columns refers to the partition of the SOURCE resource, and not necessarily the TARGET. @@ -459,7 +459,7 @@ The following columns are common to **all HFJ_SPIDX_xxx tables**. Timestamp Nullable - This is the optional partition date, if the resource is in a partition. See Partitioning. + This is the optional partition date, if the resource is in a partition. See Partitioning. Note that the partition indicated by the PARTITION_ID and PARTITION_DATE columns refers to the partition of the SOURCE resource, and not necessarily the TARGET. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/enabling_in_hapi_fhir.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/enabling_in_hapi_fhir.md new file mode 100644 index 00000000000..ec177cccad5 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/enabling_in_hapi_fhir.md @@ -0,0 +1,11 @@ +# Enabling Partitioning in HAPI FHIR + +Follow these steps to enable partitioning on the server: + +The [PartitionSettings](/hapi-fhir/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionSettings.html) bean contains configuration settings related to partitioning within the server. To enable partitioning, the [setPartitioningEnabled(boolean)](/hapi-fhir/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionSettings.html#setPartitioningEnabled(boolean)) property should be enabled. + +The following settings can be enabled: + +* **Include Partition in Search Hashes** ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionSettings.html#setIncludePartitionInSearchHashes(boolean))): If this feature is enabled, partition IDs will be factored into [Search Hashes](/hapi-fhir/docs/server_jpa/schema.html#search-hashes). When this flag is not set (as is the default), when a search requests a specific partition, an additional SQL WHERE predicate is added to the query to explicitly request the given partition ID. When this flag is set, this additional WHERE predicate is not necessary since the partition is factored into the hash value being searched on. Setting this flag avoids the need to manually adjust indexes against the HFJ_SPIDX tables. Note that this flag should **not be used in environments where partitioning is being used for security purposes**, since it is possible for a user to reverse engineer false hash collisions. + +* **Cross-Partition Reference Mode**: ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionSettings.html#setAllowReferencesAcrossPartitions(ca.uhn.fhir.jpa.model.config.PartitionSettings.CrossPartitionReferenceMode))): This setting controls whether resources in one partition should be allowed to create references to resources in other partitions. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/partitioning.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning.md similarity index 57% rename from hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/partitioning.md rename to hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning.md index ae1bd0f108c..a9f6510d447 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/partitioning.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning.md @@ -26,16 +26,16 @@ Partitioning in HAPI FHIR JPA means that every resource has a partition identity * **Partition Date**: This is an additional partition discriminator that can be used to implement partitioning strategies using a date axis. -Mappings between the **Partition Name** and the **Partition ID** are maintained using the [Partition Mapping Operations](#partition-mapping-operations). +Mappings between the **Partition Name** and the **Partition ID** are maintained using the [Partition Management Operations](./partitioning_management_operations.html). ## Logical Architecture -At the database level, partitioning involves the use of two dedicated columns to many tables within the HAPI FHIR JPA [database schema](./schema.html): +At the database level, partitioning involves the use of two dedicated columns to many tables within the HAPI FHIR JPA [database schema](/hapi-fhir/docs/server_jpa/schema.html): * **PARTITION_ID** – This is an integer indicating the specific partition that a given resource is placed in. This column can also be *NULL*, meaning that the given resource is in the **Default Partition**. * **PARTITION_DATE** – This is a date/time column that can be assigned an arbitrary value depending on your use case. Typically, this would be used for use cases where data should be automatically dropped after a certain time period using native database partition drops. -When partitioning is used, these two columns will be populated with the same value for a given resource on all resource-specific tables (this includes [HFJ_RESOURCE](./schema.html#HFJ_RESOURCE) and all tables that have a foreign key relationship to it including [HFJ_RES_VER](./schema.html#HFJ_RES_VER), [HFJ_RESLINK](./schema.html#HFJ_RES_LINK), [HFJ_SPIDX_*](./schema.html#search-indexes), etc.) +When partitioning is used, these two columns will be populated with the same value for a given resource on all resource-specific tables (this includes [HFJ_RESOURCE](/hapi-fhir/docs/server_jpa/schema.html#HFJ_RESOURCE) and all tables that have a foreign key relationship to it including [HFJ_RES_VER](/hapi-fhir/docs/server_jpa/schema.html#HFJ_RES_VER), [HFJ_RESLINK](/hapi-fhir/docs/server_jpa/schema.html#HFJ_RES_LINK), [HFJ_SPIDX_*](/hapi-fhir/docs/server_jpa/schema.html#search-indexes), etc.) When a new resource is **created**, an [interceptor hook](#partition-interceptors) is invoked to request the partition ID and date to be assigned to the resource. @@ -46,18 +46,6 @@ When a **read operation** is being performed (e.g. a read, search, history, etc. * The system can be configured to operate as a **multitenant** solution by configuring the partition interceptor to scope all read operations to read data only from the partition that request has access to.``` * The system can be configured to operate with logical segments by configuring the partition interceptor to scope read operations to access all partitions. -# Enabling Partitioning in HAPI FHIR - -Follow these steps to enable partitioning on the server: - -The [PartitionSettings](/hapi-fhir/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionSettings.html) bean contains configuration settings related to partitioning within the server. To enable partitioning, the [setPartitioningEnabled(boolean)](/hapi-fhir/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionSettings.html#setPartitioningEnabled(boolean)) property should be enabled. - -The following settings can be enabled: - -* **Include Partition in Search Hashes** ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionSettings.html#setIncludePartitionInSearchHashes(boolean))): If this feature is enabled, partition IDs will be factored into [Search Hashes](./schema.html#search-hashes). When this flag is not set (as is the default), when a search requests a specific partition, an additional SQL WHERE predicate is added to the query to explicitly request the given partition ID. When this flag is set, this additional WHERE predicate is not necessary since the partition is factored into the hash value being searched on. Setting this flag avoids the need to manually adjust indexes against the HFJ_SPIDX tables. Note that this flag should **not be used in environments where partitioning is being used for security purposes**, since it is possible for a user to reverse engineer false hash collisions. - -* **Cross-Partition Reference Mode**: ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionSettings.html#setAllowReferencesAcrossPartitions(ca.uhn.fhir.jpa.model.config.PartitionSettings.CrossPartitionReferenceMode))): This setting controls whether resources in one partition should be allowed to create references to resources in other partitions. - # Partition Interceptors @@ -122,193 +110,6 @@ The following snippet shows a server with this configuration. {{snippet:classpath:/ca/uhn/hapi/fhir/docs/PartitionExamples.java|multitenantServer}} ``` - - -# Partition Mapping Operations - -Several operations exist that can be used to manage the existence of partitions. These operations are supplied by a [plain provider](/docs/server_plain/resource_providers.html#plain-providers) called [PartitionManagementProvider](/hapi-fhir/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/partition/PartitionManagementProvider.html). - -Before a partition can be used, it must be registered using these methods. - -## Creating a Partition - -The `$partition-management-add-partition` operation can be used to create a new partition. This operation takes the following parameters: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeCardinalityDescription
idInteger1..1 - The numeric ID for the partition. This value can be any integer, positive or negative or zero. It must not be a value that has already been used. -
nameCode1..1 - A code (string) to assign to the partition. -
descriptionString0..1 - An optional description for the partition. -
- -### Example - -An HTTP POST to the following URL would be used to invoke this operation: - -```url -http://example.com/$partition-management-add-partition -``` - -The following request body could be used: - -```json -{ - "resourceType": "Parameters", - "parameter": [ { - "name": "id", - "valueInteger": 123 - }, { - "name": "name", - "valueCode": "PARTITION-123" - }, { - "name": "description", - "valueString": "a description" - } ] -} -``` - -## Updating a Partition - -The `$partition-management-update-partition` operation can be used to update an existing partition. This operation takes the following parameters: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeCardinalityDescription
idInteger1..1 - The numeric ID for the partition to update. This ID must already exist. -
nameCode1..1 - A code (string) to assign to the partition. Note that it is acceptable to change the name of a partition, but this should be done with caution since partition names may be referenced by URLs, caches, etc. -
descriptionString0..1 - An optional description for the partition. -
- -### Example - -An HTTP POST to the following URL would be used to invoke this operation: - -```url -http://example.com/$partition-management-add-partition -``` - -The following request body could be used: - -```json -{ - "resourceType": "Parameters", - "parameter": [ { - "name": "id", - "valueInteger": 123 - }, { - "name": "name", - "valueCode": "PARTITION-123" - }, { - "name": "description", - "valueString": "a description" - } ] -} -``` - -## Deleting a Partition - -The `$partition-management-delete-partition` operation can be used to delete an existing partition. This operation takes the following parameters: - - - - - - - - - - - - - - - - - - -
NameTypeCardinalityDescription
idInteger1..1 - The numeric ID for the partition to update. This ID must already exist. -
- -### Example - -An HTTP POST to the following URL would be used to invoke this operation: - -```url -http://example.com/$partition-management-delete-partition -``` - -The following request body could be used: - -```json -{ - "resourceType": "Parameters", - "parameter": [ { - "name": "id", - "valueInteger": 123 - } ] -} -``` - # Limitations @@ -328,5 +129,7 @@ None of the limitations listed here are considered permanent. Over time the HAPI * ConceptMap * **Search Parameters are not partitioned**: There is only one set of SearchParameter resources for the entire system, and any search parameters will apply to resources in all partitions. All SearchParameter resources must be stored in the default partition. + +* **Cross-partition History Operations are not supported**: It is not possible to perform a `_history` operation that spans all partitions (`_history` does work when applied to a single partition however). * **Bulk Operations are not partition aware**: Bulk export operations will export data across all partitions. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning_management_operations.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning_management_operations.md new file mode 100644 index 00000000000..610d3f8a1d6 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_partitioning/partitioning_management_operations.md @@ -0,0 +1,185 @@ +# Partition Mapping Operations + +Several operations exist that can be used to manage the existence of partitions. These operations are supplied by a [plain provider](/docs/server_plain/resource_providers.html#plain-providers) called [PartitionManagementProvider](/hapi-fhir/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/partition/PartitionManagementProvider.html). + +Before a partition can be used, it must be registered using these methods. + +## Creating a Partition + +The `$partition-management-create-partition` operation can be used to create a new partition. This operation takes the following parameters: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeCardinalityDescription
idInteger1..1 + The numeric ID for the partition. This value can be any integer, positive or negative or zero. It must not be a value that has already been used. +
nameCode1..1 + A code (string) to assign to the partition. +
descriptionString0..1 + An optional description for the partition. +
+ +### Example + +An HTTP POST to the following URL would be used to invoke this operation: + +```url +http://example.com/$partition-management-create-partition +``` + +The following request body could be used: + +```json +{ + "resourceType": "Parameters", + "parameter": [ { + "name": "id", + "valueInteger": 123 + }, { + "name": "name", + "valueCode": "PARTITION-123" + }, { + "name": "description", + "valueString": "a description" + } ] +} +``` + +## Updating a Partition + +The `$partition-management-update-partition` operation can be used to update an existing partition. This operation takes the following parameters: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeCardinalityDescription
idInteger1..1 + The numeric ID for the partition to update. This ID must already exist. +
nameCode1..1 + A code (string) to assign to the partition. Note that it is acceptable to change the name of a partition, but this should be done with caution since partition names may be referenced by URLs, caches, etc. +
descriptionString0..1 + An optional description for the partition. +
+ +### Example + +An HTTP POST to the following URL would be used to invoke this operation: + +```url +http://example.com/$partition-management-create-partition +``` + +The following request body could be used: + +```json +{ + "resourceType": "Parameters", + "parameter": [ { + "name": "id", + "valueInteger": 123 + }, { + "name": "name", + "valueCode": "PARTITION-123" + }, { + "name": "description", + "valueString": "a description" + } ] +} +``` + +## Deleting a Partition + +The `$partition-management-delete-partition` operation can be used to delete an existing partition. This operation takes the following parameters: + + + + + + + + + + + + + + + + + + +
NameTypeCardinalityDescription
idInteger1..1 + The numeric ID for the partition to update. This ID must already exist. +
+ +### Example + +An HTTP POST to the following URL would be used to invoke this operation: + +```url +http://example.com/$partition-management-delete-partition +``` + +The following request body could be used: + +```json +{ + "resourceType": "Parameters", + "parameter": [ { + "name": "id", + "valueInteger": 123 + } ] +} +``` + diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/client/JaxRsHttpRequest.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/client/JaxRsHttpRequest.java index ea02fb64e91..86cc93bda7c 100644 --- a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/client/JaxRsHttpRequest.java +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/client/JaxRsHttpRequest.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jaxrs.client; */ import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.client.api.BaseHttpRequest; import ca.uhn.fhir.rest.client.api.IHttpRequest; import ca.uhn.fhir.rest.client.api.IHttpResponse; import ca.uhn.fhir.util.StopWatch; @@ -28,7 +29,11 @@ import ca.uhn.fhir.util.StopWatch; import javax.ws.rs.client.Entity; import javax.ws.rs.client.Invocation; import javax.ws.rs.core.Response; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; /** * A Http Request based on JaxRs. This is an adapter around the class @@ -36,7 +41,7 @@ import java.util.*; * * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare */ -public class JaxRsHttpRequest implements IHttpRequest { +public class JaxRsHttpRequest extends BaseHttpRequest implements IHttpRequest { private final Map> myHeaders = new HashMap<>(); private Invocation.Builder myRequest; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImpl.java index 4c5547fc7cc..0dbf4bcfd4e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImpl.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.bulk; */ import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; @@ -231,7 +232,7 @@ public class BulkDataExportSvcImpl implements IBulkDataExportSvc { map.setLastUpdated(new DateRangeParam(job.getSince(), null)); } - IResultIterator resultIterator = sb.createQuery(map, new SearchRuntimeDetails(null, theJobUuid), null, null); + IResultIterator resultIterator = sb.createQuery(map, new SearchRuntimeDetails(null, theJobUuid), null, RequestPartitionId.allPartitions()); storeResultsToFiles(nextCollection, sb, resultIterator, jobResourceCounter, jobStopwatch); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 06379bf1d55..e77589637d7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -5,22 +5,27 @@ import ca.uhn.fhir.i18n.HapiLocalizer; import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.interceptor.executor.InterceptorService; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IDao; import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider; import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; import ca.uhn.fhir.jpa.bulk.BulkDataExportProvider; import ca.uhn.fhir.jpa.bulk.BulkDataExportSvcImpl; import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.dao.HistoryBuilder; +import ca.uhn.fhir.jpa.dao.HistoryBuilderFactory; import ca.uhn.fhir.jpa.dao.ISearchBuilder; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; import ca.uhn.fhir.jpa.dao.index.DaoResourceLinkResolver; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.graphql.JpaStorageServices; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; import ca.uhn.fhir.jpa.model.sched.ISchedulerService; import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; -import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperService; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl; import ca.uhn.fhir.jpa.partition.PartitionManagementProvider; -import ca.uhn.fhir.jpa.partition.RequestPartitionHelperService; +import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.sched.AutowiringSpringBeanJobFactory; @@ -44,6 +49,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices; import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor; import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -64,6 +70,9 @@ import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import javax.annotation.Nullable; +import java.util.Date; + /* * #%L * HAPI FHIR JPA Server @@ -103,9 +112,12 @@ public abstract class BaseConfig { public static final String JPA_VALIDATION_SUPPORT_CHAIN = "myJpaValidationSupportChain"; public static final String TASK_EXECUTOR_NAME = "hapiJpaTaskExecutor"; public static final String GRAPHQL_PROVIDER_NAME = "myGraphQLProvider"; - private static final String HAPI_DEFAULT_SCHEDULER_GROUP = "HAPI"; public static final String PERSISTED_JPA_BUNDLE_PROVIDER = "PersistedJpaBundleProvider"; + public static final String PERSISTED_JPA_BUNDLE_PROVIDER_BY_SEARCH = "PersistedJpaBundleProvider_BySearch"; public static final String PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER = "PersistedJpaSearchFirstPageBundleProvider"; + private static final String HAPI_DEFAULT_SCHEDULER_GROUP = "HAPI"; + public static final String SEARCH_BUILDER = "SearchBuilder"; + public static final String HISTORY_BUILDER = "HistoryBuilder"; @Autowired protected Environment myEnv; @@ -213,8 +225,8 @@ public abstract class BaseConfig { } @Bean - public IRequestPartitionHelperService requestPartitionHelperService() { - return new RequestPartitionHelperService(); + public IRequestPartitionHelperSvc requestPartitionHelperService() { + return new RequestPartitionHelperSvc(); } @Bean @@ -291,18 +303,46 @@ public abstract class BaseConfig { return new PersistedJpaBundleProviderFactory(); } - @Bean(name= PERSISTED_JPA_BUNDLE_PROVIDER) + @Bean(name = PERSISTED_JPA_BUNDLE_PROVIDER) @Scope("prototype") public PersistedJpaBundleProvider persistedJpaBundleProvider(RequestDetails theRequest, String theUuid) { return new PersistedJpaBundleProvider(theRequest, theUuid); } - @Bean(name= PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER) + @Bean(name = PERSISTED_JPA_BUNDLE_PROVIDER_BY_SEARCH) + @Scope("prototype") + public PersistedJpaBundleProvider persistedJpaBundleProvider(RequestDetails theRequest, Search theSearch) { + return new PersistedJpaBundleProvider(theRequest, theSearch); + } + + @Bean(name = PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER) @Scope("prototype") public PersistedJpaSearchFirstPageBundleProvider persistedJpaSearchFirstPageBundleProvider(RequestDetails theRequest, Search theSearch, SearchCoordinatorSvcImpl.SearchTask theSearchTask, ISearchBuilder theSearchBuilder) { return new PersistedJpaSearchFirstPageBundleProvider(theSearch, theSearchTask, theSearchBuilder, theRequest); } + @Bean + public SearchBuilderFactory searchBuilderFactory() { + return new SearchBuilderFactory(); + } + + @Bean(name = SEARCH_BUILDER) + @Scope("prototype") + public SearchBuilder persistedJpaSearchFirstPageBundleProvider(IDao theDao, String theResourceName, Class theResourceType) { + return new SearchBuilder(theDao, theResourceName, theResourceType); + } + + @Bean + public HistoryBuilderFactory historyBuilderFactory() { + return new HistoryBuilderFactory(); + } + + @Bean(name = HISTORY_BUILDER) + @Scope("prototype") + public HistoryBuilder persistedJpaSearchFirstPageBundleProvider(@Nullable String theResourceType, @Nullable Long theResourceId, @Nullable Date theRangeStartInclusive, @Nullable Date theRangeEndInclusive) { + return new HistoryBuilder(theResourceType, theResourceId, theRangeStartInclusive, theRangeEndInclusive); + } + public static void configureEntityManagerFactory(LocalContainerEntityManagerFactoryBean theFactory, FhirContext theCtx) { theFactory.setJpaDialect(hibernateJpaDialect(theCtx.getLocalizer())); theFactory.setPackagesToScan("ca.uhn.fhir.jpa.model.entity", "ca.uhn.fhir.jpa.entity"); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 4892135426e..efa2e1f8cfc 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -11,6 +11,7 @@ import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IDao; @@ -26,15 +27,28 @@ import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor; import ca.uhn.fhir.jpa.delete.DeleteConflictService; +import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.jpa.entity.ResourceSearchView; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.SearchTypeEnum; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; -import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.entity.BaseHasResource; +import ca.uhn.fhir.jpa.model.entity.BaseTag; +import ca.uhn.fhir.jpa.model.entity.ForcedId; +import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity; +import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTag; +import ca.uhn.fhir.jpa.model.entity.TagDefinition; +import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; import ca.uhn.fhir.jpa.model.search.SearchStatusEnum; import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; +import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory; import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc; import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; @@ -76,7 +90,16 @@ import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.Validate; -import org.hl7.fhir.instance.model.api.*; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseCoding; +import org.hl7.fhir.instance.model.api.IBaseHasExtensions; +import org.hl7.fhir.instance.model.api.IBaseMetaType; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IDomainResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Bundle.HTTPVerb; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -100,8 +123,19 @@ import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Root; import javax.xml.stream.events.Characters; import javax.xml.stream.events.XMLEvent; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.UUID; import static org.apache.commons.lang3.StringUtils.defaultIfBlank; import static org.apache.commons.lang3.StringUtils.defaultString; @@ -164,14 +198,17 @@ public abstract class BaseHapiFhirDao extends BaseStora protected IResourceTableDao myResourceTableDao; @Autowired protected IResourceTagDao myResourceTagDao; - @Autowired protected DeleteConflictService myDeleteConflictService; @Autowired protected IInterceptorBroadcaster myInterceptorBroadcaster; @Autowired + protected DaoRegistry myDaoRegistry; + @Autowired ExpungeService myExpungeService; @Autowired + private HistoryBuilderFactory myHistoryBuilderFactory; + @Autowired private DaoConfig myConfig; @Autowired private PlatformTransactionManager myPlatformTransactionManager; @@ -180,8 +217,6 @@ public abstract class BaseHapiFhirDao extends BaseStora @Autowired private ISearchParamPresenceSvc mySearchParamPresenceSvc; @Autowired - protected DaoRegistry myDaoRegistry; - @Autowired private SearchParamWithInlineReferencesExtractor mySearchParamWithInlineReferencesExtractor; @Autowired private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer; @@ -191,6 +226,12 @@ public abstract class BaseHapiFhirDao extends BaseStora private ApplicationContext myApplicationContext; @Autowired private PartitionSettings myPartitionSettings; + @Autowired + private RequestPartitionHelperSvc myRequestPartitionHelperSvc; + @Autowired + private PersistedJpaBundleProviderFactory myPersistedJpaBundleProviderFactory; + @Autowired + private IPartitionLookupSvc myPartitionLookupSvc; @Override protected IInterceptorBroadcaster getInterceptorBroadcaster() { @@ -384,48 +425,23 @@ public abstract class BaseHapiFhirDao extends BaseStora } } + protected IBundleProvider history(RequestDetails theRequest, String theResourceType, Long theResourcePid, Date theRangeStartInclusive, Date theRangeEndInclusive) { - protected IBundleProvider history(RequestDetails theRequest, String theResourceName, Long theId, Date theSince, Date theUntil) { - - String resourceName = defaultIfBlank(theResourceName, null); + String resourceName = defaultIfBlank(theResourceType, null); Search search = new Search(); search.setDeleted(false); search.setCreated(new Date()); - search.setLastUpdated(theSince, theUntil); + search.setLastUpdated(theRangeStartInclusive, theRangeEndInclusive); search.setUuid(UUID.randomUUID().toString()); search.setResourceType(resourceName); - search.setResourceId(theId); + search.setResourceId(theResourcePid); search.setSearchType(SearchTypeEnum.HISTORY); search.setStatus(SearchStatusEnum.FINISHED); - if (theSince != null) { - if (resourceName == null) { - search.setTotalCount(myResourceHistoryTableDao.countForAllResourceTypes(theSince)); - } else if (theId == null) { - search.setTotalCount(myResourceHistoryTableDao.countForResourceType(resourceName, theSince)); - } else { - search.setTotalCount(myResourceHistoryTableDao.countForResourceInstance(theId, theSince)); - } - } else { - if (resourceName == null) { - search.setTotalCount(myResourceHistoryTableDao.countForAllResourceTypes()); - } else if (theId == null) { - search.setTotalCount(myResourceHistoryTableDao.countForResourceType(resourceName)); - } else { - search.setTotalCount(myResourceHistoryTableDao.countForResourceInstance(theId)); - } - } - - search = mySearchCacheSvc.save(search); - - return myPersistedJpaBundleProviderFactory.newInstance(theRequest, search.getUuid()); + return myPersistedJpaBundleProviderFactory.newInstance(theRequest, search); } - @Autowired - private PersistedJpaBundleProviderFactory myPersistedJpaBundleProviderFactory; - - void incrementId(T theResource, ResourceTable theSavedEntity, IIdType theResourceId) { String newVersion; long newVersionLong; @@ -807,7 +823,11 @@ public abstract class BaseHapiFhirDao extends BaseStora ResourceHistoryTable history = (ResourceHistoryTable) theEntity; resourceBytes = history.getResource(); resourceEncoding = history.getEncoding(); - myTagList = history.getTags(); + if (history.isHasTags()) { + myTagList = history.getTags(); + } else { + myTagList = Collections.emptyList(); + } version = history.getVersion(); if (history.getProvenance() != null) { provenanceRequestId = history.getProvenance().getRequestId(); @@ -829,7 +849,11 @@ public abstract class BaseHapiFhirDao extends BaseStora } resourceBytes = history.getResource(); resourceEncoding = history.getEncoding(); - myTagList = resource.getTags(); + if (resource.isHasTags()) { + myTagList = resource.getTags(); + } else { + myTagList = Collections.emptyList(); + } version = history.getVersion(); if (history.getProvenance() != null) { provenanceRequestId = history.getProvenance().getRequestId(); @@ -924,6 +948,17 @@ public abstract class BaseHapiFhirDao extends BaseStora } + // 7. Add partition information + if (myPartitionSettings.isPartitioningEnabled()) { + RequestPartitionId partitionId = theEntity.getPartitionId(); + if (partitionId != null && partitionId.getPartitionId() != null) { + PartitionEntity persistedPartition = myPartitionLookupSvc.getPartitionById(partitionId.getPartitionId()); + retVal.setUserData(Constants.RESOURCE_PARTITION_ID, persistedPartition.toRequestPartitionId()); + } else { + retVal.setUserData(Constants.RESOURCE_PARTITION_ID, null); + } + } + return retVal; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 45d07ea8fa0..08996e75323 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -45,7 +45,7 @@ import ca.uhn.fhir.jpa.model.entity.TagDefinition; import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails; import ca.uhn.fhir.jpa.model.util.JpaConstants; -import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperService; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; @@ -145,7 +145,7 @@ public abstract class BaseHapiFhirResourceDao extends B private String myResourceName; private Class myResourceType; @Autowired - private IRequestPartitionHelperService myRequestPartitionHelperService; + private IRequestPartitionHelperSvc myRequestPartitionHelperService; @Autowired private PartitionSettings myPartitionSettings; @@ -216,7 +216,7 @@ public abstract class BaseHapiFhirResourceDao extends B theResource.setUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED, Boolean.TRUE); } - RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequestDetails, theResource); + RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequestDetails, theResource, getResourceName()); return doCreate(theResource, theIfNoneExist, thePerformIndexing, theUpdateTimestamp, theRequestDetails, requestPartitionId); } @@ -685,11 +685,6 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public IBundleProvider history(Date theSince, Date theUntil, RequestDetails theRequestDetails) { - if (myPartitionSettings.isPartitioningEnabled()) { - String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "noSystemOrTypeHistoryForPartitionAwareServer"); - throw new MethodNotAllowedException(msg); - } - // Notify interceptors ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails); notifyInterceptors(RestOperationTypeEnum.HISTORY_TYPE, requestDetails); @@ -1005,19 +1000,19 @@ public abstract class BaseHapiFhirResourceDao extends B public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId, RequestDetails theRequest) { validateResourceTypeAndThrowInvalidRequestException(theId); - @Nullable RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, getResourceName()); + RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, getResourceName()); ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(requestPartitionId, getResourceName(), theId.getIdPart()); BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid.getIdAsLong()); // Verify that the resource is for the correct partition - if (requestPartitionId != null) { + if (!requestPartitionId.isAllPartitions()) { if (requestPartitionId.getPartitionId() == null) { - if (entity.getPartitionId() != null) { + if (entity.getPartitionId().getPartitionId() != null) { ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", requestPartitionId, entity.getPartitionId()); entity = null; } - } else if (entity.getPartitionId() != null) { - if (!entity.getPartitionId().getPartitionId().equals(requestPartitionId.getPartitionId())) { + } else if (entity.getPartitionId().getPartitionId() != null) { + if (!requestPartitionId.getPartitionId().equals(entity.getPartitionId().getPartitionId())) { ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", requestPartitionId, entity.getPartitionId()); entity = null; } @@ -1314,7 +1309,7 @@ public abstract class BaseHapiFhirResourceDao extends B */ resourceId = theResource.getIdElement(); - RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequest, theResource); + RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequest, theResource, getResourceName()); try { entity = readEntityLatestVersion(resourceId, requestPartitionId); } catch (ResourceNotFoundException e) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java index 7d179146267..10adc9abcb7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java @@ -8,7 +8,6 @@ import ca.uhn.fhir.jpa.util.ResourceCountCache; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.util.StopWatch; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -80,11 +79,6 @@ public abstract class BaseHapiFhirSystemDao extends BaseHapiFhirDao criteriaQuery = cb.createQuery(Long.class); + Root from = criteriaQuery.from(ResourceHistoryTable.class); + criteriaQuery.select(cb.count(from)); + + addPredicatesToQuery(cb, thePartitionId, criteriaQuery, from); + + TypedQuery query = myEntityManager.createQuery(criteriaQuery); + return query.getSingleResult(); + } + + @SuppressWarnings("OptionalIsPresent") + public List fetchEntities(RequestPartitionId thePartitionId, int theFromIndex, int theToIndex) { + CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = cb.createQuery(ResourceHistoryTable.class); + Root from = criteriaQuery.from(ResourceHistoryTable.class); + + addPredicatesToQuery(cb, thePartitionId, criteriaQuery, from); + + from.fetch("myProvenance", JoinType.LEFT); + + criteriaQuery.orderBy(cb.desc(from.get("myUpdated"))); + + TypedQuery query = myEntityManager.createQuery(criteriaQuery); + + query.setFirstResult(theFromIndex); + query.setMaxResults(theToIndex - theFromIndex); + + List tables = query.getResultList(); + if (tables.size() > 0) { + ImmutableListMultimap resourceIdToHistoryEntries = Multimaps.index(tables, ResourceHistoryTable::getResourceId); + + Map> pidToForcedId = myIdHelperService.translatePidsToForcedIds(resourceIdToHistoryEntries.keySet()); + ourLog.trace("Translated IDs: {}", pidToForcedId); + + for (Long nextResourceId : resourceIdToHistoryEntries.keySet()) { + List historyTables = resourceIdToHistoryEntries.get(nextResourceId); + + String resourceId; + Optional forcedId = pidToForcedId.get(nextResourceId); + if (forcedId.isPresent()) { + resourceId = forcedId.get(); + } else { + resourceId = nextResourceId.toString(); + } + + for (ResourceHistoryTable nextHistoryTable : historyTables) { + nextHistoryTable.setTransientForcedId(resourceId); + } + } + } + + return tables; + } + + private void addPredicatesToQuery(CriteriaBuilder theCriteriaBuilder, RequestPartitionId thePartitionId, CriteriaQuery theQuery, Root theFrom) { + List predicates = new ArrayList<>(); + + if (!thePartitionId.isAllPartitions()) { + if (thePartitionId.getPartitionId() != null) { + predicates.add(theCriteriaBuilder.equal(theFrom.get("myPartitionIdValue").as(Integer.class), thePartitionId.getPartitionId())); + } else { + predicates.add(theCriteriaBuilder.isNull(theFrom.get("myPartitionIdValue").as(Integer.class))); + } + } + + if (myResourceId != null) { + predicates.add(theCriteriaBuilder.equal(theFrom.get("myResourceId"), myResourceId)); + } else if (myResourceType != null) { + validateNotSearchingAllPartitions(thePartitionId); + predicates.add(theCriteriaBuilder.equal(theFrom.get("myResourceType"), myResourceType)); + } else { + validateNotSearchingAllPartitions(thePartitionId); + } + + if (myRangeStartInclusive != null) { + predicates.add(theCriteriaBuilder.greaterThanOrEqualTo(theFrom.get("myUpdated").as(Date.class), myRangeStartInclusive)); + } + if (myRangeEndInclusive != null) { + predicates.add(theCriteriaBuilder.lessThanOrEqualTo(theFrom.get("myUpdated").as(Date.class), myRangeEndInclusive)); + } + + if (predicates.size() > 0) { + theQuery.where(toPredicateArray(predicates)); + } + } + + private void validateNotSearchingAllPartitions(RequestPartitionId thePartitionId) { + if (myPartitionSettings.isPartitioningEnabled()) { + if (thePartitionId.isAllPartitions()) { + String msg = myCtx.getLocalizer().getMessage(HistoryBuilder.class, "noSystemOrTypeHistoryForPartitionAwareServer"); + throw new InvalidRequestException(msg); + } + } + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilderFactory.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilderFactory.java new file mode 100644 index 00000000000..0905ac92311 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilderFactory.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.jpa.dao; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.config.BaseConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; + +import javax.annotation.Nullable; +import java.util.Date; + +public class HistoryBuilderFactory { + + @Autowired + private ApplicationContext myApplicationContext; + + public HistoryBuilder newHistoryBuilder(@Nullable String theResourceType, @Nullable Long theResourceId, @Nullable Date theRangeStartInclusive, @Nullable Date theRangeEndInclusive) { + return (HistoryBuilder) myApplicationContext.getBean(BaseConfig.HISTORY_BUILDER, theResourceType, theResourceId, theRangeStartInclusive, theRangeEndInclusive); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchBuilder.java index 090e47a7a1e..a8055e0a8e7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchBuilder.java @@ -30,6 +30,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.DateRangeParam; import org.hl7.fhir.instance.model.api.IBaseResource; +import javax.annotation.Nonnull; import javax.persistence.EntityManager; import java.util.Collection; import java.util.Iterator; @@ -38,7 +39,7 @@ import java.util.Set; public interface ISearchBuilder { - IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntime, RequestDetails theRequest, RequestPartitionId theRequestPartitionId); + IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntime, RequestDetails theRequest, @Nonnull RequestPartitionId theRequestPartitionId); Iterator createCountQuery(SearchParameterMap theParams, String theSearchUuid, RequestDetails theRequest, RequestPartitionId theRequestPartitionId); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index ea93205d9ab..795fbc65877 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -56,6 +56,7 @@ import ca.uhn.fhir.jpa.searchparam.util.Dstu3DistanceHelper; import ca.uhn.fhir.jpa.util.BaseIterator; import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; +import ca.uhn.fhir.jpa.util.QueryChunker; import ca.uhn.fhir.jpa.util.ScrollableResultsIterator; import ca.uhn.fhir.jpa.util.SqlQueryList; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -88,8 +89,6 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Component; import javax.annotation.Nonnull; import javax.persistence.EntityManager; @@ -122,8 +121,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * The SearchBuilder is responsible for actually forming the SQL query that handles * searches for resources */ -@Component -@Scope("prototype") public class SearchBuilder implements ISearchBuilder { /** @@ -175,7 +172,7 @@ public class SearchBuilder implements ISearchBuilder { /** * Constructor */ - SearchBuilder(IDao theDao, String theResourceName, Class theResourceType) { + public SearchBuilder(IDao theDao, String theResourceName, Class theResourceType) { myCallingDao = theDao; myResourceName = theResourceName; myResourceType = theResourceType; @@ -225,7 +222,9 @@ public class SearchBuilder implements ISearchBuilder { } @Override - public Iterator createCountQuery(SearchParameterMap theParams, String theSearchUuid, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { + public Iterator createCountQuery(SearchParameterMap theParams, String theSearchUuid, RequestDetails theRequest, @Nonnull RequestPartitionId theRequestPartitionId) { + assert theRequestPartitionId != null; + init(theParams, theSearchUuid, theRequestPartitionId); TypedQuery query = createQuery(null, null, true, theRequest); @@ -241,7 +240,9 @@ public class SearchBuilder implements ISearchBuilder { } @Override - public IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { + public IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest, @Nonnull RequestPartitionId theRequestPartitionId) { + assert theRequestPartitionId != null; + init(theParams, theSearchRuntimeDetails.getSearchUuid(), theRequestPartitionId); if (myPidSet == null) { @@ -359,7 +360,7 @@ public class SearchBuilder implements ISearchBuilder { myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myResourceType"), myResourceName)); } myQueryRoot.addPredicate(myCriteriaBuilder.isNull(myQueryRoot.get("myDeleted"))); - if (myRequestPartitionId != null) { + if (!myRequestPartitionId.isAllPartitions()) { if (myRequestPartitionId.getPartitionId() != null) { myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myPartitionIdValue").as(Integer.class), myRequestPartitionId.getPartitionId())); } else { @@ -614,19 +615,10 @@ public class SearchBuilder implements ISearchBuilder { theResourceListToPopulate.add(null); } - /* - * As always, Oracle can't handle things that other databases don't mind.. In this - * case it doesn't like more than ~1000 IDs in a single load, so we break this up - * if it's lots of IDs. I suppose maybe we should be doing this as a join anyhow - * but this should work too. Sigh. - */ List pids = new ArrayList<>(thePids); - for (int i = 0; i < pids.size(); i += MAXIMUM_PAGE_SIZE) { - int to = i + MAXIMUM_PAGE_SIZE; - to = Math.min(to, pids.size()); - List pidsSubList = pids.subList(i, to); - doLoadPids(pidsSubList, theIncludedPids, theResourceListToPopulate, theForHistoryOperation, position, theDetails); - } + new QueryChunker().chunk(pids, t->{ + doLoadPids(t, theIncludedPids, theResourceListToPopulate, theForHistoryOperation, position, theDetails); + }); } @@ -895,7 +887,7 @@ public class SearchBuilder implements ISearchBuilder { private void addPredicateCompositeStringUnique(@Nonnull SearchParameterMap theParams, String theIndexedString, RequestPartitionId theRequestPartitionId) { Join join = myQueryRoot.join("myParamsCompositeStringUnique", JoinType.LEFT); - if (theRequestPartitionId != null) { + if (!theRequestPartitionId.isAllPartitions()) { Integer partitionId = theRequestPartitionId.getPartitionId(); Predicate predicate = myCriteriaBuilder.equal(join.get("myPartitionIdValue").as(Integer.class), partitionId); myQueryRoot.addPredicate(predicate); @@ -1276,7 +1268,7 @@ public class SearchBuilder implements ISearchBuilder { return ResourcePersistentId.fromLongList(query.getResultList()); } - private static Predicate[] toPredicateArray(List thePredicates) { + static Predicate[] toPredicateArray(List thePredicates) { return thePredicates.toArray(new Predicate[0]); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java index 820e45e7875..f1d93c91d13 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java @@ -21,12 +21,18 @@ package ca.uhn.fhir.jpa.dao; */ import ca.uhn.fhir.jpa.api.dao.IDao; +import ca.uhn.fhir.jpa.config.BaseConfig; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.springframework.beans.factory.annotation.Lookup; -import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; + +public class SearchBuilderFactory { + + @Autowired + private ApplicationContext myApplicationContext; + + public ISearchBuilder newSearchBuilder(IDao theDao, String theResourceName, Class theResourceType) { + return (ISearchBuilder) myApplicationContext.getBean(BaseConfig.SEARCH_BUILDER, theDao, theResourceName, theResourceType); + } -@Service -public abstract class SearchBuilderFactory { - @Lookup - public abstract ISearchBuilder newSearchBuilder(IDao theDao, String theResourceName, Class theResourceType); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java index 1a8d31c684e..a64c375adcb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java @@ -1,5 +1,11 @@ package ca.uhn.fhir.jpa.dao.data; +import ca.uhn.fhir.jpa.model.entity.ForcedId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + import java.util.Collection; import java.util.List; import java.util.Optional; @@ -24,15 +30,11 @@ import java.util.Optional; * #L% */ -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import ca.uhn.fhir.jpa.model.entity.ForcedId; - public interface IForcedIdDao extends JpaRepository { + @Query("SELECT f FROM ForcedId f WHERE myResourcePid IN (:resource_pids)") + List findAllByResourcePid(@Param("resource_pids") List theResourcePids); + @Query("SELECT f.myResourcePid FROM ForcedId f WHERE myForcedId IN (:forced_id)") List findByForcedId(@Param("forced_id") Collection theForcedId); @@ -46,7 +48,7 @@ public interface IForcedIdDao extends JpaRepository { Optional findByPartitionIdAndTypeAndForcedId(@Param("partition_id") Integer thePartitionId, @Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId); @Query("SELECT f FROM ForcedId f WHERE f.myResourcePid = :resource_pid") - ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid); + Optional findByResourcePid(@Param("resource_pid") Long theResourcePid); @Modifying @Query("DELETE FROM ForcedId t WHERE t.myId = :pid") @@ -75,7 +77,7 @@ public interface IForcedIdDao extends JpaRepository { /** * Warning: No DB index exists for this particular query, so it may not perform well - * + *

* This method returns a Collection where each row is an element in the collection. Each element in the collection * is an object array, where the order matters (the array represents columns returned by the query). Be careful if you change this query in any way. */ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTableDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTableDao.java index 2366c7d9eaa..63d1779ae2d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTableDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTableDao.java @@ -6,13 +6,8 @@ import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.Temporal; import org.springframework.data.repository.query.Param; -import javax.persistence.TemporalType; -import java.util.Collection; -import java.util.Date; - /* * #%L * HAPI FHIR JPA Server @@ -35,36 +30,6 @@ import java.util.Date; public interface IResourceHistoryTableDao extends JpaRepository { - @Query("SELECT COUNT(*) FROM ResourceHistoryTable t WHERE t.myUpdated >= :cutoff") - int countForAllResourceTypes( - @Temporal(value = TemporalType.TIMESTAMP) @Param("cutoff") Date theCutoff - ); - - @Query("SELECT COUNT(*) FROM ResourceHistoryTable t") - int countForAllResourceTypes( - ); - - @Query("SELECT COUNT(*) FROM ResourceHistoryTable t WHERE t.myResourceId = :id AND t.myUpdated >= :cutoff") - int countForResourceInstance( - @Param("id") Long theId, - @Temporal(value = TemporalType.TIMESTAMP) @Param("cutoff") Date theCutoff - ); - - @Query("SELECT COUNT(*) FROM ResourceHistoryTable t WHERE t.myResourceId = :id") - int countForResourceInstance( - @Param("id") Long theId - ); - - @Query("SELECT COUNT(*) FROM ResourceHistoryTable t WHERE t.myResourceType = :type AND t.myUpdated >= :cutoff") - int countForResourceType( - @Param("type") String theType, - @Temporal(value = TemporalType.TIMESTAMP) @Param("cutoff") Date theCutoff - ); - - @Query("SELECT COUNT(*) FROM ResourceHistoryTable t WHERE t.myResourceType = :type") - int countForResourceType( - @Param("type") String theType - ); @Query("SELECT t FROM ResourceHistoryTable t LEFT OUTER JOIN FETCH t.myProvenance WHERE t.myResourceId = :id AND t.myResourceVersion = :version") ResourceHistoryTable findForIdAndVersionAndFetchProvenance(@Param("id") long theId, @Param("version") long theVersion); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java index 88c86d150bd..25fc81202f6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java @@ -43,6 +43,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.springframework.beans.factory.annotation.Autowired; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @@ -63,7 +64,7 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver { private DaoRegistry myDaoRegistry; @Override - public IResourceLookup findTargetResource(RequestPartitionId theRequestPartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theResourceType, Class theType, IBaseReference theReference, RequestDetails theRequest) { + public IResourceLookup findTargetResource(@Nonnull RequestPartitionId theRequestPartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theResourceType, Class theType, IBaseReference theReference, RequestDetails theRequest) { IResourceLookup resolvedResource; String idPart = theSourceResourceId.getIdPart(); try { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java index fb7ff3be39e..5610817446d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java @@ -21,7 +21,9 @@ package ca.uhn.fhir.jpa.dao.index; */ import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndex; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; import ca.uhn.fhir.jpa.util.AddRemoveCount; @@ -41,18 +43,22 @@ public class DaoSearchParamSynchronizer { protected EntityManager myEntityManager; @Autowired private DaoConfig myDaoConfig; + @Autowired + private PartitionSettings myPartitionSettings; + @Autowired + private ModelConfig myModelConfig; public AddRemoveCount synchronizeSearchParamsToDatabase(ResourceIndexedSearchParams theParams, ResourceTable theEntity, ResourceIndexedSearchParams existingParams) { AddRemoveCount retVal = new AddRemoveCount(); - synchronize(theParams, theEntity, retVal, theParams.myStringParams, existingParams.myStringParams); - synchronize(theParams, theEntity, retVal, theParams.myTokenParams, existingParams.myTokenParams); - synchronize(theParams, theEntity, retVal, theParams.myNumberParams, existingParams.myNumberParams); - synchronize(theParams, theEntity, retVal, theParams.myQuantityParams, existingParams.myQuantityParams); - synchronize(theParams, theEntity, retVal, theParams.myDateParams, existingParams.myDateParams); - synchronize(theParams, theEntity, retVal, theParams.myUriParams, existingParams.myUriParams); - synchronize(theParams, theEntity, retVal, theParams.myCoordsParams, existingParams.myCoordsParams); - synchronize(theParams, theEntity, retVal, theParams.myLinks, existingParams.myLinks); + synchronize(theEntity, retVal, theParams.myStringParams, existingParams.myStringParams); + synchronize(theEntity, retVal, theParams.myTokenParams, existingParams.myTokenParams); + synchronize(theEntity, retVal, theParams.myNumberParams, existingParams.myNumberParams); + synchronize(theEntity, retVal, theParams.myQuantityParams, existingParams.myQuantityParams); + synchronize(theEntity, retVal, theParams.myDateParams, existingParams.myDateParams); + synchronize(theEntity, retVal, theParams.myUriParams, existingParams.myUriParams); + synchronize(theEntity, retVal, theParams.myCoordsParams, existingParams.myCoordsParams); + synchronize(theEntity, retVal, theParams.myLinks, existingParams.myLinks); // make sure links are indexed theEntity.setResourceLinks(theParams.myLinks); @@ -60,26 +66,26 @@ public class DaoSearchParamSynchronizer { return retVal; } - private void synchronize(ResourceIndexedSearchParams theParams, ResourceTable theEntity, AddRemoveCount theAddRemoveCount, Collection theNewParms, Collection theExistingParms) { - for (T next : theNewParms) { + private void synchronize(ResourceTable theEntity, AddRemoveCount theAddRemoveCount, Collection theNewParams, Collection theExistingParams) { + for (T next : theNewParams) { next.setPartitionId(theEntity.getPartitionId()); next.calculateHashes(); } - List quantitiesToRemove = subtract(theExistingParms, theNewParms); - List quantitiesToAdd = subtract(theNewParms, theExistingParms); - tryToReuseIndexEntities(quantitiesToRemove, quantitiesToAdd); + List paramsToRemove = subtract(theExistingParams, theNewParams); + List paramsToAdd = subtract(theNewParams, theExistingParams); + tryToReuseIndexEntities(paramsToRemove, paramsToAdd); - for (T next : quantitiesToRemove) { + for (T next : paramsToRemove) { myEntityManager.remove(next); theEntity.getParamsQuantity().remove(next); } - for (T next : quantitiesToAdd) { + for (T next : paramsToAdd) { myEntityManager.merge(next); } - theAddRemoveCount.addToAddCount(quantitiesToAdd.size()); - theAddRemoveCount.addToRemoveCount(quantitiesToRemove.size()); + theAddRemoveCount.addToAddCount(paramsToRemove.size()); + theAddRemoveCount.addToRemoveCount(paramsToRemove.size()); } /** @@ -108,6 +114,7 @@ public class DaoSearchParamSynchronizer { // Take a row we were going to remove, and repurpose its ID T entityToReuse = theIndexesToRemove.remove(theIndexesToRemove.size() - 1); entityToReuse.copyMutableValuesFrom(targetEntity); + entityToReuse.calculateHashes(); theIndexesToAdd.set(addIndex, entityToReuse); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java index 49f9c9e18b8..674843fc957 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java @@ -30,6 +30,7 @@ import ca.uhn.fhir.jpa.model.cross.IResourceLookup; import ca.uhn.fhir.jpa.model.cross.ResourceLookup; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.ForcedId; +import ca.uhn.fhir.jpa.util.QueryChunker; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; @@ -42,23 +43,22 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.checkerframework.checker.nullness.qual.NonNull; import org.hl7.fhir.instance.model.api.IIdType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.stereotype.Service; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import javax.annotation.PostConstruct; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -84,7 +84,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ @Service public class IdHelperService { - private static final Logger ourLog = LoggerFactory.getLogger(IdHelperService.class); @Autowired protected IForcedIdDao myForcedIdDao; @@ -99,11 +98,13 @@ public class IdHelperService { private Cache myPersistentIdCache; private Cache myResourceLookupCache; + private Cache> myForcedIdCache; @PostConstruct public void start() { myPersistentIdCache = newCache(); myResourceLookupCache = newCache(); + myForcedIdCache = newCache(); } @@ -118,7 +119,7 @@ public class IdHelperService { * @throws ResourceNotFoundException If the ID can not be found */ @Nonnull - public IResourceLookup resolveResourceIdentity(RequestPartitionId theRequestPartitionId, String theResourceType, String theResourceId, RequestDetails theRequestDetails) throws ResourceNotFoundException { + public IResourceLookup resolveResourceIdentity(@Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, String theResourceId, RequestDetails theRequestDetails) throws ResourceNotFoundException { // We only pass 1 input in so only 0..1 will come back IdDt id = new IdDt(theResourceType, theResourceId); Collection matches = translateForcedIdToPids(theRequestPartitionId, theRequestDetails, Collections.singletonList(id)); @@ -135,7 +136,7 @@ public class IdHelperService { * @throws ResourceNotFoundException If the ID can not be found */ @Nonnull - public ResourcePersistentId resolveResourcePersistentIds(RequestPartitionId theRequestPartitionId, String theResourceType, String theId) { + public ResourcePersistentId resolveResourcePersistentIds(@Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, String theId) { Long retVal; if (myDaoConfig.getResourceClientIdStrategy() == DaoConfig.ClientIdStrategyEnum.ANY || !isValidPid(theId)) { if (myDaoConfig.isDeleteEnabled()) { @@ -159,7 +160,7 @@ public class IdHelperService { * are deleted (but note that forced IDs can't change, so the cache can't return incorrect results) */ @Nonnull - public List resolveResourcePersistentIdsWithCache(RequestPartitionId theRequestPartitionId, List theIds, RequestDetails theRequest) { + public List resolveResourcePersistentIdsWithCache(RequestPartitionId theRequestPartitionId, List theIds) { theIds.forEach(id -> Validate.isTrue(id.hasIdPart())); if (theIds.isEmpty()) { @@ -202,14 +203,14 @@ public class IdHelperService { if (nextIds.size() > 0) { Collection views; - if (theRequestPartitionId != null) { + if (theRequestPartitionId.isAllPartitions()) { + views = myForcedIdDao.findByTypeAndForcedId(nextResourceType, nextIds); + } else { if (theRequestPartitionId.getPartitionId() != null) { views = myForcedIdDao.findByTypeAndForcedIdInPartition(nextResourceType, nextIds, theRequestPartitionId.getPartitionId()); } else { views = myForcedIdDao.findByTypeAndForcedIdInPartitionNull(nextResourceType, nextIds); } - } else { - views = myForcedIdDao.findByTypeAndForcedId(nextResourceType, nextIds); } for (Object[] nextView : views) { String forcedId = (String) nextView[0]; @@ -233,17 +234,20 @@ public class IdHelperService { @Nonnull public IIdType translatePidIdToForcedId(FhirContext theCtx, String theResourceType, ResourcePersistentId theId) { IIdType retVal = theCtx.getVersion().newIdType(); - retVal.setValue(translatePidIdToForcedId(theResourceType, theId)); + + Optional forcedId = translatePidIdToForcedId(theId); + if (forcedId.isPresent()) { + retVal.setValue(theResourceType + '/' + forcedId.get()); + } else { + retVal.setValue(theResourceType + '/' + theId.toString()); + } + return retVal; } - private String translatePidIdToForcedId(String theResourceType, ResourcePersistentId theId) { - ForcedId forcedId = myForcedIdDao.findByResourcePid(theId.getIdAsLong()); - if (forcedId != null) { - return forcedId.getResourceType() + '/' + forcedId.getForcedId(); - } else { - return theResourceType + '/' + theId.toString(); - } + + public Optional translatePidIdToForcedId(ResourcePersistentId theId) { + return myForcedIdCache.get(theId.getIdAsLong(), pid -> myForcedIdDao.findByResourcePid(pid).map(t -> t.getForcedId())); } private ListMultimap organizeIdsByResourceType(Collection theIds) { @@ -260,15 +264,9 @@ public class IdHelperService { return typeToIds; } - private Long resolveResourceIdentity(@Nullable RequestPartitionId theRequestPartitionId, @Nonnull String theResourceType, @Nonnull String theId) { + private Long resolveResourceIdentity(@Nonnull RequestPartitionId theRequestPartitionId, @Nonnull String theResourceType, @Nonnull String theId) { Optional pid; - if (theRequestPartitionId != null) { - if (theRequestPartitionId.getPartitionId() == null) { - pid = myForcedIdDao.findByPartitionIdNullAndTypeAndForcedId(theResourceType, theId); - } else { - pid = myForcedIdDao.findByPartitionIdAndTypeAndForcedId(theRequestPartitionId.getPartitionId(), theResourceType, theId); - } - } else { + if (theRequestPartitionId.isAllPartitions()) { try { pid = myForcedIdDao.findByTypeAndForcedId(theResourceType, theId); } catch (IncorrectResultSizeDataAccessException e) { @@ -280,6 +278,12 @@ public class IdHelperService { String msg = myFhirCtx.getLocalizer().getMessage(IdHelperService.class, "nonUniqueForcedId"); throw new PreconditionFailedException(msg); } + } else { + if (theRequestPartitionId.getPartitionId() == null) { + pid = myForcedIdDao.findByPartitionIdNullAndTypeAndForcedId(theResourceType, theId); + } else { + pid = myForcedIdDao.findByPartitionIdAndTypeAndForcedId(theRequestPartitionId.getPartitionId(), theResourceType, theId); + } } if (!pid.isPresent()) { @@ -288,7 +292,7 @@ public class IdHelperService { return pid.get(); } - private Collection translateForcedIdToPids(RequestPartitionId theRequestPartitionId, RequestDetails theRequest, Collection theId) { + private Collection translateForcedIdToPids(@Nonnull RequestPartitionId theRequestPartitionId, RequestDetails theRequest, Collection theId) { theId.forEach(id -> Validate.isTrue(id.hasIdPart())); if (theId.isEmpty()) { @@ -329,14 +333,14 @@ public class IdHelperService { Collection views; assert isNotBlank(nextResourceType); - if (theRequestPartitionId != null) { + if (theRequestPartitionId.isAllPartitions()) { + views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextResourceType, nextIds); + } else { if (theRequestPartitionId.getPartitionId() != null) { views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartition(nextResourceType, nextIds, theRequestPartitionId.getPartitionId()); } else { views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartitionNull(nextResourceType, nextIds); } - } else { - views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextResourceType, nextIds); } for (Object[] next : views) { @@ -359,16 +363,16 @@ public class IdHelperService { return retVal; } - private void resolvePids(RequestPartitionId theRequestPartitionId, List thePidsToResolve, List theTarget) { + private void resolvePids(@Nonnull RequestPartitionId theRequestPartitionId, List thePidsToResolve, List theTarget) { Collection lookup; - if (theRequestPartitionId != null) { + if (theRequestPartitionId.isAllPartitions()) { + lookup = myResourceTableDao.findLookupFieldsByResourcePid(thePidsToResolve); + } else { if (theRequestPartitionId.getPartitionId() != null) { lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartition(thePidsToResolve, theRequestPartitionId.getPartitionId()); } else { lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionNull(thePidsToResolve); } - } else { - lookup = myResourceTableDao.findLookupFieldsByResourcePid(thePidsToResolve); } lookup .stream() @@ -379,6 +383,7 @@ public class IdHelperService { public void clearCache() { myPersistentIdCache.invalidateAll(); myResourceLookupCache.invalidateAll(); + myForcedIdCache.invalidateAll(); } private @NonNull Cache newCache() { @@ -389,6 +394,38 @@ public class IdHelperService { .build(); } + public Map> translatePidsToForcedIds(Set thePids) { + + Map> retVal = new HashMap<>(myForcedIdCache.getAllPresent(thePids)); + + List remainingPids = thePids + .stream() + .filter(t -> !retVal.containsKey(t)) + .collect(Collectors.toList()); + + new QueryChunker().chunk(remainingPids, t -> { + List forcedIds = myForcedIdDao.findAllByResourcePid(t); + + for (ForcedId forcedId : forcedIds) { + Long nextResourcePid = forcedId.getResourceId(); + Optional nextForcedId = Optional.of(forcedId.getForcedId()); + retVal.put(nextResourcePid, nextForcedId); + myForcedIdCache.put(nextResourcePid, nextForcedId); + } + }); + + remainingPids = thePids + .stream() + .filter(t -> !retVal.containsKey(t)) + .collect(Collectors.toList()); + for (Long nextResourcePid : remainingPids) { + retVal.put(nextResourcePid, Optional.empty()); + myForcedIdCache.put(nextResourcePid, Optional.empty()); + } + + return retVal; + } + public static boolean isValidPid(IIdType theId) { if (theId == null) { return false; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java index bc8413c2185..6fb669c6ab3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java @@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.dao.index; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.MatchResourceUrlService; @@ -96,7 +97,14 @@ public class SearchParamWithInlineReferencesExtractor { public void populateFromResource(ResourceIndexedSearchParams theParams, Date theUpdateTime, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams theExistingParams, RequestDetails theRequest) { extractInlineReferences(theResource, theRequest); - mySearchParamExtractorService.extractFromResource(theEntity.getPartitionId(), theRequest, theParams, theEntity, theResource, theUpdateTime, true); + RequestPartitionId partitionId; + if (myPartitionSettings.isPartitioningEnabled()) { + partitionId = theEntity.getPartitionId(); + } else { + partitionId = RequestPartitionId.allPartitions(); + } + + mySearchParamExtractorService.extractFromResource(partitionId, theRequest, theParams, theEntity, theResource, theUpdateTime, true); Set> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet(); if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java index 1015b7d24c5..35530f06c85 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java @@ -131,7 +131,7 @@ abstract class BasePredicateBuilder { } void addPredicateParamMissingForNonReference(String theResourceName, String theParamName, boolean theMissing, Join theJoin, RequestPartitionId theRequestPartitionId) { - if (theRequestPartitionId != null) { + if (!theRequestPartitionId.isAllPartitions()) { if (theRequestPartitionId.getPartitionId() != null) { myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myPartitionIdValue"), theRequestPartitionId.getPartitionId())); } else { @@ -224,7 +224,7 @@ abstract class BasePredicateBuilder { } void addPartitionIdPredicate(RequestPartitionId theRequestPartitionId, From theJoin, List theCodePredicates) { - if (theRequestPartitionId != null) { + if (!theRequestPartitionId.isAllPartitions()) { Integer partitionId = theRequestPartitionId.getPartitionId(); Predicate partitionPredicate; if (partitionId != null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java index 51082ac8784..cf9fa450533 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java @@ -61,7 +61,17 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.rest.param.CompositeParam; +import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.HasParam; +import ca.uhn.fhir.rest.param.NumberParam; +import ca.uhn.fhir.rest.param.ParameterUtil; +import ca.uhn.fhir.rest.param.QuantityParam; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.SpecialParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; @@ -201,7 +211,7 @@ class PredicateBuilderReference extends BasePredicateBuilder { } // Resources by ID - List targetPids = myIdHelperService.resolveResourcePersistentIdsWithCache(theRequestPartitionId, targetIds, theRequest); + List targetPids = myIdHelperService.resolveResourcePersistentIdsWithCache(theRequestPartitionId, targetIds); if (!targetPids.isEmpty()) { ourLog.debug("Searching for resource link with target PIDs: {}", targetPids); Predicate pathPredicate; @@ -565,7 +575,7 @@ class PredicateBuilderReference extends BasePredicateBuilder { if (nextParamDef != null) { if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.isIncludePartitionInSearchHashes()) { - if (theRequestPartitionId == null) { + if (theRequestPartitionId.isAllPartitions()) { throw new PreconditionFailedException("This server is not configured to support search against all partitions"); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/PartitionEntity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/PartitionEntity.java index 002e5d32c12..60edf439eef 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/PartitionEntity.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/PartitionEntity.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.entity; * #L% */ +import ca.uhn.fhir.interceptor.model.RequestPartitionId; + import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; @@ -73,4 +75,7 @@ public class PartitionEntity { myDescription = theDescription; } + public RequestPartitionId toRequestPartitionId() { + return RequestPartitionId.fromPartitionIdAndName(getId(), getName()); + } } 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 553336e9e90..ceab0a6d8f8 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 @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.entity; */ import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.model.entity.ForcedId; import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity; import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; @@ -48,6 +49,7 @@ import java.util.Date; " h.res_updated as res_updated, " + " h.res_text as res_text, " + " 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," + " f.forced_id as FORCED_PID " + @@ -94,6 +96,8 @@ public class ResourceSearchView implements IBaseResourceEntity, Serializable { private ResourceEncodingEnum myEncoding; @Column(name = "FORCED_PID", length = ForcedId.MAX_FORCED_ID_LENGTH) private String myForcedPid; + @Column(name = "PARTITION_ID") + private Integer myPartitionId; public ResourceSearchView() { } @@ -187,6 +191,11 @@ public class ResourceSearchView implements IBaseResourceEntity, Serializable { return myHasTags; } + @Override + public RequestPartitionId getPartitionId() { + return RequestPartitionId.fromPartitionId(myPartitionId); + } + public byte[] getResource() { return myResource; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IPartitionLookupSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IPartitionLookupSvc.java index 272b51834ac..95c783d9522 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IPartitionLookupSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IPartitionLookupSvc.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.partition; */ import ca.uhn.fhir.jpa.entity.PartitionEntity; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; public interface IPartitionLookupSvc { @@ -30,11 +31,14 @@ public interface IPartitionLookupSvc { void start(); /** - * @throws IllegalArgumentException If the name is not known + * @throws ResourceNotFoundException If the name is not known */ - PartitionEntity getPartitionByName(String theName) throws IllegalArgumentException; + PartitionEntity getPartitionByName(String theName) throws ResourceNotFoundException; - PartitionEntity getPartitionById(Integer theId); + /** + * @throws ResourceNotFoundException If the ID is not known + */ + PartitionEntity getPartitionById(Integer theId) throws ResourceNotFoundException; void clearCaches(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperSvc.java similarity index 73% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperService.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperSvc.java index 083cb28d9c6..e23742a2056 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperSvc.java @@ -27,10 +27,10 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import javax.annotation.Nonnull; import javax.annotation.Nullable; -public interface IRequestPartitionHelperService { - @Nullable - RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, String theResourceType); +public interface IRequestPartitionHelperSvc { + @Nonnull + RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, String theResourceType); - @Nullable - RequestPartitionId determineCreatePartitionForRequest(@Nullable RequestDetails theRequest, @Nonnull IBaseResource theResource); + @Nonnull + RequestPartitionId determineCreatePartitionForRequest(@Nullable RequestDetails theRequest, @Nonnull IBaseResource theResource, @Nonnull String theResourceType); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionLookupSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionLookupSvcImpl.java index 696e38c5529..eb9be70668e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionLookupSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionLookupSvcImpl.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.data.IPartitionDao; import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import com.github.benmanes.caffeine.cache.CacheLoader; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; @@ -97,9 +98,9 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc { } @Override - public PartitionEntity getPartitionById(Integer theId) { - Validate.notNull(theId, "The ID must not be null"); - return myIdToPartitionCache.get(theId); + public PartitionEntity getPartitionById(Integer thePartitionId) { + validatePartitionIdSupplied(myFhirCtx, thePartitionId); + return myIdToPartitionCache.get(thePartitionId); } @Override @@ -158,7 +159,7 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc { @Override @Transactional public void deletePartition(Integer thePartitionId) { - Validate.notNull(thePartitionId); + validatePartitionIdSupplied(myFhirCtx, thePartitionId); if (DEFAULT_PERSISTED_PARTITION_ID == thePartitionId) { String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "cantDeleteDefaultPartition"); @@ -204,7 +205,7 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc { .findForName(theName) .orElseThrow(() -> { String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "invalidName", theName); - return new IllegalArgumentException(msg); + return new ResourceNotFoundException(msg); })); } } @@ -217,8 +218,15 @@ public class PartitionLookupSvcImpl implements IPartitionLookupSvc { .findById(theId) .orElseThrow(() -> { String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "unknownPartitionId", theId); - return new IllegalArgumentException(msg); + return new ResourceNotFoundException(msg); })); } } + + public static void validatePartitionIdSupplied(FhirContext theFhirContext, Integer thePartitionId) { + if (thePartitionId == null) { + String msg = theFhirContext.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "noIdSupplied"); + throw new InvalidRequestException(msg); + } + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionManagementProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionManagementProvider.java index 62512764be1..38e66b167ee 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionManagementProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/PartitionManagementProvider.java @@ -32,12 +32,14 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; +import static ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.validatePartitionIdSupplied; import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.hl7.fhir.instance.model.api.IPrimitiveType.toValueOrNull; /** * This HAPI FHIR Server Plain Provider class provides the following operations: *

    - *
  • partition-management-add-partition
  • + *
  • partition-management-create-partition
  • *
  • partition-management-update-partition
  • *
  • partition-management-delete-partition
  • *
@@ -47,29 +49,52 @@ public class PartitionManagementProvider { @Autowired private FhirContext myCtx; @Autowired - private IPartitionLookupSvc myPartitionConfigSvc; + private IPartitionLookupSvc myPartitionLookupSvc; /** * Add Partition: * - * $partition-management-add-partition + * $partition-management-create-partition * */ - @Operation(name = ProviderConstants.PARTITION_MANAGEMENT_ADD_PARTITION) + @Operation(name = ProviderConstants.PARTITION_MANAGEMENT_CREATE_PARTITION) public IBaseParameters addPartition( @ResourceParam IBaseParameters theRequest, @OperationParam(name = ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, min = 1, max = 1, typeName = "integer") IPrimitiveType thePartitionId, @OperationParam(name = ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, min = 1, max = 1, typeName = "code") IPrimitiveType thePartitionName, @OperationParam(name = ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC, min = 0, max = 1, typeName = "string") IPrimitiveType thePartitionDescription ) { + validatePartitionIdSupplied(myCtx, toValueOrNull(thePartitionId)); PartitionEntity input = parseInput(thePartitionId, thePartitionName, thePartitionDescription); - PartitionEntity output = myPartitionConfigSvc.createPartition(input); + + // Note: Input validation happens inside IPartitionLookupSvc + PartitionEntity output = myPartitionLookupSvc.createPartition(input); + IBaseParameters retVal = prepareOutput(output); return retVal; } + /** + * Add Partition: + * + * $partition-management-read-partition + * + */ + @Operation(name = ProviderConstants.PARTITION_MANAGEMENT_READ_PARTITION, idempotent = true) + public IBaseParameters addPartition( + @ResourceParam IBaseParameters theRequest, + @OperationParam(name = ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, min = 1, max = 1, typeName = "integer") IPrimitiveType thePartitionId + ) { + validatePartitionIdSupplied(myCtx, toValueOrNull(thePartitionId)); + + // Note: Input validation happens inside IPartitionLookupSvc + PartitionEntity output = myPartitionLookupSvc.getPartitionById(thePartitionId.getValue()); + + return prepareOutput(output); + } + /** * Add Partition: * @@ -83,9 +108,13 @@ public class PartitionManagementProvider { @OperationParam(name = ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, min = 1, max = 1, typeName = "code") IPrimitiveType thePartitionName, @OperationParam(name = ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC, min = 0, max = 1, typeName = "string") IPrimitiveType thePartitionDescription ) { + validatePartitionIdSupplied(myCtx, toValueOrNull(thePartitionId)); PartitionEntity input = parseInput(thePartitionId, thePartitionName, thePartitionDescription); - PartitionEntity output = myPartitionConfigSvc.updatePartition(input); + + // Note: Input validation happens inside IPartitionLookupSvc + PartitionEntity output = myPartitionLookupSvc.updatePartition(input); + IBaseParameters retVal = prepareOutput(output); return retVal; @@ -102,8 +131,9 @@ public class PartitionManagementProvider { @ResourceParam IBaseParameters theRequest, @OperationParam(name = ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, min = 1, max = 1, typeName = "integer") IPrimitiveType thePartitionId ) { - - myPartitionConfigSvc.deletePartition(thePartitionId.getValue()); + validatePartitionIdSupplied(myCtx, toValueOrNull(thePartitionId)); + + myPartitionLookupSvc.deletePartition(thePartitionId.getValue()); IBaseParameters retVal = ParametersUtil.newInstance(myCtx); ParametersUtil.addParameterToParametersString(myCtx, retVal, "message", "Success"); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java similarity index 58% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperService.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index 861de5f6bd9..c3df30add7d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -24,7 +24,6 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.jpa.model.config.PartitionSettings; @@ -41,14 +40,13 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.HashSet; +import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.doCallHooks; import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.doCallHooksAndReturnObject; -public class RequestPartitionHelperService implements IRequestPartitionHelperService { +public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { private final HashSet myPartitioningBlacklist; - @Autowired - private DaoConfig myDaoConfig; @Autowired private IInterceptorBroadcaster myInterceptorBroadcaster; @Autowired @@ -58,7 +56,7 @@ public class RequestPartitionHelperService implements IRequestPartitionHelperSer @Autowired private PartitionSettings myPartitionSettings; - public RequestPartitionHelperService() { + public RequestPartitionHelperSvc() { myPartitioningBlacklist = new HashSet<>(); // Infrastructure @@ -78,37 +76,45 @@ public class RequestPartitionHelperService implements IRequestPartitionHelperSer /** * Invoke the STORAGE_PARTITION_IDENTIFY_READ interceptor pointcut to determine the tenant for a read request */ - @Nullable + @Nonnull @Override public RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, String theResourceType) { - if (myPartitioningBlacklist.contains(theResourceType)) { - return null; - } - - RequestPartitionId requestPartitionId = null; + RequestPartitionId requestPartitionId; if (myPartitionSettings.isPartitioningEnabled()) { + // Handle system requests + if (theRequest == null && myPartitioningBlacklist.contains(theResourceType)) { + return RequestPartitionId.defaultPartition(); + } + // Interceptor call: STORAGE_PARTITION_IDENTIFY_READ HookParams params = new HookParams() .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest); requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_READ, params); - validatePartition(requestPartitionId, theResourceType); + validatePartition(requestPartitionId, theResourceType, Pointcut.STORAGE_PARTITION_IDENTIFY_READ); + + return normalizeAndNotifyHooks(requestPartitionId, theRequest); } - return normalize(requestPartitionId); + return RequestPartitionId.allPartitions(); } /** * Invoke the STORAGE_PARTITION_IDENTIFY_CREATE interceptor pointcut to determine the tenant for a read request */ - @Nullable + @Nonnull @Override - public RequestPartitionId determineCreatePartitionForRequest(@Nullable RequestDetails theRequest, @Nonnull IBaseResource theResource) { + public RequestPartitionId determineCreatePartitionForRequest(@Nullable RequestDetails theRequest, @Nonnull IBaseResource theResource, @Nonnull String theResourceType) { + RequestPartitionId requestPartitionId; - RequestPartitionId requestPartitionId = null; if (myPartitionSettings.isPartitioningEnabled()) { + // Handle system requests + if (theRequest == null && myPartitioningBlacklist.contains(theResourceType)) { + return RequestPartitionId.defaultPartition(); + } + // Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE HookParams params = new HookParams() .add(IBaseResource.class, theResource) @@ -117,62 +123,70 @@ public class RequestPartitionHelperService implements IRequestPartitionHelperSer requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params); String resourceName = myFhirContext.getResourceDefinition(theResource).getName(); - validatePartition(requestPartitionId, resourceName); + validatePartition(requestPartitionId, resourceName, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE); + + return normalizeAndNotifyHooks(requestPartitionId, theRequest); } - return normalize(requestPartitionId); + return RequestPartitionId.allPartitions(); } /** * If the partition only has a name but not an ID, this method resolves the ID - * @param theRequestPartitionId - * @return */ - private RequestPartitionId normalize(RequestPartitionId theRequestPartitionId) { - if (theRequestPartitionId != null) { - if (theRequestPartitionId.getPartitionName() != null) { + @Nonnull + private RequestPartitionId normalizeAndNotifyHooks(@Nonnull RequestPartitionId theRequestPartitionId, RequestDetails theRequest) { + RequestPartitionId retVal = theRequestPartitionId; - PartitionEntity partition; - try { - partition = myPartitionConfigSvc.getPartitionByName(theRequestPartitionId.getPartitionName()); - } catch (IllegalArgumentException e) { - String msg = myFhirContext.getLocalizer().getMessage(RequestPartitionHelperService.class, "unknownPartitionName", theRequestPartitionId.getPartitionName()); - throw new ResourceNotFoundException(msg); - } + if (retVal.getPartitionName() != null) { - if (theRequestPartitionId.getPartitionId() != null) { - Validate.isTrue(theRequestPartitionId.getPartitionId().equals(partition.getId()), "Partition name %s does not match ID %n", theRequestPartitionId.getPartitionName(), theRequestPartitionId.getPartitionId()); - return theRequestPartitionId; - } else { - return RequestPartitionId.forPartitionNameAndId(theRequestPartitionId.getPartitionName(), partition.getId(), theRequestPartitionId.getPartitionDate()); - } + PartitionEntity partition; + try { + partition = myPartitionConfigSvc.getPartitionByName(retVal.getPartitionName()); + } catch (IllegalArgumentException e) { + String msg = myFhirContext.getLocalizer().getMessage(RequestPartitionHelperSvc.class, "unknownPartitionName", retVal.getPartitionName()); + throw new ResourceNotFoundException(msg); } - if (theRequestPartitionId.getPartitionId() != null) { - PartitionEntity partition; - try { - partition = myPartitionConfigSvc.getPartitionById(theRequestPartitionId.getPartitionId()); - } catch (IllegalArgumentException e) { - String msg = myFhirContext.getLocalizer().getMessage(RequestPartitionHelperService.class, "unknownPartitionId", theRequestPartitionId.getPartitionId()); - throw new ResourceNotFoundException(msg); - } - return RequestPartitionId.forPartitionNameAndId(partition.getName(), partition.getId(), theRequestPartitionId.getPartitionDate()); + if (retVal.getPartitionId() != null) { + Validate.isTrue(retVal.getPartitionId().equals(partition.getId()), "Partition name %s does not match ID %n", retVal.getPartitionName(), retVal.getPartitionId()); + } else { + retVal = RequestPartitionId.forPartitionIdAndName(partition.getId(), retVal.getPartitionName(), retVal.getPartitionDate()); } + } else if (retVal.getPartitionId() != null) { + + PartitionEntity partition; + try { + partition = myPartitionConfigSvc.getPartitionById(retVal.getPartitionId()); + } catch (IllegalArgumentException e) { + String msg = myFhirContext.getLocalizer().getMessage(RequestPartitionHelperSvc.class, "unknownPartitionId", retVal.getPartitionId()); + throw new ResourceNotFoundException(msg); + } + retVal = RequestPartitionId.forPartitionIdAndName(partition.getId(), partition.getName(), retVal.getPartitionDate()); + } - // It's still possible that the partition only has a date but no name/id, - // or it could just be null - return theRequestPartitionId; + // Note: It's still possible that the partition only has a date but no name/id + + HookParams params = new HookParams() + .add(RequestPartitionId.class, retVal) + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest); + doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_SELECTED, params); + + return retVal; } - private void validatePartition(@Nullable RequestPartitionId theRequestPartitionId, @Nonnull String theResourceName) { - if (theRequestPartitionId != null && theRequestPartitionId.getPartitionId() != null) { + private void validatePartition(@Nullable RequestPartitionId theRequestPartitionId, @Nonnull String theResourceName, Pointcut thePointcut) { + Validate.notNull(theRequestPartitionId, "Interceptor did not provide a value for pointcut: %s", thePointcut); + + if (theRequestPartitionId.getPartitionId() != null) { // Make sure we're not using one of the conformance resources in a non-default partition if (myPartitioningBlacklist.contains(theResourceName)) { - String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperService.class, "blacklistedResourceTypeForPartitioning", theResourceName); + String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperSvc.class, "blacklistedResourceTypeForPartitioning", theResourceName); throw new UnprocessableEntityException(msg); } @@ -180,7 +194,7 @@ public class RequestPartitionHelperService implements IRequestPartitionHelperSer try { myPartitionConfigSvc.getPartitionById(theRequestPartitionId.getPartitionId()); } catch (IllegalArgumentException e) { - String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperService.class, "unknownPartitionId", theRequestPartitionId.getPartitionId()); + String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperSvc.class, "unknownPartitionId", theRequestPartitionId.getPartitionId()); throw new InvalidRequestException(msg); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java index f3652502ab9..9308f8545cf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java @@ -24,9 +24,12 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; +import ca.uhn.fhir.jpa.dao.HistoryBuilder; +import ca.uhn.fhir.jpa.dao.HistoryBuilderFactory; import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; import ca.uhn.fhir.jpa.entity.Search; @@ -34,6 +37,7 @@ import ca.uhn.fhir.jpa.entity.SearchTypeEnum; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.BaseHasResource; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc; import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc; import ca.uhn.fhir.jpa.util.InterceptorUtil; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; @@ -47,6 +51,7 @@ import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import com.google.common.annotations.VisibleForTesting; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -59,14 +64,8 @@ import org.springframework.transaction.support.TransactionTemplate; import javax.annotation.Nonnull; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; -import javax.persistence.TypedQuery; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; import java.util.ArrayList; import java.util.Collections; -import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -87,6 +86,8 @@ public class PersistedJpaBundleProvider implements IBundleProvider { @Autowired private SearchBuilderFactory mySearchBuilderFactory; @Autowired + private HistoryBuilderFactory myHistoryBuilderFactory; + @Autowired private DaoRegistry myDaoRegistry; @Autowired protected PlatformTransactionManager myTxManager; @@ -96,6 +97,8 @@ public class PersistedJpaBundleProvider implements IBundleProvider { private ISearchCoordinatorSvc mySearchCoordinatorSvc; @Autowired private ISearchCacheSvc mySearchCacheSvc; + @Autowired + private RequestPartitionHelperSvc myRequestPartitionHelperSvc; /* * Non autowired fields (will be different for every instance @@ -106,6 +109,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider { private Search mySearchEntity; private String myUuid; private boolean myCacheHit; + private RequestPartitionId myRequestPartitionId; /** * Constructor @@ -115,6 +119,14 @@ public class PersistedJpaBundleProvider implements IBundleProvider { myUuid = theSearchUuid; } + /** + * Constructor + */ + public PersistedJpaBundleProvider(RequestDetails theRequest, Search theSearch) { + myRequest = theRequest; + mySearchEntity = theSearch; + } + /** * When HAPI FHIR server is running "for real", a new * instance of the bundle provider is created to serve @@ -127,45 +139,17 @@ public class PersistedJpaBundleProvider implements IBundleProvider { mySearchEntity = null; } + /** + * Perform a history search + */ private List doHistoryInTransaction(int theFromIndex, int theToIndex) { - List results; - CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); - CriteriaQuery q = cb.createQuery(ResourceHistoryTable.class); - Root from = q.from(ResourceHistoryTable.class); - List predicates = new ArrayList<>(); + HistoryBuilder historyBuilder = myHistoryBuilderFactory.newHistoryBuilder(mySearchEntity.getResourceType(), mySearchEntity.getResourceId(), mySearchEntity.getLastUpdatedLow(), mySearchEntity.getLastUpdatedHigh()); - if (mySearchEntity.getResourceType() == null) { - // All resource types - } else if (mySearchEntity.getResourceId() == null) { - predicates.add(cb.equal(from.get("myResourceType"), mySearchEntity.getResourceType())); - } else { - predicates.add(cb.equal(from.get("myResourceId"), mySearchEntity.getResourceId())); - } + RequestPartitionId partitionId = getRequestPartitionId(); + List results = historyBuilder.fetchEntities(partitionId, theFromIndex, theToIndex); - if (mySearchEntity.getLastUpdatedLow() != null) { - predicates.add(cb.greaterThanOrEqualTo(from.get("myUpdated").as(Date.class), mySearchEntity.getLastUpdatedLow())); - } - if (mySearchEntity.getLastUpdatedHigh() != null) { - predicates.add(cb.lessThanOrEqualTo(from.get("myUpdated").as(Date.class), mySearchEntity.getLastUpdatedHigh())); - } - - if (predicates.size() > 0) { - q.where(predicates.toArray(new Predicate[0])); - } - - q.orderBy(cb.desc(from.get("myUpdated"))); - - TypedQuery query = myEntityManager.createQuery(q); - - if (theToIndex - theFromIndex > 0) { - query.setFirstResult(theFromIndex); - query.setMaxResults(theToIndex - theFromIndex); - } - - results = query.getResultList(); - - ArrayList retVal = new ArrayList<>(); + List retVal = new ArrayList<>(); for (ResourceHistoryTable next : results) { BaseHasResource resource; resource = next; @@ -199,12 +183,26 @@ public class PersistedJpaBundleProvider implements IBundleProvider { .add(RequestDetails.class, myRequest) .addIfMatchesType(ServletRequestDetails.class, myRequest); JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); + retVal = showDetails.toList(); } return retVal; } + @NotNull + private RequestPartitionId getRequestPartitionId() { + if (myRequestPartitionId == null) { + if (mySearchEntity.getResourceId() != null) { + // If we have an ID, we've already checked the partition and made sure it's appropriate + myRequestPartitionId = RequestPartitionId.allPartitions(); + } else { + myRequestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(myRequest, mySearchEntity.getResourceType()); + } + } + return myRequestPartitionId; + } + protected List doSearchOrEverything(final int theFromIndex, final int theToIndex) { if (mySearchEntity.getTotalCount() != null && mySearchEntity.getNumFound() <= 0) { // No resources to fetch (e.g. we did a _summary=count search) @@ -239,6 +237,17 @@ public class PersistedJpaBundleProvider implements IBundleProvider { return true; } + + if (mySearchEntity.getSearchType() == SearchTypeEnum.HISTORY) { + if (mySearchEntity.getTotalCount() == null) { + new TransactionTemplate(myTxManager).executeWithoutResult(t->{ + HistoryBuilder historyBuilder = myHistoryBuilderFactory.newHistoryBuilder(mySearchEntity.getResourceType(), mySearchEntity.getResourceId(), mySearchEntity.getLastUpdatedLow(), mySearchEntity.getLastUpdatedHigh()); + Long count = historyBuilder.fetchCount(getRequestPartitionId()); + mySearchEntity.setTotalCount(count.intValue()); + }); + } + } + return true; } @@ -361,7 +370,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider { List resources = new ArrayList<>(); theSearchBuilder.loadResourcesByPid(thePids, includedPidList, resources, false, myRequest); - InterceptorUtil.fireStoragePreshowResource(resources, myRequest, myInterceptorBroadcaster); + resources = InterceptorUtil.fireStoragePreshowResource(resources, myRequest, myInterceptorBroadcaster); return resources; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProviderFactory.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProviderFactory.java index 3c695cd0b12..15ec1a196e5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProviderFactory.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProviderFactory.java @@ -37,6 +37,11 @@ public class PersistedJpaBundleProviderFactory { return (PersistedJpaBundleProvider) retVal; } + public PersistedJpaBundleProvider newInstance(RequestDetails theRequest, Search theSearch) { + Object retVal = myApplicationContext.getBean(BaseConfig.PERSISTED_JPA_BUNDLE_PROVIDER_BY_SEARCH, theRequest, theSearch); + return (PersistedJpaBundleProvider) retVal; + } + public PersistedJpaSearchFirstPageBundleProvider newInstanceFirstPage(RequestDetails theRequestDetails, Search theSearch, SearchCoordinatorSvcImpl.SearchTask theTask, ISearchBuilder theSearchBuilder) { return (PersistedJpaSearchFirstPageBundleProvider) myApplicationContext.getBean(BaseConfig.PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER, theRequestDetails, theSearch, theTask, theSearchBuilder); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java index 022067ac6b7..f9f32efe541 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java @@ -33,7 +33,7 @@ import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.dao.IResultIterator; import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; -import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperService; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.SearchInclude; import ca.uhn.fhir.jpa.entity.SearchTypeEnum; @@ -156,7 +156,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { @Autowired private PersistedJpaBundleProviderFactory myPersistedJpaBundleProviderFactory; @Autowired - private IRequestPartitionHelperService myRequestPartitionHelperService; + private IRequestPartitionHelperSvc myRequestPartitionHelperService; /** * Constructor @@ -514,7 +514,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { theSb.loadResourcesByPid(pids, includedPidsList, resources, false, theRequestDetails); // Hook: STORAGE_PRESHOW_RESOURCES - InterceptorUtil.fireStoragePreshowResource(resources, theRequestDetails, myInterceptorBroadcaster); + resources = InterceptorUtil.fireStoragePreshowResource(resources, theRequestDetails, myInterceptorBroadcaster); return new SimpleBundleProvider(resources); }); @@ -594,7 +594,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { } @VisibleForTesting - public void setRequestPartitionHelperService(IRequestPartitionHelperService theRequestPartitionHelperService) { + public void setRequestPartitionHelperService(IRequestPartitionHelperSvc theRequestPartitionHelperService) { myRequestPartitionHelperService = theRequestPartitionHelperService; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcImpl.java index 9129553834c..05b5f059d14 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcImpl.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.term; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.IHapiJpaRepository; @@ -117,7 +118,7 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { @Override public ResourcePersistentId getValueSetResourcePid(IIdType theIdType) { - return myIdHelperService.resolveResourcePersistentIds(null, theIdType.getResourceType(), theIdType.getIdPart()); + return myIdHelperService.resolveResourcePersistentIds(RequestPartitionId.allPartitions(), theIdType.getResourceType(), theIdType.getIdPart()); } @Transactional @@ -291,7 +292,7 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { Validate.notBlank(theCodeSystemResource.getUrl(), "theCodeSystemResource must have a URL"); IIdType csId = myTerminologyVersionAdapterSvc.createOrUpdateCodeSystem(theCodeSystemResource); - ResourcePersistentId codeSystemResourcePid = myIdHelperService.resolveResourcePersistentIds(null, csId.getResourceType(), csId.getIdPart()); + ResourcePersistentId codeSystemResourcePid = myIdHelperService.resolveResourcePersistentIds(RequestPartitionId.allPartitions(), csId.getResourceType(), csId.getIdPart()); ResourceTable resource = myResourceTableDao.getOne(codeSystemResourcePid.getIdAsLong()); ourLog.info("CodeSystem resource has ID: {}", csId.getValue()); @@ -551,7 +552,7 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { } private ResourcePersistentId getCodeSystemResourcePid(IIdType theIdType) { - return myIdHelperService.resolveResourcePersistentIds(null, theIdType.getResourceType(), theIdType.getIdPart()); + return myIdHelperService.resolveResourcePersistentIds(RequestPartitionId.allPartitions(), theIdType.getResourceType(), theIdType.getIdPart()); } private void persistChildren(TermConcept theConcept, TermCodeSystemVersion theCodeSystem, IdentityHashMap theConceptsStack, int theTotalConcepts) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/InterceptorUtil.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/InterceptorUtil.java index a6713bd19e1..3d3ad993a99 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/InterceptorUtil.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/InterceptorUtil.java @@ -36,24 +36,28 @@ public class InterceptorUtil { /** * Fires {@link Pointcut#STORAGE_PRESHOW_RESOURCES} interceptor hook, and potentially remove resources * from the resource list + * @return */ - public static void fireStoragePreshowResource(List theResources, RequestDetails theRequest, IInterceptorBroadcaster theInterceptorBroadcaster) { - theResources.removeIf(t -> t == null); + public static List fireStoragePreshowResource(List theResources, RequestDetails theRequest, IInterceptorBroadcaster theInterceptorBroadcaster) { + List retVal = theResources; + retVal.removeIf(t -> t == null); // Interceptor call: STORAGE_PRESHOW_RESOURCE // This can be used to remove results from the search result details before // the user has a chance to know that they were in the results - if (theResources.size() > 0) { - SimplePreResourceShowDetails accessDetails = new SimplePreResourceShowDetails(theResources); + if (retVal.size() > 0) { + SimplePreResourceShowDetails accessDetails = new SimplePreResourceShowDetails(retVal); HookParams params = new HookParams() .add(IPreResourceShowDetails.class, accessDetails) .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest); JpaInterceptorBroadcaster.doCallHooks(theInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); - theResources.removeIf(t -> t == null); + retVal = accessDetails.toList(); + retVal.removeIf(t -> t == null); } + return retVal; } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/QueryChunker.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/QueryChunker.java new file mode 100644 index 00000000000..c549c9fd481 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/QueryChunker.java @@ -0,0 +1,45 @@ +package ca.uhn.fhir.jpa.util; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.dao.SearchBuilder; + +import java.util.List; +import java.util.function.Consumer; + +/** + * As always, Oracle can't handle things that other databases don't mind.. In this + * case it doesn't like more than ~1000 IDs in a single load, so we break this up + * if it's lots of IDs. I suppose maybe we should be doing this as a join anyhow + * but this should work too. Sigh. + */ +public class QueryChunker { + + public void chunk(List theInput, Consumer> theBatchConsumer) { + for (int i = 0; i < theInput.size(); i += SearchBuilder.MAXIMUM_PAGE_SIZE) { + int to = i + SearchBuilder.MAXIMUM_PAGE_SIZE; + to = Math.min(to, theInput.size()); + List batch = theInput.subList(i, to); + theBatchConsumer.accept(batch); + } + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index f1a87ee772c..e502e72fce3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.executor.InterceptorService; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; import ca.uhn.fhir.test.BaseTest; import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc; @@ -111,6 +112,8 @@ public abstract class BaseJpaTest extends BaseTest { protected ISearchCacheSvc mySearchCacheSvc; @Autowired protected IPartitionLookupSvc myPartitionConfigSvc; + @Autowired + private IdHelperService myIdHelperService; @After public void afterPerformCleanup() { @@ -121,6 +124,10 @@ public abstract class BaseJpaTest extends BaseTest { if (myPartitionConfigSvc != null) { myPartitionConfigSvc.clearCaches(); } + if (myIdHelperService != null) { + myIdHelperService.clearCache(); + } + } @After diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java index 0e9e981bc50..56bc2c1dadd 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.dao.dstu2; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao; import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoDstu3Test; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; @@ -29,10 +30,12 @@ import org.hamcrest.core.StringContains; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.*; +import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.*; +import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.hamcrest.Matchers.*; @@ -1051,6 +1054,9 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { assertGone(org2Id); } + @Autowired + private IForcedIdDao myForcedIdDao; + @Test public void testHistoryByForcedId() { IIdType idv1; @@ -1067,6 +1073,10 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { idv2 = myPatientDao.update(patient, mySrd).getId(); } + runInTransaction(()->{ + ourLog.info("Forced IDs:\n{}", myForcedIdDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n"))); + }); + List patients = toList(myPatientDao.history(idv1.toVersionless(), null, null, mySrd)); assertTrue(patients.size() == 2); // Newest first @@ -1111,7 +1121,7 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { for (int i = 0; i < fullSize; i++) { String expected = id.withVersion(Integer.toString(fullSize + 1 - i)).getValue(); String actual = history.getResources(i, i + 1).get(0).getIdElement().getValue(); - assertEquals(expected, actual); + assertEquals("Failure at " + i, expected, actual); } // By type diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index c4fc933cc49..217e407297e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.interceptor.api.IInterceptorService; +import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem; @@ -12,14 +13,40 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoStructureDefinition; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoSubscription; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.api.rp.ResourceProviderFactory; +import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider; import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc; import ca.uhn.fhir.jpa.config.TestR4Config; import ca.uhn.fhir.jpa.dao.BaseJpaTest; -import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; -import ca.uhn.fhir.jpa.dao.data.*; +import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; +import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; +import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTagDao; +import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao; +import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamDateDao; +import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamQuantityDao; +import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamStringDao; +import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamTokenDao; +import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; +import ca.uhn.fhir.jpa.dao.data.IResourceReindexJobDao; +import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; +import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; +import ca.uhn.fhir.jpa.dao.data.ISearchDao; +import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao; +import ca.uhn.fhir.jpa.dao.data.ISearchResultDao; +import ca.uhn.fhir.jpa.dao.data.ITagDefinitionDao; +import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; +import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptDesignationDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptMapDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementTargetDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptParentChildLinkDao; +import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDao; +import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDesignationDao; +import ca.uhn.fhir.jpa.dao.data.ITermValueSetDao; import ca.uhn.fhir.jpa.dao.dstu2.FhirResourceDaoDstu2SearchNoFtTest; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.entity.TermCodeSystem; @@ -33,7 +60,6 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; -import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.search.warm.ICacheWarmingSvc; @@ -47,7 +73,6 @@ import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; import ca.uhn.fhir.jpa.term.api.ITermReadSvcR4; import ca.uhn.fhir.jpa.util.ResourceCountCache; -import ca.uhn.fhir.jpa.api.rp.ResourceProviderFactory; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.api.Constants; @@ -64,11 +89,63 @@ import org.hibernate.search.jpa.Search; import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.AllergyIntolerance; +import org.hl7.fhir.r4.model.Appointment; +import org.hl7.fhir.r4.model.AuditEvent; +import org.hl7.fhir.r4.model.Binary; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CarePlan; +import org.hl7.fhir.r4.model.CareTeam; +import org.hl7.fhir.r4.model.ChargeItem; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Communication; +import org.hl7.fhir.r4.model.CommunicationRequest; +import org.hl7.fhir.r4.model.CompartmentDefinition; +import org.hl7.fhir.r4.model.ConceptMap; import org.hl7.fhir.r4.model.ConceptMap.ConceptMapGroupComponent; import org.hl7.fhir.r4.model.ConceptMap.SourceElementComponent; import org.hl7.fhir.r4.model.ConceptMap.TargetElementComponent; +import org.hl7.fhir.r4.model.Condition; +import org.hl7.fhir.r4.model.Consent; +import org.hl7.fhir.r4.model.Coverage; +import org.hl7.fhir.r4.model.Device; +import org.hl7.fhir.r4.model.DiagnosticReport; +import org.hl7.fhir.r4.model.DocumentReference; +import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence; +import org.hl7.fhir.r4.model.EpisodeOfCare; +import org.hl7.fhir.r4.model.Group; +import org.hl7.fhir.r4.model.Immunization; +import org.hl7.fhir.r4.model.ImmunizationRecommendation; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.Media; +import org.hl7.fhir.r4.model.Medication; +import org.hl7.fhir.r4.model.MedicationAdministration; +import org.hl7.fhir.r4.model.MedicationRequest; +import org.hl7.fhir.r4.model.Meta; +import org.hl7.fhir.r4.model.MolecularSequence; +import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.OperationDefinition; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.PractitionerRole; +import org.hl7.fhir.r4.model.Procedure; +import org.hl7.fhir.r4.model.Provenance; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.RiskAssessment; +import org.hl7.fhir.r4.model.SearchParameter; +import org.hl7.fhir.r4.model.ServiceRequest; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.Subscription; +import org.hl7.fhir.r4.model.Substance; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.UriType; +import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r5.utils.IResourceValidator; import org.junit.After; import org.junit.AfterClass; @@ -102,6 +179,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { private static IValidationSupport ourJpaValidationSupportChainR4; private static IFhirResourceDaoValueSet ourValueSetDao; + @Autowired protected IPartitionLookupSvc myPartitionConfigSvc; @Autowired @@ -375,7 +453,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { private PerformanceTracingLoggingInterceptor myPerformanceTracingLoggingInterceptor; private List mySystemInterceptors; @Autowired - private DaoRegistry myDaoRegistry; + protected DaoRegistry myDaoRegistry; @Autowired private IBulkDataExportSvc myBulkDataExportSvc; @Autowired @@ -408,8 +486,6 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { BaseTermReadSvcImpl.clearOurLastResultsFromTranslationWithReverseCache(); TermDeferredStorageSvcImpl termDeferredStorageSvc = AopTestUtils.getTargetObject(myTerminologyDeferredStorageSvc); termDeferredStorageSvc.clearDeferred(); - - myIdHelperService.clearCache(); } @After() diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ConsentEventsDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ConsentEventsDaoR4Test.java index fbf1a3aae18..2b58bde59a6 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ConsentEventsDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ConsentEventsDaoR4Test.java @@ -316,7 +316,6 @@ public class ConsentEventsDaoR4Test extends BaseJpaR4SystemTest { ourLog.info("Search UUID: {}", outcome.getUuid()); // Fetch the first 10 (don't cross a fetch boundary) - outcome = myPagingProvider.retrieveResultList(mySrd, outcome.getUuid()); List resources = outcome.getResources(0, 10); List returnedIdValues = toUnqualifiedVersionlessIdValues(resources); ourLog.info("Returned values: {}", returnedIdValues); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index 59793377383..1928c1d82ec 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -2,8 +2,11 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.util.SqlQuery; import ca.uhn.fhir.jpa.util.TestUtil; +import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.ReferenceParam; +import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Patient; @@ -12,6 +15,8 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; +import java.util.List; + import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; @@ -224,6 +229,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.logAllQueriesForCurrentThread(); // select: lookup forced ID assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertNoPartitionSelectors(); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); // insert to: HFJ_RESOURCE, HFJ_RES_VER, HFJ_RES_LINK assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); @@ -246,6 +252,127 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { } + public void assertNoPartitionSelectors() { + List selectQueries = myCaptureQueriesListener.getSelectQueriesForCurrentThread(); + for (SqlQuery next : selectQueries) { + assertEquals(0, StringUtils.countMatches(next.getSql(true, true).toLowerCase(), "partition_id is null")); + assertEquals(0, StringUtils.countMatches(next.getSql(true, true).toLowerCase(), "partition_id=")); + assertEquals(0, StringUtils.countMatches(next.getSql(true, true).toLowerCase(), "partition_id =")); + } + } + + @Test + public void testHistory_Server() { + runInTransaction(() -> { + Patient p = new Patient(); + p.setId("A"); + p.addIdentifier().setSystem("urn:system").setValue("1"); + myPatientDao.update(p).getId().toUnqualified(); + + p = new Patient(); + p.setId("B"); + p.addIdentifier().setSystem("urn:system").setValue("2"); + myPatientDao.update(p).getId().toUnqualified(); + + p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue("2"); + myPatientDao.create(p).getId().toUnqualified(); + }); + + myCaptureQueriesListener.clear(); + runInTransaction(() -> { + IBundleProvider history = mySystemDao.history(null, null, null); + assertEquals(3, history.getResources(0, 99).size()); + }); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + // Perform count, Search history table, resolve forced IDs + assertEquals(3, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + assertNoPartitionSelectors(); + myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); + myCaptureQueriesListener.logInsertQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); + myCaptureQueriesListener.logDeleteQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); + + // Second time should leverage forced ID cache + myCaptureQueriesListener.clear(); + runInTransaction(() -> { + IBundleProvider history = mySystemDao.history(null, null, null); + assertEquals(3, history.getResources(0, 99).size()); + }); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + // Perform count, Search history table + assertEquals(2, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); + myCaptureQueriesListener.logInsertQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); + myCaptureQueriesListener.logDeleteQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); + } + + + /** + * This could definitely stand to be optimized some, since we load tags individually + * for each resource + */ + @Test + public void testHistory_Server_WithTags() { + runInTransaction(() -> { + Patient p = new Patient(); + p.getMeta().addTag("system", "code1", "displaY1"); + p.getMeta().addTag("system", "code2", "displaY2"); + p.setId("A"); + p.addIdentifier().setSystem("urn:system").setValue("1"); + myPatientDao.update(p).getId().toUnqualified(); + + p = new Patient(); + p.getMeta().addTag("system", "code1", "displaY1"); + p.getMeta().addTag("system", "code2", "displaY2"); + p.setId("B"); + p.addIdentifier().setSystem("urn:system").setValue("2"); + myPatientDao.update(p).getId().toUnqualified(); + + p = new Patient(); + p.getMeta().addTag("system", "code1", "displaY1"); + p.getMeta().addTag("system", "code2", "displaY2"); + p.addIdentifier().setSystem("urn:system").setValue("2"); + myPatientDao.create(p).getId().toUnqualified(); + }); + + myCaptureQueriesListener.clear(); + runInTransaction(() -> { + IBundleProvider history = mySystemDao.history(null, null, null); + assertEquals(3, history.getResources(0, 3).size()); + }); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + // Perform count, Search history table, resolve forced IDs, load tags (x3) + assertEquals(6, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); + myCaptureQueriesListener.logInsertQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); + myCaptureQueriesListener.logDeleteQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); + + // Second time should leverage forced ID cache + myCaptureQueriesListener.clear(); + runInTransaction(() -> { + IBundleProvider history = mySystemDao.history(null, null, null); + assertEquals(3, history.getResources(0, 3).size()); + }); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + // Perform count, Search history table, load tags (x3) + assertEquals(5, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); + myCaptureQueriesListener.logInsertQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); + myCaptureQueriesListener.logDeleteQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); + } + @Test public void testSearchUsingForcedIdReference() { @@ -267,6 +394,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertEquals(1, myObservationDao.search(map).size().intValue()); // Resolve forced ID, Perform search, load result assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertNoPartitionSelectors(); assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index 2d8b3c5da40..309f26ae770 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java @@ -28,7 +28,27 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.rest.param.CompositeParam; +import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.HasAndListParam; +import ca.uhn.fhir.rest.param.HasOrListParam; +import ca.uhn.fhir.rest.param.HasParam; +import ca.uhn.fhir.rest.param.NumberParam; +import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import ca.uhn.fhir.rest.param.QuantityParam; +import ca.uhn.fhir.rest.param.ReferenceAndListParam; +import ca.uhn.fhir.rest.param.ReferenceOrListParam; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.TokenParamModifier; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.param.UriParamQualifierEnum; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import com.google.common.collect.Lists; @@ -38,15 +58,66 @@ import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Age; +import org.hl7.fhir.r4.model.Appointment; +import org.hl7.fhir.r4.model.AuditEvent; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Bundle.BundleType; import org.hl7.fhir.r4.model.Bundle.HTTPVerb; +import org.hl7.fhir.r4.model.CareTeam; +import org.hl7.fhir.r4.model.ChargeItem; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.CommunicationRequest; +import org.hl7.fhir.r4.model.Condition; import org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.Device; +import org.hl7.fhir.r4.model.DiagnosticReport; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.r4.model.EpisodeOfCare; +import org.hl7.fhir.r4.model.Group; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.InstantType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.Medication; +import org.hl7.fhir.r4.model.MedicationAdministration; +import org.hl7.fhir.r4.model.MedicationRequest; +import org.hl7.fhir.r4.model.MolecularSequence; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Period; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Provenance; +import org.hl7.fhir.r4.model.Quantity; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Range; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.RiskAssessment; +import org.hl7.fhir.r4.model.SearchParameter; +import org.hl7.fhir.r4.model.ServiceRequest; +import org.hl7.fhir.r4.model.SimpleQuantity; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.Subscription; import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType; import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus; +import org.hl7.fhir.r4.model.Substance; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Timing; +import org.hl7.fhir.r4.model.ValueSet; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java index 0bb92635771..ccbb3e40a87 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java @@ -3,7 +3,14 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.model.config.PartitionSettings; -import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; +import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.jpa.util.TestUtil; @@ -12,7 +19,27 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.rest.param.CompositeParam; +import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.HasAndListParam; +import ca.uhn.fhir.rest.param.HasOrListParam; +import ca.uhn.fhir.rest.param.HasParam; +import ca.uhn.fhir.rest.param.NumberParam; +import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import ca.uhn.fhir.rest.param.QuantityParam; +import ca.uhn.fhir.rest.param.ReferenceAndListParam; +import ca.uhn.fhir.rest.param.ReferenceOrListParam; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.TokenParamModifier; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.param.UriParamQualifierEnum; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import org.apache.commons.io.IOUtils; @@ -21,16 +48,56 @@ import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Appointment; +import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Bundle.BundleType; import org.hl7.fhir.r4.model.Bundle.HTTPVerb; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Condition; import org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.Device; +import org.hl7.fhir.r4.model.DiagnosticReport; +import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.r4.model.Group; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.Medication; +import org.hl7.fhir.r4.model.MedicationAdministration; +import org.hl7.fhir.r4.model.MedicationRequest; +import org.hl7.fhir.r4.model.MolecularSequence; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Period; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Quantity; +import org.hl7.fhir.r4.model.Range; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.RiskAssessment; +import org.hl7.fhir.r4.model.ServiceRequest; +import org.hl7.fhir.r4.model.SimpleQuantity; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Subscription; import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType; import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus; -import org.junit.*; +import org.hl7.fhir.r4.model.Substance; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; @@ -41,11 +108,25 @@ import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @SuppressWarnings({"unchecked", "Duplicates"}) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java index 673bc5a5c3a..7569fa30594 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.data.ISearchResultDao; import ca.uhn.fhir.jpa.entity.Search; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.search.SearchStatusEnum; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java index f7099709ef4..377e54b38fd 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java @@ -1,12 +1,25 @@ package ca.uhn.fhir.jpa.dao.r4; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.HookParams; +import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor; import ca.uhn.fhir.interceptor.api.Interceptor; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.jpa.model.config.PartitionSettings; -import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.entity.ForcedId; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTag; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTag; +import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; @@ -21,11 +34,11 @@ import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.TokenParamModifier; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.test.utilities.ITestDataBuilder; import ca.uhn.fhir.util.TestUtil; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; @@ -43,6 +56,7 @@ import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import javax.servlet.ServletException; @@ -54,7 +68,8 @@ import java.util.List; import java.util.function.Consumer; import java.util.stream.Collectors; -import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast; +import static org.apache.commons.lang3.StringUtils.countMatches; import static org.hamcrest.Matchers.matchesPattern; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; @@ -62,10 +77,14 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @SuppressWarnings("unchecked") -public class PartitioningR4Test extends BaseJpaR4SystemTest { +public class PartitioningR4Test extends BaseJpaR4SystemTest implements ITestDataBuilder { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PartitioningR4Test.class); @@ -124,8 +143,6 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testCreateSearchParameter_DefaultPartition() { - addCreateNoPartition(); - SearchParameter sp = new SearchParameter(); sp.addBase("Patient"); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); @@ -137,7 +154,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { runInTransaction(() -> { ResourceTable resourceTable = myResourceTableDao.findById(id).orElseThrow(IllegalArgumentException::new); - assertNull(resourceTable.getPartitionId()); + assertEquals(RequestPartitionId.defaultPartition(), resourceTable.getPartitionId()); }); } @@ -242,13 +259,13 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testCreate_SamePartitionReference_DefaultPartition_ByPid() { // Create patient in partition NULL - addCreateNoPartitionId(myPartitionDate); + addCreateDefaultPartition(myPartitionDate); Patient patient = new Patient(); patient.setActive(true); IIdType patientId = myPatientDao.create(patient).getId().toUnqualifiedVersionless(); // Create observation in partition NULL - addCreateNoPartitionId(myPartitionDate); + addCreateDefaultPartition(myPartitionDate); Observation obs = new Observation(); obs.getSubject().setReference(patientId.getValue()); IIdType obsId = myObservationDao.create(obs).getId().toUnqualifiedVersionless(); @@ -265,14 +282,14 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testCreate_SamePartitionReference_DefaultPartition_ByForcedId() { // Create patient in partition NULL - addCreateNoPartitionId(myPartitionDate); + addCreateDefaultPartition(myPartitionDate); Patient patient = new Patient(); patient.setId("ONE"); patient.setActive(true); IIdType patientId = myPatientDao.update(patient).getId().toUnqualifiedVersionless(); // Create observation in partition NULL - addCreateNoPartitionId(myPartitionDate); + addCreateDefaultPartition(myPartitionDate); Observation obs = new Observation(); obs.getSubject().setReference(patientId.getValue()); IIdType obsId = myObservationDao.create(obs).getId().toUnqualifiedVersionless(); @@ -288,7 +305,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testCreateSearchParameter_DefaultPartitionWithDate() { - addCreateNoPartitionId(myPartitionDate); + addCreateDefaultPartition(myPartitionDate); SearchParameter sp = new SearchParameter(); sp.addBase("Patient"); @@ -297,7 +314,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { sp.setCode("extpatorg"); sp.setName("extpatorg"); sp.setExpression("Patient.extension('http://patext').value.as(Reference)"); - Long id = mySearchParameterDao.create(sp).getId().getIdPartAsLong(); + Long id = mySearchParameterDao.create(sp, mySrd).getId().getIdPartAsLong(); runInTransaction(() -> { // HFJ_RESOURCE @@ -320,7 +337,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { sp.setName("extpatorg"); sp.setExpression("Patient.extension('http://patext').value.as(Reference)"); try { - mySearchParameterDao.create(sp); + mySearchParameterDao.create(sp, mySrd); fail(); } catch (UnprocessableEntityException e) { assertEquals("Resource type SearchParameter can not be partitioned", e.getMessage()); @@ -337,15 +354,15 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { try { myPatientDao.create(p); fail(); - } catch (InvalidRequestException e) { - assertEquals("Unknown partition ID: 99", e.getMessage()); + } catch (ResourceNotFoundException e) { + assertEquals("No partition exists with ID 99", e.getMessage()); } } @Test public void testCreate_ServerId_NoPartition() { - addCreateNoPartition(); + addCreateDefaultPartition(); Patient p = new Patient(); p.addIdentifier().setSystem("system").setValue("value"); @@ -354,7 +371,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { runInTransaction(() -> { ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(IllegalArgumentException::new); - assertNull(resourceTable.getPartitionId()); + assertEquals(RequestPartitionId.defaultPartition(), resourceTable.getPartitionId()); }); } @@ -448,12 +465,12 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { createUniqueCompositeSp(); createRequestId(); - addCreateNoPartitionId(myPartitionDate); + addCreateDefaultPartition(myPartitionDate); Organization org = new Organization(); org.setName("org"); IIdType orgId = myOrganizationDao.create(org).getId().toUnqualifiedVersionless(); - addCreateNoPartitionId(myPartitionDate); + addCreateDefaultPartition(myPartitionDate); Patient p = new Patient(); p.getMeta().addTag("http://system", "code", "diisplay"); p.addName().setFamily("FAM"); @@ -556,13 +573,13 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testCreate_ForcedId_NoPartition() { - addCreateNoPartition(); + addCreateDefaultPartition(); Organization org = new Organization(); org.setId("org"); org.setName("org"); IIdType orgId = myOrganizationDao.update(org).getId().toUnqualifiedVersionless(); - addCreateNoPartition(); + addCreateDefaultPartition(); Patient p = new Patient(); p.setId("pat"); p.getManagingOrganization().setReferenceElement(orgId); @@ -572,21 +589,21 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { // HFJ_FORCED_ID List forcedIds = myForcedIdDao.findAll(); assertEquals(2, forcedIds.size()); - assertEquals(null, forcedIds.get(0).getPartitionId()); - assertEquals(null, forcedIds.get(1).getPartitionId()); + assertEquals(null, forcedIds.get(0).getPartitionId().getPartitionId()); + assertEquals(null, forcedIds.get(1).getPartitionId().getPartitionId()); }); } @Test public void testCreate_ForcedId_DefaultPartition() { - addCreateNoPartitionId(myPartitionDate); + addCreateDefaultPartition(myPartitionDate); Organization org = new Organization(); org.setId("org"); org.setName("org"); IIdType orgId = myOrganizationDao.update(org).getId().toUnqualifiedVersionless(); - addCreateNoPartitionId(myPartitionDate); + addCreateDefaultPartition(myPartitionDate); Patient p = new Patient(); p.setId("pat"); p.getManagingOrganization().setReferenceElement(orgId); @@ -668,11 +685,11 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testRead_PidId_AllPartitions() { - IIdType patientId1 = createPatient(1, withActiveTrue()); - IIdType patientId2 = createPatient(2, withActiveTrue()); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue()); + IIdType patientId2 = createPatient(withPartition(2) , withActiveTrue()); { - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); IdType gotId1 = myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless(); assertEquals(patientId1, gotId1); @@ -685,7 +702,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID")); } { - addReadPartition(null); + addReadAllPartitions(); IdType gotId2 = myPatientDao.read(patientId2, mySrd).getIdElement().toUnqualifiedVersionless(); assertEquals(patientId2, gotId2); @@ -700,9 +717,9 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testRead_PidId_SpecificPartition() { - IIdType patientIdNull = createPatient(null, withActiveTrue()); - IIdType patientId1 = createPatient(1, withActiveTrue()); - IIdType patientId2 = createPatient(2, withActiveTrue()); + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue()); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue()); + IIdType patientId2 = createPatient(withPartition(2), withActiveTrue()); // Read in correct Partition { @@ -744,14 +761,14 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testRead_PidId_DefaultPartition() { - IIdType patientIdNull = createPatient(null, withActiveTrue()); - IIdType patientId1 = createPatient(1, withActiveTrue()); - createPatient(2, withActiveTrue()); + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue()); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue()); + createPatient(withPartition(2), withActiveTrue()); // Read in correct Partition { myCaptureQueriesListener.clear(); - addDefaultReadPartition(); + addReadDefaultPartition(); IdType gotId1 = myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless(); assertEquals(patientIdNull, gotId1); @@ -765,7 +782,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { // Read in wrong Partition { - addDefaultReadPartition(); + addReadDefaultPartition(); try { myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless(); fail(); @@ -777,9 +794,9 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testRead_ForcedId_SpecificPartition() { - IIdType patientIdNull = createPatient(null, withActiveTrue(), withId("NULL")); - IIdType patientId1 = createPatient(1, withActiveTrue(), withId("ONE")); - IIdType patientId2 = createPatient(2, withActiveTrue(), withId("TWO")); + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withId("NULL")); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withId("ONE")); + IIdType patientId2 = createPatient(withPartition(2), withActiveTrue(), withId("TWO")); // Read in correct Partition addReadPartition(1); @@ -807,17 +824,17 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testRead_ForcedId_DefaultPartition() { - IIdType patientIdNull = createPatient(null, withActiveTrue(), withId("NULL")); - IIdType patientId1 = createPatient(1, withActiveTrue(), withId("ONE")); - IIdType patientId2 = createPatient(2, withActiveTrue(), withId("TWO")); + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withId("NULL")); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withId("ONE")); + IIdType patientId2 = createPatient(withPartition(2), withActiveTrue(), withId("TWO")); // Read in correct Partition - addDefaultReadPartition(); + addReadDefaultPartition(); IdType gotId1 = myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless(); assertEquals(patientIdNull, gotId1); // Read in null Partition - addDefaultReadPartition(); + addReadDefaultPartition(); try { myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless(); fail(); @@ -826,7 +843,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { } // Read in wrong Partition - addDefaultReadPartition(); + addReadDefaultPartition(); try { myPatientDao.read(patientId2, mySrd).getIdElement().toUnqualifiedVersionless(); fail(); @@ -837,38 +854,32 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testRead_ForcedId_AllPartition() { - IIdType patientIdNull = createPatient(null, withActiveTrue(), withId("NULL")); - IIdType patientId1 = createPatient(1, withActiveTrue(), withId("ONE")); - IIdType patientId2 = createPatient(2, withActiveTrue(), withId("TWO")); + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withId("NULL")); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withId("ONE")); + createPatient(withPartition(2), withActiveTrue(), withId("TWO")); { - addReadPartition(null); + addReadAllPartitions(); IdType gotId1 = myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless(); assertEquals(patientIdNull, gotId1); } { - addReadPartition(null); + addReadAllPartitions(); IdType gotId1 = myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless(); assertEquals(patientId1, gotId1); } - { - // Read in wrong Partition - addReadPartition(null); - IdType gotId1 = myPatientDao.read(patientId2, mySrd).getIdElement().toUnqualifiedVersionless(); - assertEquals(patientId2, gotId1); - } } @Test public void testRead_ForcedId_AllPartition_WithDuplicate() { dropForcedIdUniqueConstraint(); - IIdType patientIdNull = createPatient(null, withActiveTrue(), withId("FOO")); - IIdType patientId1 = createPatient(1, withActiveTrue(), withId("FOO")); - IIdType patientId2 = createPatient(2, withActiveTrue(), withId("FOO")); + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withId("FOO")); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withId("FOO")); + IIdType patientId2 = createPatient(withPartition(2), withActiveTrue(), withId("FOO")); assertEquals(patientIdNull, patientId1); assertEquals(patientIdNull, patientId2); { - addReadPartition(null); + addReadAllPartitions(); try { myPatientDao.read(patientIdNull, mySrd); fail(); @@ -882,13 +893,13 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_MissingParamString_SearchAllPartitions() { myPartitionSettings.setIncludePartitionInSearchHashes(false); - IIdType patientIdNull = createPatient(null, withFamily("FAMILY")); - IIdType patientId1 = createPatient(1, withFamily("FAMILY")); - IIdType patientId2 = createPatient(2, withFamily("FAMILY")); + IIdType patientIdNull = createPatient(withPartition(null), withFamily("FAMILY")); + IIdType patientId1 = createPatient(withPartition(1), withFamily("FAMILY")); + IIdType patientId2 = createPatient(withPartition(2), withFamily("FAMILY")); // :missing=true { - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Patient.SP_ACTIVE, new StringParam().setMissing(true)); @@ -905,7 +916,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { // :missing=false { - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Patient.SP_FAMILY, new StringParam().setMissing(false)); @@ -924,9 +935,9 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_MissingParamString_SearchOnePartition() { - createPatient(null, withFamily("FAMILY")); - IIdType patientId1 = createPatient(1, withFamily("FAMILY")); - createPatient(2, withFamily("FAMILY")); + createPatient(withPartition(null), withFamily("FAMILY")); + IIdType patientId1 = createPatient(withPartition(1), withFamily("FAMILY")); + createPatient(withPartition(2), withFamily("FAMILY")); // :missing=true { @@ -965,13 +976,13 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_MissingParamString_SearchDefaultPartition() { - IIdType patientIdNull = createPatient(null, withFamily("FAMILY")); - createPatient(1, withFamily("FAMILY")); - createPatient(2, withFamily("FAMILY")); + IIdType patientIdNull = createPatient(withPartition(null), withFamily("FAMILY")); + createPatient(withPartition(1), withFamily("FAMILY")); + createPatient(withPartition(2), withFamily("FAMILY")); // :missing=true { - addDefaultReadPartition(); + addReadDefaultPartition(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Patient.SP_ACTIVE, new StringParam().setMissing(true)); @@ -988,7 +999,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { // :missing=false { - addDefaultReadPartition(); + addReadDefaultPartition(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Patient.SP_FAMILY, new StringParam().setMissing(false)); @@ -1008,13 +1019,13 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_MissingParamReference_SearchAllPartitions() { myPartitionSettings.setIncludePartitionInSearchHashes(false); - IIdType patientIdNull = createPatient(null, withFamily("FAMILY")); - IIdType patientId1 = createPatient(1, withFamily("FAMILY")); - IIdType patientId2 = createPatient(2, withFamily("FAMILY")); + IIdType patientIdNull = createPatient(withPartition(null), withFamily("FAMILY")); + IIdType patientId1 = createPatient(withPartition(1), withFamily("FAMILY")); + IIdType patientId2 = createPatient(withPartition(2), withFamily("FAMILY")); // :missing=true { - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Patient.SP_GENERAL_PRACTITIONER, new StringParam().setMissing(true)); @@ -1035,9 +1046,9 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_MissingParamReference_SearchOnePartition_IncludePartitionInHashes() { myPartitionSettings.setIncludePartitionInSearchHashes(true); - createPatient(null, withFamily("FAMILY")); - IIdType patientId1 = createPatient(1, withFamily("FAMILY")); - createPatient(2, withFamily("FAMILY")); + createPatient(withPartition(null), withFamily("FAMILY")); + IIdType patientId1 = createPatient(withPartition(1), withFamily("FAMILY")); + createPatient(withPartition(2), withFamily("FAMILY")); // :missing=true { @@ -1063,9 +1074,9 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_MissingParamReference_SearchOnePartition_DontIncludePartitionInHashes() { myPartitionSettings.setIncludePartitionInSearchHashes(false); - createPatient(null, withFamily("FAMILY")); - IIdType patientId1 = createPatient(1, withFamily("FAMILY")); - createPatient(2, withFamily("FAMILY")); + createPatient(withPartition(null), withFamily("FAMILY")); + IIdType patientId1 = createPatient(withPartition(1), withFamily("FAMILY")); + createPatient(withPartition(2), withFamily("FAMILY")); // :missing=true { @@ -1089,13 +1100,13 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_MissingParamReference_SearchDefaultPartition() { - IIdType patientIdDefault = createPatient(null, withFamily("FAMILY")); - createPatient(1, withFamily("FAMILY")); - createPatient(2, withFamily("FAMILY")); + IIdType patientIdDefault = createPatient(withPartition(null), withFamily("FAMILY")); + createPatient(withPartition(1), withFamily("FAMILY")); + createPatient(withPartition(2), withFamily("FAMILY")); // :missing=true { - addDefaultReadPartition(); + addReadDefaultPartition(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Patient.SP_GENERAL_PRACTITIONER, new StringParam().setMissing(true)); @@ -1116,11 +1127,11 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_NoParams_SearchAllPartitions() { - IIdType patientIdNull = createPatient(null, withActiveTrue()); - IIdType patientId1 = createPatient(1, withActiveTrue()); - IIdType patientId2 = createPatient(2, withActiveTrue()); + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue()); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue()); + IIdType patientId2 = createPatient(withPartition(2), withActiveTrue()); - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); @@ -1136,9 +1147,9 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_NoParams_SearchOnePartition() { - createPatient(null, withActiveTrue()); - IIdType patientId1 = createPatient(1, withActiveTrue()); - createPatient(2, withActiveTrue()); + createPatient(withPartition(null), withActiveTrue()); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue()); + createPatient(withPartition(2), withActiveTrue()); addReadPartition(1); @@ -1158,16 +1169,16 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_DateParam_SearchAllPartitions() { myPartitionSettings.setIncludePartitionInSearchHashes(false); - IIdType patientIdNull = createPatient(null, withBirthdate("2020-04-20")); - IIdType patientId1 = createPatient(1, withBirthdate("2020-04-20")); - IIdType patientId2 = createPatient(2, withBirthdate("2020-04-20")); - createPatient(null, withBirthdate("2021-04-20")); - createPatient(1, withBirthdate("2021-04-20")); - createPatient(2, withBirthdate("2021-04-20")); + IIdType patientIdNull = createPatient(withPartition(null), withBirthdate("2020-04-20")); + IIdType patientId1 = createPatient(withPartition(1), withBirthdate("2020-04-20")); + IIdType patientId2 = createPatient(withPartition(2), withBirthdate("2020-04-20")); + createPatient(withPartition(null), withBirthdate("2021-04-20")); + createPatient(withPartition(1), withBirthdate("2021-04-20")); + createPatient(withPartition(2), withBirthdate("2021-04-20")); // Date param - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Patient.SP_BIRTHDATE, new DateParam("2020-04-20")); @@ -1183,7 +1194,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { // Date OR param - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); map = new SearchParameterMap(); map.add(Patient.SP_BIRTHDATE, new DateOrListParam().addOr(new DateParam("2020-04-20")).addOr(new DateParam("2020-04-22"))); @@ -1199,7 +1210,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { // Date AND param - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); map = new SearchParameterMap(); map.add(Patient.SP_BIRTHDATE, new DateAndListParam().addAnd(new DateOrListParam().addOr(new DateParam("2020"))).addAnd(new DateOrListParam().addOr(new DateParam("2020-04-20")))); @@ -1215,7 +1226,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { // DateRangeParam - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); map = new SearchParameterMap(); map.add(Patient.SP_BIRTHDATE, new DateRangeParam(new DateParam("2020-01-01"), new DateParam("2020-04-25"))); @@ -1236,21 +1247,24 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_DateParam_SearchSpecificPartitions() { myPartitionSettings.setIncludePartitionInSearchHashes(false); - IIdType patientIdNull = createPatient(null, withBirthdate("2020-04-20")); - IIdType patientId1 = createPatient(1, withBirthdate("2020-04-20")); - IIdType patientId2 = createPatient(2, withBirthdate("2020-04-20")); - createPatient(null, withBirthdate("2021-04-20")); - createPatient(1, withBirthdate("2021-04-20")); - createPatient(2, withBirthdate("2021-04-20")); + IIdType patientIdNull = createPatient(withPartition(null), withBirthdate("2020-04-20")); + IIdType patientId1 = createPatient(withPartition(1), withBirthdate("2020-04-20")); + IIdType patientId2 = createPatient(withPartition(2), withBirthdate("2020-04-20")); + createPatient(withPartition(null), withBirthdate("2021-04-20")); + createPatient(withPartition(1), withBirthdate("2021-04-20")); + createPatient(withPartition(2), withBirthdate("2021-04-20")); // Date param + ourLog.info("Date indexes:\n * {}", myResourceIndexedSearchParamDateDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * "))); addReadPartition(1); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Patient.SP_BIRTHDATE, new DateParam("2020-04-20")); map.setLoadSynchronous(true); + myCaptureQueriesListener.clear(); IBundleProvider results = myPatientDao.search(map); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); List ids = toUnqualifiedVersionlessIds(results); assertThat(ids, Matchers.contains(patientId1)); @@ -1314,16 +1328,16 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_DateParam_SearchDefaultPartitions() { myPartitionSettings.setIncludePartitionInSearchHashes(false); - IIdType patientIdNull = createPatient(null, withBirthdate("2020-04-20")); - IIdType patientId1 = createPatient(1, withBirthdate("2020-04-20")); - IIdType patientId2 = createPatient(2, withBirthdate("2020-04-20")); - createPatient(null, withBirthdate("2021-04-20")); - createPatient(1, withBirthdate("2021-04-20")); - createPatient(2, withBirthdate("2021-04-20")); + IIdType patientIdNull = createPatient(withPartition(null), withBirthdate("2020-04-20")); + IIdType patientId1 = createPatient(withPartition(1), withBirthdate("2020-04-20")); + IIdType patientId2 = createPatient(withPartition(2), withBirthdate("2020-04-20")); + createPatient(withPartition(null), withBirthdate("2021-04-20")); + createPatient(withPartition(1), withBirthdate("2021-04-20")); + createPatient(withPartition(2), withBirthdate("2021-04-20")); // Date param - addDefaultReadPartition(); + addReadDefaultPartition(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Patient.SP_BIRTHDATE, new DateParam("2020-04-20")); @@ -1339,7 +1353,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { // Date OR param - addDefaultReadPartition(); + addReadDefaultPartition(); myCaptureQueriesListener.clear(); map = new SearchParameterMap(); map.add(Patient.SP_BIRTHDATE, new DateOrListParam().addOr(new DateParam("2020-04-20")).addOr(new DateParam("2020-04-22"))); @@ -1355,7 +1369,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { // Date AND param - addDefaultReadPartition(); + addReadDefaultPartition(); myCaptureQueriesListener.clear(); map = new SearchParameterMap(); map.add(Patient.SP_BIRTHDATE, new DateAndListParam().addAnd(new DateOrListParam().addOr(new DateParam("2020"))).addAnd(new DateOrListParam().addOr(new DateParam("2020-04-20")))); @@ -1371,7 +1385,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { // DateRangeParam - addDefaultReadPartition(); + addReadDefaultPartition(); myCaptureQueriesListener.clear(); map = new SearchParameterMap(); map.add(Patient.SP_BIRTHDATE, new DateRangeParam(new DateParam("2020-01-01"), new DateParam("2020-04-25"))); @@ -1392,11 +1406,11 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_StringParam_SearchAllPartitions() { myPartitionSettings.setIncludePartitionInSearchHashes(false); - IIdType patientIdNull = createPatient(null, withFamily("FAMILY")); - IIdType patientId1 = createPatient(1, withFamily("FAMILY")); - IIdType patientId2 = createPatient(2, withFamily("FAMILY")); + IIdType patientIdNull = createPatient(withPartition(null), withFamily("FAMILY")); + IIdType patientId1 = createPatient(withPartition(1), withFamily("FAMILY")); + IIdType patientId2 = createPatient(withPartition(2), withFamily("FAMILY")); - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); @@ -1414,11 +1428,11 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_StringParam_SearchDefaultPartition() { - IIdType patientIdNull = createPatient(null, withFamily("FAMILY")); - createPatient(1, withFamily("FAMILY")); - createPatient(2, withFamily("FAMILY")); + IIdType patientIdNull = createPatient(withPartition(null), withFamily("FAMILY")); + createPatient(withPartition(1), withFamily("FAMILY")); + createPatient(withPartition(2), withFamily("FAMILY")); - addDefaultReadPartition(); + addReadDefaultPartition(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); @@ -1438,9 +1452,9 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_StringParam_SearchOnePartition() { - createPatient(null, withFamily("FAMILY")); - IIdType patientId1 = createPatient(1, withFamily("FAMILY")); - createPatient(2, withFamily("FAMILY")); + createPatient(withPartition(null), withFamily("FAMILY")); + IIdType patientId1 = createPatient(withPartition(1), withFamily("FAMILY")); + createPatient(withPartition(2), withFamily("FAMILY")); addReadPartition(1); @@ -1463,14 +1477,15 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_StringParam_SearchAllPartitions_IncludePartitionInHashes() { myPartitionSettings.setIncludePartitionInSearchHashes(true); - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Patient.SP_FAMILY, new StringParam("FAMILY")); map.setLoadSynchronous(true); try { - myPatientDao.search(map); + IBundleProvider value = myPatientDao.search(map); + value.size(); fail(); } catch (PreconditionFailedException e) { assertEquals("This server is not configured to support search against all partitions", e.getMessage()); @@ -1481,11 +1496,11 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_StringParam_SearchDefaultPartition_IncludePartitionInHashes() { myPartitionSettings.setIncludePartitionInSearchHashes(true); - IIdType patientIdNull = createPatient(null, withFamily("FAMILY")); - createPatient(1, withFamily("FAMILY")); - createPatient(2, withFamily("FAMILY")); + IIdType patientIdNull = createPatient(withPartition(null), withFamily("FAMILY")); + createPatient(withPartition(1), withFamily("FAMILY")); + createPatient(withPartition(2), withFamily("FAMILY")); - addDefaultReadPartition(); + addReadDefaultPartition(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); @@ -1507,9 +1522,9 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_StringParam_SearchOnePartition_IncludePartitionInHashes() { myPartitionSettings.setIncludePartitionInSearchHashes(true); - createPatient(null, withFamily("FAMILY")); - IIdType patientId1 = createPatient(1, withFamily("FAMILY")); - createPatient(2, withFamily("FAMILY")); + createPatient(withPartition(null), withFamily("FAMILY")); + IIdType patientId1 = createPatient(withPartition(1), withFamily("FAMILY")); + createPatient(withPartition(2), withFamily("FAMILY")); addReadPartition(1); @@ -1530,14 +1545,14 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_TagNotParam_SearchAllPartitions() { - IIdType patientIdNull = createPatient(null, withActiveTrue(), withTag("http://system", "code")); - IIdType patientId1 = createPatient(1, withActiveTrue(), withTag("http://system", "code")); - IIdType patientId2 = createPatient(2, withActiveTrue(), withTag("http://system", "code")); - createPatient(null, withActiveTrue(), withTag("http://system", "code2")); - createPatient(1, withActiveTrue(), withTag("http://system", "code2")); - createPatient(2, withActiveTrue(), withTag("http://system", "code2")); + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code")); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code")); + IIdType patientId2 = createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code")); + createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code2")); + createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code2")); + createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code2")); - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); @@ -1555,11 +1570,11 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_TagNotParam_SearchDefaultPartition() { - IIdType patientIdNull = createPatient(null, withActiveTrue(), withTag("http://system", "code")); - createPatient(1, withActiveTrue(), withTag("http://system", "code")); - createPatient(2, withActiveTrue(), withTag("http://system", "code")); + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code")); + createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code")); + createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code")); - addDefaultReadPartition(); + addReadDefaultPartition(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); @@ -1579,12 +1594,12 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_TagNotParam_SearchOnePartition() { - createPatient(null, withActiveTrue(), withTag("http://system", "code")); - IIdType patientId1 = createPatient(1, withActiveTrue(), withTag("http://system", "code")); - createPatient(2, withActiveTrue(), withTag("http://system", "code")); - createPatient(null, withActiveTrue(), withTag("http://system", "code2")); - createPatient(1, withActiveTrue(), withTag("http://system", "code2")); - createPatient(2, withActiveTrue(), withTag("http://system", "code2")); + createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code")); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code")); + createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code")); + createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code2")); + createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code2")); + createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code2")); addReadPartition(1); @@ -1604,11 +1619,11 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_TagParam_SearchAllPartitions() { - IIdType patientIdNull = createPatient(null, withActiveTrue(), withTag("http://system", "code")); - IIdType patientId1 = createPatient(1, withActiveTrue(), withTag("http://system", "code")); - IIdType patientId2 = createPatient(2, withActiveTrue(), withTag("http://system", "code")); + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code")); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code")); + IIdType patientId2 = createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code")); - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); @@ -1626,9 +1641,9 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_TagParam_SearchOnePartition() { - createPatient(null, withActiveTrue(), withTag("http://system", "code")); - IIdType patientId1 = createPatient(1, withActiveTrue(), withTag("http://system", "code")); - createPatient(2, withActiveTrue(), withTag("http://system", "code")); + createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code")); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code")); + createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code")); addReadPartition(1); @@ -1650,14 +1665,14 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_TagParamNot_SearchAllPartitions() { - IIdType patientIdNull = createPatient(null, withActiveTrue(), withTag("http://system", "code")); - IIdType patientId1 = createPatient(1, withActiveTrue(), withTag("http://system", "code")); - IIdType patientId2 = createPatient(2, withActiveTrue(), withTag("http://system", "code")); - createPatient(null, withActiveTrue(), withTag("http://system", "code"), withTag("http://system", "code2")); - createPatient(1, withActiveTrue(), withTag("http://system", "code"), withTag("http://system", "code2")); - createPatient(2, withActiveTrue(), withTag("http://system", "code"), withTag("http://system", "code2")); + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code")); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code")); + IIdType patientId2 = createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code")); + createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code"), withTag("http://system", "code2")); + createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code"), withTag("http://system", "code2")); + createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code"), withTag("http://system", "code2")); - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); @@ -1675,12 +1690,12 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_TagParamNot_SearchOnePartition() { - createPatient(null, withActiveTrue(), withTag("http://system", "code")); - IIdType patientId1 = createPatient(1, withActiveTrue(), withTag("http://system", "code")); - createPatient(2, withActiveTrue(), withTag("http://system", "code")); - createPatient(null, withActiveTrue(), withTag("http://system", "code"), withTag("http://system", "code2")); - createPatient(1, withActiveTrue(), withTag("http://system", "code"), withTag("http://system", "code2")); - createPatient(2, withActiveTrue(), withTag("http://system", "code"), withTag("http://system", "code2")); + createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code")); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code")); + createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code")); + createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code"), withTag("http://system", "code2")); + createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code"), withTag("http://system", "code2")); + createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code"), withTag("http://system", "code2")); addReadPartition(1); @@ -1702,9 +1717,9 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_UniqueParam_SearchAllPartitions() { createUniqueCompositeSp(); - IIdType id = createPatient(1, withBirthdate("2020-01-01")); + IIdType id = createPatient(withPartition(1), withBirthdate("2020-01-01")); - addReadPartition(null); + addReadAllPartitions(); myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); @@ -1726,7 +1741,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_UniqueParam_SearchOnePartition() { createUniqueCompositeSp(); - IIdType id = createPatient(1, withBirthdate("2020-01-01")); + IIdType id = createPatient(withPartition(1), withBirthdate("2020-01-01")); addReadPartition(1); myCaptureQueriesListener.clear(); @@ -1760,8 +1775,8 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_RefParam_TargetPid_SearchOnePartition() { createUniqueCompositeSp(); - IIdType patientId = createPatient(myPartitionId, withBirthdate("2020-01-01")); - IIdType observationId = createObservation(myPartitionId, withSubject(patientId)); + IIdType patientId = createPatient(withPartition(myPartitionId), withBirthdate("2020-01-01")); + IIdType observationId = createObservation(withPartition(myPartitionId), withSubject(patientId)); addReadPartition(myPartitionId); myCaptureQueriesListener.clear(); @@ -1797,10 +1812,10 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_RefParam_TargetPid_SearchDefaultPartition() { createUniqueCompositeSp(); - IIdType patientId = createPatient(null, withBirthdate("2020-01-01")); - IIdType observationId = createObservation(null, withSubject(patientId)); + IIdType patientId = createPatient(withPartition(null), withBirthdate("2020-01-01")); + IIdType observationId = createObservation(withPartition(null), withSubject(patientId)); - addDefaultReadPartition(); + addReadDefaultPartition(); ; myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); @@ -1835,8 +1850,8 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_RefParam_TargetForcedId_SearchOnePartition() { createUniqueCompositeSp(); - IIdType patientId = createPatient(myPartitionId, withId("ONE"), withBirthdate("2020-01-01")); - IIdType observationId = createObservation(myPartitionId, withSubject(patientId)); + IIdType patientId = createPatient(withPartition(myPartitionId), withId("ONE"), withBirthdate("2020-01-01")); + IIdType observationId = createObservation(withPartition(myPartitionId), withSubject(patientId)); addReadPartition(myPartitionId); myCaptureQueriesListener.clear(); @@ -1871,11 +1886,11 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { public void testSearch_RefParam_TargetForcedId_SearchDefaultPartition() { createUniqueCompositeSp(); - IIdType patientId = createPatient(null, withId("ONE"), withBirthdate("2020-01-01")); - IIdType observationId = createObservation(null, withSubject(patientId)); + IIdType patientId = createPatient(withPartition(null), withId("ONE"), withBirthdate("2020-01-01")); + IIdType observationId = createObservation(withPartition(null), withSubject(patientId)); + + addReadDefaultPartition(); - addDefaultReadPartition(); - ; myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Observation.SP_SUBJECT, new ReferenceParam(patientId)); @@ -1904,8 +1919,8 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { } @Test - public void testHistory_Instance_CorrectTenant() { - IIdType id = createPatient(1, withBirthdate("2020-01-01")); + public void testHistory_Instance_CorrectPartition() { + IIdType id = createPatient(withPartition(1), withBirthdate("2020-01-01")); // Update the patient addCreatePartition(myPartitionId, myPartitionDate); @@ -1917,15 +1932,36 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { addReadPartition(1); myCaptureQueriesListener.clear(); IBundleProvider results = myPatientDao.history(id, null, null, mySrd); + assertEquals(2, results.sizeOrThrowNpe()); List ids = toUnqualifiedIdValues(results); - myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertThat(ids, Matchers.contains(id.withVersion("2").getValue(), id.withVersion("1").getValue())); + assertEquals(4, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + + // Resolve resource + String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(0, countMatches(sql, "PARTITION_ID=")); + + // Fetch history resource + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(0, countMatches(sql, "PARTITION_ID")); + + // Fetch history resource + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(2).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(0, countMatches(sql, "PARTITION_IDAA")); + + // Fetch history resource + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(3).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(0, countMatches(sql, "PARTITION_IDAA")); } @Test - public void testHistory_Instance_WrongTenant() { - IIdType id = createPatient(1, withBirthdate("2020-01-01")); + public void testHistory_Instance_WrongPartition() { + IIdType id = createPatient(withPartition(1), withBirthdate("2020-01-01")); // Update the patient addCreatePartition(myPartitionId, myPartitionDate); @@ -1943,28 +1979,251 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { } } + @Test + public void testHistory_Instance_DefaultPartition() { + IIdType id = createPatient(withPartition(null), withBirthdate("2020-01-01")); + + // Update the patient + addCreateDefaultPartition(); + Patient p = new Patient(); + p.setActive(false); + p.setId(id); + myPatientDao.update(p); + + addReadDefaultPartition(); + myCaptureQueriesListener.clear(); + IBundleProvider results = myPatientDao.history(id, null, null, mySrd); + assertEquals(2, results.sizeOrThrowNpe()); + List ids = toUnqualifiedIdValues(results); + assertThat(ids, Matchers.contains(id.withVersion("2").getValue(), id.withVersion("1").getValue())); + + assertEquals(4, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + + // Resolve resource + String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(0, countMatches(sql, "PARTITION_ID=")); + + // Fetch history resource + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(0, countMatches(sql, "PARTITION_ID")); + + // Fetch history resource + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(2).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(0, countMatches(sql, "PARTITION_IDAA")); + + // Fetch history resource + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(3).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(0, countMatches(sql, "PARTITION_IDAA")); + } + + @Test + public void testHistory_Instance_AllPartitions() { + IIdType id = createPatient(withPartition(1), withBirthdate("2020-01-01")); + + // Update the patient + addCreatePartition(myPartitionId, myPartitionDate); + Patient p = new Patient(); + p.setActive(false); + p.setId(id); + myPatientDao.update(p); + + addReadAllPartitions(); + myCaptureQueriesListener.clear(); + IBundleProvider results = myPatientDao.history(id, null, null, mySrd); + assertEquals(2, results.sizeOrThrowNpe()); + List ids = toUnqualifiedIdValues(results); + assertThat(ids, Matchers.contains(id.withVersion("2").getValue(), id.withVersion("1").getValue())); + } + @Test public void testHistory_Server() { + addReadAllPartitions(); try { - mySystemDao.history(null, null, mySrd); + mySystemDao.history(null, null, mySrd).size(); fail(); - } catch (MethodNotAllowedException e) { - assertEquals("Type- and Server- level history operation not supported on partitioned server", e.getMessage()); + } catch (InvalidRequestException e) { + assertEquals("Type- and Server- level history operation not supported across partitions on partitioned server", e.getMessage()); } } @Test - public void testHistory_Type() { + public void testHistory_Server_SpecificPartition() { + IIdType id1A = createPatient(withPartition(1), withBirthdate("2020-01-01")); + sleepAtLeast(10); + IIdType id1B = createPatient(withPartition(1), withBirthdate("2020-01-01")); + sleepAtLeast(10); + createPatient(withPartition(2), withBirthdate("2020-01-01")); + sleepAtLeast(10); + createPatient(withPartition(2), withBirthdate("2020-01-01")); + + addReadPartition(1); + myCaptureQueriesListener.clear(); + IBundleProvider results = mySystemDao.history(null, null, mySrd); + assertEquals(2, results.sizeOrThrowNpe()); + List ids = toUnqualifiedIdValues(results); + assertThat(ids, Matchers.contains(id1B.withVersion("1").getValue(), id1A.withVersion("1").getValue())); + + assertEquals(3, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + + // Count + String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(1, countMatches(sql, "count(")); + assertEquals(1, countMatches(sql, "PARTITION_ID='1'")); + + // Fetch history + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(1, countMatches(sql, "PARTITION_ID='1'")); + + // Fetch history resource + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(2).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(0, countMatches(sql, "PARTITION_IDAA")); + } + + @Test + public void testHistory_Server_DefaultPartition() { + IIdType id1A = createPatient(withPartition(null), withBirthdate("2020-01-01")); + sleepAtLeast(10); + IIdType id1B = createPatient(withPartition(null), withBirthdate("2020-01-01")); + sleepAtLeast(10); + createPatient(withPartition(2), withBirthdate("2020-01-01")); + sleepAtLeast(10); + createPatient(withPartition(2), withBirthdate("2020-01-01")); + + addReadDefaultPartition(); + myCaptureQueriesListener.clear(); + IBundleProvider results = mySystemDao.history(null, null, mySrd); + assertEquals(2, results.sizeOrThrowNpe()); + List ids = toUnqualifiedIdValues(results); + assertThat(ids, Matchers.contains(id1B.withVersion("1").getValue(), id1A.withVersion("1").getValue())); + + assertEquals(3, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + + // Count + String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(1, countMatches(sql, "PARTITION_ID is null")); + + // Fetch history + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(1, countMatches(sql, "PARTITION_ID is null")); + + // Fetch history resource + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(2).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(0, countMatches(sql, "PARTITION_IDzzz")); + } + + @Test + public void testHistory_Type_AllPartitions() { + addReadAllPartitions(); try { - myPatientDao.history(null, null, mySrd); + myPatientDao.history(null, null, mySrd).size(); fail(); - } catch (MethodNotAllowedException e) { - assertEquals("Type- and Server- level history operation not supported on partitioned server", e.getMessage()); + } catch (InvalidRequestException e) { + assertEquals("Type- and Server- level history operation not supported across partitions on partitioned server", e.getMessage()); + } + } + + @Test + public void testHistory_Type_SpecificPartition() { + IIdType id1A = createPatient(withPartition(1), withBirthdate("2020-01-01")); + sleepAtLeast(10); + IIdType id1B = createPatient(withPartition(1), withBirthdate("2020-01-01")); + sleepAtLeast(10); + createPatient(withPartition(2), withBirthdate("2020-01-01")); + sleepAtLeast(10); + createPatient(withPartition(2), withBirthdate("2020-01-01")); + + addReadPartition(1); + myCaptureQueriesListener.clear(); + IBundleProvider results = myPatientDao.history(null, null, mySrd); + assertEquals(2, results.sizeOrThrowNpe()); + List ids = toUnqualifiedIdValues(results); + assertThat(ids, Matchers.contains(id1B.withVersion("1").getValue(), id1A.withVersion("1").getValue())); + + assertEquals(3, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + + // Count + String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(1, countMatches(sql, "count(")); + assertEquals(1, countMatches(sql, "PARTITION_ID='1'")); + + // Fetch history resources + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(1, countMatches(sql, "PARTITION_ID='1'")); + + // Resolve forced ID + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(2).getSql(true, true); + ourLog.info("SQL:{}", sql); + assertEquals(0, countMatches(sql, "PARTITION_ID='1'")); + } + + + @Test + public void testHistory_Type_DefaultPartition() { + IIdType id1A = createPatient(withPartition(null), withBirthdate("2020-01-01")); + sleepAtLeast(10); + IIdType id1B = createPatient(withPartition(null), withBirthdate("2020-01-01")); + sleepAtLeast(10); + createPatient(withPartition(2), withBirthdate("2020-01-01")); + sleepAtLeast(10); + createPatient(withPartition(2), withBirthdate("2020-01-01")); + + addReadDefaultPartition(); + myCaptureQueriesListener.clear(); + IBundleProvider results = myPatientDao.history(null, null, mySrd); + assertEquals(2, results.sizeOrThrowNpe()); + List ids = toUnqualifiedIdValues(results); + assertThat(ids, Matchers.contains(id1B.withVersion("1").getValue(), id1A.withVersion("1").getValue())); + + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(3, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + + // Resolve resource + String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + assertEquals(1, countMatches(sql, "PARTITION_ID is null")); + assertEquals(1, countMatches(sql, "PARTITION_ID")); + + // Fetch history resource + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(1).getSql(true, true); + assertEquals(1, countMatches(sql, "PARTITION_ID is null")); + + // Resolve forced IDs + sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(2).getSql(true, true); + assertEquals(sql, 1, countMatches(sql, "forcedid0_.RESOURCE_PID in")); + assertEquals(sql,0, countMatches(sql, "PARTITION_ID is null")); + } + + @Test + public void testPartitionNotify() { + IAnonymousInterceptor interceptor = mock(IAnonymousInterceptor.class); + myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PARTITION_SELECTED, interceptor); + try { + createPatient(withPartition(1), withBirthdate("2020-01-01")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(HookParams.class); + verify(interceptor, times(1)).invoke(eq(Pointcut.STORAGE_PARTITION_SELECTED), captor.capture()); + + RequestPartitionId partitionId = captor.getValue().get(RequestPartitionId.class); + assertEquals(1, partitionId.getPartitionId().intValue()); + assertEquals("PART-1", partitionId.getPartitionName()); + + } finally { + myInterceptorRegistry.unregisterInterceptor(interceptor); } } private void createUniqueCompositeSp() { - addCreateNoPartition(); SearchParameter sp = new SearchParameter(); sp.setId("SearchParameter/patient-birthdate"); sp.setType(Enumerations.SearchParamType.DATE); @@ -1974,7 +2233,6 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { sp.addBase("Patient"); mySearchParameterDao.update(sp); - addCreateNoPartition(); sp = new SearchParameter(); sp.setId("SearchParameter/patient-birthdate-unique"); sp.setType(Enumerations.SearchParamType.COMPOSITE); @@ -2005,97 +2263,57 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { myPartitionInterceptor.addCreatePartition(requestPartitionId); } - private void addCreateNoPartition() { - myPartitionInterceptor.addCreatePartition(null); + private void addCreateDefaultPartition() { + myPartitionInterceptor.addCreatePartition(RequestPartitionId.defaultPartition()); } - private void addCreateNoPartitionId(LocalDate thePartitionDate) { + private void addCreateDefaultPartition(LocalDate thePartitionDate) { RequestPartitionId requestPartitionId = RequestPartitionId.fromPartitionId(null, thePartitionDate); myPartitionInterceptor.addCreatePartition(requestPartitionId); } private void addReadPartition(Integer thePartitionId) { - RequestPartitionId requestPartitionId = null; - if (thePartitionId != null) { - requestPartitionId = RequestPartitionId.fromPartitionId(thePartitionId, null); - } - myPartitionInterceptor.addReadPartition(requestPartitionId); + Validate.notNull(thePartitionId); + myPartitionInterceptor.addReadPartition(RequestPartitionId.fromPartitionId(thePartitionId, null)); } - private void addDefaultReadPartition() { - RequestPartitionId requestPartitionId = RequestPartitionId.fromPartitionId(null, null); - myPartitionInterceptor.addReadPartition(requestPartitionId); + private void addReadDefaultPartition() { + myPartitionInterceptor.addReadPartition(RequestPartitionId.defaultPartition()); } - public IIdType createPatient(Integer thePartitionId, Consumer... theModifiers) { - if (thePartitionId != null) { - addCreatePartition(thePartitionId, null); - } else { - addCreateNoPartition(); - } - - - Patient p = new Patient(); - for (Consumer next : theModifiers) { - next.accept(p); - } - - if (isNotBlank(p.getId())) { - return myPatientDao.update(p, mySrd).getId().toUnqualifiedVersionless(); - } else { - return myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); - } + private void addReadAllPartitions() { + myPartitionInterceptor.addReadPartition(RequestPartitionId.allPartitions()); } public void createRequestId() { when(mySrd.getRequestId()).thenReturn("REQUEST_ID"); } - private Consumer withActiveTrue() { - return t -> t.setActive(true); - } - - private Consumer withFamily(String theFamily) { - return t -> t.addName().setFamily(theFamily); - } - - private Consumer withBirthdate(String theBirthdate) { - return t -> t.getBirthDateElement().setValueAsString(theBirthdate); - } - - private Consumer withId(String theId) { + private Consumer withPartition(Integer thePartitionId) { return t -> { - assertThat(theId, matchesPattern("[a-zA-Z0-9]+")); - t.setId(theId); + if (thePartitionId != null) { + addCreatePartition(thePartitionId, null); + } else { + addCreateDefaultPartition(); + } }; } - private Consumer withTag(String theSystem, String theCode) { - return t -> t.getMeta().addTag(theSystem, theCode, theCode); + @Override + public IIdType doCreateResource(IBaseResource theResource) { + IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass()); + return dao.create(theResource, mySrd).getId().toUnqualifiedVersionless(); } - public IIdType createObservation(Integer thePartitionId, Consumer... theModifiers) { - if (thePartitionId != null) { - addCreatePartition(thePartitionId, null); - } else { - addCreateNoPartition(); - } - - - Observation observation = new Observation(); - for (Consumer next : theModifiers) { - next.accept(observation); - } - - if (isNotBlank(observation.getId())) { - return myObservationDao.update(observation, mySrd).getId().toUnqualifiedVersionless(); - } else { - return myObservationDao.create(observation, mySrd).getId().toUnqualifiedVersionless(); - } + @Override + public IIdType doUpdateResource(IBaseResource theResource) { + IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass()); + return dao.update(theResource, mySrd).getId().toUnqualifiedVersionless(); } - private Consumer withSubject(IIdType theSubject) { - return t -> t.getSubject().setReferenceElement(theSubject.toUnqualifiedVersionless()); + @Override + public FhirContext getFhirContext() { + return myFhirCtx; } @Interceptor @@ -2132,6 +2350,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { assertEquals(0, myCreateRequestPartitionIds.size()); assertEquals(0, myReadRequestPartitionIds.size()); } + } @AfterClass diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchCoordinatorSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchCoordinatorSvcImplTest.java index 1f7714951fe..4a1483a248b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchCoordinatorSvcImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchCoordinatorSvcImplTest.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.dao.r4; +import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.data.ISearchIncludeDao; import ca.uhn.fhir.jpa.dao.data.ISearchResultDao; @@ -7,7 +8,6 @@ import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.SearchResult; import ca.uhn.fhir.jpa.entity.SearchTypeEnum; import ca.uhn.fhir.jpa.model.search.SearchStatusEnum; -import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.cache.DatabaseSearchCacheSvcImpl; import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc; import ca.uhn.fhir.util.TestUtil; @@ -21,9 +21,7 @@ import java.util.Date; import java.util.UUID; import static ca.uhn.fhir.jpa.search.cache.DatabaseSearchCacheSvcImpl.DEFAULT_MAX_DELETE_CANDIDATES_TO_FIND; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; public class SearchCoordinatorSvcImplTest extends BaseJpaR4Test { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionManagementProviderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionManagementProviderTest.java index 8a323060656..23aa520a6ae 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionManagementProviderTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionManagementProviderTest.java @@ -5,6 +5,7 @@ import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.jpa.model.util.ProviderConstants; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.test.utilities.server.RestfulServerRule; import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.IntegerType; @@ -27,6 +28,7 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; @@ -61,19 +63,16 @@ public class PartitionManagementProviderTest { } @Test - public void testAddPartition() { + public void testCreatePartition() { when(myPartitionConfigSvc.createPartition(any())).thenAnswer(createAnswer()); - Parameters input = new Parameters(); - input.addParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, new IntegerType(123)); - input.addParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, new CodeType("PARTITION-123")); - input.addParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC, new StringType("a description")); + Parameters input = createInputPartition(); ourLog.info("Input:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input)); Parameters response = myClient .operation() .onServer() - .named(ProviderConstants.PARTITION_MANAGEMENT_ADD_PARTITION) + .named(ProviderConstants.PARTITION_MANAGEMENT_CREATE_PARTITION) .withParameters(input) .encodedXml() .execute(); @@ -87,14 +86,80 @@ public class PartitionManagementProviderTest { assertEquals("a description", ((StringType) response.getParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC)).getValue()); } - @Test - public void testUpdatePartition() { - when(myPartitionConfigSvc.updatePartition(any())).thenAnswer(createAnswer()); - + @NotNull + private Parameters createInputPartition() { Parameters input = new Parameters(); input.addParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, new IntegerType(123)); input.addParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, new CodeType("PARTITION-123")); input.addParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC, new StringType("a description")); + return input; + } + + @Test + public void testCreatePartition_InvalidInput() { + try { + myClient + .operation() + .onServer() + .named(ProviderConstants.PARTITION_MANAGEMENT_CREATE_PARTITION) + .withNoParameters(Parameters.class) + .encodedXml() + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertEquals("HTTP 400 Bad Request: No Partition ID supplied", e.getMessage()); + } + verify(myPartitionConfigSvc, times(0)).createPartition(any()); + } + + @Test + public void testReadPartition() { + PartitionEntity partition = new PartitionEntity(); + partition.setId(123); + partition.setName("PARTITION-123"); + partition.setDescription("a description"); + when(myPartitionConfigSvc.getPartitionById(eq(123))).thenReturn(partition); + + Parameters response = myClient + .operation() + .onServer() + .named(ProviderConstants.PARTITION_MANAGEMENT_READ_PARTITION) + .withParameter(Parameters.class, ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, new IntegerType(123)) + .useHttpGet() + .encodedXml() + .execute(); + + ourLog.info("Response:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(response)); + verify(myPartitionConfigSvc, times(1)).getPartitionById(any()); + verifyNoMoreInteractions(myPartitionConfigSvc); + + assertEquals(123, ((IntegerType) response.getParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID)).getValue().intValue()); + assertEquals("PARTITION-123", ((StringType) response.getParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME)).getValue()); + assertEquals("a description", ((StringType) response.getParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC)).getValue()); + } + + @Test + public void testReadPartition_InvalidInput() { + try { + myClient + .operation() + .onServer() + .named(ProviderConstants.PARTITION_MANAGEMENT_READ_PARTITION) + .withNoParameters(Parameters.class) + .encodedXml() + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertEquals("HTTP 400 Bad Request: No Partition ID supplied", e.getMessage()); + } + verify(myPartitionConfigSvc, times(0)).getPartitionById(any()); + } + + @Test + public void testUpdatePartition() { + when(myPartitionConfigSvc.updatePartition(any())).thenAnswer(createAnswer()); + + Parameters input = createInputPartition(); ourLog.info("Input:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input)); Parameters response = myClient @@ -114,6 +179,23 @@ public class PartitionManagementProviderTest { assertEquals("a description", ((StringType) response.getParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_DESC)).getValue()); } + @Test + public void testUpdatePartition_InvalidInput() { + try { + myClient + .operation() + .onServer() + .named(ProviderConstants.PARTITION_MANAGEMENT_UPDATE_PARTITION) + .withNoParameters(Parameters.class) + .encodedXml() + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertEquals("HTTP 400 Bad Request: No Partition ID supplied", e.getMessage()); + } + verify(myPartitionConfigSvc, times(0)).createPartition(any()); + } + @Test public void testDeletePartition() { Parameters input = new Parameters(); @@ -133,6 +215,23 @@ public class PartitionManagementProviderTest { verifyNoMoreInteractions(myPartitionConfigSvc); } + @Test + public void testDeletePartition_InvalidInput() { + try { + myClient + .operation() + .onServer() + .named(ProviderConstants.PARTITION_MANAGEMENT_CREATE_PARTITION) + .withNoParameters(Parameters.class) + .encodedXml() + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertEquals("HTTP 400 Bad Request: No Partition ID supplied", e.getMessage()); + } + verify(myPartitionConfigSvc, times(0)).createPartition(any()); + } + @Configuration public static class MyConfig { @@ -150,9 +249,7 @@ public class PartitionManagementProviderTest { @NotNull private static Answer createAnswer() { - return t -> { - return t.getArgument(0, PartitionEntity.class); - }; + return t -> t.getArgument(0, PartitionEntity.class); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionSettingsSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionSettingsSvcImplTest.java index a37983dd655..b64a418d37e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionSettingsSvcImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/partition/PartitionSettingsSvcImplTest.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.partition; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -43,7 +44,7 @@ public class PartitionSettingsSvcImplTest extends BaseJpaR4Test { try { myPartitionConfigSvc.getPartitionById(123); fail(); - } catch (IllegalArgumentException e) { + } catch (ResourceNotFoundException e) { assertEquals("No partition exists with ID 123", e.getMessage()); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorJpaR4Test.java similarity index 97% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorResourceProviderR4Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorJpaR4Test.java index 83b4d3e58fc..3b8cc5ecae3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorJpaR4Test.java @@ -10,7 +10,11 @@ import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; -import ca.uhn.fhir.rest.server.interceptor.auth.*; +import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor; +import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRule; +import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRuleTester; +import ca.uhn.fhir.rest.server.interceptor.auth.PolicyEnum; +import ca.uhn.fhir.rest.server.interceptor.auth.RuleBuilder; import ca.uhn.fhir.util.TestUtil; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; @@ -19,8 +23,19 @@ import org.apache.http.entity.ContentType; 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.*; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Condition; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Reference; import org.junit.AfterClass; import org.junit.Test; import org.slf4j.Logger; @@ -31,11 +46,14 @@ import java.util.Arrays; import java.util.List; import static org.hamcrest.Matchers.startsWith; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; -public class AuthorizationInterceptorResourceProviderR4Test extends BaseResourceProviderR4Test { +public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Test { - private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptorResourceProviderR4Test.class); + private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptorJpaR4Test.class); @Override public void before() throws Exception { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorMultitenantJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorMultitenantJpaR4Test.java new file mode 100644 index 00000000000..f639013be2a --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorMultitenantJpaR4Test.java @@ -0,0 +1,233 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.util.TestUtil; +import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; +import ca.uhn.fhir.rest.server.interceptor.auth.RuleBuilder; +import ca.uhn.fhir.test.utilities.ITestDataBuilder; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.junit.AfterClass; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@SuppressWarnings("Duplicates") +public class AuthorizationInterceptorMultitenantJpaR4Test extends BaseMultitenantResourceProviderR4Test implements ITestDataBuilder { + + private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptorMultitenantJpaR4Test.class); + + @Test + public void testCreateInTenant_Allowed() { + enableAuthorizationInterceptor(() -> new RuleBuilder() + .allow().create().allResources().withAnyId().forTenantIds(TENANT_A) + .build()); + + IIdType idA = createPatient(withTenant(TENANT_A), withActiveTrue()); + + runInTransaction(() -> { + Optional patient = myResourceTableDao.findById(idA.getIdPartAsLong()); + assertTrue(patient.isPresent()); + }); + } + + @Test + public void testCreateInTenant_Blocked() { + createPatient(withTenant(TENANT_A), withActiveTrue()); + IIdType idB = createPatient(withTenant(TENANT_B), withActiveFalse()); + + enableAuthorizationInterceptor(() -> new RuleBuilder() + .allow().create().allResources().withAnyId().forTenantIds(TENANT_A) + .build()); + + myTenantClientInterceptor.setTenantId(TENANT_B); + try { + ourClient.read().resource(Patient.class).withId(idB).execute(); + fail(); + } catch (ForbiddenOperationException e) { + // good + } + } + + @Test + public void testReadInTenant_Allowed() { + IIdType idA = createPatient(withTenant(TENANT_A), withActiveTrue()); + createPatient(withTenant(TENANT_B), withActiveFalse()); + + enableAuthorizationInterceptor(() -> new RuleBuilder() + .allow().read().allResources().withAnyId().forTenantIds(TENANT_A) + .build()); + + myTenantClientInterceptor.setTenantId(TENANT_A); + Patient p = ourClient.read().resource(Patient.class).withId(idA).execute(); + assertTrue(p.getActive()); + } + + @Test + public void testReadInTenant_Blocked() { + createPatient(withTenant(TENANT_A), withActiveTrue()); + IIdType idB = createPatient(withTenant(TENANT_B), withActiveFalse()); + + enableAuthorizationInterceptor(() -> new RuleBuilder() + .allow().read().allResources().withAnyId().forTenantIds(TENANT_A) + .build()); + + myTenantClientInterceptor.setTenantId(TENANT_B); + try { + ourClient.read().resource(Patient.class).withId(idB).execute(); + fail(); + } catch (ForbiddenOperationException e) { + // good + } + } + + @Test + public void testReadInDefaultTenant_Allowed() { + IIdType idA = createPatient(withTenant("DEFAULT"), withActiveTrue()); + + enableAuthorizationInterceptor(() -> new RuleBuilder() + .allow().read().allResources().withAnyId().forTenantIds("DEFAULT") + .build()); + + myTenantClientInterceptor.setTenantId("DEFAULT"); + Patient p = ourClient.read().resource(Patient.class).withId(idA).execute(); + assertTrue(p.getActive()); + } + + @Test + public void testReadInDefaultTenant_Blocked() { + IIdType idA = createPatient(withTenant(TENANT_A), withActiveTrue()); + + enableAuthorizationInterceptor(() -> new RuleBuilder() + .allow().read().allResources().withAnyId().forTenantIds("DEFAULT") + .build()); + + myTenantClientInterceptor.setTenantId(TENANT_A); + try { + ourClient.read().resource(Patient.class).withId(idA).execute(); + fail(); + } catch (ForbiddenOperationException e) { + // good + } + } + + @Test + public void testReadAcrossTenants_Allowed() { + myPartitionSettings.setAllowReferencesAcrossPartitions(PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED); + + IIdType patientId = createPatient(withTenant(TENANT_A), withActiveTrue()); + createObservation(withTenant(TENANT_B), withSubject(patientId.toUnqualifiedVersionless())); + + enableAuthorizationInterceptor(() -> new RuleBuilder() + .allow().read().allResources().withAnyId().forTenantIds(TENANT_A, TENANT_B) + .build()); + + myTenantClientInterceptor.setTenantId(TENANT_B); + + Bundle output = ourClient + .search() + .forResource("Observation") + .include(Observation.INCLUDE_ALL) + .returnBundle(Bundle.class) + .execute(); + assertEquals(2, output.getEntry().size()); + } + + @Test + public void testReadAcrossTenants_Blocked() { + myPartitionSettings.setAllowReferencesAcrossPartitions(PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED); + + IIdType patientId = createPatient(withTenant(TENANT_A), withActiveTrue()); + createObservation(withTenant(TENANT_B), withSubject(patientId.toUnqualifiedVersionless())); + + enableAuthorizationInterceptor(() -> new RuleBuilder() + .allow().read().allResources().withAnyId().forTenantIds(TENANT_A) + .build()); + + myTenantClientInterceptor.setTenantId(TENANT_B); + + try { + ourClient + .search() + .forResource("Observation") + .include(Observation.INCLUDE_ALL) + .returnBundle(Bundle.class) + .execute(); + fail(); + } catch (ForbiddenOperationException e) { + // good + } + } + + @Test + public void testSearchPagingAcrossTenants_Blocked() { + myPartitionSettings.setAllowReferencesAcrossPartitions(PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED); + + // Create 9 Observations: 1-8 have no subject, 9 has a subject in a different tenant + IIdType patientIdA = createPatient(withTenant(TENANT_A), withActiveTrue()).toUnqualifiedVersionless(); + IIdType patientIdB = createPatient(withTenant(TENANT_B), withActiveTrue()).toUnqualifiedVersionless(); + List observationIds = Lists.newArrayList(); + for (int i = 1; i <= 9; i++) { + IIdType subject = i == 9 ? patientIdB : patientIdA; + IIdType id = createObservation(withTenant(TENANT_A), withIdentifier("foo" + i, "val" + i), withStatus("final"), withSubject(subject)).toUnqualifiedVersionless(); + observationIds.add(id); + } + + enableAuthorizationInterceptor(() -> new RuleBuilder() + .allow().read().allResources().withAnyId().forTenantIds(TENANT_A) + .build()); + + myTenantClientInterceptor.setTenantId(TENANT_A); + + // Search and fetch the first 3 + Bundle bundle = ourClient + .search() + .forResource("Observation") + .include(Observation.INCLUDE_ALL) + .sort().ascending(Observation.IDENTIFIER) + .returnBundle(Bundle.class) + .count(3) + .execute(); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).setEncodeElements(Sets.newHashSet("Bundle.link")).encodeResourceToString(bundle)); + assertThat(toUnqualifiedVersionlessIds(bundle).toString(), toUnqualifiedVersionlessIds(bundle), contains(observationIds.get(0), observationIds.get(1), observationIds.get(2), patientIdA)); + + // Fetch the next 3 + bundle = ourClient + .loadPage() + .next(bundle) + .execute(); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).setEncodeElements(Sets.newHashSet("Bundle.link")).encodeResourceToString(bundle)); + assertThat(toUnqualifiedVersionlessIds(bundle).toString(), toUnqualifiedVersionlessIds(bundle), contains(observationIds.get(3), observationIds.get(4), observationIds.get(5), patientIdA)); + + // Fetch the next 3 - This should fail as the last observation has a cross-partition reference + try { + bundle = ourClient + .loadPage() + .next(bundle) + .execute(); + fail(); + } catch (ForbiddenOperationException e) { + // good + } + + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseMultitenantResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseMultitenantResourceProviderR4Test.java new file mode 100644 index 00000000000..20068415158 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseMultitenantResourceProviderR4Test.java @@ -0,0 +1,141 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.model.util.ProviderConstants; +import ca.uhn.fhir.jpa.partition.PartitionManagementProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.client.interceptor.UrlTenantSelectionInterceptor; +import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor; +import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRule; +import ca.uhn.fhir.rest.server.interceptor.auth.PolicyEnum; +import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor; +import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy; +import ca.uhn.fhir.test.utilities.ITestDataBuilder; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Parameters; +import org.junit.After; +import org.junit.Before; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.DEFAULT_PERSISTED_PARTITION_NAME; + +public abstract class BaseMultitenantResourceProviderR4Test extends BaseResourceProviderR4Test implements ITestDataBuilder { + + public static final int TENANT_A_ID = 1; + public static final int TENANT_B_ID = 2; + public static final String TENANT_B = "TENANT-B"; + public static final String TENANT_A = "TENANT-A"; + @Autowired + private RequestTenantPartitionInterceptor myRequestTenantPartitionInterceptor; + @Autowired + private PartitionManagementProvider myPartitionManagementProvider; + + protected CapturingInterceptor myCapturingInterceptor; + protected UrlTenantSelectionInterceptor myTenantClientInterceptor; + protected AuthorizationInterceptor myAuthorizationInterceptor; + + @Override + @Before + public void before() throws Exception { + super.before(); + + myPartitionSettings.setPartitioningEnabled(true); + ourRestServer.registerInterceptor(myRequestTenantPartitionInterceptor); + ourRestServer.registerProvider(myPartitionManagementProvider); + ourRestServer.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy()); + + myCapturingInterceptor = new CapturingInterceptor(); + ourClient.getInterceptorService().registerInterceptor(myCapturingInterceptor); + + myTenantClientInterceptor = new UrlTenantSelectionInterceptor(); + ourClient.getInterceptorService().registerInterceptor(myTenantClientInterceptor); + + ourClient.getInterceptorService().registerInterceptor(new LoggingInterceptor()); + + createTenants(); + } + + @Override + @After + public void after() throws Exception { + super.after(); + + myPartitionSettings.setPartitioningEnabled(new PartitionSettings().isPartitioningEnabled()); + ourRestServer.unregisterInterceptor(myRequestTenantPartitionInterceptor); + if (myAuthorizationInterceptor != null) { + ourRestServer.unregisterInterceptor(myAuthorizationInterceptor); + } + ourRestServer.unregisterProvider(myPartitionManagementProvider); + ourRestServer.setTenantIdentificationStrategy(null); + + ourClient.getInterceptorService().unregisterAllInterceptors(); + } + + @Override + protected boolean shouldLogClient() { + return true; + } + + + private void createTenants() { + myTenantClientInterceptor.setTenantId(DEFAULT_PERSISTED_PARTITION_NAME); + + ourClient + .operation() + .onServer() + .named(ProviderConstants.PARTITION_MANAGEMENT_CREATE_PARTITION) + .withParameter(Parameters.class, ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, new IntegerType(TENANT_A_ID)) + .andParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, new CodeType(TENANT_A)) + .execute(); + + ourClient + .operation() + .onServer() + .named(ProviderConstants.PARTITION_MANAGEMENT_CREATE_PARTITION) + .withParameter(Parameters.class, ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, new IntegerType(TENANT_B_ID)) + .andParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, new CodeType(TENANT_B)) + .execute(); + } + + public void enableAuthorizationInterceptor(Supplier> theRuleSupplier) { + myAuthorizationInterceptor = new AuthorizationInterceptor() { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return theRuleSupplier.get(); + } + }.setDefaultPolicy(PolicyEnum.DENY); + ourRestServer.registerInterceptor(myAuthorizationInterceptor); + } + + + + protected Consumer withTenant(String theTenantId) { + return t -> myTenantClientInterceptor.setTenantId(theTenantId); + } + + @Override + public IIdType doCreateResource(IBaseResource theResource) { + return ourClient.create().resource(theResource).execute().getId(); + } + + @Override + public IIdType doUpdateResource(IBaseResource theResource) { + return ourClient.update().resource(theResource).execute().getId(); + } + + @Override + public FhirContext getFhirContext() { + return myFhirCtx; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java index 7235561d227..79742b7eb82 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/MultitenantServerR4Test.java @@ -1,27 +1,14 @@ package ca.uhn.fhir.jpa.provider.r4; -import ca.uhn.fhir.jpa.model.config.PartitionSettings; -import ca.uhn.fhir.jpa.model.util.ProviderConstants; -import ca.uhn.fhir.jpa.partition.PartitionManagementProvider; -import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor; import ca.uhn.fhir.jpa.util.TestUtil; -import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; -import ca.uhn.fhir.rest.client.interceptor.UrlTenantSelectionInterceptor; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy; +import ca.uhn.fhir.test.utilities.ITestDataBuilder; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.CapabilityStatement; -import org.hl7.fhir.r4.model.CodeType; -import org.hl7.fhir.r4.model.IntegerType; -import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; -import org.junit.After; import org.junit.AfterClass; -import org.junit.Before; import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; -import static ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl.DEFAULT_PERSISTED_PARTITION_NAME; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; @@ -29,56 +16,11 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @SuppressWarnings("Duplicates") -public class MultitenantServerR4Test extends BaseResourceProviderR4Test { - - @Autowired - private RequestTenantPartitionInterceptor myRequestTenantPartitionInterceptor; - @Autowired - private PartitionManagementProvider myPartitionManagementProvider; - - private CapturingInterceptor myCapturingInterceptor; - private UrlTenantSelectionInterceptor myTenantInterceptor; - - @Override - @Before - public void before() throws Exception { - super.before(); - - myPartitionSettings.setPartitioningEnabled(true); - ourRestServer.registerInterceptor(myRequestTenantPartitionInterceptor); - ourRestServer.registerProvider(myPartitionManagementProvider); - ourRestServer.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy()); - - myCapturingInterceptor = new CapturingInterceptor(); - ourClient.getInterceptorService().registerInterceptor(myCapturingInterceptor); - - myTenantInterceptor = new UrlTenantSelectionInterceptor(); - ourClient.getInterceptorService().registerInterceptor(myTenantInterceptor); - - createTenants(); - } - - @Override - @After - public void after() throws Exception { - super.after(); - - myPartitionSettings.setPartitioningEnabled(new PartitionSettings().isPartitioningEnabled()); - ourRestServer.unregisterInterceptor(myRequestTenantPartitionInterceptor); - ourRestServer.unregisterProvider(myPartitionManagementProvider); - ourRestServer.setTenantIdentificationStrategy(null); - - ourClient.getInterceptorService().unregisterAllInterceptors(); - } - - @Override - protected boolean shouldLogClient() { - return true; - } +public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Test implements ITestDataBuilder { @Test public void testFetchCapabilityStatement() { - myTenantInterceptor.setTenantId("TENANT-A"); + myTenantClientInterceptor.setTenantId(TENANT_A); CapabilityStatement cs = ourClient.capabilities().ofType(CapabilityStatement.class).execute(); assertEquals("HAPI FHIR Server", cs.getSoftware().getName()); @@ -88,23 +30,18 @@ public class MultitenantServerR4Test extends BaseResourceProviderR4Test { @Test public void testCreateAndRead() { - myTenantInterceptor.setTenantId("TENANT-A"); - Patient patientA = new Patient(); - patientA.setActive(true); - IIdType idA = ourClient.create().resource(patientA).execute().getId().toUnqualifiedVersionless(); + // Create patients - myTenantInterceptor.setTenantId("TENANT-B"); - Patient patientB = new Patient(); - patientB.setActive(true); - ourClient.create().resource(patientB).execute(); + IIdType idA = createPatient(withTenant(TENANT_A), withActiveTrue()); + createPatient(withTenant(TENANT_B), withActiveFalse()); // Now read back - myTenantInterceptor.setTenantId("TENANT-A"); + myTenantClientInterceptor.setTenantId(TENANT_A); Patient response = ourClient.read().resource(Patient.class).withId(idA).execute(); assertTrue(response.getActive()); - myTenantInterceptor.setTenantId("TENANT-B"); + myTenantClientInterceptor.setTenantId(TENANT_B); try { ourClient.read().resource(Patient.class).withId(idA).execute(); fail(); @@ -116,37 +53,18 @@ public class MultitenantServerR4Test extends BaseResourceProviderR4Test { @Test public void testCreate_InvalidTenant() { - myTenantInterceptor.setTenantId("TENANT-ZZZ"); + myTenantClientInterceptor.setTenantId("TENANT-ZZZ"); Patient patientA = new Patient(); patientA.setActive(true); try { ourClient.create().resource(patientA).execute(); fail(); } catch (ResourceNotFoundException e) { - assertThat(e.getMessage(), containsString("Unknown partition name: TENANT-ZZZ")); + assertThat(e.getMessage(), containsString("Partition name \"TENANT-ZZZ\" is not valid")); } } - private void createTenants() { - myTenantInterceptor.setTenantId(DEFAULT_PERSISTED_PARTITION_NAME); - - ourClient - .operation() - .onServer() - .named(ProviderConstants.PARTITION_MANAGEMENT_ADD_PARTITION) - .withParameter(Parameters.class, ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, new IntegerType(1)) - .andParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, new CodeType("TENANT-A")) - .execute(); - - ourClient - .operation() - .onServer() - .named(ProviderConstants.PARTITION_MANAGEMENT_ADD_PARTITION) - .withParameter(Parameters.class, ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, new IntegerType(2)) - .andParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, new CodeType("TENANT-B")) - .execute(); - } @AfterClass diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java index d18b5060ba7..f5c0efd8eb8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java @@ -14,7 +14,7 @@ import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.SearchTypeEnum; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.model.search.SearchStatusEnum; -import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperService; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc; import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; @@ -110,7 +110,7 @@ public class SearchCoordinatorSvcImplTest { @Mock private PersistedJpaBundleProviderFactory myPersistedJpaBundleProviderFactory; @Mock - private IRequestPartitionHelperService myPartitionHelperSvc; + private IRequestPartitionHelperSvc myPartitionHelperSvc; @After public void after() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherR4Test.java index b4155968795..2d46f1b52c5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherR4Test.java @@ -7,19 +7,60 @@ import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult; import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher; -import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; -import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.InMemorySubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator; +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; +import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; import ca.uhn.fhir.jpa.util.CoordCalculatorTest; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; -import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.rest.param.CompositeParam; +import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.HasParam; +import ca.uhn.fhir.rest.param.NumberParam; +import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import ca.uhn.fhir.rest.param.QuantityParam; +import ca.uhn.fhir.rest.param.ReferenceAndListParam; +import ca.uhn.fhir.rest.param.ReferenceOrListParam; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.TokenParamModifier; +import ca.uhn.fhir.rest.param.UriParam; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Condition; +import org.hl7.fhir.r4.model.ContactPoint; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.MedicationAdministration; +import org.hl7.fhir.r4.model.MolecularSequence; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Quantity; +import org.hl7.fhir.r4.model.Range; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.RiskAssessment; +import org.hl7.fhir.r4.model.SimpleQuantity; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Subscription; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.ValueSet; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -31,7 +72,10 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {TestR4Config.class}) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java index 67321530b34..7a35525ca0c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java @@ -33,6 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import static org.junit.Assert.fail; @@ -219,8 +220,20 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; + ourLog.info("Creating 2 subscriptions"); Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase); Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase); + + + runInTransaction(()->{ + ourLog.info("All token indexes:\n * {}", myResourceIndexedSearchParamTokenDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * "))); + }); + + myCaptureQueriesListener.clear(); + mySubscriptionLoader.doSyncSubscriptionsForUnitTest(); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + + ourLog.info("Waiting for activation"); waitForActivatedSubscriptionCount(2); Observation observation1 = sendObservation(code, "SNOMED-CT"); diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/JdbcUtils.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/JdbcUtils.java index 1a73e3e542a..962d5d80ea3 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/JdbcUtils.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/JdbcUtils.java @@ -31,7 +31,12 @@ import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver; import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter; import org.hibernate.engine.jdbc.dialect.spi.DialectResolver; import org.hibernate.engine.jdbc.env.internal.NormalizingIdentifierHelperImpl; -import org.hibernate.engine.jdbc.env.spi.*; +import org.hibernate.engine.jdbc.env.spi.ExtractedDatabaseMetaData; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.jdbc.env.spi.LobCreatorBuilder; +import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; +import org.hibernate.engine.jdbc.env.spi.QualifiedObjectNameFormatter; import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.engine.jdbc.spi.TypeInfo; import org.hibernate.service.ServiceRegistry; @@ -44,8 +49,18 @@ import org.springframework.jdbc.core.ColumnMapRowMapper; import javax.annotation.Nullable; import javax.sql.DataSource; -import java.sql.*; -import java.util.*; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; import static org.thymeleaf.util.StringUtils.toUpperCase; @@ -223,6 +238,7 @@ public class JdbcUtils { int dataType = indexes.getInt("DATA_TYPE"); Long length = indexes.getLong("COLUMN_SIZE"); switch (dataType) { + case Types.BIT: case Types.BOOLEAN: return new ColumnType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN, length); case Types.VARCHAR: diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java index a1bf1ff9ff7..618c51a43b1 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java @@ -35,6 +35,8 @@ import javax.persistence.Transient; import java.util.Collection; import java.util.Date; +import static org.apache.commons.lang3.StringUtils.defaultString; + @MappedSuperclass public abstract class BaseHasResource extends BasePartitionable implements IBaseResourceEntity, IBasePersistedResource { @@ -73,6 +75,7 @@ public abstract class BaseHasResource extends BasePartitionable implements IBase } public void setTransientForcedId(String theTransientForcedId) { + assert !defaultString(theTransientForcedId).contains("/") : "Forced ID should not include type: " + theTransientForcedId; myTransientForcedId = theTransientForcedId; } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BasePartitionable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BasePartitionable.java index 83af36ef4d5..a051701c06d 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BasePartitionable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BasePartitionable.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.model.entity; import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.persistence.Column; import javax.persistence.Embedded; @@ -41,11 +42,12 @@ public class BasePartitionable implements Serializable { @Column(name = PartitionablePartitionId.PARTITION_ID, insertable = false, updatable = false, nullable = true) private Integer myPartitionIdValue; + @Nonnull public RequestPartitionId getPartitionId() { if (myPartitionId != null) { return myPartitionId.toPartitionId(); } else { - return null; + return RequestPartitionId.defaultPartition(); } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java index 346fdf5340f..658589ad8f2 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java @@ -33,6 +33,7 @@ import com.google.common.hash.Hashing; import org.hibernate.search.annotations.ContainedIn; import org.hibernate.search.annotations.Field; +import javax.annotation.Nullable; import javax.persistence.Column; import javax.persistence.FetchType; import javax.persistence.JoinColumn; @@ -85,6 +86,9 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { @Transient private transient PartitionSettings myPartitionSettings; + @Transient + private transient ModelConfig myModelConfig; + @Override public abstract Long getId(); @@ -112,6 +116,9 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { myMissing = source.myMissing; myParamName = source.myParamName; myUpdated = source.myUpdated; + myModelConfig = source.myModelConfig; + myPartitionSettings = source.myPartitionSettings; + setPartitionId(source.getPartitionId()); } public Long getResourcePid() { @@ -145,6 +152,11 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { public abstract IQueryParameterType toQueryParameterType(); + @Override + public void setPartitionId(@Nullable RequestPartitionId theRequestPartitionId) { + super.setPartitionId(theRequestPartitionId); + } + public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) { throw new UnsupportedOperationException("No parameter matcher for " + theParam); } @@ -158,6 +170,15 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { return this; } + public BaseResourceIndexedSearchParam setModelConfig(ModelConfig theModelConfig) { + myModelConfig = theModelConfig; + return this; + } + + public ModelConfig getModelConfig() { + return myModelConfig; + } + public static long calculateHashIdentity(PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, String theParamName) { return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName); } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java index 5ace5c24aaf..3f13e84256f 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java @@ -20,9 +20,22 @@ package ca.uhn.fhir.jpa.model.entity; * #L% */ +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.annotations.ColumnDefault; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; @Entity() @Table(name = ForcedId.HFJ_FORCED_ID, uniqueConstraints = { @@ -95,4 +108,17 @@ public class ForcedId extends BasePartitionable { return myId; } + public Long getResourceId() { + return myResourcePid; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("pid", myId) + .append("resourceType", myResourceType) + .append("forcedId", myForcedId) + .append("resourcePid", myResourcePid) + .toString(); + } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java index 45c1aa9968b..aac0a67b8a6 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.model.entity; */ import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; @@ -49,4 +50,6 @@ public interface IBaseResourceEntity { long getVersion(); boolean isHasTags(); + + RequestPartitionId getPartitionId(); } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java index 08f7b2b5d53..8415b278b6a 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java @@ -25,7 +25,25 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.Constants; import org.hibernate.annotations.OptimisticLock; -import javax.persistence.*; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.Lob; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; @@ -182,14 +200,19 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl @Override public IdDt getIdDt() { - if (getResourceTable().getForcedId() == null) { - Long id = getResourceId(); - return new IdDt(getResourceType() + '/' + id + '/' + Constants.PARAM_HISTORY + '/' + getVersion()); + // Avoid a join query if possible + String resourceIdPart; + if (getTransientForcedId() != null) { + resourceIdPart = getTransientForcedId(); } else { - // Avoid a join query if possible - String forcedId = getTransientForcedId() != null ? getTransientForcedId() : getResourceTable().getForcedId().getForcedId(); - return new IdDt(getResourceType() + '/' + forcedId + '/' + Constants.PARAM_HISTORY + '/' + getVersion()); + if (getResourceTable().getForcedId() == null) { + Long id = getResourceId(); + resourceIdPart = id.toString(); + } else { + resourceIdPart = getResourceTable().getForcedId().getForcedId(); + } } + return new IdDt(getResourceType() + '/' + resourceIdPart + '/' + Constants.PARAM_HISTORY + '/' + getVersion()); } @Override diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java index d998fab6ae3..c8c0df06ea4 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java @@ -247,10 +247,12 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar @Override public String toString() { ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); + b.append("partitionId", getPartitionId()); b.append("paramName", getParamName()); b.append("resourceId", getResourcePid()); b.append("valueLow", new InstantDt(getValueLow())); b.append("valueHigh", new InstantDt(getValueHigh())); + b.append("hashIdentity", myHashIdentity); b.append("missing", isMissing()); return b.build(); } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java index b930e1a4875..0f9849dd2f8 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java @@ -49,7 +49,6 @@ import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.SequenceGenerator; import javax.persistence.Table; -import javax.persistence.Transient; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.left; @@ -119,8 +118,6 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP */ @Column(name = "HASH_EXACT", nullable = true) private Long myHashExact; - @Transient - private transient ModelConfig myModelConfig; public ResourceIndexedSearchParamString() { super(); @@ -153,7 +150,7 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP String paramName = getParamName(); String valueNormalized = getValueNormalized(); String valueExact = getValueExact(); - setHashNormalizedPrefix(calculateHashNormalized(getPartitionSettings(), getPartitionId(), myModelConfig, resourceType, paramName, valueNormalized)); + setHashNormalizedPrefix(calculateHashNormalized(getPartitionSettings(), getPartitionId(), getModelConfig(), resourceType, paramName, valueNormalized)); setHashExact(calculateHashExact(getPartitionSettings(), getPartitionId(), resourceType, paramName, valueExact)); setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); } @@ -248,11 +245,6 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP return b.toHashCode(); } - public BaseResourceIndexedSearchParam setModelConfig(ModelConfig theModelConfig) { - myModelConfig = theModelConfig; - return this; - } - @Override public IQueryParameterType toQueryParameterType() { return new StringParam(getValueExact()); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java index b69edb12fa0..b70b7e25129 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java @@ -40,6 +40,7 @@ import javax.persistence.Id; import javax.persistence.Index; import javax.persistence.SequenceGenerator; import javax.persistence.Table; +import javax.validation.constraints.NotNull; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.trim; @@ -243,6 +244,7 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa b.append("paramName", getParamName()); b.append("system", getSystem()); b.append("value", getValue()); + b.append("hashIdentity", myHashIdentity); return b.build(); } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/ProviderConstants.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/ProviderConstants.java index 4f524f07cea..5cf02d3b526 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/ProviderConstants.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/ProviderConstants.java @@ -27,7 +27,7 @@ public class ProviderConstants { /** * Operation name: add partition */ - public static final String PARTITION_MANAGEMENT_ADD_PARTITION = "$partition-management-add-partition"; + public static final String PARTITION_MANAGEMENT_CREATE_PARTITION = "$partition-management-create-partition"; /** * Operation name: update partition diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDateTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDateTest.java index 74fac849060..41ce91a6e52 100644 --- a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDateTest.java +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDateTest.java @@ -9,7 +9,10 @@ import java.time.Instant; import java.util.Calendar; import java.util.Date; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; public class ResourceIndexedSearchParamDateTest { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java index 2f8af038d57..513a5fcfe49 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java @@ -22,7 +22,19 @@ package ca.uhn.fhir.jpa.searchparam.extractor; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.model.config.PartitionSettings; -import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndex; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; +import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.param.ReferenceParam; diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java index b656e8f63e4..1c9f3e713b0 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java @@ -29,7 +29,17 @@ import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.cross.IResourceLookup; -import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; +import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; @@ -45,6 +55,8 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.springframework.beans.factory.annotation.Autowired; +import javax.annotation.Nonnull; +import javax.validation.constraints.NotNull; import java.util.Collection; import java.util.Date; import java.util.HashMap; @@ -174,7 +186,7 @@ public class SearchParamExtractorService { theEntity.setHasLinks(theParams.myLinks.size() > 0); } - private void extractResourceLinks(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, ResourceTable theEntity, Date theUpdateTime, RuntimeSearchParam theRuntimeSearchParam, PathAndRef thePathAndRef, boolean theFailOnInvalidReference, RequestDetails theRequest, Map theResourceIdToResolvedTarget) { + private void extractResourceLinks(@NotNull RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, ResourceTable theEntity, Date theUpdateTime, RuntimeSearchParam theRuntimeSearchParam, PathAndRef thePathAndRef, boolean theFailOnInvalidReference, RequestDetails theRequest, Map theResourceIdToResolvedTarget) { IBaseReference nextReference = thePathAndRef.getRef(); IIdType nextId = nextReference.getReferenceElement(); String path = thePathAndRef.getPath(); @@ -276,7 +288,7 @@ public class SearchParamExtractorService { theParams.myLinks.add(resourceLink); } - private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull(RequestPartitionId theRequestPartitionId, ResourceTable theEntity, Date theUpdateTime, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, PathAndRef nextPathAndRef, IIdType theNextId, String theTypeString, Class theType, IBaseReference theReference, RequestDetails theRequest, Map theResourceIdToResolvedTarget) { + private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull(@Nonnull RequestPartitionId theRequestPartitionId, ResourceTable theEntity, Date theUpdateTime, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, PathAndRef nextPathAndRef, IIdType theNextId, String theTypeString, Class theType, IBaseReference theReference, RequestDetails theRequest, Map theResourceIdToResolvedTarget) { /* * We keep a cache of resolved target resources. This is good since for some resource types, there * are multiple search parameters that map to the same element path within a resource (e.g. @@ -286,7 +298,7 @@ public class SearchParamExtractorService { RequestPartitionId targetRequestPartitionId = theRequestPartitionId; if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.getAllowReferencesAcrossPartitions() == PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED) { - targetRequestPartitionId = null; + targetRequestPartitionId = RequestPartitionId.allPartitions(); } String key = RequestPartitionId.stringifyForKey(targetRequestPartitionId) + "/" + theNextId.getValue(); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/provider/SearchableHashMapResourceProvider.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/provider/SearchableHashMapResourceProvider.java index 951145dfe4a..a30cdbbe9c8 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/provider/SearchableHashMapResourceProvider.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/provider/SearchableHashMapResourceProvider.java @@ -47,17 +47,19 @@ public class SearchableHashMapResourceProvider extends mySearchParamMatcher = theSearchParamMatcher; } - public List searchByCriteria(String theCriteria, RequestDetails theRequest) { + public List searchByCriteria(String theCriteria, RequestDetails theRequest) { return searchBy(resource -> mySearchParamMatcher.match(theCriteria, resource, theRequest), theRequest); } - public List searchByParams(SearchParameterMap theSearchParams, RequestDetails theRequest) { + public List searchByParams(SearchParameterMap theSearchParams, RequestDetails theRequest) { return searchBy(resource -> mySearchParamMatcher.match(theSearchParams.toNormalizedQueryString(getFhirContext()), resource, theRequest), theRequest); } - private List searchBy(Function theMatcher, RequestDetails theRequest) { - List allEResources = searchAll(theRequest); + private List searchBy(Function theMatcher, RequestDetails theRequest) { + mySearchCount.incrementAndGet(); + List allEResources = getAllResources(); + List matches = new ArrayList<>(); for (T resource : allEResources) { InMemoryMatchResult result = theMatcher.apply(resource); @@ -68,6 +70,6 @@ public class SearchableHashMapResourceProvider extends matches.add(resource); } } - return matches; + return fireInterceptorsAndFilterAsNeeded(matches, theRequest); } } diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherR5Test.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherR5Test.java index d333fe3889e..869cc01ea95 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherR5Test.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcherR5Test.java @@ -30,7 +30,9 @@ import java.time.Duration; import java.time.Instant; import java.util.Date; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistry.java index fd55e180981..4cb5ed5e0f3 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistry.java @@ -90,11 +90,12 @@ public class SubscriptionRegistry { String channelName = mySubscriptionDeliveryChannelNamer.nameFromSubscription(canonicalized); - ourLog.info("Registering active subscription {}", subscriptionId); ActiveSubscription activeSubscription = new ActiveSubscription(canonicalized, channelName); mySubscriptionChannelRegistry.add(activeSubscription); myActiveSubscriptionCache.put(subscriptionId, activeSubscription); + ourLog.info("Registered active subscription {} - Have {} registered", subscriptionId, myActiveSubscriptionCache.size()); + // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED HookParams params = new HookParams() .add(CanonicalSubscription.class, canonicalized); @@ -105,10 +106,10 @@ public class SubscriptionRegistry { public void unregisterSubscriptionIfRegistered(String theSubscriptionId) { Validate.notNull(theSubscriptionId); - ourLog.info("Unregistering active subscription {}", theSubscriptionId); ActiveSubscription activeSubscription = myActiveSubscriptionCache.remove(theSubscriptionId); if (activeSubscription != null) { mySubscriptionChannelRegistry.remove(activeSubscription); + ourLog.info("Unregistered active subscription {} - Have {} registered", theSubscriptionId, myActiveSubscriptionCache.size()); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/IBundleProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/IBundleProvider.java index 26e54eae2c6..3f87a817433 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/IBundleProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/IBundleProvider.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.rest.api.server; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; @@ -163,4 +164,12 @@ public interface IBundleProvider { return getResources(0, 1).isEmpty(); } + /** + * Returns the value of {@link #size()} and throws a {@link NullPointerException} of it is null + */ + default int sizeOrThrowNpe() { + Integer retVal = size(); + Validate.notNull(retVal, "size() returned null"); + return retVal; + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SimplePreResourceShowDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SimplePreResourceShowDetails.java index ed7b8b79a32..3bc356d9d9c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SimplePreResourceShowDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SimplePreResourceShowDetails.java @@ -24,50 +24,64 @@ import com.google.common.collect.Lists; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; import java.util.List; -public class SimplePreResourceShowDetails implements IPreResourceShowDetails, Iterable { +public class SimplePreResourceShowDetails implements IPreResourceShowDetails { - private final List myResources; - private final boolean[] mySubSets; + private static final IBaseResource[] EMPTY_RESOURCE_ARRAY = new IBaseResource[0]; + private final IBaseResource[] myResources; + private final boolean[] myResourceMarkedAsSubset; + + /** + * Constructor for a single resource + */ public SimplePreResourceShowDetails(IBaseResource theResource) { this(Lists.newArrayList(theResource)); } - public SimplePreResourceShowDetails(List theResources) { - //noinspection unchecked - myResources = (List) theResources; - mySubSets = new boolean[myResources.size()]; + /** + * Constructor for a collection of resources + */ + public SimplePreResourceShowDetails(Collection theResources) { + myResources = theResources.toArray(EMPTY_RESOURCE_ARRAY); + myResourceMarkedAsSubset = new boolean[myResources.length]; } @Override public int size() { - return myResources.size(); + return myResources.length; } @Override public IBaseResource getResource(int theIndex) { - return myResources.get(theIndex); + return myResources[theIndex]; } @Override public void setResource(int theIndex, IBaseResource theResource) { Validate.isTrue(theIndex >= 0, "Invalid index %d - theIndex must not be < 0", theIndex); - Validate.isTrue(theIndex < myResources.size(), "Invalid index {} - theIndex must be < %d", theIndex, myResources.size()); - myResources.set(theIndex, theResource); + Validate.isTrue(theIndex < myResources.length, "Invalid index {} - theIndex must be < %d", theIndex, myResources.length); + myResources[theIndex] = theResource; } @Override public void markResourceAtIndexAsSubset(int theIndex) { Validate.isTrue(theIndex >= 0, "Invalid index %d - theIndex must not be < 0", theIndex); - Validate.isTrue(theIndex < myResources.size(), "Invalid index {} - theIndex must be < %d", theIndex, myResources.size()); - mySubSets[theIndex] = true; + Validate.isTrue(theIndex < myResources.length, "Invalid index {} - theIndex must be < %d", theIndex, myResources.length); + myResourceMarkedAsSubset[theIndex] = true; } @Override public Iterator iterator() { - return myResources.iterator(); + return Arrays.asList(myResources).iterator(); + } + + public List toList() { + return Lists.newArrayList(myResources); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index 2221b02b56c..3c80504455a 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -34,13 +34,23 @@ import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.annotation.Destroy; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Initialize; -import ca.uhn.fhir.rest.api.*; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PreferReturnEnum; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IFhirVersionServer; import ca.uhn.fhir.rest.api.server.IRestfulServer; import ca.uhn.fhir.rest.api.server.ParseAction; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding; -import ca.uhn.fhir.rest.server.exceptions.*; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.method.BaseMethodBinding; @@ -48,7 +58,11 @@ import ca.uhn.fhir.rest.server.method.ConformanceMethodBinding; import ca.uhn.fhir.rest.server.method.MethodMatchEnum; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.tenant.ITenantIdentificationStrategy; -import ca.uhn.fhir.util.*; +import ca.uhn.fhir.util.CoverageIgnore; +import ca.uhn.fhir.util.ReflectionUtil; +import ca.uhn.fhir.util.UrlPathTokenizer; +import ca.uhn.fhir.util.UrlUtil; +import ca.uhn.fhir.util.VersionUtil; import com.google.common.collect.Lists; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; @@ -71,7 +85,15 @@ import java.io.Writer; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java index 3bbacdcf283..80ad36dc865 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java @@ -41,7 +41,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import static org.apache.commons.lang3.StringUtils.defaultString; @@ -224,9 +230,10 @@ public class AuthorizationInterceptor implements IRuleApplier { * * @param theDefaultPolicy The policy (must not be null) */ - public void setDefaultPolicy(PolicyEnum theDefaultPolicy) { + public AuthorizationInterceptor setDefaultPolicy(PolicyEnum theDefaultPolicy) { Validate.notNull(theDefaultPolicy, "theDefaultPolicy must not be null"); myDefaultPolicy = theDefaultPolicy; + return this; } /** diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java index 0f748fb6cb5..706fa5cf344 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java @@ -35,7 +35,6 @@ abstract class BaseRule implements IAuthRule { private String myName; private PolicyEnum myMode; private List myTesters; - private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker; BaseRule(String theRuleName) { myName = theRuleName; @@ -53,7 +52,9 @@ abstract class BaseRule implements IAuthRule { theTesters.forEach(this::addTester); } - boolean applyTesters(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource, IBaseResource theOutputResource) { + private boolean applyTesters(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource, IBaseResource theOutputResource) { + assert !(theInputResource != null && theOutputResource != null); + boolean retVal = true; if (theOutputResource == null) { for (IAuthRuleTester next : getTesters()) { @@ -62,7 +63,15 @@ abstract class BaseRule implements IAuthRule { break; } } + } else { + for (IAuthRuleTester next : getTesters()) { + if (!next.matchesOutput(theOperation, theRequestDetails, theOutputResource)) { + retVal = false; + break; + } + } } + return retVal; } @@ -80,14 +89,6 @@ abstract class BaseRule implements IAuthRule { return myName; } - public RuleBuilder.ITenantApplicabilityChecker getTenantApplicabilityChecker() { - return myTenantApplicabilityChecker; - } - - public final void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) { - myTenantApplicabilityChecker = theTenantApplicabilityChecker; - } - public List getTesters() { if (myTesters == null) { return Collections.emptyList(); @@ -95,17 +96,10 @@ abstract class BaseRule implements IAuthRule { return Collections.unmodifiableList(myTesters); } - public boolean isOtherTenant(RequestDetails theRequestDetails) { - boolean otherTenant = false; - if (getTenantApplicabilityChecker() != null) { - if (!getTenantApplicabilityChecker().applies(theRequestDetails)) { - otherTenant = true; - } + Verdict newVerdict(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource) { + if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { + return null; } - return otherTenant; - } - - Verdict newVerdict() { return new Verdict(myMode, this); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleFinished.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleFinished.java index 50941d103d9..a9bb8b9b6c0 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleFinished.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleFinished.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth; * #L% */ +import javax.annotation.Nullable; import java.util.List; public interface IAuthRuleFinished { @@ -45,7 +46,9 @@ public interface IAuthRuleFinished { * ..the tester will be invoked on any $everything operations on Patient * resources as a final check as to whether the rule applies or not. In this * example, the tester is not invoked for other operations. + * + * @param theTester The tester to add, or null */ - IAuthRuleFinished withTester(IAuthRuleTester theTester); + IAuthRuleFinished withTester(@Nullable IAuthRuleTester theTester); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleTester.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleTester.java index 0ccb37fd85b..2b15386f5d1 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleTester.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleTester.java @@ -41,8 +41,28 @@ public interface IAuthRuleTester { * THIS IS AN EXPERIMENTAL API! Feedback is welcome, and this API * may change. * + * @param theOperation The FHIR operation being performed - Note that this is not necessarily the same as the value obtained from invoking + * {@link RequestDetails#getRestOperationType()} on {@literal theRequestDetails} because multiple operations can be nested within + * an HTTP request using FHIR transaction and batch operations * @since 3.4.0 */ - boolean matches(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource); + default boolean matches(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource) { + return true; + } + + /** + * Allows user-supplied logic for authorization rules. + *

+ * THIS IS AN EXPERIMENTAL API! Feedback is welcome, and this API + * may change. + * + * @param theOperation The FHIR operation being performed - Note that this is not necessarily the same as the value obtained from invoking + * {@link RequestDetails#getRestOperationType()} on {@literal theRequestDetails} because multiple operations can be nested within + * an HTTP request using FHIR transaction and batch operations + * @since 5.0.0 + */ + default boolean matchesOutput(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theOutputResource) { + return true; + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java index 0eaea1e5798..38e8a10783b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java @@ -84,10 +84,6 @@ class OperationRule extends BaseRule implements IAuthRule { public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set theFlags, Pointcut thePointcut) { FhirContext ctx = theRequestDetails.getServer().getFhirContext(); - if (isOtherTenant(theRequestDetails)) { - return null; - } - boolean applies = false; switch (theOperation) { case ADD_TAGS: @@ -197,15 +193,7 @@ class OperationRule extends BaseRule implements IAuthRule { return null; } - if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { - return null; - } - - return newVerdict(); - } - - public String getOperationName() { - return myOperationName; + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } /** diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java index 51849c8f7e4..5f8ba9cd60c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java @@ -20,8 +20,10 @@ package ca.uhn.fhir.rest.server.interceptor.auth; * #L% */ +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.model.api.annotation.ResourceDef; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import com.google.common.collect.Lists; @@ -29,7 +31,13 @@ import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; @@ -67,7 +75,9 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOpClassifierFinished allowAll(String theRuleName) { RuleImplOp rule = new RuleImplOp(theRuleName); - myRules.add(rule.setOp(RuleOpEnum.ALLOW_ALL)); + rule.setOp(RuleOpEnum.ALL); + rule.setMode(PolicyEnum.ALLOW); + myRules.add(rule); return new RuleBuilderFinished(rule); } @@ -97,18 +107,15 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOpClassifierFinished denyAll(String theRuleName) { RuleImplOp rule = new RuleImplOp(theRuleName); - myRules.add(rule.setOp(RuleOpEnum.DENY_ALL)); + rule.setOp(RuleOpEnum.ALL); + rule.setMode(PolicyEnum.DENY); + myRules.add(rule); return new RuleBuilderFinished(rule); } - public interface ITenantApplicabilityChecker { - boolean applies(RequestDetails theRequest); - } - private class RuleBuilderFinished implements IAuthRuleFinished, IAuthRuleBuilderRuleOpClassifierFinished, IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId { protected final BaseRule myOpRule; - ITenantApplicabilityChecker myTenantApplicabilityChecker; private List myTesters; RuleBuilderFinished(BaseRule theRule) { @@ -142,7 +149,7 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId forTenantIds(final Collection theTenantIds) { - setTenantApplicabilityChecker(theRequest -> theTenantIds.contains(theRequest.getTenantId())); + withTester(new TenantCheckingTester(theTenantIds, true)); return this; } @@ -160,25 +167,63 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId notForTenantIds(final Collection theTenantIds) { - setTenantApplicabilityChecker(theRequest -> !theTenantIds.contains(theRequest.getTenantId())); + withTester(new TenantCheckingTester(theTenantIds, false)); return this; } - private void setTenantApplicabilityChecker(ITenantApplicabilityChecker theTenantApplicabilityChecker) { - myTenantApplicabilityChecker = theTenantApplicabilityChecker; - myOpRule.setTenantApplicabilityChecker(myTenantApplicabilityChecker); - } - @Override public IAuthRuleFinished withTester(IAuthRuleTester theTester) { - if (myTesters == null) { - myTesters = new ArrayList<>(); + if (theTester != null) { + if (myTesters == null) { + myTesters = new ArrayList<>(); + } + myTesters.add(theTester); + myOpRule.addTester(theTester); } - myTesters.add(theTester); - myOpRule.addTester(theTester); return this; } + + private class TenantCheckingTester implements IAuthRuleTester { + private final Collection myTenantIds; + private final boolean myOutcome; + + public TenantCheckingTester(Collection theTenantIds, boolean theOutcome) { + myTenantIds = theTenantIds; + myOutcome = theOutcome; + } + + @Override + public boolean matches(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource) { + if (!myTenantIds.contains(theRequestDetails.getTenantId())) { + return !myOutcome; + } + + return matchesResource(theInputResource); + } + + @Override + public boolean matchesOutput(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theOutputResource) { + if (!myTenantIds.contains(theRequestDetails.getTenantId())) { + return !myOutcome; + } + + return matchesResource(theOutputResource); + } + + private boolean matchesResource(IBaseResource theResource) { + if (theResource != null) { + RequestPartitionId partitionId = (RequestPartitionId) theResource.getUserData(Constants.RESOURCE_PARTITION_ID); + if (partitionId != null) { + if (!myTenantIds.contains(partitionId.getPartitionName())) { + return !myOutcome; + } + } + } + + return myOutcome; + } + } } private class RuleBuilderRule implements IAuthRuleBuilderRule { @@ -310,7 +355,6 @@ public class RuleBuilder implements IAuthRuleBuilder { rule.setOperationType(myOperationType); rule.setAppliesTo(myAppliesTo); rule.setAppliesToTypes(myAppliesToTypes); - rule.setTenantApplicabilityChecker(myTenantApplicabilityChecker); rule.addTesters(getTesters()); myRules.add(rule); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplConditional.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplConditional.java index 7e0a1bced08..1aca1599b45 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplConditional.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplConditional.java @@ -42,10 +42,7 @@ public class RuleImplConditional extends BaseRule implements IAuthRule { @Override public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set theFlags, Pointcut thePointcut) { - - if (isOtherTenant(theRequestDetails)) { - return null; - } + assert !(theInputResource != null && theOutputResource != null); if (theInputResourceId != null && theInputResourceId.hasIdPart()) { return null; @@ -75,17 +72,7 @@ public class RuleImplConditional extends BaseRule implements IAuthRule { break; } - if (getTenantApplicabilityChecker() != null) { - if (!getTenantApplicabilityChecker().applies(theRequestDetails)) { - return null; - } - } - - if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { - return null; - } - - return newVerdict(); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } return null; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java index c3ce77a2967..2db10566b56 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java @@ -13,9 +13,9 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict; import ca.uhn.fhir.util.BundleUtil; -import ca.uhn.fhir.util.bundle.BundleEntryParts; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.UrlUtil; +import ca.uhn.fhir.util.bundle.BundleEntryParts; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -25,7 +25,12 @@ import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -83,10 +88,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set theFlags, Pointcut thePointcut) { - if (isOtherTenant(theRequestDetails)) { - return null; - } - FhirContext ctx = theRequestDetails.getServer().getFhirContext(); IBaseResource appliesToResource; @@ -96,9 +97,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { switch (myOp) { case READ: if (theOutputResource == null) { - if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { - return null; - } switch (theOperation) { case READ: @@ -109,12 +107,12 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { case SEARCH_SYSTEM: case HISTORY_SYSTEM: if (theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) { - return new Verdict(PolicyEnum.ALLOW, this); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } break; case SEARCH_TYPE: if (theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) { - return new Verdict(PolicyEnum.ALLOW, this); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } appliesToResourceType = theRequestDetails.getResourceName(); appliesToSearchParams = theRequestDetails.getParameters(); @@ -148,18 +146,18 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { break; case HISTORY_TYPE: if (theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) { - return new Verdict(PolicyEnum.ALLOW, this); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } appliesToResourceType = theRequestDetails.getResourceName(); break; case HISTORY_INSTANCE: if (theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) { - return new Verdict(PolicyEnum.ALLOW, this); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } appliesToResourceId = Collections.singleton(theInputResourceId); break; case GET_PAGE: - return new Verdict(PolicyEnum.ALLOW, this); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); // None of the following are checked on the way in case ADD_TAGS: @@ -240,10 +238,10 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { if (theInputResourceId.hasIdPart() == false) { // This is a conditional DELETE, so we'll authorize it using STORAGE events instead // so just let it through for now.. - return newVerdict(); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } if (theInputResource== null && myClassifierCompartmentOwners != null && myClassifierCompartmentOwners.size() > 0) { - return newVerdict(); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } appliesToResource = theInputResource; @@ -254,7 +252,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { break; case GRAPHQL: if (theOperation == RestOperationTypeEnum.GRAPHQL_REQUEST) { - return newVerdict(); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } else { return null; } @@ -264,7 +262,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { } if (theInputResource != null && requestAppliesToTransaction(ctx, myOp, theInputResource)) { if (getMode() == PolicyEnum.DENY) { - return new Verdict(PolicyEnum.DENY, this); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } List inputResources = BundleUtil.toListOfEntries(ctx, (IBaseBundle) theInputResource); Verdict verdict = null; @@ -338,7 +336,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { * be applying security on the way out */ if (allComponentsAreGets) { - return newVerdict(); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } return verdict; @@ -364,22 +362,11 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { } else { return null; } - case ALLOW_ALL: - if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { - return null; - } - return new Verdict(PolicyEnum.ALLOW, this); - case DENY_ALL: - if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { - return null; - } - return new Verdict(PolicyEnum.DENY, this); + case ALL: + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); case METADATA: if (theOperation == RestOperationTypeEnum.METADATA) { - if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { - return null; - } - return newVerdict(); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } return null; default: @@ -402,9 +389,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { if (!next.getIdPart().equals(requestAppliesToResource.getIdPart())) { continue; } - if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { - return null; - } haveMatches++; break; } @@ -412,18 +396,15 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { } if (haveMatches == appliesToResourceId.size()) { - return newVerdict(); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } } return null; case ALL_RESOURCES: if (appliesToResourceType != null) { - if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { - return null; - } if (myClassifierType == ClassifierTypeEnum.ANY_ID) { - return new Verdict(PolicyEnum.ALLOW, this); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } } break; @@ -450,11 +431,8 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { if (!myAppliesToTypes.contains(appliesToResourceType)) { return null; } - if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { - return null; - } if (myClassifierType == ClassifierTypeEnum.ANY_ID) { - return newVerdict(); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } else if (myClassifierType == ClassifierTypeEnum.IN_COMPARTMENT) { // ok we'll check below } @@ -495,7 +473,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { * it makes sense. */ if (next.getResourceType().equals(appliesToResourceType)) { - Verdict verdict = checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(appliesToSearchParams, next, IAnyResource.SP_RES_ID); + Verdict verdict = checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(appliesToSearchParams, next, IAnyResource.SP_RES_ID, theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); if (verdict != null) { return verdict; } @@ -528,13 +506,13 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { if (appliesToSearchParams != null && !theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) { for (RuntimeSearchParam nextRuntimeSearchParam : params) { String name = nextRuntimeSearchParam.getName(); - Verdict verdict = checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(appliesToSearchParams, next, name); + Verdict verdict = checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(appliesToSearchParams, next, name, theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); if (verdict != null) { return verdict; } } - } else { - return new Verdict(PolicyEnum.ALLOW, this); + } else if (getMode() == PolicyEnum.ALLOW){ + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } break; } @@ -549,25 +527,21 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { throw new IllegalStateException("Unable to apply security to event of applies to type " + myAppliesTo); } - if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { - return null; - } - - return newVerdict(); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } - private Verdict checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(Map theSearchParams, IIdType theCompartmentOwner, String theSearchParamName) { + private Verdict checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(Map theSearchParams, IIdType theCompartmentOwner, String theSearchParamName, RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource) { Verdict verdict = null; if (theSearchParams != null) { String[] values = theSearchParams.get(theSearchParamName); if (values != null) { for (String nextParameterValue : values) { if (nextParameterValue.equals(theCompartmentOwner.getValue())) { - verdict = new Verdict(PolicyEnum.ALLOW, this); + verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); break; } if (nextParameterValue.equals(theCompartmentOwner.getIdPart())) { - verdict = new Verdict(PolicyEnum.ALLOW, this); + verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); break; } } @@ -576,10 +550,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { return verdict; } - public TransactionAppliesToEnum getTransactionAppliesToOp() { - return myTransactionAppliesToOp; - } - public void setTransactionAppliesToOp(TransactionAppliesToEnum theOp) { myTransactionAppliesToOp = theOp; } @@ -637,7 +607,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { builder.append("transactionAppliesToOp", myTransactionAppliesToOp); builder.append("appliesTo", myAppliesTo); builder.append("appliesToTypes", myAppliesToTypes); - builder.append("appliesToTenant", getTenantApplicabilityChecker()); builder.append("classifierCompartmentName", myClassifierCompartmentName); builder.append("classifierCompartmentOwners", myClassifierCompartmentOwners); builder.append("classifierType", myClassifierType); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplPatch.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplPatch.java index 580ca677c54..d10de640b4d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplPatch.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplPatch.java @@ -37,14 +37,11 @@ class RuleImplPatch extends BaseRule { @Override public AuthorizationInterceptor.Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set theFlags, Pointcut thePointcut) { - if (isOtherTenant(theRequestDetails)) { - return null; - } if (myAllRequests) { if (theOperation == RestOperationTypeEnum.PATCH) { if (theInputResource == null && theOutputResource == null) { - return newVerdict(); + return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); } } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java index 2101435778e..2615ace11f1 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java @@ -23,8 +23,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth; enum RuleOpEnum { READ, WRITE, - ALLOW_ALL, - DENY_ALL, + ALL, /** * Transaction applies to both transaction and batch */ diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.java index d1abbd993a5..bbda7be8626 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.java @@ -57,7 +57,7 @@ public class RequestTenantPartitionInterceptor { } @Nonnull - private RequestPartitionId extractPartitionIdFromRequest(ServletRequestDetails theRequestDetails) { + protected RequestPartitionId extractPartitionIdFromRequest(ServletRequestDetails theRequestDetails) { // We will use the tenant ID that came from the request as the partition name String tenantId = theRequestDetails.getTenantId(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProvider.java index 8b8d5097de7..6f7acd8fd70 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProvider.java @@ -29,7 +29,16 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; -import ca.uhn.fhir.rest.annotation.*; +import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; +import ca.uhn.fhir.rest.annotation.Create; +import ca.uhn.fhir.rest.annotation.Delete; +import ca.uhn.fhir.rest.annotation.History; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; @@ -52,6 +61,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; @@ -89,7 +99,7 @@ public class HashMapResourceProvider implements IResour protected LinkedList myTypeHistory = new LinkedList<>(); private long myNextId; private AtomicLong myDeleteCount = new AtomicLong(0); - private AtomicLong mySearchCount = new AtomicLong(0); + protected AtomicLong mySearchCount = new AtomicLong(0); private AtomicLong myUpdateCount = new AtomicLong(0); private AtomicLong myCreateCount = new AtomicLong(0); private AtomicLong myReadCount = new AtomicLong(0); @@ -217,7 +227,7 @@ public class HashMapResourceProvider implements IResour } @History - public List historyInstance(@IdParam IIdType theId, RequestDetails theRequestDetails) { + public List historyInstance(@IdParam IIdType theId, RequestDetails theRequestDetails) { LinkedList retVal = myIdToHistory.get(theId.getIdPart()); if (retVal == null) { throw new ResourceNotFoundException(theId); @@ -265,7 +275,14 @@ public class HashMapResourceProvider implements IResour } @Search - public List searchAll(RequestDetails theRequestDetails) { + public List searchAll(RequestDetails theRequestDetails) { + mySearchCount.incrementAndGet(); + List retVal = getAllResources(); + return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails); + } + + @Nonnull + protected List getAllResources() { List retVal = new ArrayList<>(); for (TreeMap next : myIdToVersionToResourceMap.values()) { @@ -277,13 +294,11 @@ public class HashMapResourceProvider implements IResour } } - mySearchCount.incrementAndGet(); - - return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails); + return retVal; } @Search - public List searchById( + public List searchById( @RequiredParam(name = "_id") TokenAndListParam theIds, RequestDetails theRequestDetails) { List retVal = new ArrayList<>(); @@ -472,7 +487,7 @@ public class HashMapResourceProvider implements IResour } private static T fireInterceptorsAndFilterAsNeeded(T theResource, RequestDetails theRequestDetails) { - List output = fireInterceptorsAndFilterAsNeeded(Lists.newArrayList(theResource), theRequestDetails); + List output = fireInterceptorsAndFilterAsNeeded(Lists.newArrayList(theResource), theRequestDetails); if (output.size() == 1) { return theResource; } else { @@ -480,8 +495,8 @@ public class HashMapResourceProvider implements IResour } } - private static List fireInterceptorsAndFilterAsNeeded(List theResources, RequestDetails theRequestDetails) { - ArrayList resourcesToReturn = new ArrayList<>(theResources); + protected static List fireInterceptorsAndFilterAsNeeded(List theResources, RequestDetails theRequestDetails) { + List resourcesToReturn = new ArrayList<>(theResources); if (theRequestDetails != null) { IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster(); @@ -502,6 +517,7 @@ public class HashMapResourceProvider implements IResour .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) .add(IPreResourceShowDetails.class, preResourceShowDetails); interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, preShowParams); + resourcesToReturn = preResourceShowDetails.toList(); } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/interceptor/UrlTenantSelectionInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/interceptor/UrlTenantSelectionInterceptorTest.java index 3f2587651aa..78be4d1c208 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/interceptor/UrlTenantSelectionInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/interceptor/UrlTenantSelectionInterceptorTest.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.rest.client.interceptor; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.client.BaseGenericClientR4Test; import ca.uhn.fhir.rest.client.api.IGenericClient; import org.apache.http.client.methods.HttpUriRequest; @@ -75,4 +76,41 @@ public class UrlTenantSelectionInterceptorTest extends BaseGenericClientR4Test { assertEquals("http://example.com/fhir/TENANT-A/Patient", capt.getAllValues().get(0).getURI().toString()); } + + @Test + public void testPagingLinksRetainTenant() throws Exception { + ArgumentCaptor capt = prepareClientForSearchResponse(); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + client.registerInterceptor(new UrlTenantSelectionInterceptor("TENANT-A")); + + Bundle bundle = new Bundle(); + bundle.addLink().setRelation("next").setUrl("http://example.com/fhir/TENANT-A/?" + Constants.PARAM_PAGINGACTION + "=123456"); + + client + .loadPage() + .next(bundle) + .execute(); + + assertEquals("http://example.com/fhir/TENANT-A/?_getpages=123456", capt.getAllValues().get(0).getURI().toString()); + } + + @Test + public void testPagingLinksRetainTenant2() throws Exception { + ArgumentCaptor capt = prepareClientForSearchResponse(); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + client.registerInterceptor(new UrlTenantSelectionInterceptor("TENANT-A")); + + Bundle bundle = new Bundle(); + bundle.addLink().setRelation("next").setUrl("http://example.com/fhir/TENANT-A?" + Constants.PARAM_PAGINGACTION + "=123456"); + + client + .loadPage() + .next(bundle) + .execute(); + + assertEquals("http://example.com/fhir/TENANT-A?_getpages=123456", capt.getAllValues().get(0).getURI().toString()); + } + } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java index ab8ee8a1838..c03761fe0c9 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java @@ -51,8 +51,6 @@ import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -//import static org.hamcrest.Matchers.any; - @RunWith(MockitoJUnitRunner.class) public class ConsentInterceptorTest { diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java index b0240f7e796..fbe6816e5a3 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java @@ -6,9 +6,30 @@ import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.annotation.*; +import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; +import ca.uhn.fhir.rest.annotation.Create; +import ca.uhn.fhir.rest.annotation.Delete; +import ca.uhn.fhir.rest.annotation.GraphQL; +import ca.uhn.fhir.rest.annotation.GraphQLQuery; +import ca.uhn.fhir.rest.annotation.History; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.Patch; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.annotation.Transaction; +import ca.uhn.fhir.rest.annotation.TransactionParam; +import ca.uhn.fhir.rest.annotation.Update; +import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.*; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.ValidationModeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.TokenAndListParam; @@ -27,7 +48,13 @@ import com.google.common.collect.Lists; import org.apache.commons.io.IOUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; -import org.apache.http.client.methods.*; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; @@ -38,19 +65,49 @@ import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.*; -import org.junit.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CarePlan; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Condition; +import org.hl7.fhir.r4.model.Consent; +import org.hl7.fhir.r4.model.DiagnosticReport; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.ServiceRequest; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; import java.util.concurrent.TimeUnit; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class AuthorizationInterceptorR4Test { @@ -905,7 +962,7 @@ public class AuthorizationInterceptorR4Test { */ @Test public void testDenyByCompartmentWithType() throws Exception { - ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.ALLOW) { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder().deny("Rule 1").read().resourcesOfType(CarePlan.class).inCompartment("Patient", new IdType("Patient/845bd9f1-3635-4866-a6c8-1ca085df5c1a")).andThen().allowAll() @@ -1809,7 +1866,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization().withTester(new IAuthRuleTester() { + .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization().withTester(null /* null should be ignored */ ).withTester(new IAuthRuleTester() { @Override public boolean matches(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource) { return theInputResourceId.getIdPart().equals("1"); diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 614e36ef8fd..4b88e362458 100644 --- a/hapi-fhir-test-utilities/pom.xml +++ b/hapi-fhir-test-utilities/pom.xml @@ -59,9 +59,20 @@ spring-context true + junit junit + + + org.hamcrest + hamcrest-core + + + + + org.hamcrest + hamcrest diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java new file mode 100644 index 00000000000..e5788a08e2f --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java @@ -0,0 +1,183 @@ +package ca.uhn.fhir.test.utilities; + +/*- + * #%L + * HAPI FHIR Test Utilities + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; +import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.ICompositeType; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +import javax.annotation.Nullable; +import java.util.function.Consumer; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.hamcrest.Matchers.matchesPattern; +import static org.junit.Assert.assertThat; + +/** + * This is an experiment to see if we can make test data creation for storage unit tests a bit more readable. + */ +@SuppressWarnings({"unchecked", "ConstantConditions"}) +public interface ITestDataBuilder { + + /** + * Set Patient.active = true + */ + default Consumer withActiveTrue() { + return t -> __setPrimitiveChild(getFhirContext(), t, "active", "boolean", "true"); + } + + /** + * Set Patient.active = false + */ + default Consumer withActiveFalse() { + return t -> __setPrimitiveChild(getFhirContext(), t, "active", "boolean", "false"); + } + + default Consumer withFamily(String theFamily) { + return t -> { + IPrimitiveType family = (IPrimitiveType) getFhirContext().getElementDefinition("string").newInstance(); + family.setValueAsString(theFamily); + + BaseRuntimeElementCompositeDefinition humanNameDef = (BaseRuntimeElementCompositeDefinition) getFhirContext().getElementDefinition("HumanName"); + ICompositeType humanName = (ICompositeType) humanNameDef.newInstance(); + humanNameDef.getChildByName("family").getMutator().addValue(humanName, family); + + RuntimeResourceDefinition resourceDef = getFhirContext().getResourceDefinition(t.getClass()); + resourceDef.getChildByName("name").getMutator().addValue(t, humanName); + }; + } + + /** + * Set Patient.birthdate + */ + default Consumer withBirthdate(String theBirthdate) { + return t -> __setPrimitiveChild(getFhirContext(), t, "birthDate", "dateTime", theBirthdate); + } + + /** + * Set Observation.status + */ + default Consumer withStatus(String theStatus) { + return t -> __setPrimitiveChild(getFhirContext(), t, "status", "code", theStatus); + } + + /** + * Set [Resource].identifier.system and [Resource].identifier.value + */ + default Consumer withIdentifier(String theSystem, String theValue) { + return t -> { + IPrimitiveType system = (IPrimitiveType) getFhirContext().getElementDefinition("uri").newInstance(); + system.setValueAsString(theSystem); + + IPrimitiveType value = (IPrimitiveType) getFhirContext().getElementDefinition("string").newInstance(); + value.setValueAsString(theValue); + + BaseRuntimeElementCompositeDefinition identifierDef = (BaseRuntimeElementCompositeDefinition) getFhirContext().getElementDefinition("Identifier"); + ICompositeType identifier = (ICompositeType) identifierDef.newInstance(); + identifierDef.getChildByName("system").getMutator().addValue(identifier, system); + identifierDef.getChildByName("value").getMutator().addValue(identifier, value); + + RuntimeResourceDefinition resourceDef = getFhirContext().getResourceDefinition(t.getClass()); + resourceDef.getChildByName("identifier").getMutator().addValue(t, identifier); + }; + } + + default Consumer withId(String theId) { + return t -> { + assertThat(theId, matchesPattern("[a-zA-Z0-9]+")); + t.setId(theId); + }; + } + + default Consumer withTag(String theSystem, String theCode) { + return t -> t.getMeta().addTag().setSystem(theSystem).setCode(theCode).setDisplay(theCode); + } + + default IIdType createObservation(Consumer... theModifiers) { + return createResource("Observation", theModifiers); + } + + default IIdType createPatient(Consumer... theModifiers) { + return createResource("Patient", theModifiers); + } + + default IIdType createResource(String theResourceType, Consumer[] theModifiers) { + IBaseResource resource = getFhirContext().getResourceDefinition(theResourceType).newInstance(); + for (Consumer next : theModifiers) { + next.accept(resource); + } + + if (isNotBlank(resource.getIdElement().getValue())) { + return doUpdateResource(resource); + } else { + return doCreateResource(resource); + } + } + + + default Consumer withSubject(@Nullable IIdType theSubject) { + return t -> { + if (theSubject != null) { + IBaseReference reference = (IBaseReference) getFhirContext().getElementDefinition("Reference").newInstance(); + reference.setReference(theSubject.getValue()); + + RuntimeResourceDefinition resourceDef = getFhirContext().getResourceDefinition(t.getClass()); + resourceDef.getChildByName("subject").getMutator().addValue(t, reference); + } + }; + } + + + /** + * Name chosen to avoid potential for conflict. This is an internal API to this interface. + */ + static void __setPrimitiveChild(FhirContext theFhirContext, IBaseResource theTarget, String theElementName, String theElementType, String theValue) { + RuntimeResourceDefinition def = theFhirContext.getResourceDefinition(theTarget.getClass()); + BaseRuntimeChildDefinition activeChild = def.getChildByName(theElementName); + + IPrimitiveType booleanType = (IPrimitiveType) activeChild.getChildByName(theElementName).newInstance(); + booleanType.setValueAsString(theValue); + activeChild.getMutator().addValue(theTarget, booleanType); + } + + /** + * Users of this API must implement this method + */ + IIdType doCreateResource(IBaseResource theResource); + + /** + * Users of this API must implement this method + */ + IIdType doUpdateResource(IBaseResource theResource); + + /** + * Users of this API must implement this method + */ + FhirContext getFhirContext(); + + +} diff --git a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/SchemaBaseValidatorTest.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/SchemaBaseValidatorTest.java index 36b50b7cd37..d4cfdb3e4b4 100644 --- a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/SchemaBaseValidatorTest.java +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/validation/SchemaBaseValidatorTest.java @@ -7,7 +7,9 @@ import org.junit.Test; import javax.xml.transform.Source; import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.Assert.*; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; public class SchemaBaseValidatorTest {