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/context/RuntimeSearchParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java index c7312354925..dbae7e49544 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java @@ -5,7 +5,6 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; -import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IIdType; @@ -105,19 +104,30 @@ public class RuntimeSearchParam { } } + /** + * Constructor + */ public RuntimeSearchParam(String theName, String theDescription, String thePath, RestSearchParameterTypeEnum theParamType, Set theProvidesMembershipInCompartments, Set theTargets, RuntimeSearchParamStatusEnum theStatus) { this(null, null, theName, theDescription, thePath, theParamType, null, theProvidesMembershipInCompartments, theTargets, theStatus); } /** - * Retrieve user data - This can be used to store any application-specific data - * - * @return + * Copy constructor */ + public RuntimeSearchParam(RuntimeSearchParam theSp) { + this(theSp.getId(), theSp.getUri(), theSp.getName(), theSp.getDescription(), theSp.getPath(), theSp.getParamType(), theSp.getCompositeOf(), theSp.getProvidesMembershipInCompartments(), theSp.getTargets(), theSp.getStatus(), theSp.getBase()); + } + + /** + * Retrieve user data - This can be used to store any application-specific data + */ + @Nonnull public List> getExtensions(String theKey) { List> retVal = myExtensions.get(theKey); if (retVal != null) { retVal = Collections.unmodifiableList(retVal); + } else { + retVal = Collections.emptyList(); } return retVal; } 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 3a5d5a9f189..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 @@ -231,9 +231,9 @@ public enum Pointcut { /** * Server Hook: * This hook is invoked before an incoming request is processed. Note that this method is called - * after the server has begin preparing the response to the incoming client request. + * after the server has begun preparing the response to the incoming client request. * As such, it is not able to supply a response to the incoming request in the way that - * SERVER_INCOMING_REQUEST_PRE_HANDLED and + * SERVER_INCOMING_REQUEST_PRE_PROCESSED and * {@link #SERVER_INCOMING_REQUEST_POST_PROCESSED} * are. *

@@ -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: + *

+ *
    + *
  • + * ca.uhn.fhir.interceptor.model.RequestPartitionId - The partition ID that was selected + *
  • + *
  • + * ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the + * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been + * pulled out of the servlet request. Note that the bean + * properties are not all guaranteed to be populated, depending on how early during processing the + * exception occurred. + *
  • + *
  • + * ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the + * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been + * pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will + * only be populated when operating in a RestfulServer implementation. It is provided as a convenience. + *
  • + *
+ *

+ * 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/BaseOrListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseOrListParam.java index 201022a1994..21e09b8e19d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseOrListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseOrListParam.java @@ -28,7 +28,7 @@ import ca.uhn.fhir.rest.api.QualifiedParamList; import java.util.ArrayList; import java.util.List; -abstract class BaseOrListParam, PT extends IQueryParameterType> implements IQueryParameterOr { +public abstract class BaseOrListParam, PT extends IQueryParameterType> implements IQueryParameterOr { private List myList = new ArrayList<>(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseParam.java index 498a0765b2f..2df4d9898c6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseParam.java @@ -31,7 +31,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; /** * Base class for RESTful operation parameter types */ -abstract class BaseParam implements IQueryParameterType { +public abstract class BaseParam implements IQueryParameterType { private Boolean myMissing; 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 f36e4dd82f0..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 @@ -6,12 +6,19 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import org.apache.commons.lang3.time.DateUtils; +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; @@ -263,6 +270,67 @@ public class DateRangeParam implements IQueryParameterAnd { return this; } + /** + * Return the current lower bound as an integer representative of the date. + * + * e.g. 2019-02-22T04:22:00-0500 -> 20120922 + */ + public Integer getLowerBoundAsDateInteger() { + if (myLowerBound == null || myLowerBound.getValue() == null) { + return null; + } + int retVal = DateUtils.convertDatetoDayInteger(myLowerBound.getValue()); + + if (myLowerBound.getPrefix() != null) { + switch (myLowerBound.getPrefix()) { + case GREATERTHAN: + case STARTS_AFTER: + retVal += 1; + break; + case EQUAL: + case GREATERTHAN_OR_EQUALS: + break; + case LESSTHAN: + case APPROXIMATE: + case LESSTHAN_OR_EQUALS: + case ENDS_BEFORE: + case NOT_EQUAL: + throw new IllegalStateException("Invalid lower bound comparator: " + myLowerBound.getPrefix()); + } + } + return retVal; + } + + /** + * Return the current upper bound as an integer representative of the date + * + * e.g. 2019-02-22T04:22:00-0500 -> 2019122 + */ + public Integer getUpperBoundAsDateInteger() { + if (myUpperBound == null || myUpperBound.getValue() == null) { + return null; + } + int retVal = DateUtils.convertDatetoDayInteger(myUpperBound.getValue()); + if (myUpperBound.getPrefix() != null) { + switch (myUpperBound.getPrefix()) { + case LESSTHAN: + case ENDS_BEFORE: + retVal -= 1; + break; + case EQUAL: + case LESSTHAN_OR_EQUALS: + break; + case GREATERTHAN_OR_EQUALS: + case GREATERTHAN: + case APPROXIMATE: + case NOT_EQUAL: + case STARTS_AFTER: + throw new IllegalStateException("Invalid upper bound comparator: " + myUpperBound.getPrefix()); + } + } + return retVal; + } + public Date getLowerBoundAsInstant() { if (myLowerBound == null || myLowerBound.getValue() == null) { return null; @@ -270,10 +338,7 @@ public class DateRangeParam implements IQueryParameterAnd { Date retVal = myLowerBound.getValue(); if (myLowerBound.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) { - Calendar cal = DateUtils.toCalendar(retVal); - cal.setTimeZone(TimeZone.getTimeZone("GMT-11:30")); - cal = DateUtils.truncate(cal, Calendar.DATE); - retVal = cal.getTime(); + retVal = DateUtils.getLowestInstantFromDate(retVal); } if (myLowerBound.getPrefix() != null) { @@ -335,10 +400,7 @@ public class DateRangeParam implements IQueryParameterAnd { Date retVal = myUpperBound.getValue(); if (myUpperBound.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) { - Calendar cal = DateUtils.toCalendar(retVal); - cal.setTimeZone(TimeZone.getTimeZone("GMT+11:30")); - cal = DateUtils.truncate(cal, Calendar.DATE); - retVal = cal.getTime(); + retVal = DateUtils.getHighestInstantFromDate(retVal); } if (myUpperBound.getPrefix() != null) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java new file mode 100644 index 00000000000..16fd3619ccd --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java @@ -0,0 +1,126 @@ +package ca.uhn.fhir.util; + +/*- + * #%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% + */ + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import com.google.common.base.Charsets; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.BOMInputStream; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; +import java.util.function.Function; +import java.util.zip.GZIPInputStream; + +/** + * Use this API with caution, it may change! + */ +public class ClasspathUtil { + + private static final Logger ourLog = LoggerFactory.getLogger(ClasspathUtil.class); + + public static String loadResource(String theClasspath) { + Function streamTransform = t -> t; + return loadResource(theClasspath, streamTransform); + } + + /** + * Load a classpath resource, throw an {@link InternalErrorException} if not found + */ + @Nonnull + public static InputStream loadResourceAsStream(String theClasspath) { + InputStream retVal = ClasspathUtil.class.getResourceAsStream(theClasspath); + if (retVal == null) { + throw new InternalErrorException("Unable to find classpath resource: " + theClasspath); + } + return retVal; + } + + /** + * Load a classpath resource, throw an {@link InternalErrorException} if not found + */ + @Nonnull + public static String loadResource(String theClasspath, Function theStreamTransform) { + InputStream stream = ClasspathUtil.class.getResourceAsStream(theClasspath); + try { + if (stream == null) { + throw new IOException("Unable to find classpath resource: " + theClasspath); + } + try { + InputStream newStream = theStreamTransform.apply(stream); + return IOUtils.toString(newStream, Charsets.UTF_8); + } finally { + stream.close(); + } + } catch (IOException e) { + throw new InternalErrorException(e); + } + } + + @Nonnull + public static String loadCompressedResource(String theClasspath) { + Function streamTransform = t -> { + try { + return new GZIPInputStream(t); + } catch (IOException e) { + throw new InternalErrorException(e); + } + }; + return loadResource(theClasspath, streamTransform); + } + + @Nonnull + public static T loadResource(FhirContext theCtx, Class theType, String theClasspath) { + String raw = loadResource(theClasspath); + return EncodingEnum.detectEncodingNoDefault(raw).newParser(theCtx).parseResource(theType, raw); + } + + public static void close(InputStream theInput) { + try { + if (theInput != null) { + theInput.close(); + } + } catch (IOException e) { + ourLog.debug("Closing InputStream threw exception", e); + } + } + + public static Function withBom() { + return t -> new BOMInputStream(t); + } + + public static byte[] loadResourceAsByteArray(String theClasspath) { + InputStream stream = loadResourceAsStream(theClasspath); + try { + return IOUtils.toByteArray(stream); + } catch (IOException e) { + throw new InternalErrorException(e); + } finally { + close(stream); + } + } +} 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 f99ea685652..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 @@ -65,6 +70,8 @@ public final class DateUtils { @SuppressWarnings("WeakerAccess") public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy"; + private static final String PATTERN_INTEGER_DATE = "yyyyMMdd"; + private static final String[] DEFAULT_PATTERNS = new String[]{ PATTERN_RFC1123, PATTERN_RFC1036, @@ -153,6 +160,35 @@ public final class DateUtils { return null; } + + public static Date getHighestInstantFromDate(Date theDateValue) { + return getInstantFromDateWithTimezone(theDateValue, TimeZone.getTimeZone("GMT+11:30")); + + } + public static Date getLowestInstantFromDate(Date theDateValue) { + return getInstantFromDateWithTimezone(theDateValue, TimeZone.getTimeZone("GMT-11:30")); + } + + public static Date getInstantFromDateWithTimezone(Date theDateValue, TimeZone theTimezone) { + Calendar cal = org.apache.commons.lang3.time.DateUtils.toCalendar(theDateValue); + cal.setTimeZone(theTimezone); + cal = org.apache.commons.lang3.time.DateUtils.truncate(cal, Calendar.DATE); + return cal.getTime(); + } + + public static int convertDatetoDayInteger(final Date theDateValue) { + notNull(theDateValue, "Date value"); + SimpleDateFormat format = new SimpleDateFormat(PATTERN_INTEGER_DATE); + String theDateString = format.format(theDateValue); + return Integer.parseInt(theDateString); + } + + public static String convertDateToIso8601String(final Date theDateValue){ + notNull(theDateValue, "Date value"); + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); + return format.format(theDateValue); + } + /** * Formats the given date according to the RFC 1123 pattern. * 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/ca/uhn/fhir/util/VersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java index 5c6b7b9e4ea..c2114aff18c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java @@ -59,6 +59,7 @@ public enum VersionEnum { V4_0_3, V4_1_0, V4_2_0, + @Deprecated V4_3_0, // 4.3.0 was renamed to 5.0.0 during the cycle V5_0_0; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java index e2fe1cb2e97..cec0ff9194d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java @@ -23,9 +23,7 @@ package ca.uhn.fhir.validation; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.input.BOMInputStream; +import ca.uhn.fhir.util.ClasspathUtil; import org.hl7.fhir.instance.model.api.IBaseResource; import org.w3c.dom.ls.LSInput; import org.w3c.dom.ls.LSResourceResolver; @@ -41,10 +39,7 @@ import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.StringReader; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -152,20 +147,9 @@ public class SchemaBaseValidator implements IValidatorModule { Source loadXml(String theSchemaName) { String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSchemaName; ourLog.debug("Going to load resource: {}", pathToBase); - try (InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase)) { - if (baseIs == null) { - throw new InternalErrorException("Schema not found. " + RESOURCES_JAR_NOTE); - } - try (BOMInputStream bomInputStream = new BOMInputStream(baseIs, false)) { - try (InputStreamReader baseReader = new InputStreamReader(bomInputStream, StandardCharsets.UTF_8)) { - // Buffer so that we can close the input stream - String contents = IOUtils.toString(baseReader); - return new StreamSource(new StringReader(contents), null); - } - } - } catch (IOException e) { - throw new InternalErrorException(e); - } + + String contents = ClasspathUtil.loadResource(pathToBase, ClasspathUtil.withBom()); + return new StreamSource(new StringReader(contents), null); } @Override @@ -188,16 +172,8 @@ public class SchemaBaseValidator implements IValidatorModule { ourLog.debug("Loading referenced schema file: " + pathToBase); - try (InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase)) { - if (baseIs == null) { - throw new InternalErrorException("Schema file not found: " + pathToBase); - } - - byte[] bytes = IOUtils.toByteArray(baseIs); - input.setByteStream(new ByteArrayInputStream(bytes)); - } catch (IOException e) { - throw new InternalErrorException(e); - } + byte[] bytes = ClasspathUtil.loadResourceAsByteArray(pathToBase); + input.setByteStream(new ByteArrayInputStream(bytes)); return input; } 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 605bed74a59..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 @@ -122,6 +121,9 @@ ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoConceptMapR4.matchesFound=Matches found! ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoConceptMapR4.noMatchesFound=No matches found! ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoSearchParameterR4.invalidSearchParamExpression=The expression "{0}" can not be evaluated and may be invalid: {1} +ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderToken.textModifierDisabledForSearchParam=The :text modifier is disabled for this search parameter +ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderToken.textModifierDisabledForServer=The :text modifier is disabled on this server + ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor.successMsg=Cascaded delete to {0} resources: {1} ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor.noParam=Note that cascading deletes are not active for this request. You can enable cascading deletes by using the "_cascade=delete" URL parameter. @@ -141,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} @@ -151,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} @@ -160,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-base/src/test/java/ca/uhn/fhir/util/ClasspathUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ClasspathUtilTest.java new file mode 100644 index 00000000000..c70687c910d --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ClasspathUtilTest.java @@ -0,0 +1,61 @@ +package ca.uhn.fhir.util; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +public class ClasspathUtilTest { + + @Test + public void testLoadResourceNotFound() { + try { + ClasspathUtil.loadResource("/FOOOOOO"); + } catch (InternalErrorException e) { + assertEquals("Unable to find classpath resource: /FOOOOOO", e.getMessage()); + } + } + + @Test + public void testLoadResourceAsStreamNotFound() { + try { + ClasspathUtil.loadResourceAsStream("/FOOOOOO"); + } catch (InternalErrorException e) { + assertEquals("Unable to find classpath resource: /FOOOOOO", e.getMessage()); + } + } + + /** + * Should not throw any exception + */ + @Test + public void testClose_Null() { + ClasspathUtil.close(null); + } + + /** + * Should not throw any exception + */ + @Test + public void testClose_Ok() { + ClasspathUtil.close(new ByteArrayInputStream(new byte[]{0,1,2})); + } + + + /** + * Should not throw any exception + */ + @Test + public void testClose_ThrowException() throws IOException { + InputStream is = mock(InputStream.class); + doThrow(new IOException("FOO")).when(is).close(); + ClasspathUtil.close(is); + } + +} 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/1499-dont-touch-timezones-on-date-search.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1499-dont-touch-timezones-on-date-search.yaml new file mode 100644 index 00000000000..890f0a947da --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1499-dont-touch-timezones-on-date-search.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 1499 +title: "When performing a search with a DateParam that has DAY precision, rely on new ordinal date field for comparison + instead of attempting to find oldest and newest instant that could be valid." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1710-baseorlistparam-visibility.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1710-baseorlistparam-visibility.yaml new file mode 100644 index 00000000000..a3e9d956f85 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1710-baseorlistparam-visibility.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 1710 +title: The classes BaseOrListParam and BaseParam now have public visibility in order to make it easier to + create more generic APIs. Thanks to GitHub user @ibacher for the pull request! diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1734-fix-pointcut.javadoc.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1734-fix-pointcut.javadoc.yaml new file mode 100644 index 00000000000..464579f6ea2 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1734-fix-pointcut.javadoc.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 1734 +title: The Pointcut JavaDoc had an incorrect link from one pointcut to another and has been fixed. Thanks + to Bert Roos for the pull request! diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1749-add-constructor-to-restfulserver.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1749-add-constructor-to-restfulserver.yaml new file mode 100644 index 00000000000..71e8bae87b1 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1749-add-constructor-to-restfulserver.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 1749 +title: A new constructor has been added to RestfulServer that accepts an InterceptorService. Thanks to gematik FuE for the + pull request! diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1788-improve-apacheproxyaddressstrategy.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1788-improve-apacheproxyaddressstrategy.yaml new file mode 100644 index 00000000000..50ea2dd8c2a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1788-improve-apacheproxyaddressstrategy.yaml @@ -0,0 +1,6 @@ +--- +type: add +issue: 1788 +title: "The ApacheProxyAddressStrategy has been improved to add support for additional proxy headers inclusing + `X-Forwarded-Host`, `X-Forwarded-Proto`, `X-Forwarded-Port`, and `X-Forwarded-Prefix`. Thanks to Thomas Papke + for the pull request!" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1812-account-for-jaxrs-super-methodss.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1812-account-for-jaxrs-super-methodss.yaml new file mode 100644 index 00000000000..b7222ade76b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1812-account-for-jaxrs-super-methodss.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 1812 +title: The JAX-RS server will now scan and serve ResourceProvider methods defined in super-classes as well. Thanks + to Zhe Wang for the pull request! 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/1824-add-ucum-support.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1824-add-ucum-support.yaml new file mode 100644 index 00000000000..70522664e71 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1824-add-ucum-support.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 1824 +title: Native support for UCUM has been added to the validation stack, meaning that UCUM codes can be validated + at runtime without the need for any external validation. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1829-scope-tag-search-to-correct-resource-type.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1829-scope-tag-search-to-correct-resource-type.yaml new file mode 100644 index 00000000000..205692228d5 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1829-scope-tag-search-to-correct-resource-type.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 1829 +title: "In the JPA server, performing a search where the only search parameter was the `_tag` parameter could cause resources + of the wrong type to be included in search results. This has been corrected." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1831-allow-disabling-text-modifier.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1831-allow-disabling-text-modifier.yaml new file mode 100644 index 00000000000..6af2c4fff2b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1831-allow-disabling-text-modifier.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 1831 +title: "Indexing for the :text modifier can now be globally or selectively disabled in the JPA server. This can have a measurable + impact on index sizes and write speed in servers with large numbers of token indexes." 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 bc38dd31845..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 @@ -9,6 +9,7 @@
  • Hibernate Validator (JPA): 5.4.2.Final -> 6.1.3.Final
  • Guava (JPA): 28.0 -> 28.2
  • Spring Boot (Boot): 2.2.0.RELEASE -> 2.2.6.RELEASE
  • +
  • FlywayDB (JPA) 6.1.0 -> 6.4.1
  • " - item: issue: "1583" @@ -57,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/performance.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/performance.md index 68eac58c2af..0c7f94f2ef4 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/performance.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/performance.md @@ -9,3 +9,27 @@ On servers where a large amount of data will be ingested, the following consider * Optimize your database thread pool count and HTTP client thread count: Every environment will have a different optimal setting for the number of concurrent writes that are permitted, and the maximum number of database connections allowed. * Disable deletes: If the JPA server is configured to have the FHIR delete operation disabled, it is able to skip some resource reference deletion checks during resource creation, which can have a measurable improvement to performance over large datasets. + +# Disabling :text Indexing + +On servers storing large numbers of Codings and CodeableConcepts (as well as any other token SearchParameter target where the `:text` modifier is supported), the indexes required to support the `:text` modifier can consume a large amount of index space, and cause a masurable impact on write times. + +This modifier can be disabled globally by using the ModelConfig#setSuppressStringIndexingInTokens setting. + +It can also be disabled at a more granular level (or selectively re-enabled if it disabled globally) by using an extension on individual SearchParameter resources. For example, the following SearchParameter disables text indexing on the Observation:code parameter: + +```json +{ + "resourceType": "SearchParameter", + "id": "observation-code", + "extension": [ { + "url": "http://hapifhir.io/fhir/StructureDefinition/searchparameter-token-suppress-text-index", + "valueBoolean": true + } ], + "status": "active", + "code": "code", + "base": [ "Observation" ], + "type": "token", + "expression": "Observation.code" +} +``` 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/search.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/search.md index 26fe08dc30c..44f8fc19b8f 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/search.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/search.md @@ -2,10 +2,6 @@ The HAPI FHIR JPA Server fully implements most [FHIR search](https://www.hl7.org/fhir/search.html) operations for most versions of FHIR. However, there are some known limitations of the current implementation. Here is a partial list of search functionality that is not currently supported in HAPI FHIR: -### Date searches without timestamp - -Searching by date with no timestamp currently doesn't match all records it should. See [Issue 1499](https://github.com/jamesagnew/hapi-fhir/issues/1499). - ### Chains within _has Chains within _has are not currently supported for performance reasons. For example, this search is not currently supported 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-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md index 5b559f560f9..13ad3028476 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md @@ -98,6 +98,17 @@ The following table lists vocabulary that is validated by this module: added in the future, please get in touch if you would like to help. + + Unified Codes for Units of Measure (UCUM) + + ValueSet:
    (...)/ValueSet/ucum-units +
    + CodeSystem: http://unitsofmeasure.org + + + Codes are validated using the UcumEssenceService provided by the UCUM Java library. + + 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-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindings.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindings.java index 8406c5e64b9..df8826dc1d5 100644 --- a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindings.java +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindings.java @@ -20,17 +20,20 @@ package ca.uhn.fhir.jaxrs.server.util; * #L% */ -import java.lang.reflect.Method; -import java.util.concurrent.ConcurrentHashMap; - -import org.apache.commons.lang3.StringUtils; - import ca.uhn.fhir.jaxrs.server.AbstractJaxRsProvider; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; -import ca.uhn.fhir.rest.server.method.*; +import ca.uhn.fhir.rest.server.method.BaseMethodBinding; +import ca.uhn.fhir.rest.server.method.OperationMethodBinding; +import ca.uhn.fhir.rest.server.method.SearchMethodBinding; import ca.uhn.fhir.util.ReflectionUtil; +import org.apache.commons.lang3.StringUtils; + +import java.lang.reflect.Method; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; /** * Class that contains the method bindings defined by a ResourceProvider @@ -52,7 +55,9 @@ public class JaxRsMethodBindings { * @param theProviderClass the class definition contaning the operations */ public JaxRsMethodBindings(AbstractJaxRsProvider theProvider, Class theProviderClass) { - for (final Method m : ReflectionUtil.getDeclaredMethods(theProviderClass)) { + List declaredMethodsForCurrentProvider = ReflectionUtil.getDeclaredMethods(theProviderClass); + declaredMethodsForCurrentProvider.addAll(ReflectionUtil.getDeclaredMethods(theProviderClass.getSuperclass())); + for (final Method m : declaredMethodsForCurrentProvider) { final BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, theProvider.getFhirContext(), theProvider); if (foundMethodBinding == null) { continue; diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/AbstractDummyPatientProvider.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/AbstractDummyPatientProvider.java new file mode 100644 index 00000000000..7a36fbc812b --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/AbstractDummyPatientProvider.java @@ -0,0 +1,34 @@ +package ca.uhn.fhir.jaxrs.server.test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsResourceProvider; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.param.StringParam; +import org.hl7.fhir.r4.model.Patient; + +import java.util.List; + +/** + * A dummy patient provider exposing no methods + */ +public abstract class AbstractDummyPatientProvider extends AbstractJaxRsResourceProvider { + + public AbstractDummyPatientProvider() { + super(FhirContext.forR4()); + } + + @Override + public abstract String getBaseForServer(); + + + @Search + public List search(@RequiredParam(name = Patient.SP_NAME) final StringParam name) { + return null; + } + + @Override + public Class getResourceType() { + return Patient.class; + } +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsDummyPatientProviderR4.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsDummyPatientProviderR4.java index 1c3f91f4c1d..981128316bc 100644 --- a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsDummyPatientProviderR4.java +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsDummyPatientProviderR4.java @@ -1,15 +1,11 @@ package ca.uhn.fhir.jaxrs.server.test; -import ca.uhn.fhir.jaxrs.server.AbstractJaxRsResourceProvider; -import org.hl7.fhir.r4.model.Patient; - /** * A dummy patient provider exposing no methods */ -public class TestJaxRsDummyPatientProviderR4 extends AbstractJaxRsResourceProvider { +public class TestJaxRsDummyPatientProviderR4 extends AbstractDummyPatientProvider { - @Override - public Class getResourceType() { - return Patient.class; + @Override public String getBaseForServer() { + return "https://fhirserver/fhir/r4"; } } diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsDummyPatientProviderR4MimeType.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsDummyPatientProviderR4MimeType.java new file mode 100644 index 00000000000..719206b1453 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsDummyPatientProviderR4MimeType.java @@ -0,0 +1,12 @@ +package ca.uhn.fhir.jaxrs.server.test; + +/** + * A dummy patient provider exposing no methods + */ +public class TestJaxRsDummyPatientProviderR4MimeType extends AbstractDummyPatientProvider { + + @Override public String getBaseForServer() { + return "https://fhirserver/fhir"; + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindingsMimeTypeTest.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindingsMimeTypeTest.java new file mode 100644 index 00000000000..c4ea3fb3530 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindingsMimeTypeTest.java @@ -0,0 +1,28 @@ +package ca.uhn.fhir.jaxrs.server.util; + +import ca.uhn.fhir.jaxrs.server.test.AbstractDummyPatientProvider; +import ca.uhn.fhir.jaxrs.server.test.TestJaxRsDummyPatientProviderR4; +import ca.uhn.fhir.jaxrs.server.test.TestJaxRsDummyPatientProviderR4MimeType; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +import static org.junit.Assert.assertEquals; + +@FixMethodOrder(MethodSorters.DEFAULT) +public class JaxRsMethodBindingsMimeTypeTest { + + @Before + public void setUp() { + JaxRsMethodBindings.getClassBindings().clear(); + } + + @Test + public void testFindMethodsFor2ProvidersWithMethods() { + assertEquals(AbstractDummyPatientProvider.class, new TestJaxRsDummyPatientProviderR4().getBindings().getBinding(RestOperationTypeEnum.SEARCH_TYPE, "").getMethod().getDeclaringClass()); + assertEquals(AbstractDummyPatientProvider.class, new TestJaxRsDummyPatientProviderR4MimeType().getBindings().getBinding(RestOperationTypeEnum.SEARCH_TYPE, "").getMethod().getDeclaringClass()); + } + + } diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java index b79ca0c13d5..d2807f4cc35 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.api.config; import ca.uhn.fhir.jpa.api.model.WarmCacheEntry; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.rest.api.SearchTotalModeEnum; import com.google.common.annotations.VisibleForTesting; @@ -932,6 +933,43 @@ public class DaoConfig { myModelConfig.setAllowExternalReferences(theAllowExternalReferences); } + /** + *

    + * Should searches use the integer field {@code SP_VALUE_LOW_DATE_ORDINAL} and {@code SP_VALUE_HIGH_DATE_ORDINAL} in + * {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate} when resolving searches where all predicates are using + * precision of {@link ca.uhn.fhir.model.api.TemporalPrecisionEnum#DAY}. + * + * For example, if enabled, the search of {@code Observation?date=2020-02-25} will cause the date to be collapsed down to an + * ordinal {@code 20200225}. It would then be compared against {@link ResourceIndexedSearchParamDate#getValueLowDateOrdinal()} + * and {@link ResourceIndexedSearchParamDate#getValueHighDateOrdinal()} + *

    + * Default is {@literal true} beginning in HAPI FHIR 5.0 + *

    + * + * @since 5.0 + */ + public void setUseOrdinalDatesForDayPrecisionSearches(boolean theUseOrdinalDates) { + myModelConfig.setUseOrdinalDatesForDayPrecisionSearches(theUseOrdinalDates); + } + + /** + *

    + * Should searches use the integer field {@code SP_VALUE_LOW_DATE_ORDINAL} and {@code SP_VALUE_HIGH_DATE_ORDINAL} in + * {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate} when resolving searches where all predicates are using + * precision of {@link ca.uhn.fhir.model.api.TemporalPrecisionEnum#DAY}. + * + * For example, if enabled, the search of {@code Observation?date=2020-02-25} will cause the date to be collapsed down to an + * integer representing the ordinal date {@code 20200225}. It would then be compared against {@link ResourceIndexedSearchParamDate#getValueLowDateOrdinal()} + * and {@link ResourceIndexedSearchParamDate#getValueHighDateOrdinal()} + *

    + * Default is {@literal true} beginning in HAPI FHIR 5.0 + *

    + * + * @since 5.0 + */ + public boolean getUseOrdinalDatesForDayPrecisionSearches() { + return myModelConfig.getUseOrdinalDatesForDayPrecisionSearches(); + } /** * @see #setAllowInlineMatchUrlReferences(boolean) */ 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 01d1e9332ff..7afabe7f2ee 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())); // TODO: Looking at moving the lastn entities into jpa.model.entity package. Note that moving the lastn entities may require re-building elasticsearch indexes. 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 1f8fee714df..2e023ed6464 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 @@ -57,6 +57,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; @@ -89,8 +90,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; @@ -124,8 +123,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 { /** @@ -179,7 +176,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; @@ -229,7 +226,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); @@ -245,7 +244,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) { @@ -386,7 +387,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 { @@ -685,19 +686,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); + }); } @@ -966,7 +958,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); @@ -1347,7 +1339,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 11910a1e6ff..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,24 +66,26 @@ public class DaoSearchParamSynchronizer { return retVal; } - private void synchronize(ResourceIndexedSearchParams theParams, ResourceTable theEntity, AddRemoveCount theAddRemoveCount, Collection theNewParms, Collection theExistingParms) { - List quantitiesToRemove = subtract(theExistingParms, theNewParms); - List quantitiesToAdd = subtract(theNewParms, theExistingParms); - tryToReuseIndexEntities(quantitiesToRemove, quantitiesToAdd); - for (T next : quantitiesToRemove) { + private void synchronize(ResourceTable theEntity, AddRemoveCount theAddRemoveCount, Collection theNewParams, Collection theExistingParams) { + for (T next : theNewParams) { + next.setPartitionId(theEntity.getPartitionId()); + next.calculateHashes(); + } + + List paramsToRemove = subtract(theExistingParams, theNewParams); + List paramsToAdd = subtract(theNewParams, theExistingParams); + tryToReuseIndexEntities(paramsToRemove, paramsToAdd); + + for (T next : paramsToRemove) { myEntityManager.remove(next); theEntity.getParamsQuantity().remove(next); } - for (T next : quantitiesToAdd) { - next.setPartitionId(theEntity.getPartitionId()); - } - theParams.calculateHashes(theNewParms); - 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()); } /** @@ -106,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/PredicateBuilderDate.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java index cf98ba254ea..d999c293603 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.ParamPrefixEnum; @@ -150,67 +151,95 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi return p; } + private boolean isNullOrDayPrecision(DateParam theDateParam) { + return theDateParam == null || theDateParam.getPrecision().ordinal() == TemporalPrecisionEnum.DAY.ordinal(); + } + private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder, From theFrom, DateRangeParam theRange, SearchFilterParser.CompareOperation operation) { - Date lowerBound = theRange.getLowerBoundAsInstant(); - Date upperBound = theRange.getUpperBoundAsInstant(); - Predicate lt; - Predicate gt; + Date lowerBoundInstant = theRange.getLowerBoundAsInstant(); + Date upperBoundInstant = theRange.getUpperBoundAsInstant(); + + DateParam lowerBound = theRange.getLowerBound(); + DateParam upperBound = theRange.getUpperBound(); + Integer lowerBoundAsOrdinal = theRange.getLowerBoundAsDateInteger(); + Integer upperBoundAsOrdinal = theRange.getUpperBoundAsDateInteger(); + Comparable genericLowerBound; + Comparable genericUpperBound; + /** + * If all present search parameters are of DAY precision, and {@link DaoConfig#getUseOrdinalDatesForDayPrecisionSearches()} is true, + * then we attempt to use the ordinal field for date comparisons instead of the date field. + */ + boolean isOrdinalComparison = isNullOrDayPrecision(lowerBound) && isNullOrDayPrecision(upperBound) && myDaoConfig.getModelConfig().getUseOrdinalDatesForDayPrecisionSearches(); + + Predicate lt = null; + Predicate gt = null; Predicate lb = null; Predicate ub = null; + String lowValueField; + String highValueField; + + if (isOrdinalComparison) { + lowValueField = "myValueLowDateOrdinal"; + highValueField = "myValueHighDateOrdinal"; + genericLowerBound = lowerBoundAsOrdinal; + genericUpperBound = upperBoundAsOrdinal; + } else { + lowValueField = "myValueLow"; + highValueField = "myValueHigh"; + genericLowerBound = lowerBoundInstant; + genericUpperBound = upperBoundInstant; + } if (operation == SearchFilterParser.CompareOperation.lt) { - if (lowerBound == null) { + if (lowerBoundInstant == null) { throw new InvalidRequestException("lowerBound value not correctly specified for compare operation"); } - lb = theBuilder.lessThan(theFrom.get("myValueLow"), lowerBound); + //im like 80% sure this should be ub and not lb, as it is an UPPER bound. + lb = theBuilder.lessThan(theFrom.get(lowValueField), genericLowerBound); } else if (operation == SearchFilterParser.CompareOperation.le) { - if (upperBound == null) { + if (upperBoundInstant == null) { throw new InvalidRequestException("upperBound value not correctly specified for compare operation"); } - lb = theBuilder.lessThanOrEqualTo(theFrom.get("myValueHigh"), upperBound); + //im like 80% sure this should be ub and not lb, as it is an UPPER bound. + lb = theBuilder.lessThanOrEqualTo(theFrom.get(highValueField), genericUpperBound); } else if (operation == SearchFilterParser.CompareOperation.gt) { - if (upperBound == null) { + if (upperBoundInstant == null) { throw new InvalidRequestException("upperBound value not correctly specified for compare operation"); } - lb = theBuilder.greaterThan(theFrom.get("myValueHigh"), upperBound); - } else if (operation == SearchFilterParser.CompareOperation.ge) { - if (lowerBound == null) { - throw new InvalidRequestException("lowerBound value not correctly specified for compare operation"); - } - lb = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueLow"), lowerBound); - } else if (operation == SearchFilterParser.CompareOperation.ne) { - if ((lowerBound == null) || - (upperBound == null)) { + lb = theBuilder.greaterThan(theFrom.get(highValueField), genericUpperBound); + } else if (operation == SearchFilterParser.CompareOperation.ge) { + if (lowerBoundInstant == null) { + throw new InvalidRequestException("lowerBound value not correctly specified for compare operation"); + } + lb = theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound); + } else if (operation == SearchFilterParser.CompareOperation.ne) { + if ((lowerBoundInstant == null) || + (upperBoundInstant == null)) { throw new InvalidRequestException("lowerBound and/or upperBound value not correctly specified for compare operation"); } - /*Predicate*/ - lt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueLow"), lowerBound); - /*Predicate*/ - gt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueHigh"), upperBound); + lt = theBuilder.lessThan(theFrom.get(lowValueField), genericLowerBound); + gt = theBuilder.greaterThan(theFrom.get(highValueField), genericUpperBound); lb = theBuilder.or(lt, gt); - } else if ((operation == SearchFilterParser.CompareOperation.eq) || - (operation == null)) { - if (lowerBound != null) { - /*Predicate*/ - gt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueLow"), lowerBound); - /*Predicate*/ - lt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueHigh"), lowerBound); - if (theRange.getLowerBound().getPrefix() == ParamPrefixEnum.STARTS_AFTER || theRange.getLowerBound().getPrefix() == ParamPrefixEnum.EQUAL) { + } else if ((operation == SearchFilterParser.CompareOperation.eq) || (operation == null)) { + if (lowerBoundInstant != null) { + gt = theBuilder.greaterThanOrEqualTo(theFrom.get(lowValueField), genericLowerBound); + lt = theBuilder.greaterThanOrEqualTo(theFrom.get(highValueField), genericLowerBound); + if (lowerBound.getPrefix() == ParamPrefixEnum.STARTS_AFTER || lowerBound.getPrefix() == ParamPrefixEnum.EQUAL) { lb = gt; } else { lb = theBuilder.or(gt, lt); } } - if (upperBound != null) { - /*Predicate*/ - gt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueLow"), upperBound); - /*Predicate*/ - lt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueHigh"), upperBound); + if (upperBoundInstant != null) { + gt = theBuilder.lessThanOrEqualTo(theFrom.get(lowValueField), genericUpperBound); + lt = theBuilder.lessThanOrEqualTo(theFrom.get(highValueField), genericUpperBound); + + if (theRange.getUpperBound().getPrefix() == ParamPrefixEnum.ENDS_BEFORE || theRange.getUpperBound().getPrefix() == ParamPrefixEnum.EQUAL) { ub = lt; } else { @@ -221,8 +250,11 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi throw new InvalidRequestException(String.format("Unsupported operator specified, operator=%s", operation.name())); } - - ourLog.trace("Date range is {} - {}", lowerBound, upperBound); + if (isOrdinalComparison) { + ourLog.trace("Ordinal date range is {} - {} ", lowerBoundAsOrdinal, upperBoundAsOrdinal); + } else { + ourLog.trace("Date range is {} - {}", lowerBoundInstant, upperBoundInstant); + } if (lb != null && ub != null) { return (theBuilder.and(lb, ub)); 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/dao/predicate/PredicateBuilderTag.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java index 04bab4248ea..389204c408f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java @@ -160,10 +160,6 @@ class PredicateBuilderTag extends BasePredicateBuilder { continue; - } else { - - myQueryRoot.setHasIndexJoins(); - } Join tagJoin = myQueryRoot.join("myTags", JoinType.LEFT); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java index b2f62941c5f..6a91e496fd1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java @@ -28,8 +28,10 @@ import ca.uhn.fhir.context.support.ValueSetExpansionOptions; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -39,6 +41,7 @@ import ca.uhn.fhir.rest.param.NumberParam; 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.util.VersionIndependentConcept; import com.google.common.collect.Sets; import org.hibernate.query.criteria.internal.CriteriaBuilderImpl; @@ -72,6 +75,8 @@ class PredicateBuilderToken extends BasePredicateBuilder implements IPredicateBu private ITermReadSvc myTerminologySvc; @Autowired private ISearchParamRegistry mySearchParamRegistry; + @Autowired + private ModelConfig myModelConfig; PredicateBuilderToken(SearchBuilder theSearchBuilder, PredicateBuilder thePredicateBuilder) { super(theSearchBuilder); @@ -99,6 +104,20 @@ class PredicateBuilderToken extends BasePredicateBuilder implements IPredicateBu if (nextOr instanceof TokenParam) { TokenParam id = (TokenParam) nextOr; if (id.isText()) { + + // Check whether the :text modifier is actually enabled here + RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); + boolean tokenTextIndexingEnabled = BaseSearchParamExtractor.tokenTextIndexingEnabledForSearchParam(myModelConfig, param); + if (!tokenTextIndexingEnabled) { + String msg; + if (myModelConfig.isSuppressStringIndexingInTokens()) { + msg = myContext.getLocalizer().getMessage(PredicateBuilderToken.class, "textModifierDisabledForServer"); + }else{ + msg = myContext.getLocalizer().getMessage(PredicateBuilderToken.class, "textModifierDisabledForSearchParam"); + } + throw new MethodNotAllowedException(msg); + } + myPredicateBuilder.addPredicateString(theResourceName, theParamName, theList, theRequestPartitionId); break; } 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/TolerantJsonParserR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TolerantJsonParserR4Test.java index 6b462d7db2f..dea758dc150 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TolerantJsonParserR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TolerantJsonParserR4Test.java @@ -46,6 +46,22 @@ public class TolerantJsonParserR4Test { assertEquals("0.5", obs.getValueQuantity().getValueElement().getValueAsString()); } + @Test + public void testParseInvalidNumeric_DoubleZeros() { + String input = "{\n" + + "\"resourceType\": \"Observation\",\n" + + "\"valueQuantity\": {\n" + + " \"value\": 00\n" + + " }\n" + + "}"; + + + TolerantJsonParser parser = new TolerantJsonParser(myFhirContext, new LenientErrorHandler()); + Observation obs = parser.parseResource(Observation.class, input); + + assertEquals("0", obs.getValueQuantity().getValueElement().getValueAsString()); + } + @Test public void testParseInvalidNumeric2() { String input = "{\n" + 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 ae4f2564ac7..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 @@ -7,6 +7,7 @@ import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; @@ -16,6 +17,7 @@ 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.model.util.JpaConstants; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; @@ -26,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; @@ -36,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; @@ -108,6 +181,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { @Before public void beforeDisableCacheReuse() { + myModelConfig.setSuppressStringIndexingInTokens(new ModelConfig().isSuppressStringIndexingInTokens()); myDaoConfig.setReuseCachedSearchResultsForMillis(null); } @@ -4353,15 +4427,58 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { assertEquals(1, results.getResources(0, 10).size()); // We expect a new one because we don't cache the search URL for very long search URLs assertEquals(2, mySearchEntityDao.count()); - } + @Test + public void testTokenTextDisabled_Global() { + myModelConfig.setSuppressStringIndexingInTokens(true); + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(Observation.SP_CODE, new TokenParam("hello").setModifier(TokenParamModifier.TEXT)); + try { + myObservationDao.search(map); + } catch (MethodNotAllowedException e) { + assertEquals("The :text modifier is disabled on this server", e.getMessage()); + } + } + + @Test + public void testTokenTextDisabled_ForSearchParam() { + { + SearchParameter sp = new SearchParameter(); + sp.setId("observation-code"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.addBase("Observation"); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setCode("code"); + sp.setExpression("Observation.code"); + sp.addExtension() + .setUrl(JpaConstants.EXT_SEARCHPARAM_TOKEN_SUPPRESS_TEXT_INDEXING) + .setValue(new BooleanType(true)); + ourLog.info("SP:\n{}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp)); + mySearchParameterDao.update(sp); + mySearchParamRegistry.forceRefresh(); + } + + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(Observation.SP_CODE, new TokenParam("hello").setModifier(TokenParamModifier.TEXT)); + try { + myObservationDao.search(map); + } catch (MethodNotAllowedException e) { + assertEquals("The :text modifier is disabled for this search parameter", e.getMessage()); + } + } + + @Test public void testDateSearchParametersShouldBeTimezoneIndependent() { - createObservationWithEffective("NO1", "2011-01-02T23:00:00-11:30"); - createObservationWithEffective("NO2", "2011-01-03T00:00:00+01:00"); + createObservationWithEffective("NO1", "2011-01-03T00:00:00+01:00"); + createObservationWithEffective("YES00", "2011-01-02T23:00:00-11:30"); createObservationWithEffective("YES01", "2011-01-02T00:00:00-11:30"); createObservationWithEffective("YES02", "2011-01-02T00:00:00-10:00"); createObservationWithEffective("YES03", "2011-01-02T00:00:00-09:00"); @@ -4394,6 +4511,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { List values = toUnqualifiedVersionlessIdValues(results); Collections.sort(values); assertThat(values.toString(), values, contains( + "Observation/YES00", "Observation/YES01", "Observation/YES02", "Observation/YES03", @@ -4420,6 +4538,68 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { )); } + @Test + public void testDateSearchParametersShouldBeHourIndependent() { + + createObservationWithEffective("YES01", "2011-01-02T00:00:00"); + createObservationWithEffective("YES02", "2011-01-02T01:00:00"); + createObservationWithEffective("YES03", "2011-01-02T02:00:00"); + createObservationWithEffective("YES04", "2011-01-02T03:00:00"); + createObservationWithEffective("YES05", "2011-01-02T04:00:00"); + createObservationWithEffective("YES06", "2011-01-02T05:00:00"); + createObservationWithEffective("YES07", "2011-01-02T06:00:00"); + createObservationWithEffective("YES08", "2011-01-02T07:00:00"); + createObservationWithEffective("YES09", "2011-01-02T08:00:00"); + createObservationWithEffective("YES10", "2011-01-02T09:00:00"); + createObservationWithEffective("YES11", "2011-01-02T10:00:00"); + createObservationWithEffective("YES12", "2011-01-02T11:00:00"); + createObservationWithEffective("YES13", "2011-01-02T12:00:00"); + createObservationWithEffective("YES14", "2011-01-02T13:00:00"); + createObservationWithEffective("YES15", "2011-01-02T14:00:00"); + createObservationWithEffective("YES16", "2011-01-02T15:00:00"); + createObservationWithEffective("YES17", "2011-01-02T16:00:00"); + createObservationWithEffective("YES18", "2011-01-02T17:00:00"); + createObservationWithEffective("YES19", "2011-01-02T18:00:00"); + createObservationWithEffective("YES20", "2011-01-02T19:00:00"); + createObservationWithEffective("YES21", "2011-01-02T20:00:00"); + createObservationWithEffective("YES22", "2011-01-02T21:00:00"); + createObservationWithEffective("YES23", "2011-01-02T22:00:00"); + createObservationWithEffective("YES24", "2011-01-02T23:00:00"); + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(Observation.SP_DATE, new DateParam("2011-01-02")); + IBundleProvider results = myObservationDao.search(map); + List values = toUnqualifiedVersionlessIdValues(results); + Collections.sort(values); + assertThat(values.toString(), values, contains( + "Observation/YES01", + "Observation/YES02", + "Observation/YES03", + "Observation/YES04", + "Observation/YES05", + "Observation/YES06", + "Observation/YES07", + "Observation/YES08", + "Observation/YES09", + "Observation/YES10", + "Observation/YES11", + "Observation/YES12", + "Observation/YES13", + "Observation/YES14", + "Observation/YES15", + "Observation/YES16", + "Observation/YES17", + "Observation/YES18", + "Observation/YES19", + "Observation/YES20", + "Observation/YES21", + "Observation/YES22", + "Observation/YES23", + "Observation/YES24" + )); + } + private void createObservationWithEffective(String theId, String theEffective) { Observation obs = new Observation(); obs.setId(theId); 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 183ba6cefd4..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"}) @@ -3280,8 +3361,8 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { @Test public void testDateSearchParametersShouldBeTimezoneIndependent() { - createObservationWithEffective("NO1", "2011-01-02T23:00:00-11:30"); - createObservationWithEffective("NO2", "2011-01-03T00:00:00+01:00"); + createObservationWithEffective("NO1", "2011-01-01T23:00:00-11:30"); + createObservationWithEffective("NO2", "2011-01-03T23:00:00+01:30"); createObservationWithEffective("YES01", "2011-01-02T00:00:00-11:30"); createObservationWithEffective("YES02", "2011-01-02T00:00:00-10:00"); 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/FhirResourceDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java index facda6c685c..fcae2ae2e59 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java @@ -1,9 +1,9 @@ package ca.uhn.fhir.jpa.dao.r4; +import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 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.JpaResourceDao; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; @@ -34,6 +34,7 @@ import ca.uhn.fhir.rest.param.ReferenceParam; 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.UriParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; @@ -51,16 +52,52 @@ import org.hamcrest.core.StringContains; 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.Age; +import org.hl7.fhir.r4.model.Attachment; +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.CanonicalType; +import org.hl7.fhir.r4.model.CarePlan; +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.CompartmentDefinition; +import org.hl7.fhir.r4.model.ConceptMap; +import org.hl7.fhir.r4.model.Condition; +import org.hl7.fhir.r4.model.Consent; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; +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.Enumerations.PublicationStatus; +import org.hl7.fhir.r4.model.IdType; +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.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.OperationDefinition; +import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.r4.model.OperationOutcome.IssueType; +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.Quantity; import org.hl7.fhir.r4.model.Quantity.QuantityComparator; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.Range; +import org.hl7.fhir.r4.model.Reference; +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.Timing; +import org.hl7.fhir.r4.model.UriType; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -1627,6 +1664,29 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { } + @Test + public void testDeleteByTagWrongType() { + Organization org = new Organization(); + org.getMeta().addTag().setCode("term"); + IIdType orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + myPatientDao.deleteByUrl("Patient?_tag=term", mySrd); + // The organization is still there + myOrganizationDao.read(orgId); + } + + @Test + public void testSearchByTagWrongType() { + Organization org = new Organization(); + org.getMeta().addTag().setCode("term"); + myOrganizationDao.create(org, mySrd); + SearchParameterMap map = new SearchParameterMap(); + map.add("_tag", new UriParam("term")); + map.setLoadSynchronous(true); + IBundleProvider result = myPatientDao.search(map); + List resources = result.getResources(0, 1); + assertEquals(0, resources.size()); + } + @Test public void testDeleteWithMatchUrlQualifierMissing() { String methodName = "testDeleteWithMatchUrlChainedProfile"; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java index ecc4961b9b7..94674ee4610 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.util.TestUtil; @@ -85,6 +86,38 @@ public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test { } + /** + * Just in case any hash values are missing + */ + @Test + public void testCreateAndUpdateStringAndTokenWhereHashesAreNull() { + Patient p = new Patient(); + p.addIdentifier().setSystem("sys1").setValue("val1"); + p.addName().setFamily("FAMILY1"); + IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless(); + + runInTransaction(()->{ + myEntityManager.createQuery("UPDATE ResourceIndexedSearchParamString s SET s.myHashIdentity = null").executeUpdate(); + myEntityManager.createQuery("UPDATE ResourceIndexedSearchParamString s SET s.myHashExact = null").executeUpdate(); + myEntityManager.createQuery("UPDATE ResourceIndexedSearchParamString s SET s.myHashNormalizedPrefix = null").executeUpdate(); + myEntityManager.createQuery("UPDATE ResourceIndexedSearchParamToken s SET s.myHashIdentity = null").executeUpdate(); + myEntityManager.createQuery("UPDATE ResourceIndexedSearchParamToken s SET s.myHashSystem = null").executeUpdate(); + myEntityManager.createQuery("UPDATE ResourceIndexedSearchParamToken s SET s.myHashValue = null").executeUpdate(); + myEntityManager.createQuery("UPDATE ResourceIndexedSearchParamToken s SET s.myHashSystemAndValue = null").executeUpdate(); + }); + + p = new Patient(); + p.setId(id); + p.addIdentifier().setSystem("sys2").setValue("val2"); + p.addName().setFamily("FAMILY2"); + myPatientDao.update(p); + + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.add(Patient.SP_FAMILY, new StringParam("FAMILY2")); + Patient newPatient = (Patient) myPatientDao.search(map).getResources(0,1).get(0); + assertEquals("FAMILY2", newPatient.getName().get(0).getFamily()); + } @Test public void testUpdateNotModifiedDoesNotAffectDates() { 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 8a0a7ab0dc4..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); @@ -1642,20 +1657,22 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); ourLog.info("Search SQL:\n{}", searchSql); - assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); + + // If this ever got optimized down to 1 that would be OK too + assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID")); assertEquals(1, StringUtils.countMatches(searchSql, "TAG_SYSTEM='http://system'")); } @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(); @@ -1673,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); @@ -1700,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(); @@ -1724,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(); @@ -1758,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(); @@ -1795,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(); @@ -1833,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(); @@ -1869,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)); @@ -1902,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); @@ -1915,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); @@ -1941,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); @@ -1972,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); @@ -2003,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 @@ -2130,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 b8ce8d63a72..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,12 +1,13 @@ 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; 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; @@ -20,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 { @@ -37,6 +36,8 @@ public class SearchCoordinatorSvcImplTest extends BaseJpaR4Test { @Autowired private ISearchResultDao mySearchResultDao; + @Autowired + private ISearchIncludeDao mySearchIncludeDao; @Autowired private ISearchCoordinatorSvc mySearchCoordinator; @@ -55,6 +56,11 @@ public class SearchCoordinatorSvcImplTest extends BaseJpaR4Test { DatabaseSearchCacheSvcImpl.setMaximumResultsToDeleteInOnePassForUnitTest(5); DatabaseSearchCacheSvcImpl.setMaximumSearchesToCheckForDeletionCandidacyForUnitTest(10); + runInTransaction(()->{ + mySearchResultDao.deleteAll(); + mySearchIncludeDao.deleteAll(); + mySearchDao.deleteAll(); + }); runInTransaction(()->{ assertEquals(0, mySearchDao.count()); assertEquals(0, mySearchResultDao.count()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java index 87d30dc8172..a185e21c874 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java @@ -3,12 +3,15 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.model.entity.ModelConfig; 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.util.JpaConstants; import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; import ca.uhn.fhir.jpa.searchparam.extractor.PathAndRef; @@ -17,11 +20,12 @@ import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Sets; -import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; +import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Consent; import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Quantity; @@ -43,6 +47,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import static java.util.Comparator.comparing; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -52,11 +57,13 @@ public class SearchParamExtractorR4Test { private static FhirContext ourCtx = FhirContext.forR4(); private static IValidationSupport ourValidationSupport; private MySearchParamRegistry mySearchParamRegistry; + private PartitionSettings myPartitionSettings; @Before public void before() { mySearchParamRegistry = new MySearchParamRegistry(); + myPartitionSettings = new PartitionSettings(); } @@ -65,8 +72,7 @@ public class SearchParamExtractorR4Test { Observation obs = new Observation(); obs.addCategory().addCoding().setSystem("SYSTEM").setCode("CODE"); - SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); - extractor.setPartitionConfigForUnitTest(new PartitionSettings()); + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, mySearchParamRegistry); Set tokens = extractor.extractSearchParamTokens(obs); assertEquals(1, tokens.size()); ResourceIndexedSearchParamToken token = (ResourceIndexedSearchParamToken) tokens.iterator().next(); @@ -80,8 +86,7 @@ public class SearchParamExtractorR4Test { SearchParameter sp = new SearchParameter(); sp.addUseContext().setCode(new Coding().setSystem("http://system").setCode("code")); - SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); - extractor.setPartitionConfigForUnitTest(new PartitionSettings()); + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, mySearchParamRegistry); Set tokens = extractor.extractSearchParamTokens(sp); assertEquals(1, tokens.size()); ResourceIndexedSearchParamToken token = (ResourceIndexedSearchParamToken) tokens.iterator().next(); @@ -90,12 +95,170 @@ public class SearchParamExtractorR4Test { assertEquals("code", token.getValue()); } + @Test + public void testTokenText_Enabled_Coding() { + Observation obs = new Observation(); + obs.getCode().addCoding().setSystem("http://system").setCode("code").setDisplay("Help Im a Bug"); + + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), myPartitionSettings, ourCtx, ourValidationSupport, mySearchParamRegistry); + + List tokens = extractor.extractSearchParamTokens(obs) + .stream() + .filter(t -> t.getParamName().equals("code")) + .sorted(comparing(o -> o.getClass().getName()).reversed()) + .collect(Collectors.toList()); + assertEquals(2, tokens.size()); + + ResourceIndexedSearchParamToken token = (ResourceIndexedSearchParamToken) tokens.get(0); + assertEquals("code", token.getParamName()); + assertEquals("http://system", token.getSystem()); + assertEquals("code", token.getValue()); + + ResourceIndexedSearchParamString string = (ResourceIndexedSearchParamString) tokens.get(1); + assertEquals("code", string.getParamName()); + assertEquals("Help Im a Bug", string.getValueExact()); + } + + @Test + public void testTokenText_DisabledInSearchParam_Coding() { + RuntimeSearchParam existingCodeSp = mySearchParamRegistry.getActiveSearchParams("Observation").get("code"); + RuntimeSearchParam codeSearchParam = new RuntimeSearchParam(existingCodeSp); + codeSearchParam.addExtension(JpaConstants.EXT_SEARCHPARAM_TOKEN_SUPPRESS_TEXT_INDEXING, new Extension(JpaConstants.EXT_SEARCHPARAM_TOKEN_SUPPRESS_TEXT_INDEXING, new BooleanType(true))); + mySearchParamRegistry.addSearchParam(codeSearchParam); + + Observation obs = new Observation(); + obs.getCode().addCoding().setSystem("http://system").setCode("code").setDisplay("Help Im a Bug"); + + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), myPartitionSettings, ourCtx, ourValidationSupport, mySearchParamRegistry); + + List tokens = extractor.extractSearchParamTokens(obs) + .stream() + .filter(t -> t.getParamName().equals("code")) + .sorted(comparing(o -> o.getClass().getName()).reversed()) + .collect(Collectors.toList()); + assertEquals(1, tokens.size()); + + ResourceIndexedSearchParamToken token = (ResourceIndexedSearchParamToken) tokens.get(0); + assertEquals("code", token.getParamName()); + assertEquals("http://system", token.getSystem()); + assertEquals("code", token.getValue()); + + } + + @Test + public void testTokenText_DisabledInModelConfig_Coding() { + ModelConfig modelConfig = new ModelConfig(); + modelConfig.setSuppressStringIndexingInTokens(true); + + Observation obs = new Observation(); + obs.getCode().addCoding().setSystem("http://system").setCode("code").setDisplay("Help Im a Bug"); + + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(modelConfig, myPartitionSettings, ourCtx, ourValidationSupport, mySearchParamRegistry); + + List tokens = extractor.extractSearchParamTokens(obs) + .stream() + .filter(t -> t.getParamName().equals("code")) + .sorted(comparing(o -> o.getClass().getName()).reversed()) + .collect(Collectors.toList()); + assertEquals(1, tokens.size()); + + ResourceIndexedSearchParamToken token = (ResourceIndexedSearchParamToken) tokens.get(0); + assertEquals("code", token.getParamName()); + assertEquals("http://system", token.getSystem()); + assertEquals("code", token.getValue()); + + } + + @Test + public void testTokenText_DisabledInModelConfigButForcedInSearchParam_Coding() { + ModelConfig modelConfig = new ModelConfig(); + modelConfig.setSuppressStringIndexingInTokens(true); + + RuntimeSearchParam existingCodeSp = mySearchParamRegistry.getActiveSearchParams("Observation").get("code"); + RuntimeSearchParam codeSearchParam = new RuntimeSearchParam(existingCodeSp); + codeSearchParam.addExtension(JpaConstants.EXT_SEARCHPARAM_TOKEN_SUPPRESS_TEXT_INDEXING, new Extension(JpaConstants.EXT_SEARCHPARAM_TOKEN_SUPPRESS_TEXT_INDEXING, new BooleanType(false))); + mySearchParamRegistry.addSearchParam(codeSearchParam); + + Observation obs = new Observation(); + obs.getCode().addCoding().setSystem("http://system").setCode("code").setDisplay("Help Im a Bug"); + + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(modelConfig, myPartitionSettings, ourCtx, ourValidationSupport, mySearchParamRegistry); + + List tokens = extractor.extractSearchParamTokens(obs) + .stream() + .filter(t -> t.getParamName().equals("code")) + .sorted(comparing(o -> o.getClass().getName()).reversed()) + .collect(Collectors.toList()); + assertEquals(2, tokens.size()); + + ResourceIndexedSearchParamToken token = (ResourceIndexedSearchParamToken) tokens.get(0); + assertEquals("code", token.getParamName()); + assertEquals("http://system", token.getSystem()); + assertEquals("code", token.getValue()); + + ResourceIndexedSearchParamString string = (ResourceIndexedSearchParamString) tokens.get(1); + assertEquals("code", string.getParamName()); + assertEquals("Help Im a Bug", string.getValueExact()); + } + + + @Test + public void testTokenText_Enabled_Identifier() { + Observation obs = new Observation(); + obs.addIdentifier().setSystem("sys").setValue("val").getType().setText("Help Im a Bug"); + + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), myPartitionSettings, ourCtx, ourValidationSupport, mySearchParamRegistry); + + List tokens = extractor.extractSearchParamTokens(obs) + .stream() + .filter(t -> t.getParamName().equals("identifier")) + .sorted(comparing(o -> o.getClass().getName()).reversed()) + .collect(Collectors.toList()); + assertEquals(2, tokens.size()); + + ResourceIndexedSearchParamToken token = (ResourceIndexedSearchParamToken) tokens.get(0); + assertEquals("identifier", token.getParamName()); + assertEquals("sys", token.getSystem()); + assertEquals("val", token.getValue()); + + ResourceIndexedSearchParamString string = (ResourceIndexedSearchParamString) tokens.get(1); + assertEquals("identifier", string.getParamName()); + assertEquals("Help Im a Bug", string.getValueExact()); + } + + @Test + public void testTokenText_DisabledInSearchParam_Identifier() { + RuntimeSearchParam existingCodeSp = mySearchParamRegistry.getActiveSearchParams("Observation").get("identifier"); + RuntimeSearchParam codeSearchParam = new RuntimeSearchParam(existingCodeSp); + codeSearchParam.addExtension(JpaConstants.EXT_SEARCHPARAM_TOKEN_SUPPRESS_TEXT_INDEXING, new Extension(JpaConstants.EXT_SEARCHPARAM_TOKEN_SUPPRESS_TEXT_INDEXING, new BooleanType(true))); + + mySearchParamRegistry.addSearchParam(codeSearchParam); + + Observation obs = new Observation(); + obs.addIdentifier().setSystem("sys").setValue("val").getType().setText("Help Im a Bug"); + + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), myPartitionSettings, ourCtx, ourValidationSupport, mySearchParamRegistry); + + List tokens = extractor.extractSearchParamTokens(obs) + .stream() + .filter(t -> t.getParamName().equals("identifier")) + .sorted(comparing(o -> o.getClass().getName()).reversed()) + .collect(Collectors.toList()); + assertEquals(1, tokens.size()); + + ResourceIndexedSearchParamToken token = (ResourceIndexedSearchParamToken) tokens.get(0); + assertEquals("identifier", token.getParamName()); + assertEquals("sys", token.getSystem()); + assertEquals("val", token.getValue()); + + } + @Test public void testReferenceWithResolve() { Encounter enc = new Encounter(); enc.addLocation().setLocation(new Reference("Location/123")); - SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, mySearchParamRegistry); RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam("Encounter", "location"); assertNotNull(param); ISearchParamExtractor.SearchParamSet links = extractor.extractResourceLinks(enc); @@ -110,8 +273,7 @@ public class SearchParamExtractorR4Test { Consent consent = new Consent(); consent.setSource(new Reference().setReference("Consent/999")); - SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); - extractor.setPartitionConfigForUnitTest(new PartitionSettings()); + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, mySearchParamRegistry); RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam("Consent", Consent.SP_SOURCE_REFERENCE); assertNotNull(param); ISearchParamExtractor.SearchParamSet links = extractor.extractResourceLinks(consent); @@ -126,8 +288,7 @@ public class SearchParamExtractorR4Test { Patient p = new Patient(); p.addIdentifier().setSystem("sys").setValue("val"); - SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); - extractor.setPartitionConfigForUnitTest(new PartitionSettings()); + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, mySearchParamRegistry); RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam("Patient", Patient.SP_IDENTIFIER); assertNotNull(param); ISearchParamExtractor.SearchParamSet params = extractor.extractSearchParamTokens(p, param); @@ -149,7 +310,7 @@ public class SearchParamExtractorR4Test { Patient patient = new Patient(); patient.addExtension("http://patext", new Reference("Organization/AAA")); - SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, mySearchParamRegistry); ISearchParamExtractor.SearchParamSet links = extractor.extractResourceLinks(patient); assertEquals(1, links.size()); @@ -165,7 +326,7 @@ public class SearchParamExtractorR4Test { .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code2"))) .setValue(new Quantity().setSystem("http://bar").setCode("code2").setValue(200)); - SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, mySearchParamRegistry); Set links = extractor.extractSearchParamQuantity(o1); ourLog.info("Links:\n {}", links.stream().map(t -> t.toString()).collect(Collectors.joining("\n "))); assertEquals(4, links.size()); 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 c66c4bc382a..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}) @@ -887,10 +931,10 @@ public class InMemorySubscriptionMatcherR4Test { public void testDateSearchParametersShouldBeTimezoneIndependent() { List nlist = new ArrayList<>(); - nlist.add(createObservationWithEffective("NO1", "2011-01-02T23:00:00-11:30")); nlist.add(createObservationWithEffective("NO2", "2011-01-03T00:00:00+01:00")); List ylist = new ArrayList<>(); + nlist.add(createObservationWithEffective("YES00", "2011-01-02T23:00:00-11:30")); ylist.add(createObservationWithEffective("YES01", "2011-01-02T00:00:00-11:30")); ylist.add(createObservationWithEffective("YES02", "2011-01-02T00:00:00-10:00")); ylist.add(createObservationWithEffective("YES03", "2011-01-02T00:00:00-09:00")); 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/BaseMigrator.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/BaseMigrator.java index b37a45f2735..d2f4332078f 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/BaseMigrator.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/BaseMigrator.java @@ -21,9 +21,13 @@ package ca.uhn.fhir.jpa.migrate; */ import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask; +import org.apache.commons.lang3.Validate; +import org.flywaydb.core.api.callback.Callback; +import javax.annotation.Nonnull; import javax.sql.DataSource; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -34,6 +38,17 @@ public abstract class BaseMigrator implements IMigrator { private DriverTypeEnum myDriverType; private DataSource myDataSource; private List myExecutedStatements = new ArrayList<>(); + private List myCallbacks = Collections.emptyList(); + + @Nonnull + public List getCallbacks() { + return myCallbacks; + } + + public void setCallbacks(@Nonnull List theCallbacks) { + Validate.notNull(theCallbacks); + myCallbacks = theCallbacks; + } public DataSource getDataSource() { return myDataSource; diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrator.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrator.java index 7373e1889ee..4b24a3a3c63 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrator.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrator.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.migrate.taskdef.InitializeSchemaTask; import com.google.common.annotations.VisibleForTesting; import org.flywaydb.core.Flyway; import org.flywaydb.core.api.MigrationInfoService; +import org.flywaydb.core.api.callback.Callback; import org.flywaydb.core.api.migration.JavaMigration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,6 +80,7 @@ public class FlywayMigrator extends BaseMigrator { .baselineOnMigrate(true) .outOfOrder(isOutOfOrderPermitted()) .javaMigrations(myTasks.toArray(new JavaMigration[0])) + .callbacks(getCallbacks().toArray(new Callback[0])) .load(); for (FlywayMigration task : myTasks) { task.setConnectionProperties(theConnectionProperties); 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-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/Migrator.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/Migrator.java index 1688de89a5e..82eab50491d 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/Migrator.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/Migrator.java @@ -132,4 +132,6 @@ public class Migrator { public void setNoColumnShrink(boolean theNoColumnShrink) { myNoColumnShrink = theNoColumnShrink; } + + } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/SchemaMigrator.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/SchemaMigrator.java index 3be8039ac3d..f4b98c3f615 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/SchemaMigrator.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/SchemaMigrator.java @@ -24,20 +24,23 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask; import org.flywaydb.core.api.MigrationInfo; import org.flywaydb.core.api.MigrationInfoService; +import org.flywaydb.core.api.callback.Callback; import org.hibernate.cfg.AvailableSettings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Properties; public class SchemaMigrator { - private static final Logger ourLog = LoggerFactory.getLogger(SchemaMigrator.class); public static final String HAPI_FHIR_MIGRATION_TABLENAME = "FLY_HFJ_MIGRATION"; + private static final Logger ourLog = LoggerFactory.getLogger(SchemaMigrator.class); private final DataSource myDataSource; private final boolean mySkipValidation; private final String myMigrationTableName; @@ -45,6 +48,7 @@ public class SchemaMigrator { private boolean myDontUseFlyway; private boolean myOutOfOrderPermitted; private DriverTypeEnum myDriverType; + private List myCallbacks = Collections.emptyList(); /** * Constructor @@ -61,6 +65,11 @@ public class SchemaMigrator { } } + public void setCallbacks(List theCallbacks) { + Assert.notNull(theCallbacks); + myCallbacks = theCallbacks; + } + public void setDontUseFlyway(boolean theDontUseFlyway) { myDontUseFlyway = theDontUseFlyway; } @@ -110,6 +119,7 @@ public class SchemaMigrator { migrator.setOutOfOrderPermitted(myOutOfOrderPermitted); } migrator.addTasks(myMigrationTasks); + migrator.setCallbacks(myCallbacks); return migrator; } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseColumnCalculatorTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseColumnCalculatorTask.java new file mode 100644 index 00000000000..447a95df39c --- /dev/null +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseColumnCalculatorTask.java @@ -0,0 +1,264 @@ +package ca.uhn.fhir.jpa.migrate.taskdef; + +/*- + * #%L + * HAPI FHIR JPA Server - Migration + * %% + * 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.util.StopWatch; +import ca.uhn.fhir.util.VersionEnum; +import com.google.common.collect.ForwardingMap; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.ColumnMapRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowCallbackHandler; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.*; +import java.util.function.Function; + +public abstract class BaseColumnCalculatorTask extends BaseTableColumnTask { + + protected static final Logger ourLog = LoggerFactory.getLogger(BaseColumnCalculatorTask.class); + private int myBatchSize = 10000; + private ThreadPoolExecutor myExecutor; + + public void setBatchSize(int theBatchSize) { + myBatchSize = theBatchSize; + } + + /** + * Constructor + */ + public BaseColumnCalculatorTask(VersionEnum theRelease, String theVersion) { + super(theRelease.toString(), theVersion); + } + + /** + * Allows concrete implementations to decide if they should be skipped. + * @return a boolean indicating whether or not to skip execution of the task. + */ + protected abstract boolean shouldSkipTask(); + + @Override + public synchronized void doExecute() throws SQLException { + if (isDryRun() || shouldSkipTask()) { + return; + } + + initializeExecutor(); + + try { + + while(true) { + MyRowCallbackHandler rch = new MyRowCallbackHandler(); + getTxTemplate().execute(t -> { + JdbcTemplate jdbcTemplate = newJdbcTemplate(); + jdbcTemplate.setMaxRows(100000); + + String sql = "SELECT * FROM " + getTableName() + " WHERE " + getWhereClause(); + logInfo(ourLog, "Finding up to {} rows in {} that requires calculations, using query: {}", myBatchSize, getTableName(), sql); + + jdbcTemplate.query(sql, rch); + rch.done(); + + return null; + }); + + rch.submitNext(); + List> futures = rch.getFutures(); + if (futures.isEmpty()) { + break; + } + + logInfo(ourLog, "Waiting for {} tasks to complete", futures.size()); + for (Future next : futures) { + try { + next.get(); + } catch (Exception e) { + throw new SQLException(e); + } + } + + } + + } finally { + destroyExecutor(); + } + } + + private void destroyExecutor() { + myExecutor.shutdownNow(); + } + + private void initializeExecutor() { + int maximumPoolSize = Runtime.getRuntime().availableProcessors(); + + LinkedBlockingQueue executorQueue = new LinkedBlockingQueue<>(maximumPoolSize); + BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() + .namingPattern("worker-" + "-%d") + .daemon(false) + .priority(Thread.NORM_PRIORITY) + .build(); + RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) { + logInfo(ourLog, "Note: Executor queue is full ({} elements), waiting for a slot to become available!", executorQueue.size()); + StopWatch sw = new StopWatch(); + try { + executorQueue.put(theRunnable); + } catch (InterruptedException theE) { + throw new RejectedExecutionException("Task " + theRunnable.toString() + + " rejected from " + theE.toString()); + } + logInfo(ourLog, "Slot become available after {}ms", sw.getMillis()); + } + }; + myExecutor = new ThreadPoolExecutor( + 1, + maximumPoolSize, + 0L, + TimeUnit.MILLISECONDS, + executorQueue, + threadFactory, + rejectedExecutionHandler); + } + + private Future updateRows(List> theRows) { + Runnable task = () -> { + StopWatch sw = new StopWatch(); + getTxTemplate().execute(t -> { + + // Loop through rows + assert theRows != null; + for (Map nextRow : theRows) { + + Map newValues = new HashMap<>(); + MandatoryKeyMap nextRowMandatoryKeyMap = new MandatoryKeyMap<>(nextRow); + + // Apply calculators + for (Map.Entry, Object>> nextCalculatorEntry : myCalculators.entrySet()) { + String nextColumn = nextCalculatorEntry.getKey(); + Function, Object> nextCalculator = nextCalculatorEntry.getValue(); + Object value = nextCalculator.apply(nextRowMandatoryKeyMap); + newValues.put(nextColumn, value); + } + + // Generate update SQL + StringBuilder sqlBuilder = new StringBuilder(); + List arguments = new ArrayList<>(); + sqlBuilder.append("UPDATE "); + sqlBuilder.append(getTableName()); + sqlBuilder.append(" SET "); + for (Map.Entry nextNewValueEntry : newValues.entrySet()) { + if (arguments.size() > 0) { + sqlBuilder.append(", "); + } + sqlBuilder.append(nextNewValueEntry.getKey()).append(" = ?"); + arguments.add(nextNewValueEntry.getValue()); + } + sqlBuilder.append(" WHERE SP_ID = ?"); + arguments.add((Number) nextRow.get("SP_ID")); + + // Apply update SQL + newJdbcTemplate().update(sqlBuilder.toString(), arguments.toArray()); + } + return theRows.size(); + }); + logInfo(ourLog, "Updated {} rows on {} in {}", theRows.size(), getTableName(), sw.toString()); + }; + return myExecutor.submit(task); + } + + private class MyRowCallbackHandler implements RowCallbackHandler { + + private List> myRows = new ArrayList<>(); + private List> myFutures = new ArrayList<>(); + + @Override + public void processRow(ResultSet rs) throws SQLException { + Map row = new ColumnMapRowMapper().mapRow(rs, 0); + myRows.add(row); + + if (myRows.size() >= myBatchSize) { + submitNext(); + } + } + + private void submitNext() { + if (myRows.size() > 0) { + myFutures.add(updateRows(myRows)); + myRows = new ArrayList<>(); + } + } + + public List> getFutures() { + return myFutures; + } + + public void done() { + if (myRows.size() > 0) { + submitNext(); + } + } + } + + + public static class MandatoryKeyMap extends ForwardingMap { + + private final Map myWrap; + + public MandatoryKeyMap(Map theWrap) { + myWrap = theWrap; + } + + @Override + public V get(Object theKey) { + if (!containsKey(theKey)) { + throw new IllegalArgumentException("No key: " + theKey); + } + return super.get(theKey); + } + + public String getString(String theKey) { + return (String) get(theKey); + } + + public Date getDate(String theKey) { + return (Date) get(theKey); + } + + @Override + protected Map delegate() { + return myWrap; + } + + public String getResourceType() { + return getString("RES_TYPE"); + } + + public String getParamName() { + return getString("SP_NAME"); + } + } +} diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableColumnTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableColumnTask.java index a58fbdf0f0d..703e64c510b 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableColumnTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableColumnTask.java @@ -25,11 +25,17 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.thymeleaf.util.StringUtils; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; +import java.util.function.Function; public abstract class BaseTableColumnTask extends BaseTableTask { - private String myColumnName; + protected Map, Object>> myCalculators = new HashMap<>(); + protected String myColumnName; + //If a concrete class decides to, they can define a custom WHERE clause for the task. + protected String myWhereClause; public BaseTableColumnTask(String theProductVersion, String theSchemaVersion) { super(theProductVersion, theSchemaVersion); @@ -40,11 +46,21 @@ public abstract class BaseTableColumnTask extends BaseTableTask { return this; } - public String getColumnName() { return myColumnName; } + protected void setWhereClause(String theWhereClause) { + this.myWhereClause = theWhereClause; + } + protected String getWhereClause() { + if (myWhereClause == null) { + return getColumnName() + " IS NULL"; + } else { + return myWhereClause; + } + } + @Override public void validate() { super.validate(); @@ -63,4 +79,10 @@ public abstract class BaseTableColumnTask extends BaseTableTask { super.generateHashCode(theBuilder); theBuilder.append(myColumnName); } + + public BaseTableColumnTask addCalculator(String theColumnName, Function, Object> theConsumer) { + Validate.isTrue(myCalculators.containsKey(theColumnName) == false); + myCalculators.put(theColumnName, theConsumer); + return this; + } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableTask.java index ad3596b7bc0..8701bd01885 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableTask.java @@ -27,6 +27,8 @@ import org.apache.commons.lang3.builder.HashCodeBuilder; public abstract class BaseTableTask extends BaseTask { private String myTableName; + + public BaseTableTask(String theProductVersion, String theSchemaVersion) { super(theProductVersion, theSchemaVersion); } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java index 2b8daf16972..39b666a8e61 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java @@ -167,7 +167,7 @@ public abstract class BaseTask { doExecute(); } - public abstract void doExecute() throws SQLException; + protected abstract void doExecute() throws SQLException; public void setFailureAllowed(boolean theFailureAllowed) { myFailureAllowed = theFailureAllowed; diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTask.java index c1310abd39b..b182a12c099 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTask.java @@ -34,253 +34,34 @@ import org.springframework.jdbc.core.RowCallbackHandler; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.*; +import java.util.concurrent.*; import java.util.function.Function; -public class CalculateHashesTask extends BaseTableColumnTask { - - private static final Logger ourLog = LoggerFactory.getLogger(CalculateHashesTask.class); - private int myBatchSize = 10000; - private Map, Long>> myCalculators = new HashMap<>(); - private ThreadPoolExecutor myExecutor; +public class CalculateHashesTask extends BaseColumnCalculatorTask { /** * Constructor */ public CalculateHashesTask(VersionEnum theRelease, String theVersion) { - super(theRelease.toString(), theVersion); + super(theRelease, theVersion); setDescription("Calculate resource search parameter index hashes"); } - public void setBatchSize(int theBatchSize) { - myBatchSize = theBatchSize; - } - @Override - public CalculateHashesTask setColumnName(String theColumnName) { - super.setColumnName(theColumnName); - return this; - } - - @Override - public synchronized void doExecute() throws SQLException { - if (isDryRun()) { - return; - } - - Set tableNames = JdbcUtils.getTableNames(getConnectionProperties()); - // This table was added shortly after hash indexes were added, so it is a reasonable indicator for whether this - // migration has already been run - if (tableNames.contains("HFJ_RES_REINDEX_JOB")) { - logInfo(ourLog, "The table HFJ_RES_REINDEX_JOB already exists. Skipping calculate hashes task."); - return; - } - - initializeExecutor(); + protected boolean shouldSkipTask() { try { - - while (true) { - MyRowCallbackHandler rch = new MyRowCallbackHandler(); - getTxTemplate().execute(t -> { - JdbcTemplate jdbcTemplate = newJdbcTemplate(); - jdbcTemplate.setMaxRows(100000); - String sql = "SELECT * FROM " + getTableName() + " WHERE " + getColumnName() + " IS NULL"; - logInfo(ourLog, "Finding up to {} rows in {} that requires hashes", myBatchSize, getTableName()); - - jdbcTemplate.query(sql, rch); - rch.done(); - - return null; - }); - - rch.submitNext(); - List> futures = rch.getFutures(); - if (futures.isEmpty()) { - break; - } - - logInfo(ourLog, "Waiting for {} tasks to complete", futures.size()); - for (Future next : futures) { - try { - next.get(); - } catch (Exception e) { - throw new SQLException(e); - } - } - + Set tableNames = JdbcUtils.getTableNames(getConnectionProperties()); + boolean shouldSkip = tableNames.contains("HFJ_RES_REINDEX_JOB"); + // This table was added shortly after hash indexes were added, so it is a reasonable indicator for whether this + // migration has already been run + if (shouldSkip) { + logInfo(ourLog, "The table HFJ_RES_REINDEX_JOB already exists. Skipping calculate hashes task."); } - - } finally { - destroyExecutor(); - } - } - - private void destroyExecutor() { - myExecutor.shutdownNow(); - } - - private void initializeExecutor() { - int maximumPoolSize = Runtime.getRuntime().availableProcessors(); - - LinkedBlockingQueue executorQueue = new LinkedBlockingQueue<>(maximumPoolSize); - BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() - .namingPattern("worker-" + "-%d") - .daemon(false) - .priority(Thread.NORM_PRIORITY) - .build(); - RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() { - @Override - public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) { - logInfo(ourLog, "Note: Executor queue is full ({} elements), waiting for a slot to become available!", executorQueue.size()); - StopWatch sw = new StopWatch(); - try { - executorQueue.put(theRunnable); - } catch (InterruptedException theE) { - throw new RejectedExecutionException("Task " + theRunnable.toString() + - " rejected from " + theE.toString()); - } - logInfo(ourLog, "Slot become available after {}ms", sw.getMillis()); - } - }; - myExecutor = new ThreadPoolExecutor( - 1, - maximumPoolSize, - 0L, - TimeUnit.MILLISECONDS, - executorQueue, - threadFactory, - rejectedExecutionHandler); - } - - private Future updateRows(List> theRows) { - Runnable task = () -> { - StopWatch sw = new StopWatch(); - getTxTemplate().execute(t -> { - - // Loop through rows - assert theRows != null; - for (Map nextRow : theRows) { - - Map newValues = new HashMap<>(); - MandatoryKeyMap nextRowMandatoryKeyMap = new MandatoryKeyMap<>(nextRow); - - // Apply calculators - for (Map.Entry, Long>> nextCalculatorEntry : myCalculators.entrySet()) { - String nextColumn = nextCalculatorEntry.getKey(); - Function, Long> nextCalculator = nextCalculatorEntry.getValue(); - Long value = nextCalculator.apply(nextRowMandatoryKeyMap); - newValues.put(nextColumn, value); - } - - // Generate update SQL - StringBuilder sqlBuilder = new StringBuilder(); - List arguments = new ArrayList<>(); - sqlBuilder.append("UPDATE "); - sqlBuilder.append(getTableName()); - sqlBuilder.append(" SET "); - for (Map.Entry nextNewValueEntry : newValues.entrySet()) { - if (arguments.size() > 0) { - sqlBuilder.append(", "); - } - sqlBuilder.append(nextNewValueEntry.getKey()).append(" = ?"); - arguments.add(nextNewValueEntry.getValue()); - } - sqlBuilder.append(" WHERE SP_ID = ?"); - arguments.add((Number) nextRow.get("SP_ID")); - - // Apply update SQL - newJdbcTemplate().update(sqlBuilder.toString(), arguments.toArray()); - - } - - return theRows.size(); - }); - logInfo(ourLog, "Updated {} rows on {} in {}", theRows.size(), getTableName(), sw.toString()); - }; - return myExecutor.submit(task); - } - - public CalculateHashesTask addCalculator(String theColumnName, Function, Long> theConsumer) { - Validate.isTrue(myCalculators.containsKey(theColumnName) == false); - myCalculators.put(theColumnName, theConsumer); - return this; - } - - private class MyRowCallbackHandler implements RowCallbackHandler { - - private List> myRows = new ArrayList<>(); - private List> myFutures = new ArrayList<>(); - - @Override - public void processRow(ResultSet rs) throws SQLException { - Map row = new ColumnMapRowMapper().mapRow(rs, 0); - myRows.add(row); - - if (myRows.size() >= myBatchSize) { - submitNext(); - } - } - - private void submitNext() { - if (myRows.size() > 0) { - myFutures.add(updateRows(myRows)); - myRows = new ArrayList<>(); - } - } - - public List> getFutures() { - return myFutures; - } - - public void done() { - if (myRows.size() > 0) { - submitNext(); - } - } - } - - - public static class MandatoryKeyMap extends ForwardingMap { - - private final Map myWrap; - - public MandatoryKeyMap(Map theWrap) { - myWrap = theWrap; - } - - @Override - public V get(Object theKey) { - if (!containsKey(theKey)) { - throw new IllegalArgumentException("No key: " + theKey); - } - return super.get(theKey); - } - - public String getString(String theKey) { - return (String) get(theKey); - } - - @Override - protected Map delegate() { - return myWrap; - } - - public String getResourceType() { - return getString("RES_TYPE"); - } - - public String getParamName() { - return getString("SP_NAME"); + return shouldSkip; + } catch (SQLException e) { + logInfo(ourLog, "Error retrieving table names, skipping task"); + return true; } } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateOrdinalDatesTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateOrdinalDatesTask.java new file mode 100644 index 00000000000..78cbfda2474 --- /dev/null +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateOrdinalDatesTask.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.jpa.migrate.taskdef; + +/*- + * #%L + * HAPI FHIR JPA Server - Migration + * %% + * 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.util.VersionEnum; + +public class CalculateOrdinalDatesTask extends BaseColumnCalculatorTask { + + public CalculateOrdinalDatesTask(VersionEnum theRelease, String theVersion) { + super(theRelease, theVersion); + setDescription("Calculate SP_LOW_VALUE_DATE_ORDINAL and SP_HIGH_VALUE_DATE_ORDINAL based on existing SP_VALUE_LOW and SP_VALUE_HIGH date values in Date Search Params"); + setWhereClause("(SP_VALUE_LOW_DATE_ORDINAL IS NULL AND SP_VALUE_LOW IS NOT NULL) OR (SP_VALUE_HIGH_DATE_ORDINAL IS NULL AND SP_VALUE_HIGH IS NOT NULL)"); + } + + @Override + protected boolean shouldSkipTask() { + return false; // TODO Is there a case where we should just not do this? + } +} diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTask.java index 3a2f28c6474..3664c4c271d 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTask.java @@ -34,13 +34,14 @@ import java.util.Set; public class InitializeSchemaTask extends BaseTask { private static final Logger ourLog = LoggerFactory.getLogger(InitializeSchemaTask.class); + public static final String DESCRIPTION_PREFIX = "Initialize schema for "; private final ISchemaInitializationProvider mySchemaInitializationProvider; public InitializeSchemaTask(String theProductVersion, String theSchemaVersion, ISchemaInitializationProvider theSchemaInitializationProvider) { super(theProductVersion, theSchemaVersion); mySchemaInitializationProvider = theSchemaInitializationProvider; - setDescription("Initialize schema for " + mySchemaInitializationProvider.getSchemaDescription()); + setDescription(DESCRIPTION_PREFIX + mySchemaInitializationProvider.getSchemaDescription()); } @Override diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index e553b6e05bc..e0840ff572e 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.migrate.taskdef.AddColumnTask; import ca.uhn.fhir.jpa.migrate.taskdef.ArbitrarySqlTask; import ca.uhn.fhir.jpa.migrate.taskdef.BaseTableColumnTypeTask; import ca.uhn.fhir.jpa.migrate.taskdef.CalculateHashesTask; +import ca.uhn.fhir.jpa.migrate.taskdef.CalculateOrdinalDatesTask; import ca.uhn.fhir.jpa.migrate.tasks.api.BaseMigrationTasks; import ca.uhn.fhir.jpa.migrate.tasks.api.Builder; import ca.uhn.fhir.jpa.model.config.PartitionSettings; @@ -62,17 +63,18 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { } protected void init500() { // 20200218 - present - Builder version = forVersion(VersionEnum.V4_3_0); + Builder version = forVersion(VersionEnum.V5_0_0); // Eliminate circular dependency. version.onTable("HFJ_RESOURCE").dropColumn("20200218.1", "FORCED_ID_PID"); version.onTable("HFJ_RES_VER").dropColumn("20200218.2", "FORCED_ID_PID"); version.onTable("HFJ_RES_VER").addForeignKey("20200218.3", "FK_RESOURCE_HISTORY_RESOURCE").toColumn("RES_ID").references("HFJ_RESOURCE", "RES_ID"); version.onTable("HFJ_RES_VER").modifyColumn("20200220.1", "RES_ID").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + // // Drop unused column version.onTable("HFJ_RESOURCE").dropIndex("20200419.1", "IDX_RES_PROFILE"); - version.onTable("HFJ_RESOURCE").dropColumn("20200419.2", "RES_PROFILE"); + version.onTable("HFJ_RESOURCE").dropColumn("20200419.2", "RES_PROFILE").failureAllowed(); // Add Partitioning Builder.BuilderAddTableByColumns partition = version.addTableByColumns("20200420.0", "HFJ_PARTITION", "PART_ID"); @@ -117,14 +119,24 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { version.onTable("HFJ_RES_PARAM_PRESENT").addColumn("20200420.34", "PARTITION_ID").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.INT); version.onTable("HFJ_RES_PARAM_PRESENT").addColumn("20200420.35", "PARTITION_DATE").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.DATE_ONLY); - version.onTable("HFJ_SPIDX_STRING").modifyColumn("20200420.36", "SP_MISSING").nonNullable().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); - version.onTable("HFJ_SPIDX_COORDS").modifyColumn("20200420.37", "SP_MISSING").nonNullable().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); - version.onTable("HFJ_SPIDX_NUMBER").modifyColumn("20200420.38", "SP_MISSING").nonNullable().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); - version.onTable("HFJ_SPIDX_TOKEN").modifyColumn("20200420.39", "SP_MISSING").nonNullable().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); - version.onTable("HFJ_SPIDX_DATE").modifyColumn("20200420.40", "SP_MISSING").nonNullable().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); - version.onTable("HFJ_SPIDX_URI").modifyColumn("20200420.41", "SP_MISSING").nonNullable().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); - version.onTable("HFJ_SPIDX_QUANTITY").modifyColumn("20200420.42", "SP_MISSING").nonNullable().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); + version.onTable("HFJ_SPIDX_STRING").modifyColumn("20200420.36", "SP_MISSING").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); + version.onTable("HFJ_SPIDX_COORDS").modifyColumn("20200420.37", "SP_MISSING").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); + version.onTable("HFJ_SPIDX_NUMBER").modifyColumn("20200420.38", "SP_MISSING").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); + version.onTable("HFJ_SPIDX_TOKEN").modifyColumn("20200420.39", "SP_MISSING").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); + version.onTable("HFJ_SPIDX_DATE").modifyColumn("20200420.40", "SP_MISSING").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); + version.onTable("HFJ_SPIDX_URI").modifyColumn("20200420.41", "SP_MISSING").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); + version.onTable("HFJ_SPIDX_QUANTITY").modifyColumn("20200420.42", "SP_MISSING").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); + // Add support for integer comparisons during day-precision date search. + Builder.BuilderWithTableName spidxDate = version.onTable("HFJ_SPIDX_DATE"); + spidxDate.addColumn("20200501.1", "SP_VALUE_LOW_DATE_ORDINAL").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.INT); + spidxDate.addColumn("20200501.2", "SP_VALUE_HIGH_DATE_ORDINAL").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.INT); + + spidxDate.addTask(new CalculateOrdinalDatesTask(VersionEnum.V5_0_0, "20200501.3") + .addCalculator("SP_VALUE_LOW_DATE_ORDINAL", t -> ResourceIndexedSearchParamDate.calculateOrdinalValue(t.getDate("SP_VALUE_LOW"))) + .addCalculator("SP_VALUE_HIGH_DATE_ORDINAL", t -> ResourceIndexedSearchParamDate.calculateOrdinalValue(t.getDate("SP_VALUE_HIGH"))) + .setColumnName("SP_VALUE_LOW_DATE_ORDINAL") //It doesn't matter which of the two we choose as they will both be null. + ); } @@ -527,8 +539,8 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .withColumns("HASH_IDENTITY", "SP_LATITUDE", "SP_LONGITUDE"); spidxCoords .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.5") - .setColumnName("HASH_IDENTITY") .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"))) + .setColumnName("HASH_IDENTITY") ); } @@ -550,8 +562,8 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .dropIndex("20180903.9", "IDX_SP_DATE"); spidxDate .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.10") - .setColumnName("HASH_IDENTITY") .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"))) + .setColumnName("HASH_IDENTITY") ); } @@ -571,8 +583,8 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .withColumns("HASH_IDENTITY", "SP_VALUE"); spidxNumber .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.14") - .setColumnName("HASH_IDENTITY") .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"))) + .setColumnName("HASH_IDENTITY") ); } @@ -608,10 +620,10 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .withColumns("HASH_IDENTITY_SYS_UNITS", "SP_VALUE"); spidxQuantity .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.22") - .setColumnName("HASH_IDENTITY") .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"))) .addCalculator("HASH_IDENTITY_AND_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashUnits(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_UNITS"))) .addCalculator("HASH_IDENTITY_SYS_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(new PartitionSettings(), null, t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_SYSTEM"), t.getString("SP_UNITS"))) + .setColumnName("HASH_IDENTITY") ); } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/Builder.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/Builder.java index 82cbdf4e6c0..e247953b241 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/Builder.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/Builder.java @@ -208,12 +208,13 @@ public class Builder { return new BuilderWithTableName.BuilderAddColumnWithName(myRelease, theVersion, theColumnName, this); } - public void dropColumn(String theVersion, String theColumnName) { + public BuilderCompleteTask dropColumn(String theVersion, String theColumnName) { Validate.notBlank(theColumnName); DropColumnTask task = new DropColumnTask(myRelease, theVersion); task.setTableName(myTableName); task.setColumnName(theColumnName); addTask(task); + return new BuilderCompleteTask(task); } @Override 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 dae4a888a4b..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,12 +86,8 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { @Transient private transient PartitionSettings myPartitionSettings; - /** - * Subclasses may override - */ - protected void clearHashes() { - // nothing - } + @Transient + private transient ModelConfig myModelConfig; @Override public abstract Long getId(); @@ -100,7 +97,6 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { } public void setParamName(String theName) { - clearHashes(); myParamName = theName; } @@ -109,7 +105,6 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { } public BaseResourceIndexedSearchParam setResource(ResourceTable theResource) { - clearHashes(); myResource = theResource; myResourceType = theResource.getResourceType(); return this; @@ -121,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() { @@ -154,7 +152,12 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { public abstract IQueryParameterType toQueryParameterType(); - public boolean matches(IQueryParameterType theParam) { + @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); } @@ -167,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/ModelConfig.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java index 4b571af3fef..3f4c5c9fdc1 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java @@ -61,6 +61,11 @@ public class ModelConfig { private Set mySupportedSubscriptionTypes = new HashSet<>(); private String myEmailFromAddress = "noreply@unknown.com"; private String myWebsocketContextPath = DEFAULT_WEBSOCKET_CONTEXT_PATH; + /** + * Update setter javadoc if default changes. + */ + private boolean myUseOrdinalDatesForDayPrecisionSearches = true; + private boolean mySuppressStringIndexingInTokens = false; /** * Constructor @@ -258,7 +263,6 @@ public class ModelConfig { myTreatReferencesAsLogical = new HashSet<>(); } myTreatReferencesAsLogical.add(theTreatReferencesAsLogical); - } /** @@ -315,7 +319,6 @@ public class ModelConfig { /** * This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted * to the server matching these types will be activated. - * */ public ModelConfig addSupportedSubscriptionType(Subscription.SubscriptionChannelType theSubscriptionChannelType) { mySupportedSubscriptionTypes.add(theSubscriptionChannelType); @@ -325,7 +328,6 @@ public class ModelConfig { /** * This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted * to the server matching these types will be activated. - * */ public Set getSupportedSubscriptionTypes() { return Collections.unmodifiableSet(mySupportedSubscriptionTypes); @@ -368,6 +370,74 @@ public class ModelConfig { myWebsocketContextPath = theWebsocketContextPath; } + /** + *

    + * Should searches use the integer field {@code SP_VALUE_LOW_DATE_ORDINAL} and {@code SP_VALUE_HIGH_DATE_ORDINAL} in + * {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate} when resolving searches where all predicates are using + * precision of {@link ca.uhn.fhir.model.api.TemporalPrecisionEnum#DAY}. + *

    + * For example, if enabled, the search of {@code Observation?date=2020-02-25} will cause the date to be collapsed down to an + * integer representing the ordinal date {@code 20200225}. It would then be compared against {@link ResourceIndexedSearchParamDate#getValueLowDateOrdinal()} + * and {@link ResourceIndexedSearchParamDate#getValueHighDateOrdinal()} + *

    + * Default is {@literal true} beginning in HAPI FHIR 5.0.0 + *

    + * + * @since 5.0.0 + */ + public boolean getUseOrdinalDatesForDayPrecisionSearches() { + return myUseOrdinalDatesForDayPrecisionSearches; + } + + /** + *

    + * Should searches use the integer field {@code SP_VALUE_LOW_DATE_ORDINAL} and {@code SP_VALUE_HIGH_DATE_ORDINAL} in + * {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate} when resolving searches where all predicates are using + * precision of {@link ca.uhn.fhir.model.api.TemporalPrecisionEnum#DAY}. + *

    + * For example, if enabled, the search of {@code Observation?date=2020-02-25} will cause the date to be collapsed down to an + * ordinal {@code 20200225}. It would then be compared against {@link ResourceIndexedSearchParamDate#getValueLowDateOrdinal()} + * and {@link ResourceIndexedSearchParamDate#getValueHighDateOrdinal()} + *

    + * Default is {@literal true} beginning in HAPI FHIR 5.0.0 + *

    + * + * @since 5.0.0 + */ + public void setUseOrdinalDatesForDayPrecisionSearches(boolean theUseOrdinalDates) { + myUseOrdinalDatesForDayPrecisionSearches = theUseOrdinalDates; + } + + /** + * If set to true (default is false), when indexing SearchParameter values for token SearchParameter, + * the string component to support the :text modifier will be disabled. This means that the following fields + * will not be indexed for tokens: + *
      + *
    • CodeableConcept.text
    • + *
    • Coding.display
    • + *
    • Identifier.use.text
    • + *
    + * @since 5.0.0 + */ + public boolean isSuppressStringIndexingInTokens() { + return mySuppressStringIndexingInTokens; + } + + /** + * If set to true (default is false), when indexing SearchParameter values for token SearchParameter, + * the string component to support the :text modifier will be disabled. This means that the following fields + * will not be indexed for tokens: + *
      + *
    • CodeableConcept.text
    • + *
    • Coding.display
    • + *
    • Identifier.use.text
    • + *
    + * @since 5.0.0 + */ + public void setSuppressStringIndexingInTokens(boolean theSuppressStringIndexingInTokens) { + mySuppressStringIndexingInTokens = theSuppressStringIndexingInTokens; + } + private static void validateTreatBaseUrlsAsLocal(String theUrl) { Validate.notBlank(theUrl, "Base URL must not be null or empty"); 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/ResourceIndexedSearchParamCoords.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamCoords.java index b91a7145bc0..aec6f32e7b8 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamCoords.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamCoords.java @@ -28,7 +28,15 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.search.annotations.Field; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; @Embeddable @Entity @@ -68,21 +76,14 @@ public class ResourceIndexedSearchParamCoords extends BaseResourceIndexedSearchP setParamName(theParamName); setLatitude(theLatitude); setLongitude(theLongitude); + calculateHashes(); } @Override - @PrePersist public void calculateHashes() { - if (myHashIdentity == null && getParamName() != null) { - String resourceType = getResourceType(); - String paramName = getParamName(); - setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); - } - } - - @Override - protected void clearHashes() { - myHashIdentity = null; + String resourceType = getResourceType(); + String paramName = getParamName(); + setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); } @Override @@ -126,7 +127,7 @@ public class ResourceIndexedSearchParamCoords extends BaseResourceIndexedSearchP @Override public void setId(Long theId) { - myId =theId; + myId = theId; } 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 4a033821379..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 @@ -26,6 +26,8 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.util.DateUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -33,7 +35,18 @@ import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.search.annotations.Field; import org.hl7.fhir.r4.model.DateTimeType; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.Transient; import java.util.Date; @Embeddable @@ -55,6 +68,16 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar @Temporal(TemporalType.TIMESTAMP) @Field public Date myValueLow; + + /** + * Field which stores an integer representation of YYYYMDD as calculated by Calendar + * e.g. 2019-01-20 -> 20190120 + */ + @Column(name = "SP_VALUE_LOW_DATE_ORDINAL") + public Integer myValueLowDateOrdinal; + @Column(name = "SP_VALUE_HIGH_DATE_ORDINAL") + public Integer myValueHighDateOrdinal; + @Transient private transient String myOriginalValue; @Id @@ -78,13 +101,50 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar /** * Constructor */ - public ResourceIndexedSearchParamDate(PartitionSettings thePartitionSettings, String theResourceType, String theParamName, Date theLow, Date theHigh, String theOriginalValue) { + public ResourceIndexedSearchParamDate(PartitionSettings thePartitionSettings, String theResourceType, String theParamName, Date theLow, String theLowString, Date theHigh, String theHighString, String theOriginalValue) { setPartitionSettings(thePartitionSettings); setResourceType(theResourceType); setParamName(theParamName); setValueLow(theLow); setValueHigh(theHigh); + if (theHigh != null && theHighString == null) { + theHighString = DateUtils.convertDateToIso8601String(theHigh); + } + if (theLow != null && theLowString == null) { + theLowString = DateUtils.convertDateToIso8601String(theLow); + } + computeValueHighDateOrdinal(theHighString); + computeValueLowDateOrdinal(theLowString); myOriginalValue = theOriginalValue; + calculateHashes(); + } + + private void computeValueHighDateOrdinal(String theHigh) { + if (!StringUtils.isBlank(theHigh)) { + this.myValueHighDateOrdinal = generateOrdinalDateInteger(theHigh); + } + } + + private int generateOrdinalDateInteger(String theDateString) { + if (theDateString.contains("T")) { + theDateString = theDateString.substring(0, theDateString.indexOf("T")); + } + theDateString = theDateString.replace("-", ""); + return Integer.valueOf(theDateString); + } + + private void computeValueLowDateOrdinal(String theLow) { + if (StringUtils.isNotBlank(theLow)) { + this.myValueLowDateOrdinal = generateOrdinalDateInteger(theLow); + } + } + + public Integer getValueLowDateOrdinal() { + return myValueLowDateOrdinal; + } + + public Integer getValueHighDateOrdinal() { + return myValueHighDateOrdinal; } @Override @@ -93,22 +153,16 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar ResourceIndexedSearchParamDate source = (ResourceIndexedSearchParamDate) theSource; myValueHigh = source.myValueHigh; myValueLow = source.myValueLow; + myValueHighDateOrdinal = source.myValueHighDateOrdinal; + myValueLowDateOrdinal = source.myValueLowDateOrdinal; myHashIdentity = source.myHashIdentity; } @Override - @PrePersist public void calculateHashes() { - if (myHashIdentity == null && getParamName() != null) { - String resourceType = getResourceType(); - String paramName = getParamName(); - setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); - } - } - - @Override - protected void clearHashes() { - myHashIdentity = null; + String resourceType = getResourceType(); + String paramName = getParamName(); + setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); } @Override @@ -143,7 +197,7 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar @Override public void setId(Long theId) { - myId =theId; + myId = theId; } protected Long getTimeFromDate(Date date) { @@ -193,30 +247,44 @@ 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(); } @SuppressWarnings("ConstantConditions") @Override - public boolean matches(IQueryParameterType theParam) { + public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) { if (!(theParam instanceof DateParam)) { return false; } - DateParam date = (DateParam) theParam; - DateRangeParam range = new DateRangeParam(date); + DateParam dateParam = (DateParam) theParam; + DateRangeParam range = new DateRangeParam(dateParam); + + + boolean result; + if (theUseOrdinalDatesForDayComparison) { + result = matchesOrdinalDateBounds(range); + result = matchesDateBounds(range); + } else { + result = matchesDateBounds(range); + } + + return result; + } + + private boolean matchesDateBounds(DateRangeParam range) { Date lowerBound = range.getLowerBoundAsInstant(); Date upperBound = range.getUpperBoundAsInstant(); - if (lowerBound == null && upperBound == null) { // should never happen return false; } - boolean result = true; if (lowerBound != null) { result &= (myValueLow.after(lowerBound) || myValueLow.equals(lowerBound)); @@ -229,4 +297,31 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar return result; } + private boolean matchesOrdinalDateBounds(DateRangeParam range) { + boolean result = true; + Integer lowerBoundAsDateInteger = range.getLowerBoundAsDateInteger(); + Integer upperBoundAsDateInteger = range.getUpperBoundAsDateInteger(); + if (upperBoundAsDateInteger == null && lowerBoundAsDateInteger == null) { + return false; + } + if (lowerBoundAsDateInteger != null) { + //TODO as we run into equality issues + result &= (myValueLowDateOrdinal.equals(lowerBoundAsDateInteger) || myValueLowDateOrdinal > lowerBoundAsDateInteger); + result &= (myValueHighDateOrdinal.equals(lowerBoundAsDateInteger) || myValueHighDateOrdinal > lowerBoundAsDateInteger); + } + if (upperBoundAsDateInteger != null) { + result &= (myValueHighDateOrdinal.equals(upperBoundAsDateInteger) || myValueHighDateOrdinal < upperBoundAsDateInteger); + result &= (myValueLowDateOrdinal.equals(upperBoundAsDateInteger) || myValueLowDateOrdinal < upperBoundAsDateInteger); + } + return result; + } + + + public static Long calculateOrdinalValue(Date theDate) { + if (theDate == null) { + return null; + } + return (long) DateUtils.convertDatetoDayInteger(theDate); + } + } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamNumber.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamNumber.java index 13e49ce9bee..c58817d2973 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamNumber.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamNumber.java @@ -32,7 +32,15 @@ import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.FieldBridge; import org.hibernate.search.annotations.NumericField; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; import java.math.BigDecimal; import java.util.Objects; @@ -71,6 +79,7 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP setResourceType(theResourceType); setParamName(theParamName); setValue(theValue); + calculateHashes(); } @Override @@ -83,18 +92,14 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP @Override - @PrePersist public void calculateHashes() { - if (myHashIdentity == null && getParamName() != null) { - String resourceType = getResourceType(); - String paramName = getParamName(); - setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); - } + String resourceType = getResourceType(); + String paramName = getParamName(); + setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); } - @Override - protected void clearHashes() { - myHashIdentity = null; + public Long getHashIdentity() { + return myHashIdentity; } @Override @@ -112,7 +117,7 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP EqualsBuilder b = new EqualsBuilder(); b.append(getResourceType(), obj.getResourceType()); b.append(getParamName(), obj.getParamName()); - b.append(getValue(), obj.getValue()); + b.append(getHashIdentity(), obj.getHashIdentity()); b.append(isMissing(), obj.isMissing()); return b.isEquals(); } @@ -128,7 +133,7 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP @Override public void setId(Long theId) { - myId =theId; + myId = theId; } public BigDecimal getValue() { @@ -164,7 +169,7 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP } @Override - public boolean matches(IQueryParameterType theParam) { + public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) { if (!(theParam instanceof NumberParam)) { return false; } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java index 8ef9cfbc41d..c487641d1ad 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java @@ -33,7 +33,15 @@ import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.FieldBridge; import org.hibernate.search.annotations.NumericField; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; import java.math.BigDecimal; import java.util.Objects; @@ -101,6 +109,7 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc setSystem(theSystem); setValue(theValue); setUnits(theUnits); + calculateHashes(); } @Override @@ -117,23 +126,14 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc @Override - @PrePersist public void calculateHashes() { - if (myHashIdentity == null && getParamName() != null) { - String resourceType = getResourceType(); - String paramName = getParamName(); - String units = getUnits(); - String system = getSystem(); - setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); - setHashIdentityAndUnits(calculateHashUnits(getPartitionSettings(), getPartitionId(), resourceType, paramName, units)); - setHashIdentitySystemAndUnits(calculateHashSystemAndUnits(getPartitionSettings(), getPartitionId(), resourceType, paramName, system, units)); - } - } - - @Override - protected void clearHashes() { - myHashIdentity = null; - myHashIdentityAndUnits = null; + String resourceType = getResourceType(); + String paramName = getParamName(); + String units = getUnits(); + String system = getSystem(); + setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); + setHashIdentityAndUnits(calculateHashUnits(getPartitionSettings(), getPartitionId(), resourceType, paramName, units)); + setHashIdentitySystemAndUnits(calculateHashSystemAndUnits(getPartitionSettings(), getPartitionId(), resourceType, paramName, system, units)); } @Override @@ -151,15 +151,14 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc EqualsBuilder b = new EqualsBuilder(); b.append(getResourceType(), obj.getResourceType()); b.append(getParamName(), obj.getParamName()); - b.append(getSystem(), obj.getSystem()); - b.append(getUnits(), obj.getUnits()); - b.append(getValue(), obj.getValue()); + b.append(getHashIdentity(), obj.getHashIdentity()); + b.append(getHashIdentityAndUnits(), obj.getHashIdentityAndUnits()); + b.append(getHashIdentitySystemAndUnits(), obj.getHashIdentitySystemAndUnits()); b.append(isMissing(), obj.isMissing()); return b.isEquals(); } public Long getHashIdentity() { - calculateHashes(); return myHashIdentity; } @@ -168,7 +167,6 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc } public Long getHashIdentityAndUnits() { - calculateHashes(); return myHashIdentityAndUnits; } @@ -177,7 +175,6 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc } private Long getHashIdentitySystemAndUnits() { - calculateHashes(); return myHashIdentitySystemAndUnits; } @@ -200,7 +197,6 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc } public void setSystem(String theSystem) { - clearHashes(); mySystem = theSystem; } @@ -209,7 +205,6 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc } public void setUnits(String theUnits) { - clearHashes(); myUnits = theUnits; } @@ -218,7 +213,6 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc } public ResourceIndexedSearchParamQuantity setValue(BigDecimal theValue) { - clearHashes(); myValue = theValue; return this; } @@ -228,9 +222,9 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc HashCodeBuilder b = new HashCodeBuilder(); b.append(getResourceType()); b.append(getParamName()); - b.append(getSystem()); - b.append(getUnits()); - b.append(getValue()); + b.append(getHashIdentity()); + b.append(getHashIdentityAndUnits()); + b.append(getHashIdentitySystemAndUnits()); return b.toHashCode(); } @@ -253,7 +247,7 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc } @Override - public boolean matches(IQueryParameterType theParam) { + public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) { if (!(theParam instanceof QuantityParam)) { return false; } 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 851965c43f0..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 @@ -37,7 +37,18 @@ import org.hibernate.search.annotations.Fields; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.annotations.Store; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +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.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.left; @@ -107,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(); @@ -121,6 +130,7 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP setParamName(theParamName); setValueNormalized(theValueNormalized); setValueExact(theValueExact); + calculateHashes(); } @Override @@ -135,25 +145,14 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP } @Override - @PrePersist - @PreUpdate public void calculateHashes() { - if ((myHashIdentity == null || myHashNormalizedPrefix == null || myHashExact == null) && getParamName() != null) { - String resourceType = getResourceType(); - String paramName = getParamName(); - String valueNormalized = getValueNormalized(); - String valueExact = getValueExact(); - setHashNormalizedPrefix(calculateHashNormalized(getPartitionSettings(), getPartitionId(), myModelConfig, resourceType, paramName, valueNormalized)); - setHashExact(calculateHashExact(getPartitionSettings(), getPartitionId(), resourceType, paramName, valueExact)); - setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); - } - } - - @Override - protected void clearHashes() { - myHashNormalizedPrefix = null; - myHashExact = null; - myHashIdentity = null; + String resourceType = getResourceType(); + String paramName = getParamName(); + String valueNormalized = getValueNormalized(); + String valueExact = getValueExact(); + setHashNormalizedPrefix(calculateHashNormalized(getPartitionSettings(), getPartitionId(), getModelConfig(), resourceType, paramName, valueNormalized)); + setHashExact(calculateHashExact(getPartitionSettings(), getPartitionId(), resourceType, paramName, valueExact)); + setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); } @Override @@ -179,7 +178,6 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP } private Long getHashIdentity() { - calculateHashes(); return myHashIdentity; } @@ -188,7 +186,6 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP } public Long getHashExact() { - calculateHashes(); return myHashExact; } @@ -197,7 +194,6 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP } public Long getHashNormalizedPrefix() { - calculateHashes(); return myHashNormalizedPrefix; } @@ -249,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()); @@ -270,7 +261,7 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP } @Override - public boolean matches(IQueryParameterType theParam) { + public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) { if (!(theParam instanceof StringParam)) { return false; } 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 522006c16a7..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 @@ -31,7 +31,16 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.search.annotations.Field; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +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; @@ -110,6 +119,7 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa setParamName(theParamName); setSystem(theSystem); setValue(theValue); + calculateHashes(); } @Override @@ -127,25 +137,15 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa @Override - @PrePersist public void calculateHashes() { - if (myHashSystem == null && getParamName() != null) { - String resourceType = getResourceType(); - String paramName = getParamName(); - String system = getSystem(); - String value = getValue(); - setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); - setHashSystem(calculateHashSystem(getPartitionSettings(), getPartitionId(), resourceType, paramName, system)); - setHashSystemAndValue(calculateHashSystemAndValue(getPartitionSettings(), getPartitionId(), resourceType, paramName, system, value)); - setHashValue(calculateHashValue(getPartitionSettings(), getPartitionId(), resourceType, paramName, value)); - } - } - - @Override - protected void clearHashes() { - myHashSystem = null; - myHashSystemAndValue = null; - myHashValue = null; + String resourceType = getResourceType(); + String paramName = getParamName(); + String system = getSystem(); + String value = getValue(); + setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); + setHashSystem(calculateHashSystem(getPartitionSettings(), getPartitionId(), resourceType, paramName, system)); + setHashSystemAndValue(calculateHashSystemAndValue(getPartitionSettings(), getPartitionId(), resourceType, paramName, system, value)); + setHashValue(calculateHashValue(getPartitionSettings(), getPartitionId(), resourceType, paramName, value)); } @Override @@ -161,15 +161,13 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa } ResourceIndexedSearchParamToken obj = (ResourceIndexedSearchParamToken) theObj; EqualsBuilder b = new EqualsBuilder(); - b.append(getResourceType(), obj.getResourceType()); - b.append(getParamName(), obj.getParamName()); - b.append(getSystem(), obj.getSystem()); - b.append(getValue(), obj.getValue()); + b.append(getHashSystem(), obj.getHashSystem()); + b.append(getHashValue(), obj.getHashValue()); + b.append(getHashSystemAndValue(), obj.getHashSystemAndValue()); return b.isEquals(); } Long getHashSystem() { - calculateHashes(); return myHashSystem; } @@ -182,17 +180,14 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa } Long getHashSystemAndValue() { - calculateHashes(); return myHashSystemAndValue; } private void setHashSystemAndValue(Long theHashSystemAndValue) { - calculateHashes(); myHashSystemAndValue = theHashSystemAndValue; } Long getHashValue() { - calculateHashes(); return myHashValue; } @@ -215,7 +210,6 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa } public void setSystem(String theSystem) { - clearHashes(); mySystem = StringUtils.defaultIfBlank(theSystem, null); } @@ -224,19 +218,17 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa } public ResourceIndexedSearchParamToken setValue(String theValue) { - clearHashes(); myValue = StringUtils.defaultIfBlank(theValue, null); return this; } @Override public int hashCode() { - calculateHashes(); HashCodeBuilder b = new HashCodeBuilder(); b.append(getResourceType()); - b.append(getParamName()); - b.append(getSystem()); - b.append(getValue()); + b.append(getHashValue()); + b.append(getHashSystem()); + b.append(getHashSystemAndValue()); return b.toHashCode(); } @@ -252,11 +244,12 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa b.append("paramName", getParamName()); b.append("system", getSystem()); b.append("value", getValue()); + b.append("hashIdentity", myHashIdentity); return b.build(); } @Override - public boolean matches(IQueryParameterType theParam) { + public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) { if (!(theParam instanceof TokenParam)) { return false; } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java index a3393656442..745d0ede7cb 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java @@ -30,7 +30,15 @@ import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.hibernate.search.annotations.Field; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; import static org.apache.commons.lang3.StringUtils.defaultString; @@ -87,6 +95,7 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara setResourceType(theResourceType); setParamName(theParamName); setUri(theUri); + calculateHashes(); } @Override @@ -100,20 +109,12 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara @Override - @PrePersist public void calculateHashes() { - if (myHashUri == null && getParamName() != null) { - String resourceType = getResourceType(); - String paramName = getParamName(); - String uri = getUri(); - setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); - setHashUri(calculateHashUri(getPartitionSettings(), getPartitionId(), resourceType, paramName, uri)); - } - } - - @Override - protected void clearHashes() { - myHashUri = null; + String resourceType = getResourceType(); + String paramName = getParamName(); + String uri = getUri(); + setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); + setHashUri(calculateHashUri(getPartitionSettings(), getPartitionId(), resourceType, paramName, uri)); } @Override @@ -138,7 +139,6 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara } private Long getHashIdentity() { - calculateHashes(); return myHashIdentity; } @@ -147,7 +147,6 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara } public Long getHashUri() { - calculateHashes(); return myHashUri; } @@ -182,6 +181,7 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara b.append(getParamName()); b.append(getUri()); b.append(getHashUri()); + b.append(getHashIdentity()); return b.toHashCode(); } @@ -201,7 +201,7 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara } @Override - public boolean matches(IQueryParameterType theParam) { + public boolean matches(IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) { if (!(theParam instanceof UriParam)) { return false; } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java index f1998013f99..671e0fbfc1b 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java @@ -243,6 +243,11 @@ public class JpaConstants { */ public static final String PARAM_EXPORT_TYPE_FILTER = "_typeFilter"; + /** + * Extension URL for extension on a SearchParameter indicating that text values should not be indexed + */ + public static final String EXT_SEARCHPARAM_TOKEN_SUPPRESS_TEXT_INDEXING = "http://hapifhir.io/fhir/StructureDefinition/searchparameter-token-suppress-text-index"; + /** * Non-instantiable */ 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 650744c8bb3..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 @@ -5,10 +5,14 @@ import org.junit.Before; import org.junit.Test; import java.sql.Timestamp; +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 { @@ -36,8 +40,8 @@ public class ResourceIndexedSearchParamDateTest { @Test public void equalsIsTrueForMatchingNullDates() { - ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", null, null, "SomeValue"); - ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", null, null, "SomeValue"); + ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", null, null, null, null, "SomeValue"); + ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", null, null, null, null, "SomeValue"); assertTrue(param.equals(param2)); assertTrue(param2.equals(param)); @@ -46,8 +50,8 @@ public class ResourceIndexedSearchParamDateTest { @Test public void equalsIsTrueForMatchingDates() { - ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue"); - ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1B, date2B, "SomeValue"); + ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue"); + ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1B, null, date2B, null, "SomeValue"); assertTrue(param.equals(param2)); assertTrue(param2.equals(param)); @@ -56,8 +60,8 @@ public class ResourceIndexedSearchParamDateTest { @Test public void equalsIsTrueForMatchingTimeStampsThatMatch() { - ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp1A, timestamp2A, "SomeValue"); - ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp1B, timestamp2B, "SomeValue"); + ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp1A, null, timestamp2A, null, "SomeValue"); + ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp1B, null, timestamp2B, null, "SomeValue"); assertTrue(param.equals(param2)); assertTrue(param2.equals(param)); @@ -68,8 +72,8 @@ public class ResourceIndexedSearchParamDateTest { // other will be equivalent but will be a java.sql.Timestamp. Equals should work in both directions. @Test public void equalsIsTrueForMixedTimestampsAndDates() { - ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue"); - ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp1A, timestamp2A, "SomeValue"); + ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue"); + ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp1A, null, timestamp2A, null, "SomeValue"); assertTrue(param.equals(param2)); assertTrue(param2.equals(param)); @@ -78,8 +82,8 @@ public class ResourceIndexedSearchParamDateTest { @Test public void equalsIsFalseForNonMatchingDates() { - ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue"); - ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date2A, date1A, "SomeValue"); + ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue"); + ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date2A, null, date1A, null, "SomeValue"); assertFalse(param.equals(param2)); assertFalse(param2.equals(param)); @@ -88,8 +92,8 @@ public class ResourceIndexedSearchParamDateTest { @Test public void equalsIsFalseForNonMatchingDatesNullCase() { - ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue"); - ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", null, null, "SomeValue"); + ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue"); + ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", null, null, null, null, "SomeValue"); assertFalse(param.equals(param2)); assertFalse(param2.equals(param)); @@ -98,8 +102,8 @@ public class ResourceIndexedSearchParamDateTest { @Test public void equalsIsFalseForNonMatchingTimeStamps() { - ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp1A, timestamp2A, "SomeValue"); - ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp2A, timestamp1A, "SomeValue"); + ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp1A, null, timestamp2A, null, "SomeValue"); + ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp2A, null, timestamp1A, null, "SomeValue"); assertFalse(param.equals(param2)); assertFalse(param2.equals(param)); @@ -108,8 +112,8 @@ public class ResourceIndexedSearchParamDateTest { @Test public void equalsIsFalseForMixedTimestampsAndDatesThatDoNotMatch() { - ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", date1A, date2A, "SomeValue"); - ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "SomeResource", timestamp2A, timestamp1A, "SomeValue"); + ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", date1A, null, date2A, null, "SomeValue"); + ResourceIndexedSearchParamDate param2 = new ResourceIndexedSearchParamDate(new PartitionSettings(),"Patient", "SomeResource", timestamp2A, null, timestamp1A, null, "SomeValue"); assertFalse(param.equals(param2)); assertFalse(param2.equals(param)); diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityTest.java index 8c20f544833..1e1fda3fd2c 100644 --- a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityTest.java +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityTest.java @@ -19,6 +19,7 @@ public class ResourceIndexedSearchParamQuantityTest { @Test public void testHashFunctions() { ResourceIndexedSearchParamQuantity token = createParam("NAME", "123.001", "value", "VALUE"); + token.calculateHashes(); // Make sure our hashing function gives consistent results assertEquals(834432764963581074L, token.getHashIdentity().longValue()); diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamStringTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamStringTest.java index ff07d7c6999..0db843825fb 100644 --- a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamStringTest.java +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamStringTest.java @@ -13,6 +13,7 @@ public class ResourceIndexedSearchParamStringTest { public void testHashFunctions() { ResourceIndexedSearchParamString token = new ResourceIndexedSearchParamString(new PartitionSettings(), new ModelConfig(), "Patient", "NAME", "value", "VALUE"); token.setResource(new ResourceTable().setResourceType("Patient")); + token.calculateHashes(); // Make sure our hashing function gives consistent results assertEquals(6598082761639188617L, token.getHashNormalizedPrefix().longValue()); @@ -23,6 +24,7 @@ public class ResourceIndexedSearchParamStringTest { public void testHashFunctionsPrefixOnly() { ResourceIndexedSearchParamString token = new ResourceIndexedSearchParamString(new PartitionSettings(), new ModelConfig(), "Patient", "NAME", "vZZZZZZZZZZZZZZZZ", "VZZZZZZzzzZzzzZ"); token.setResource(new ResourceTable().setResourceType("Patient")); + token.calculateHashes(); // Should be the same as in testHashFunctions() assertEquals(6598082761639188617L, token.getHashNormalizedPrefix().longValue()); @@ -38,11 +40,13 @@ public class ResourceIndexedSearchParamStringTest { .setValueExact("aaa") .setValueNormalized("AAA"); val1.setPartitionSettings(new PartitionSettings()); + val1.setModelConfig(new ModelConfig()); val1.calculateHashes(); ResourceIndexedSearchParamString val2 = new ResourceIndexedSearchParamString() .setValueExact("aaa") .setValueNormalized("AAA"); val2.setPartitionSettings(new PartitionSettings()); + val2.setModelConfig(new ModelConfig()); val2.calculateHashes(); assertEquals(val1, val1); assertEquals(val1, val2); @@ -56,11 +60,13 @@ public class ResourceIndexedSearchParamStringTest { .setValueExact("aaa") .setValueNormalized("AAA"); val1.setPartitionSettings(new PartitionSettings().setIncludePartitionInSearchHashes(true)); + val1.setModelConfig(new ModelConfig()); val1.calculateHashes(); ResourceIndexedSearchParamString val2 = new ResourceIndexedSearchParamString() .setValueExact("aaa") .setValueNormalized("AAA"); val2.setPartitionSettings(new PartitionSettings().setIncludePartitionInSearchHashes(true)); + val2.setModelConfig(new ModelConfig()); val2.calculateHashes(); assertEquals(val1, val1); assertEquals(val1, val2); diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamTokenTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamTokenTest.java index 7bd2113deae..ae5e6151a1d 100644 --- a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamTokenTest.java +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamTokenTest.java @@ -12,6 +12,7 @@ public class ResourceIndexedSearchParamTokenTest { public void testHashFunctions() { ResourceIndexedSearchParamToken token = new ResourceIndexedSearchParamToken(new PartitionSettings(), "Patient", "NAME", "SYSTEM", "VALUE"); token.setResource(new ResourceTable().setResourceType("Patient")); + token.calculateHashes(); // Make sure our hashing function gives consistent results assertEquals(-8558989679010582575L, token.getHashSystem().longValue()); @@ -23,6 +24,7 @@ public class ResourceIndexedSearchParamTokenTest { public void testHashFunctionsWithOverlapNames() { ResourceIndexedSearchParamToken token = new ResourceIndexedSearchParamToken(new PartitionSettings(), "Patient", "NAME", "SYSTEM", "VALUE"); token.setResource(new ResourceTable().setResourceType("Patient")); + token.calculateHashes(); // Make sure our hashing function gives consistent results assertEquals(-8558989679010582575L, token.getHashSystem().longValue()); diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUriTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUriTest.java index 1aadd00dfe4..8147135b367 100644 --- a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUriTest.java +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUriTest.java @@ -12,6 +12,7 @@ public class ResourceIndexedSearchParamUriTest { public void testHashFunctions() { ResourceIndexedSearchParamUri token = new ResourceIndexedSearchParamUri(new PartitionSettings(), "Patient", "NAME", "http://example.com"); token.setResource(new ResourceTable().setResourceType("Patient")); + token.calculateHashes(); // Make sure our hashing function gives consistent results assertEquals(-6132951326739875838L, token.getHashUri().longValue()); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java index a35195e6c94..71140dcdda6 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java @@ -37,6 +37,7 @@ 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.util.JpaConstants; import ca.uhn.fhir.jpa.model.util.StringNormalizer; import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; @@ -45,6 +46,7 @@ import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.Validate; import org.hibernate.search.spatial.impl.Point; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBase; @@ -131,6 +133,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor private BaseRuntimeChildDefinition myCodingDisplayValueChild; private BaseRuntimeChildDefinition myContactPointSystemValueChild; private BaseRuntimeChildDefinition myPatientCommunicationLanguageValueChild; + /** * Constructor */ @@ -141,15 +144,15 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor /** * UNIT TEST constructor */ - BaseSearchParamExtractor(FhirContext theCtx, ISearchParamRegistry theSearchParamRegistry) { + BaseSearchParamExtractor(ModelConfig theModelConfig, PartitionSettings thePartitionSettings, FhirContext theCtx, ISearchParamRegistry theSearchParamRegistry) { + Validate.notNull(theModelConfig); + Validate.notNull(theCtx); + Validate.notNull(theSearchParamRegistry); + + myModelConfig = theModelConfig; myContext = theCtx; mySearchParamRegistry = theSearchParamRegistry; - } - - @VisibleForTesting - public BaseSearchParamExtractor setPartitionConfigForUnitTest(PartitionSettings thePartitionSettings) { myPartitionSettings = thePartitionSettings; - return this; } @Override @@ -634,14 +637,20 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor createTokenIndexIfNotBlank(theResourceType, theParams, theSearchParam, system, value); } - Optional type = myIdentifierTypeValueChild.getAccessor().getFirstValueOrNull(theValue); - if (type.isPresent()) { - String text = extractValueAsString(myIdentifierTypeTextValueChild, type.get()); - createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, text); + if (shouldIndexTextComponentOfToken(theSearchParam)) { + Optional type = myIdentifierTypeValueChild.getAccessor().getFirstValueOrNull(theValue); + if (type.isPresent()) { + String text = extractValueAsString(myIdentifierTypeTextValueChild, type.get()); + createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, text); + } } } + protected boolean shouldIndexTextComponentOfToken(RuntimeSearchParam theSearchParam) { + return tokenTextIndexingEnabledForSearchParam(myModelConfig, theSearchParam); + } + @Override public List getCodingsFromCodeableConcept(IBase theValue) { String nextType = BaseSearchParamExtractor.this.toRootTypeName(theValue); @@ -668,9 +677,11 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor addToken_Coding(theResourceType, theParams, theSearchParam, nextCoding); } - String text = getDisplayTextFromCodeableConcept(theValue); - if (isNotBlank(text)) { - createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, text); + if (shouldIndexTextComponentOfToken(theSearchParam)) { + String text = getDisplayTextFromCodeableConcept(theValue); + if (isNotBlank(text)) { + createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, text); + } } } @@ -680,8 +691,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor theParams.add(resourceIndexedSearchParamToken); } - String text = getDisplayTextForCoding(theValue); - createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, text); + if (shouldIndexTextComponentOfToken(theSearchParam)) { + String text = getDisplayTextForCoding(theValue); + createStringIndexIfNotBlank(theResourceType, theParams, theSearchParam, text); + } } @Override @@ -730,9 +743,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor Date start = extractValueAsDate(myPeriodStartValueChild, theValue); String startAsString = extractValueAsString(myPeriodStartValueChild, theValue); Date end = extractValueAsDate(myPeriodEndValueChild, theValue); + String endAsString = extractValueAsString(myPeriodEndValueChild, theValue); if (start != null || end != null) { - ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), start, end, startAsString); + ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), start, startAsString, end, endAsString, startAsString); theParams.add(nextEntity); } } @@ -741,13 +755,16 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor List> values = extractValuesAsFhirDates(myTimingEventValueChild, theValue); TreeSet dates = new TreeSet<>(); + TreeSet dateStrings = new TreeSet<>(); String firstValue = null; + String finalValue = null; for (IPrimitiveType nextEvent : values) { if (nextEvent.getValue() != null) { dates.add(nextEvent.getValue()); if (firstValue == null) { firstValue = nextEvent.getValueAsString(); } + finalValue = nextEvent.getValueAsString(); } } @@ -759,14 +776,20 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor if ("Period".equals(boundsType)) { Date start = extractValueAsDate(myPeriodStartValueChild, bounds.get()); Date end = extractValueAsDate(myPeriodEndValueChild, bounds.get()); + String endString = extractValueAsString(myPeriodEndValueChild, bounds.get()); dates.add(start); dates.add(end); + //TODO Check if this logic is valid. Does the start of the first period indicate a lower bound?? + if (firstValue == null) { + firstValue = extractValueAsString(myPeriodStartValueChild, bounds.get()); + } + finalValue = endString; } } } if (!dates.isEmpty()) { - ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), dates.first(), dates.last(), firstValue); + ResourceIndexedSearchParamDate nextEntity = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), dates.first(), firstValue, dates.last(), finalValue, firstValue); theParams.add(nextEntity); } } @@ -907,7 +930,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } - private SearchParamSet extractSearchParams(IBaseResource theResource, IExtractor theExtractor, RestSearchParameterTypeEnum theSearchParamType) { SearchParamSet retVal = new SearchParamSet<>(); @@ -959,12 +981,11 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor private void addDateTimeTypes(String theResourceType, Set theParams, RuntimeSearchParam theSearchParam, IBase theValue) { IPrimitiveType nextBaseDateTime = (IPrimitiveType) theValue; if (nextBaseDateTime.getValue() != null) { - ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), nextBaseDateTime.getValue(), nextBaseDateTime.getValue(), nextBaseDateTime.getValueAsString()); + ResourceIndexedSearchParamDate param = new ResourceIndexedSearchParamDate(myPartitionSettings, theResourceType, theSearchParam.getName(), nextBaseDateTime.getValue(), nextBaseDateTime.getValueAsString(), nextBaseDateTime.getValue(), nextBaseDateTime.getValueAsString(), nextBaseDateTime.getValueAsString()); theParams.add(param); } } - private void addUri_Uri(String theResourceType, Set theParams, RuntimeSearchParam theSearchParam, IBase theValue) { IPrimitiveType value = (IPrimitiveType) theValue; String valueAsString = value.getValueAsString(); @@ -1258,6 +1279,25 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } } + public static boolean tokenTextIndexingEnabledForSearchParam(ModelConfig theModelConfig, RuntimeSearchParam theSearchParam) { + Optional noSuppressForSearchParam = theSearchParam.getExtensions(JpaConstants.EXT_SEARCHPARAM_TOKEN_SUPPRESS_TEXT_INDEXING).stream() + .map(IBaseExtension::getValue) + .map(val -> (IPrimitiveType) val) + .map(IPrimitiveType::getValueAsString) + .map(Boolean::parseBoolean) + .findFirst(); + + //if the SP doesn't care, use the system default. + if (!noSuppressForSearchParam.isPresent()) { + return !theModelConfig.isSuppressStringIndexingInTokens(); + //If the SP does care, use its value. + } else { + boolean suppressForSearchParam = noSuppressForSearchParam.get(); + ourLog.trace("Text indexing for SearchParameter {}: {}", theSearchParam.getName(), suppressForSearchParam); + return !suppressForSearchParam; + } + } + private static void addIgnoredType(FhirContext theCtx, String theType, Set theIgnoredTypes) { BaseRuntimeElementDefinition elementDefinition = theCtx.getElementDefinition(theType); if (elementDefinition != null) { 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 ab6866194c0..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; @@ -130,17 +142,11 @@ public final class ResourceIndexedSearchParams { } } - public void calculateHashes(Collection theStringParams) { - for (BaseResourceIndex next : theStringParams) { - next.calculateHashes(); - } - } - public Set getPopulatedResourceLinkParameters() { return myPopulatedResourceLinkParameters; } - public boolean matchParam(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) { + public boolean matchParam(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam, boolean theUseOrdinalDatesForDayComparison) { if (theParamDef == null) { return false; } @@ -177,7 +183,7 @@ public final class ResourceIndexedSearchParams { } Predicate namedParamPredicate = param -> param.getParamName().equalsIgnoreCase(theParamName) && - param.matches(theParam); + param.matches(theParam, theUseOrdinalDatesForDayComparison); return resourceParams.stream().anyMatch(namedParamPredicate); } @@ -305,6 +311,7 @@ public final class ResourceIndexedSearchParams { param.setResource(theEntity); param.setMissing(true); param.setParamName(nextParamName); + param.calculateHashes(); paramCollection.add((RT) param); } } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu2.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu2.java index 8c2b46f837e..bd8e724902d 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu2.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu2.java @@ -21,6 +21,8 @@ package ca.uhn.fhir.jpa.searchparam.extractor; */ import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.model.dstu2.composite.ContactPointDt; import ca.uhn.fhir.util.FhirTerser; @@ -40,8 +42,8 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen /** * Constructor for unit tests */ - SearchParamExtractorDstu2(FhirContext theCtx, ISearchParamRegistry theSearchParamRegistry) { - super(theCtx, theSearchParamRegistry); + SearchParamExtractorDstu2(ModelConfig theModelConfig, PartitionSettings thePartitionSettings, FhirContext theCtx, ISearchParamRegistry theSearchParamRegistry) { + super(theModelConfig, thePartitionSettings, theCtx, theSearchParamRegistry); start(); } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3.java index 79a7de05c92..11a23bca41f 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.searchparam.extractor; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import com.google.common.annotations.VisibleForTesting; @@ -49,8 +50,8 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen // This constructor is used by tests @VisibleForTesting - public SearchParamExtractorDstu3(ModelConfig theModelConfig, FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) { - super(theCtx, theSearchParamRegistry); + public SearchParamExtractorDstu3(ModelConfig theModelConfig, PartitionSettings thePartitionSettings, FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) { + super(theModelConfig, thePartitionSettings, theCtx, theSearchParamRegistry); initFhirPathEngine(theValidationSupport); start(); } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java index f7d216ee66a..9c5321f5404 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.searchparam.extractor; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import com.google.common.annotations.VisibleForTesting; @@ -61,8 +62,8 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements // This constructor is used by tests @VisibleForTesting - public SearchParamExtractorR4(ModelConfig theModelConfig, FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) { - super(theCtx, theSearchParamRegistry); + public SearchParamExtractorR4(ModelConfig theModelConfig, PartitionSettings thePartitionSettings, FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) { + super(theModelConfig, thePartitionSettings, theCtx, theSearchParamRegistry); initFhirPath(theValidationSupport); start(); } @@ -76,10 +77,6 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements } - - - - @Override @PostConstruct public void start() { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR5.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR5.java index b882db0f4a5..a7877830f8b 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR5.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR5.java @@ -23,6 +23,8 @@ package ca.uhn.fhir.jpa.searchparam.extractor; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.PathEngineException; @@ -56,8 +58,8 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements /** * Constructor for unit tests */ - public SearchParamExtractorR5(FhirContext theCtx, DefaultProfileValidationSupport theDefaultProfileValidationSupport, ISearchParamRegistry theSearchParamRegistry) { - super(theCtx, theSearchParamRegistry); + public SearchParamExtractorR5(ModelConfig theModelConfig, PartitionSettings thePartitionSettings, FhirContext theCtx, DefaultProfileValidationSupport theDefaultProfileValidationSupport, ISearchParamRegistry theSearchParamRegistry) { + super(theModelConfig, thePartitionSettings, theCtx, theSearchParamRegistry); initFhirPath(theDefaultProfileValidationSupport); start(); } 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/matcher/InMemoryResourceMatcher.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java index 55c5b858b32..08492d2317b 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/matcher/InMemoryResourceMatcher.java @@ -199,7 +199,7 @@ public class InMemoryResourceMatcher { if (theSearchParams == null) { return InMemoryMatchResult.successfulMatch(); } else { - return InMemoryMatchResult.fromBoolean(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theModelConfig, theResourceName, theParamName, theParamDef, nextAnd, theSearchParams))); + return InMemoryMatchResult.fromBoolean(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theModelConfig, theResourceName, theParamName, theParamDef, nextAnd, theSearchParams, myModelConfig.getUseOrdinalDatesForDayPrecisionSearches()))); } case COMPOSITE: case HAS: @@ -216,8 +216,8 @@ public class InMemoryResourceMatcher { } } - private boolean matchParams(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam paramDef, List theNextAnd, ResourceIndexedSearchParams theSearchParams) { - return theNextAnd.stream().anyMatch(token -> theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, paramDef, token)); + private boolean matchParams(ModelConfig theModelConfig, String theResourceName, String theParamName, RuntimeSearchParam paramDef, List theNextAnd, ResourceIndexedSearchParams theSearchParams,boolean theUseOrdinalDatesForDayComparison) { + return theNextAnd.stream().anyMatch(token -> theSearchParams.matchParam(theModelConfig, theResourceName, theParamName, paramDef, token, theUseOrdinalDatesForDayComparison)); } private boolean hasChain(List> theAndOrParams) { 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/IndexStressTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/IndexStressTest.java index c440243e102..70d281668f8 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/IndexStressTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/IndexStressTest.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.searchparam; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3; @@ -44,7 +45,7 @@ public class IndexStressTest { when(mockValidationSupport.getFhirContext()).thenReturn(ctx); IValidationSupport validationSupport = new CachingValidationSupport(new ValidationSupportChain(new DefaultProfileValidationSupport(ctx), mockValidationSupport)); ISearchParamRegistry searchParamRegistry = mock(ISearchParamRegistry.class); - SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ctx, validationSupport, searchParamRegistry); + SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), new PartitionSettings(), ctx, validationSupport, searchParamRegistry); extractor.start(); Map spMap = ctx diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java index 6a05e7c9280..92e4c8e642c 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java @@ -58,8 +58,7 @@ public class SearchParamExtractorDstu3Test { ISearchParamRegistry searchParamRegistry = new MySearchParamRegistry(); - SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ourCtx, ourValidationSupport, searchParamRegistry); - extractor.setPartitionConfigForUnitTest(new PartitionSettings()); + SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, searchParamRegistry); extractor.start(); Set tokens = extractor.extractSearchParamTokens(obs); assertEquals(1, tokens.size()); @@ -82,7 +81,7 @@ public class SearchParamExtractorDstu3Test { ISearchParamRegistry searchParamRegistry = new MySearchParamRegistry(); - SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ourCtx, ourValidationSupport, searchParamRegistry); + SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, searchParamRegistry); extractor.start(); Set params = extractor.extractSearchParamStrings(questionnaire); assertEquals(1, params.size()); @@ -100,7 +99,7 @@ public class SearchParamExtractorDstu3Test { ISearchParamRegistry searchParamRegistry = new MySearchParamRegistry(); - SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ourCtx, ourValidationSupport, searchParamRegistry); + SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, searchParamRegistry); extractor.start(); Set params = extractor.extractSearchParamNumber(enc); assertEquals(1, params.size()); @@ -118,7 +117,7 @@ public class SearchParamExtractorDstu3Test { ISearchParamRegistry searchParamRegistry = new MySearchParamRegistry(); - SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ourCtx, ourValidationSupport, searchParamRegistry); + SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, searchParamRegistry); extractor.start(); Set params = extractor.extractSearchParamNumber(enc); assertEquals(1, params.size()); @@ -130,7 +129,7 @@ public class SearchParamExtractorDstu3Test { public void testEmptyPath() { MySearchParamRegistry searchParamRegistry = new MySearchParamRegistry(); - SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ourCtx, ourValidationSupport, searchParamRegistry); + SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, searchParamRegistry); extractor.start(); searchParamRegistry.addSearchParam(new RuntimeSearchParam("foo", "foo", "", RestSearchParameterTypeEnum.STRING, Sets.newHashSet(), Sets.newHashSet(), RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE)); @@ -146,7 +145,7 @@ public class SearchParamExtractorDstu3Test { public void testStringMissingResourceType() { MySearchParamRegistry searchParamRegistry = new MySearchParamRegistry(); - SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ourCtx, ourValidationSupport, searchParamRegistry); + SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, searchParamRegistry); extractor.start(); searchParamRegistry.addSearchParam(new RuntimeSearchParam("foo", "foo", "communication.language.coding.system | communication.language.coding.code", RestSearchParameterTypeEnum.STRING, Sets.newHashSet(), Sets.newHashSet(), RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE)); @@ -163,8 +162,7 @@ public class SearchParamExtractorDstu3Test { public void testInvalidType() { MySearchParamRegistry searchParamRegistry = new MySearchParamRegistry(); - SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ourCtx, ourValidationSupport, searchParamRegistry); - extractor.setPartitionConfigForUnitTest(new PartitionSettings()); + SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, searchParamRegistry); extractor.start(); { @@ -215,7 +213,7 @@ public class SearchParamExtractorDstu3Test { ISearchParamRegistry searchParamRegistry = new MySearchParamRegistry(); - SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ourCtx, ourValidationSupport, searchParamRegistry); + SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), new PartitionSettings(), ourCtx, ourValidationSupport, searchParamRegistry); extractor.start(); ISearchParamExtractor.SearchParamSet coords = extractor.extractSearchParamTokens(loc); assertEquals(1, coords.size()); diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorMegaTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorMegaTest.java index e29972743c1..58124e39de6 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorMegaTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorMegaTest.java @@ -1,10 +1,26 @@ package ca.uhn.fhir.jpa.searchparam.extractor; -import ca.uhn.fhir.context.*; +import ca.uhn.fhir.context.BaseRuntimeChildDatatypeDefinition; +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.RuntimeChildChoiceDefinition; +import ca.uhn.fhir.context.RuntimeChildContainedResources; +import ca.uhn.fhir.context.RuntimeChildDirectResource; +import ca.uhn.fhir.context.RuntimeChildExtension; +import ca.uhn.fhir.context.RuntimeChildResourceBlockDefinition; +import ca.uhn.fhir.context.RuntimeChildResourceDefinition; +import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition; +import ca.uhn.fhir.context.RuntimePrimitiveDatatypeNarrativeDefinition; +import ca.uhn.fhir.context.RuntimePrimitiveDatatypeXhtmlHl7OrgDefinition; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; -import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseEnumeration; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -29,29 +45,30 @@ public class SearchParamExtractorMegaTest { /** * This test is my magnum opus :P - * + *

    * It navigates almost every possible path in every FHIR resource in every version of FHIR, * and creates a resource with that path populated, just to ensure that we can index it * without generating any warnings. */ @Test public void testAllCombinations() throws Exception { + PartitionSettings partitionSettings = new PartitionSettings(); FhirContext ctx = FhirContext.forDstu2(); ISearchParamRegistry searchParamRegistry = new MySearchParamRegistry(ctx); - process(ctx, new SearchParamExtractorDstu2(ctx, searchParamRegistry).setPartitionConfigForUnitTest(new PartitionSettings())); + process(ctx, new SearchParamExtractorDstu2(new ModelConfig(), partitionSettings, ctx, searchParamRegistry)); ctx = FhirContext.forDstu3(); searchParamRegistry = new MySearchParamRegistry(ctx); - process(ctx, new SearchParamExtractorDstu3(null, ctx, new DefaultProfileValidationSupport(ctx), searchParamRegistry).setPartitionConfigForUnitTest(new PartitionSettings())); + process(ctx, new SearchParamExtractorDstu3(new ModelConfig(), partitionSettings, ctx, new DefaultProfileValidationSupport(ctx), searchParamRegistry)); ctx = FhirContext.forR4(); searchParamRegistry = new MySearchParamRegistry(ctx); - process(ctx, new SearchParamExtractorR4(null, ctx, new DefaultProfileValidationSupport(ctx), searchParamRegistry).setPartitionConfigForUnitTest(new PartitionSettings())); + process(ctx, new SearchParamExtractorR4(new ModelConfig(), partitionSettings, ctx, new DefaultProfileValidationSupport(ctx), searchParamRegistry)); ctx = FhirContext.forR5(); searchParamRegistry = new MySearchParamRegistry(ctx); - process(ctx, new SearchParamExtractorR5(ctx, new DefaultProfileValidationSupport(ctx), searchParamRegistry).setPartitionConfigForUnitTest(new PartitionSettings())); + process(ctx, new SearchParamExtractorR5(new ModelConfig(), partitionSettings, ctx, new DefaultProfileValidationSupport(ctx), searchParamRegistry)); } private void process(FhirContext theCtx, BaseSearchParamExtractor theExtractor) throws Exception { @@ -75,7 +92,6 @@ public class SearchParamExtractorMegaTest { } - theElementStack.add(theElementDef); if (theElementDef instanceof BaseRuntimeElementCompositeDefinition) { @@ -136,7 +152,7 @@ public class SearchParamExtractorMegaTest { BaseRuntimeElementDefinition nextElement = theElementStack.get(i); if (i > 0) { - previousChildArguments = theChildStack.get(i-1).getInstanceConstructorArguments(); + previousChildArguments = theChildStack.get(i - 1).getInstanceConstructorArguments(); } IBase nextObject = nextElement.newInstance(previousChildArguments); 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 cb073567d14..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; @@ -149,8 +151,8 @@ public class InMemoryResourceMatcherR5Test { @Test public void testDateSupportedOps() { - testDateSupportedOp(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, true, true, false); testDateSupportedOp(ParamPrefixEnum.GREATERTHAN, true, false, false); + testDateSupportedOp(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, true, true, false); testDateSupportedOp(ParamPrefixEnum.EQUAL, false, true, false); testDateSupportedOp(ParamPrefixEnum.LESSTHAN_OR_EQUALS, false, true, true); testDateSupportedOp(ParamPrefixEnum.LESSTHAN, false, false, true); @@ -166,7 +168,7 @@ public class InMemoryResourceMatcherR5Test { { InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + OBSERVATION_DATE, myObservation, mySearchParams); assertTrue(result.getUnsupportedReason(), result.supported()); - assertEquals(result.matched(), theSame); + assertEquals(theSame, result.matched()); } { InMemoryMatchResult result = myInMemoryResourceMatcher.match(equation + LATE_DATE, myObservation, mySearchParams); @@ -209,7 +211,7 @@ public class InMemoryResourceMatcherR5Test { private ResourceIndexedSearchParams extractDateSearchParam(Observation theObservation) { ResourceIndexedSearchParams retval = new ResourceIndexedSearchParams(); BaseDateTimeType dateValue = (BaseDateTimeType) theObservation.getEffective(); - ResourceIndexedSearchParamDate dateParam = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "date", dateValue.getValue(), dateValue.getValue(), dateValue.getValueAsString()); + ResourceIndexedSearchParamDate dateParam = new ResourceIndexedSearchParamDate(new PartitionSettings(), "Patient", "date", dateValue.getValue(), dateValue.getValueAsString(), dateValue.getValue(), dateValue.getValueAsString(), dateValue.getValueAsString()); retval.myDateParams.add(dateParam); return retval; } 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/ApacheProxyAddressStrategy.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategy.java index eb574295f62..b6e2652543b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategy.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategy.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.rest.server; +import java.net.URI; + /* * #%L * HAPI FHIR - Server Framework @@ -20,68 +22,137 @@ package ca.uhn.fhir.rest.server; * #L% */ +import java.util.Optional; + import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.ServletServerHttpRequest; + +import static java.util.Optional.ofNullable; + +import ca.uhn.fhir.rest.server.IncomingRequestAddressStrategy; + /** - * Works like the normal {@link ca.uhn.fhir.rest.server.IncomingRequestAddressStrategy} unless there's an x-forwarded-host present, in which case that's used in place of the server's address. - *

    - * If the Apache Http Server mod_proxy isn't configured to supply x-forwarded-proto, the factory method that you use to create the address strategy will determine the default. Note that - * mod_proxy doesn't set this by default, but it can be configured via RequestHeader set X-Forwarded-Proto http (or https) - *

    - *

    - * If you want to set the protocol based on something other than the constructor argument, you should be able to do so by overriding protocol. - *

    - *

    - * Note that while this strategy was designed to work with Apache Http Server, and has been tested against it, it should work with any proxy server that sets x-forwarded-host + * Works like the normal + * {@link ca.uhn.fhir.rest.server.IncomingRequestAddressStrategy} unless there's + * an x-forwarded-host present, in which case that's used in place of the + * server's address. + *

    + * If the Apache Http Server mod_proxy isn't configured to supply + * x-forwarded-proto, the factory method that you use to create the + * address strategy will determine the default. Note that mod_proxy + * doesn't set this by default, but it can be configured via + * RequestHeader set X-Forwarded-Proto http (or https) + *

    + *

    + * List of supported forward headers: + *

      + *
    • x-forwarded-host - original host requested by the client throw proxy + * server + *
    • x-forwarded-proto - original protocol (http, https) requested by the + * client + *
    • x-forwarded-port - original port request by the client, assume default + * port if not defined + *
    • x-forwarded-prefix - original server prefix / context path requested by + * the client + *
    + *

    + *

    + * If you want to set the protocol based on something other than the constructor + * argument, you should be able to do so by overriding protocol. + *

    + *

    + * Note that while this strategy was designed to work with Apache Http Server, + * and has been tested against it, it should work with any proxy server that + * sets x-forwarded-host *

    * - * @author Created by Bill de Beaubien on 3/30/2015. */ public class ApacheProxyAddressStrategy extends IncomingRequestAddressStrategy { - private boolean myUseHttps = false; + private static final String X_FORWARDED_PREFIX = "x-forwarded-prefix"; + private static final String X_FORWARDED_PROTO = "x-forwarded-proto"; + private static final String X_FORWARDED_HOST = "x-forwarded-host"; + private static final String X_FORWARDED_PORT = "x-forwarded-port"; - protected ApacheProxyAddressStrategy(boolean theUseHttps) { - myUseHttps = theUseHttps; + private static final Logger LOG = LoggerFactory + .getLogger(ApacheProxyAddressStrategy.class); + + private final boolean useHttps; + + /** + * @param useHttps + * Is used when the {@code x-forwarded-proto} is not set in the + * request. + */ + public ApacheProxyAddressStrategy(boolean useHttps) { + this.useHttps = useHttps; } @Override - public String determineServerBase(ServletContext theServletContext, HttpServletRequest theRequest) { - String forwardedHost = getForwardedHost(theRequest); - if (forwardedHost != null) { - return forwardedServerBase(theServletContext, theRequest, forwardedHost); - } - return super.determineServerBase(theServletContext, theRequest); + public String determineServerBase(ServletContext servletContext, + HttpServletRequest request) { + String serverBase = super.determineServerBase(servletContext, request); + ServletServerHttpRequest requestWrapper = new ServletServerHttpRequest( + request); + HttpHeaders headers = requestWrapper.getHeaders(); + Optional forwardedHost = headers + .getValuesAsList(X_FORWARDED_HOST).stream().findFirst(); + return forwardedHost + .map(s -> forwardedServerBase(serverBase, headers, s)) + .orElse(serverBase); } - public String forwardedServerBase(ServletContext theServletContext, HttpServletRequest theRequest, String theForwardedHost) { - String serverBase = super.determineServerBase(theServletContext, theRequest); - String host = theRequest.getHeader("host"); - if (host != null) { - serverBase = serverBase.replace(host, theForwardedHost); - serverBase = serverBase.substring(serverBase.indexOf("://")); - return protocol(theRequest) + serverBase; - } - return serverBase; + private String forwardedServerBase(String originalServerBase, + HttpHeaders headers, String forwardedHost) { + Optional forwardedPrefix = getForwardedPrefix(headers); + LOG.debug("serverBase: {}, forwardedHost: {}, forwardedPrefix: {}", + originalServerBase, forwardedHost, forwardedPrefix); + LOG.debug("request header: {}", headers); + + String host = protocol(headers) + "://" + forwardedHost; + String hostWithOptionalPort = port(headers).map(p -> (host + ":" + p)) + .orElse(host); + + String path = forwardedPrefix + .orElseGet(() -> pathFrom(originalServerBase)); + return joinStringsWith(hostWithOptionalPort, path, "/"); } - private String getForwardedHost(HttpServletRequest theRequest) { - String forwardedHost = theRequest.getHeader("x-forwarded-host"); - if (forwardedHost != null) { - int commaPos = forwardedHost.indexOf(','); - if (commaPos >= 0) { - forwardedHost = forwardedHost.substring(0, commaPos - 1); - } - } - return forwardedHost; + private Optional port(HttpHeaders headers) { + return ofNullable(headers.getFirst(X_FORWARDED_PORT)); } - protected String protocol(HttpServletRequest theRequest) { - String protocol = theRequest.getHeader("x-forwarded-proto"); + private String pathFrom(String serverBase) { + String serverBasePath = URI.create(serverBase).getPath(); + return StringUtils.defaultIfBlank(serverBasePath, ""); + } + + private static String joinStringsWith(String left, String right, + String joiner) { + if (left.endsWith(joiner) && right.startsWith(joiner)) { + return left + right.substring(1); + } else if (left.endsWith(joiner) || right.startsWith(joiner)) { + return left + right; + } else { + return left + joiner + right; + } + } + + private Optional getForwardedPrefix(HttpHeaders headers) { + return ofNullable(headers.getFirst(X_FORWARDED_PREFIX)); + } + + private String protocol(HttpHeaders headers) { + String protocol = headers.getFirst(X_FORWARDED_PROTO); if (protocol != null) { return protocol; } - return myUseHttps ? "https" : "http"; + return useHttps ? "https" : "http"; } /** 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 b4361907e96..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; @@ -149,8 +171,12 @@ public class RestfulServer extends HttpServlet implements IRestfulServernull) */ - 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-server/src/test/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategyTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategyTest.java new file mode 100644 index 00000000000..8f5c2f3fc47 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/ApacheProxyAddressStrategyTest.java @@ -0,0 +1,101 @@ +package ca.uhn.fhir.rest.server; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +public class ApacheProxyAddressStrategyTest { + + @Test + public void testWithoutForwarded() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + true); + MockHttpServletRequest request = prepareRequest(); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("https://localhost/imagingstudy/fhir", serverBase); + } + + @Test + public void testWithForwardedHostWithoutForwardedProtoHttps() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + true); + MockHttpServletRequest request = prepareRequest(); + request.addHeader("X-Forwarded-Host", "my.example.host"); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("https://my.example.host/imagingstudy/fhir", serverBase); + } + + @Test + public void testWithForwardedHostWithoutForwardedProtoHttp() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + false); + MockHttpServletRequest request = prepareRequest(); + request.addHeader("X-Forwarded-Host", "my.example.host"); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("http://my.example.host/imagingstudy/fhir", serverBase); + } + + @Test + public void testWithForwarded() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + true); + MockHttpServletRequest request = prepareRequest(); + request.addHeader("X-Forwarded-Host", "my.example.host"); + request.addHeader("X-Forwarded-Proto", "https"); + request.addHeader("X-Forwarded-Prefix", "server-prefix/fhir"); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("https://my.example.host/server-prefix/fhir", serverBase); + } + + @Test + public void testWithForwardedWithHostPrefixWithSlash() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + true); + MockHttpServletRequest request = prepareRequest(); + request.addHeader("host", "localhost"); + + request.addHeader("X-Forwarded-Host", "my.example.host"); + request.addHeader("X-Forwarded-Proto", "https"); + request.addHeader("X-Forwarded-Prefix", "/server-prefix/fhir"); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("https://my.example.host/server-prefix/fhir", serverBase); + } + + @Test + public void testWithForwardedWithoutPrefix() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + true); + MockHttpServletRequest request = prepareRequest(); + + request.addHeader("X-Forwarded-Host", "my.example.host"); + request.addHeader("X-Forwarded-Proto", "https"); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("https://my.example.host/imagingstudy/fhir", serverBase); + } + + @Test + public void testWithForwardedHostAndPort() { + ApacheProxyAddressStrategy addressStrategy = new ApacheProxyAddressStrategy( + true); + MockHttpServletRequest request = prepareRequest(); + + request.addHeader("X-Forwarded-Host", "my.example.host"); + request.addHeader("X-Forwarded-Port", "345"); + String serverBase = addressStrategy.determineServerBase(null, request); + assertEquals("https://my.example.host:345/imagingstudy/fhir", + serverBase); + } + + private MockHttpServletRequest prepareRequest() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setScheme("https"); + request.setServerPort(443); + request.setServletPath("/fhir"); + request.setServerName("localhost"); + request.setRequestURI("/imagingstudy/fhir/imagingstudy?_format=json"); + request.setContextPath("/imagingstudy"); + return request; + } +} \ No newline at end of file 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/BaseTest.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/BaseTest.java index 0cbcf44f7b9..6072cd9445d 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/BaseTest.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/BaseTest.java @@ -21,18 +21,12 @@ package ca.uhn.fhir.test; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import com.google.common.base.Charsets; -import org.apache.commons.io.IOUtils; +import ca.uhn.fhir.util.ClasspathUtil; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hl7.fhir.instance.model.api.IBaseResource; import java.io.IOException; -import java.io.InputStream; -import java.util.function.Function; -import java.util.zip.GZIPInputStream; public class BaseTest { @@ -41,35 +35,14 @@ public class BaseTest { } protected String loadResource(String theClasspath) throws IOException { - Function streamTransform = t->t; - return loadResource(theClasspath, streamTransform); - } - - private String loadResource(String theClasspath, Function theStreamTransform) throws IOException { - try (InputStream stream = BaseTest.class.getResourceAsStream(theClasspath)) { - if (stream == null) { - throw new IllegalArgumentException("Unable to find resource: " + theClasspath); - } - - InputStream newStream = theStreamTransform.apply(stream); - - return IOUtils.toString(newStream, Charsets.UTF_8); - } + return ClasspathUtil.loadResource(theClasspath); } protected String loadCompressedResource(String theClasspath) throws IOException { - Function streamTransform = t-> { - try { - return new GZIPInputStream(t); - } catch (IOException e) { - throw new InternalErrorException(e); - } - }; - return loadResource(theClasspath, streamTransform); + return ClasspathUtil.loadCompressedResource(theClasspath); } protected T loadResource(FhirContext theCtx, Class theType, String theClasspath) throws IOException { - String raw = loadResource(theClasspath); - return EncodingEnum.detectEncodingNoDefault(raw).newParser(theCtx).parseResource(theType, raw); + return ClasspathUtil.loadResource(theCtx, theType, theClasspath); } } 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/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java index 5de2f64ed70..a007b2990fc 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java @@ -1,13 +1,22 @@ package org.hl7.fhir.common.hapi.validation.support; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.util.ClasspathUtil; +import ca.uhn.fhir.util.FileUtil; import org.apache.commons.lang3.Validate; +import org.fhir.ucum.UcumEssenceService; +import org.fhir.ucum.UcumException; import org.hl7.fhir.dstu2.model.ValueSet; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -26,12 +35,13 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { public static final String MIMETYPES_VALUESET_URL = "http://hl7.org/fhir/ValueSet/mimetypes"; public static final String CURRENCIES_CODESYSTEM_URL = "urn:iso:std:iso:4217"; public static final String CURRENCIES_VALUESET_URL = "http://hl7.org/fhir/ValueSet/currencies"; + public static final String UCUM_CODESYSTEM_URL = "http://unitsofmeasure.org"; private static final String USPS_CODESYSTEM_URL = "https://www.usps.com/"; private static final String USPS_VALUESET_URL = "http://hl7.org/fhir/us/core/ValueSet/us-core-usps-state"; + private static final Logger ourLog = LoggerFactory.getLogger(CommonCodeSystemsTerminologyService.class); + public static final String UCUM_VALUESET_URL = "http://hl7.org/fhir/ValueSet/ucum-units"; private static Map USPS_CODES = Collections.unmodifiableMap(buildUspsCodes()); private static Map ISO_4217_CODES = Collections.unmodifiableMap(buildIso4217Codes()); - - private final FhirContext myFhirContext; /** @@ -71,8 +81,22 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { return new CodeValidationResult() .setCode(theCode) .setDisplay(theDisplay); - } + case UCUM_VALUESET_URL: { + String system = theCodeSystem; + if (system == null && theOptions.isInferSystem()) { + system = UCUM_CODESYSTEM_URL; + } + LookupCodeResult lookupResult = lookupCode(theRootValidationSupport, system, theCode); + if (lookupResult != null) { + if (lookupResult.isFound()) { + return new CodeValidationResult() + .setCode(lookupResult.getSearchedForCode()) + .setDisplay(lookupResult.getCodeDisplay()); + } + } + } + } if (handlerMap != null) { String display = handlerMap.get(theCode); @@ -92,6 +116,49 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { return null; } + @Override + public LookupCodeResult lookupCode(IValidationSupport theRootValidationSupport, String theSystem, String theCode) { + + if (UCUM_CODESYSTEM_URL.equals(theSystem) && theRootValidationSupport.getFhirContext().getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { + + InputStream input = ClasspathUtil.loadResourceAsStream("/ucum-essence.xml"); + try { + UcumEssenceService svc = new UcumEssenceService(input); + String outcome = svc.analyse(theCode); + if (outcome != null) { + + LookupCodeResult retVal = new LookupCodeResult(); + retVal.setSearchedForCode(theCode); + retVal.setSearchedForSystem(theSystem); + retVal.setFound(true); + retVal.setCodeDisplay(outcome); + return retVal; + + } + } catch (UcumException e) { + ourLog.debug("Failed parse UCUM code: {}", theCode, e); + return null; + } finally { + ClasspathUtil.close(input); + } + + } + + return null; + } + + @Override + public boolean isCodeSystemSupported(IValidationSupport theRootValidationSupport, String theSystem) { + + switch (theSystem) { + case UCUM_CODESYSTEM_URL: + return true; + } + + return false; + } + + public String getValueSetUrl(@Nonnull IBaseResource theValueSet) { String url; switch (getFhirContext().getVersion().getVersion()) { diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java index 0b608ede8b1..909e628e548 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java @@ -22,6 +22,7 @@ import org.hl7.fhir.utilities.validation.ValidationMessage; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -53,7 +54,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu @Override public ValueSetExpansionOutcome expandValueSet(IValidationSupport theRootValidationSupport, ValueSetExpansionOptions theExpansionOptions, IBaseResource theValueSetToExpand) { - org.hl7.fhir.r5.model.ValueSet expansionR5 = expandValueSetToCanonical(theRootValidationSupport, theValueSetToExpand); + org.hl7.fhir.r5.model.ValueSet expansionR5 = expandValueSetToCanonical(theRootValidationSupport, theValueSetToExpand, null, null); if (expansionR5 == null) { return null; } @@ -85,20 +86,20 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu return new ValueSetExpansionOutcome(expansion, null); } - private org.hl7.fhir.r5.model.ValueSet expandValueSetToCanonical(IValidationSupport theRootValidationSupport, IBaseResource theValueSetToExpand) { + private org.hl7.fhir.r5.model.ValueSet expandValueSetToCanonical(IValidationSupport theRootValidationSupport, IBaseResource theValueSetToExpand, @Nullable String theWantSystem, @Nullable String theWantCode) { org.hl7.fhir.r5.model.ValueSet expansionR5; switch (myCtx.getVersion().getVersion()) { case DSTU2: case DSTU2_HL7ORG: { - expansionR5 = expandValueSetDstu2Hl7Org(theRootValidationSupport, (ValueSet) theValueSetToExpand); + expansionR5 = expandValueSetDstu2Hl7Org(theRootValidationSupport, (ValueSet) theValueSetToExpand, theWantSystem, theWantCode); break; } case DSTU3: { - expansionR5 = expandValueSetDstu3(theRootValidationSupport, (org.hl7.fhir.dstu3.model.ValueSet) theValueSetToExpand); + expansionR5 = expandValueSetDstu3(theRootValidationSupport, (org.hl7.fhir.dstu3.model.ValueSet) theValueSetToExpand, theWantSystem, theWantCode); break; } case R4: { - expansionR5 = expandValueSetR4(theRootValidationSupport, (org.hl7.fhir.r4.model.ValueSet) theValueSetToExpand); + expansionR5 = expandValueSetR4(theRootValidationSupport, (org.hl7.fhir.r4.model.ValueSet) theValueSetToExpand, theWantSystem, theWantCode); break; } case R5: { @@ -118,7 +119,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu @Override public CodeValidationResult validateCodeInValueSet(IValidationSupport theRootValidationSupport, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) { - org.hl7.fhir.r5.model.ValueSet expansion = expandValueSetToCanonical(theRootValidationSupport, theValueSet); + org.hl7.fhir.r5.model.ValueSet expansion = expandValueSetToCanonical(theRootValidationSupport, theValueSet, theCodeSystem, theCode); if (expansion == null) { return null; } @@ -287,7 +288,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu } @Nullable - private org.hl7.fhir.r5.model.ValueSet expandValueSetDstu2Hl7Org(IValidationSupport theRootValidationSupport, ValueSet theInput) { + private org.hl7.fhir.r5.model.ValueSet expandValueSetDstu2Hl7Org(IValidationSupport theRootValidationSupport, ValueSet theInput, @Nullable String theWantSystem, @Nullable String theWantCode) { Function codeSystemLoader = t -> { org.hl7.fhir.dstu2.model.ValueSet codeSystem = (org.hl7.fhir.dstu2.model.ValueSet) theRootValidationSupport.fetchCodeSystem(t); CodeSystem retVal = new CodeSystem(); @@ -300,7 +301,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu }; org.hl7.fhir.r5.model.ValueSet input = ValueSet10_50.convertValueSet(theInput); - org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(input, codeSystemLoader, valueSetLoader); + org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(theRootValidationSupport, input, codeSystemLoader, valueSetLoader, theWantSystem, theWantCode); return (output); } @@ -342,7 +343,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu } @Nullable - private org.hl7.fhir.r5.model.ValueSet expandValueSetDstu3(IValidationSupport theRootValidationSupport, org.hl7.fhir.dstu3.model.ValueSet theInput) { + private org.hl7.fhir.r5.model.ValueSet expandValueSetDstu3(IValidationSupport theRootValidationSupport, org.hl7.fhir.dstu3.model.ValueSet theInput, @Nullable String theWantSystem, @Nullable String theWantCode) { Function codeSystemLoader = t -> { org.hl7.fhir.dstu3.model.CodeSystem codeSystem = (org.hl7.fhir.dstu3.model.CodeSystem) theRootValidationSupport.fetchCodeSystem(t); return CodeSystem30_50.convertCodeSystem(codeSystem); @@ -353,12 +354,12 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu }; org.hl7.fhir.r5.model.ValueSet input = ValueSet30_50.convertValueSet(theInput); - org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(input, codeSystemLoader, valueSetLoader); + org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(theRootValidationSupport, input, codeSystemLoader, valueSetLoader, theWantSystem, theWantCode); return (output); } @Nullable - private org.hl7.fhir.r5.model.ValueSet expandValueSetR4(IValidationSupport theRootValidationSupport, org.hl7.fhir.r4.model.ValueSet theInput) { + private org.hl7.fhir.r5.model.ValueSet expandValueSetR4(IValidationSupport theRootValidationSupport, org.hl7.fhir.r4.model.ValueSet theInput, @Nullable String theWantSystem, @Nullable String theWantCode) { Function codeSystemLoader = t -> { org.hl7.fhir.r4.model.CodeSystem codeSystem = (org.hl7.fhir.r4.model.CodeSystem) theRootValidationSupport.fetchCodeSystem(t); return CodeSystem40_50.convertCodeSystem(codeSystem); @@ -369,7 +370,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu }; org.hl7.fhir.r5.model.ValueSet input = ValueSet40_50.convertValueSet(theInput); - org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(input, codeSystemLoader, valueSetLoader); + org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(theRootValidationSupport, input, codeSystemLoader, valueSetLoader, theWantSystem, theWantCode); return (output); } @@ -378,16 +379,16 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu Function codeSystemLoader = t -> (org.hl7.fhir.r5.model.CodeSystem) theRootValidationSupport.fetchCodeSystem(t); Function valueSetLoader = t -> (org.hl7.fhir.r5.model.ValueSet) theRootValidationSupport.fetchValueSet(t); - return expandValueSetR5(theInput, codeSystemLoader, valueSetLoader); + return expandValueSetR5(theRootValidationSupport, theInput, codeSystemLoader, valueSetLoader, null, null); } @Nullable - private org.hl7.fhir.r5.model.ValueSet expandValueSetR5(org.hl7.fhir.r5.model.ValueSet theInput, Function theCodeSystemLoader, Function theValueSetLoader) { + private org.hl7.fhir.r5.model.ValueSet expandValueSetR5(IValidationSupport theRootValidationSupport, org.hl7.fhir.r5.model.ValueSet theInput, Function theCodeSystemLoader, Function theValueSetLoader, @Nullable String theWantSystem, @Nullable String theWantCode) { Set concepts = new HashSet<>(); try { - expandValueSetR5IncludeOrExclude(concepts, theCodeSystemLoader, theValueSetLoader, theInput.getCompose().getInclude(), true); - expandValueSetR5IncludeOrExclude(concepts, theCodeSystemLoader, theValueSetLoader, theInput.getCompose().getExclude(), false); + expandValueSetR5IncludeOrExclude(theRootValidationSupport, concepts, theCodeSystemLoader, theValueSetLoader, theInput.getCompose().getInclude(), true, theWantSystem, theWantCode); + expandValueSetR5IncludeOrExclude(theRootValidationSupport, concepts, theCodeSystemLoader, theValueSetLoader, theInput.getCompose().getExclude(), false, theWantSystem, theWantCode); } catch (ExpansionCouldNotBeCompletedInternallyException e) { return null; } @@ -403,34 +404,70 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu return retVal; } - private void expandValueSetR5IncludeOrExclude(Set theConcepts, Function theCodeSystemLoader, Function theValueSetLoader, List theComposeList, boolean theComposeListIsInclude) throws ExpansionCouldNotBeCompletedInternallyException { + private void expandValueSetR5IncludeOrExclude(IValidationSupport theRootValidationSupport, Set theConcepts, Function theCodeSystemLoader, Function theValueSetLoader, List theComposeList, boolean theComposeListIsInclude, @Nullable String theWantSystem, @Nullable String theWantCode) throws ExpansionCouldNotBeCompletedInternallyException { for (org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent nextInclude : theComposeList) { List nextCodeList = new ArrayList<>(); String system = nextInclude.getSystem(); if (isNotBlank(system)) { + + if (theWantSystem != null && !theWantSystem.equals(system)) { + continue; + } + CodeSystem codeSystem = theCodeSystemLoader.apply(system); - if (codeSystem == null) { - throw new ExpansionCouldNotBeCompletedInternallyException(); - } - if (codeSystem.getContent() == CodeSystem.CodeSystemContentMode.NOTPRESENT) { - throw new ExpansionCouldNotBeCompletedInternallyException(); - } Set wantCodes; if (nextInclude.getConcept().isEmpty()) { wantCodes = null; } else { - wantCodes = nextInclude.getConcept().stream().map(t -> t.getCode()).collect(Collectors.toSet()); + wantCodes = nextInclude + .getConcept() + .stream().map(t -> t.getCode()).collect(Collectors.toSet()); + } + + boolean ableToHandleCode = false; + if (codeSystem == null) { + + if (theWantCode != null) { + LookupCodeResult lookup = theRootValidationSupport.lookupCode(theRootValidationSupport, system, theWantCode); + if (lookup != null && lookup.isFound()) { + CodeSystem.ConceptDefinitionComponent conceptDefinition = new CodeSystem.ConceptDefinitionComponent() + .addConcept() + .setCode(theWantCode) + .setDisplay(lookup.getCodeDisplay()); + List codesList = Collections.singletonList(conceptDefinition); + addCodes(system, codesList, nextCodeList, wantCodes); + ableToHandleCode = true; + } + } + + } else { + + ableToHandleCode = true; + + } + + if (!ableToHandleCode) { + throw new ExpansionCouldNotBeCompletedInternallyException(); + } + + if (codeSystem != null) { + + if (codeSystem.getContent() == CodeSystem.CodeSystemContentMode.NOTPRESENT) { + throw new ExpansionCouldNotBeCompletedInternallyException(); + } + + addCodes(system, codeSystem.getConcept(), nextCodeList, wantCodes); + } - addCodes(system, codeSystem.getConcept(), nextCodeList, wantCodes); } for (CanonicalType nextValueSetInclude : nextInclude.getValueSet()) { org.hl7.fhir.r5.model.ValueSet vs = theValueSetLoader.apply(nextValueSetInclude.getValueAsString()); if (vs != null) { - org.hl7.fhir.r5.model.ValueSet subExpansion = expandValueSetR5(vs, theCodeSystemLoader, theValueSetLoader); + org.hl7.fhir.r5.model.ValueSet subExpansion = expandValueSetR5(theRootValidationSupport, vs, theCodeSystemLoader, theValueSetLoader, theWantSystem, theWantCode); if (subExpansion == null) { throw new ExpansionCouldNotBeCompletedInternallyException(); } 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 9b81cbdf8ac..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 { @@ -26,7 +28,7 @@ public class SchemaBaseValidatorTest { validator.loadXml("foo.xsd"); fail(); } catch (InternalErrorException e) { - assertThat(e.getMessage(), containsString("Schema not found")); + assertThat(e.getMessage(), containsString("Unable to find classpath resource")); } } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java new file mode 100644 index 00000000000..0834c4de2fc --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java @@ -0,0 +1,68 @@ +package org.hl7.fhir.common.hapi.validation.support; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.ConceptValidationOptions; +import ca.uhn.fhir.context.support.IValidationSupport; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class CommonCodeSystemsTerminologyServiceTest { + + private CommonCodeSystemsTerminologyService mySvc; + private FhirContext myCtx; + + @Before + public void before() { + myCtx = FhirContext.forR4(); + mySvc = new CommonCodeSystemsTerminologyService(myCtx); + } + + @Test + public void testUcum_LookupCode_Good() { + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(myCtx.getValidationSupport(), "http://unitsofmeasure.org", "Cel"); + assertEquals(true, outcome.isFound()); + } + + @Test + public void testUcum_LookupCode_Bad() { + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(myCtx.getValidationSupport(), "http://unitsofmeasure.org", "AAAAA"); + assertNull( outcome); + } + + @Test + public void testUcum_LookupCode_UnknownSystem() { + IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(myCtx.getValidationSupport(), "http://foo", "AAAAA"); + assertNull( outcome); + } + + @Test + public void testUcum_ValidateCode_Good() { + ValueSet vs = new ValueSet(); + vs.setUrl("http://hl7.org/fhir/ValueSet/ucum-units"); + IValidationSupport.CodeValidationResult outcome = mySvc.validateCodeInValueSet(myCtx.getValidationSupport(), new ConceptValidationOptions(), "http://unitsofmeasure.org", "mg", null, vs); + assertEquals(true, outcome.isOk()); + assertEquals("(milligram)", outcome.getDisplay()); + } + + @Test + public void testUcum_ValidateCode_Good_SystemInferred() { + ValueSet vs = new ValueSet(); + vs.setUrl("http://hl7.org/fhir/ValueSet/ucum-units"); + IValidationSupport.CodeValidationResult outcome = mySvc.validateCodeInValueSet(myCtx.getValidationSupport(), new ConceptValidationOptions().setInferSystem(true), null, "mg", null, vs); + assertEquals(true, outcome.isOk()); + assertEquals("(milligram)", outcome.getDisplay()); + } + + @Test + public void testUcum_ValidateCode_Bad() { + ValueSet vs = new ValueSet(); + vs.setUrl("http://hl7.org/fhir/ValueSet/ucum-units"); + IValidationSupport.CodeValidationResult outcome = mySvc.validateCodeInValueSet(myCtx.getValidationSupport(), new ConceptValidationOptions(), "http://unitsofmeasure.org", "aaaaa", null, vs); + assertNull(outcome); + } + +} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java index f9aa04ae046..af4e78c1fa2 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java @@ -17,19 +17,43 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; -import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.conformance.ProfileUtilities; import org.hl7.fhir.r4.context.IWorkerContext; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; -import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.AllergyIntolerance; +import org.hl7.fhir.r4.model.Base; +import org.hl7.fhir.r4.model.Base64BinaryType; +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.CodeSystem; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.Consent; +import org.hl7.fhir.r4.model.ContactPoint; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.Media; +import org.hl7.fhir.r4.model.Narrative; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.OperationOutcome; +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.Procedure; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.RelatedPerson; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind; +import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent; import org.hl7.fhir.r4.terminologies.ValueSetExpander; import org.hl7.fhir.r4.utils.FHIRPathEngine; @@ -195,7 +219,16 @@ public class FhirInstanceValidatorR4Test extends BaseTest { when(mockSupport.fetchCodeSystem(nullable(String.class))).thenAnswer(new Answer() { @Override public CodeSystem answer(InvocationOnMock theInvocation) { - CodeSystem retVal = (CodeSystem) myDefaultValidationSupport.fetchCodeSystem((String) theInvocation.getArguments()[0]); + String system = theInvocation.getArgument(0, String.class); + if ("http://loinc.org".equals(system)) { + CodeSystem retVal = new CodeSystem(); + retVal.setUrl("http://loinc.org"); + retVal.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT); + ourLog.debug("fetchCodeSystem({}) : {}", new Object[]{theInvocation.getArguments()[0], retVal}); + return retVal; + } + + CodeSystem retVal = (CodeSystem) myDefaultValidationSupport.fetchCodeSystem(system); ourLog.debug("fetchCodeSystem({}) : {}", new Object[]{theInvocation.getArguments()[0], retVal}); return retVal; } @@ -216,6 +249,23 @@ public class FhirInstanceValidatorR4Test extends BaseTest { return retVal; } }); + when(mockSupport.lookupCode(any(), any(), any())).thenAnswer(t -> { + String system = t.getArgument(1, String.class); + String code = t.getArgument(2, String.class); + if (myValidConcepts.contains(system + "___" + code)) { + return new IValidationSupport.LookupCodeResult().setFound(true); + } else { + return null; + } + }); + when(mockSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenAnswer(t -> { + String system = t.getArgument(2, String.class); + String code = t.getArgument(3, String.class); + if (myValidConcepts.contains(system + "___" + code)) { + return new IValidationSupport.CodeValidationResult().setCode(code).setDisplay(code); + } + return null; + }); } @@ -1239,6 +1289,25 @@ public class FhirInstanceValidatorR4Test extends BaseTest { } + + @Test + public void testValidateWithUcum() throws IOException { + addValidConcept("http://loinc.org", "8310-5"); + + Observation input = loadResource(ourCtx, Observation.class, "/r4/observation-with-body-temp-ucum.json"); + ValidationResult output = myVal.validateWithResult(input); + List all = logResultsAndReturnNonInformationalOnes(output); + assertThat(all, empty()); + + // Change the unit to something not supported + input.getValueQuantity().setCode("Heck"); + output = myVal.validateWithResult(input); + all = logResultsAndReturnNonInformationalOnes(output); + assertEquals(1, all.size()); + assertThat(all.get(0).getMessage(), containsString("The value provided (\"Heck\") is not in the value set http://hl7.org/fhir/ValueSet/ucum-bodytemp")); + + } + @Test public void testMultiplePerformer() { Observation o = new Observation(); diff --git a/hapi-fhir-validation/src/test/resources/r4/observation-with-body-temp-ucum.json b/hapi-fhir-validation/src/test/resources/r4/observation-with-body-temp-ucum.json new file mode 100644 index 00000000000..2f6e9883305 --- /dev/null +++ b/hapi-fhir-validation/src/test/resources/r4/observation-with-body-temp-ucum.json @@ -0,0 +1,38 @@ +{ + "resourceType": "Observation", + "id": "bodytemp", + "meta": { + "profile": [ + "http://hl7.org/fhir/StructureDefinition/bodytemp" + ] + }, + "status": "final", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "vital-signs" + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://loinc.org", + "code": "8310-5" + } + ] + }, + "subject": { + "reference": "Patient/1" + }, + "effectiveDateTime": "2020-04-30T12:00:00+01:00", + "valueQuantity": { + "value": 37.5, + "unit": "Cel", + "system": "http://unitsofmeasure.org", + "code": "Cel" + } +} diff --git a/pom.xml b/pom.xml index 3bc8177e7ab..4b739d9ba0a 100644 --- a/pom.xml +++ b/pom.xml @@ -611,6 +611,28 @@ mkucharek Maciej Kucharek + + Thopap + Thomas Papke + InterComponentWare AG + + + Bert-R + Bert Roos + + + zilin375 + Zhe Wang + Agfa Healthcare + + + gematik-fue + gematik FuE + + + ibacher + Ian + @@ -659,7 +681,7 @@ 9.4.24.v20191120 3.0.2 - 6.1.0 + 6.4.1 5.4.14.Final