diff --git a/.codacy.yml b/.codacy.yml
new file mode 100644
index 0000000..c4e765c
--- /dev/null
+++ b/.codacy.yml
@@ -0,0 +1,10 @@
+---
+engines:
+ codenarc:
+ exclude_paths:
+ - "src/it/**"
+exclude_paths:
+ - "src/it/**"
+ - "**/mock-loggers.css"
+ - "**/prism.css"
+ - "**/prism.js"
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..dbe0df8
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,1220 @@
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = false
+max_line_length = 120
+tab_width = 2
+ij_continuation_indent_size = 4
+ij_formatter_off_tag = @formatter:off
+ij_formatter_on_tag = @formatter:on
+ij_formatter_tags_enabled = true
+ij_smart_tabs = false
+ij_visual_guides =
+ij_wrap_on_typing = false
+
+[*.css]
+ij_css_align_closing_brace_with_properties = false
+ij_css_blank_lines_around_nested_selector = 1
+ij_css_blank_lines_between_blocks = 1
+ij_css_block_comment_add_space = false
+ij_css_brace_placement = end_of_line
+ij_css_enforce_quotes_on_format = false
+ij_css_hex_color_long_format = false
+ij_css_hex_color_lower_case = false
+ij_css_hex_color_short_format = false
+ij_css_hex_color_upper_case = false
+ij_css_keep_blank_lines_in_code = 2
+ij_css_keep_indents_on_empty_lines = false
+ij_css_keep_single_line_blocks = false
+ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
+ij_css_space_after_colon = true
+ij_css_space_before_opening_brace = true
+ij_css_use_double_quotes = true
+ij_css_value_alignment = do_not_align
+
+[*.java]
+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_deconstruction_list_components = true
+ij_java_align_multiline_extends_list = false
+ij_java_align_multiline_for = false
+ij_java_align_multiline_method_parentheses = false
+ij_java_align_multiline_parameters = false
+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 = false
+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_align_types_in_multi_catch = true
+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 = normal
+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 = true
+ij_java_binary_operation_wrap = normal
+ij_java_blank_lines_after_anonymous_class_header = 0
+ij_java_blank_lines_after_class_header = 1
+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_add_space = false
+ij_java_block_comment_at_first_column = true
+ij_java_builder_methods =
+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 = normal
+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_deconstruction_list_wrap = normal
+ij_java_do_not_indent_top_level_class_members = false
+ij_java_do_not_wrap_after_single_annotation = false
+ij_java_do_not_wrap_after_single_annotation_in_parameter = false
+ij_java_do_while_brace_force = always
+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_prefix =
+ij_java_entity_dd_suffix = EJB
+ij_java_entity_eb_prefix =
+ij_java_entity_eb_suffix = Bean
+ij_java_entity_hi_prefix =
+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_li_suffix =
+ij_java_entity_pk_class = java.lang.String
+ij_java_entity_ri_prefix =
+ij_java_entity_ri_suffix =
+ij_java_entity_vo_prefix =
+ij_java_entity_vo_suffix = VO
+ij_java_enum_constants_wrap = off
+ij_java_enum_field_annotation_wrap = off
+ij_java_extends_keyword_wrap = off
+ij_java_extends_list_wrap = normal
+ij_java_field_annotation_wrap = split_into_lines
+ij_java_field_name_prefix =
+ij_java_field_name_suffix =
+ij_java_filter_class_prefix =
+ij_java_filter_class_suffix =
+ij_java_filter_dd_prefix =
+ij_java_filter_dd_suffix =
+ij_java_finally_on_new_line = false
+ij_java_for_brace_force = always
+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 = normal
+ij_java_generate_final_locals = false
+ij_java_generate_final_parameters = false
+ij_java_if_brace_force = always
+ij_java_imports_layout = $*,|,*
+ij_java_indent_case_from_switch = true
+ij_java_insert_inner_class_imports = true
+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 = 1
+ij_java_keep_blank_lines_in_declarations = 2
+ij_java_keep_builder_methods_indents = false
+ij_java_keep_control_statement_in_one_line = false
+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_add_space_on_reformat = false
+ij_java_line_comment_at_first_column = true
+ij_java_listener_class_prefix =
+ij_java_listener_class_suffix =
+ij_java_local_variable_name_prefix =
+ij_java_local_variable_name_suffix =
+ij_java_message_dd_prefix =
+ij_java_message_dd_suffix = EJB
+ij_java_message_eb_prefix =
+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 = normal
+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 = normal
+ij_java_modifier_list_wrap = false
+ij_java_multi_catch_types_wrap = normal
+ij_java_names_count_to_use_import_on_demand = 999
+ij_java_new_line_after_lparen_in_annotation = false
+ij_java_new_line_after_lparen_in_deconstruction_pattern = true
+ij_java_new_line_after_lparen_in_record_header = false
+ij_java_new_line_when_body_is_presented = false
+ij_java_packages_to_use_import_on_demand =
+ij_java_parameter_annotation_wrap = off
+ij_java_parameter_name_prefix =
+ij_java_parameter_name_suffix =
+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_annotations =
+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_annotation = false
+ij_java_rparen_on_new_line_in_deconstruction_pattern = true
+ij_java_rparen_on_new_line_in_record_header = false
+ij_java_servlet_class_prefix =
+ij_java_servlet_class_suffix =
+ij_java_servlet_dd_prefix =
+ij_java_servlet_dd_suffix =
+ij_java_session_dd_prefix =
+ij_java_session_dd_suffix = EJB
+ij_java_session_eb_prefix =
+ij_java_session_eb_suffix = Bean
+ij_java_session_hi_prefix =
+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_li_suffix =
+ij_java_session_ri_prefix =
+ij_java_session_ri_suffix =
+ij_java_session_si_prefix =
+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_deconstruction_list = 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_annotation_eq = 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_deconstruction_list = 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_record_header = 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 =
+ij_java_static_field_name_suffix =
+ij_java_subclass_name_prefix =
+ij_java_subclass_name_suffix = Impl
+ij_java_switch_expressions_wrap = normal
+ij_java_ternary_operation_signs_on_next_line = true
+ij_java_ternary_operation_wrap = normal
+ij_java_test_name_prefix =
+ij_java_test_name_suffix = Test
+ij_java_throws_keyword_wrap = normal
+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 = always
+ij_java_while_on_new_line = false
+ij_java_wrap_comments = true
+ij_java_wrap_first_method_in_call_chain = false
+ij_java_wrap_long_lines = false
+ij_java_wrap_semicolon_after_call_chain = false
+
+[*.less]
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_less_align_closing_brace_with_properties = false
+ij_less_blank_lines_around_nested_selector = 1
+ij_less_blank_lines_between_blocks = 1
+ij_less_block_comment_add_space = false
+ij_less_brace_placement = 0
+ij_less_enforce_quotes_on_format = false
+ij_less_hex_color_long_format = false
+ij_less_hex_color_lower_case = false
+ij_less_hex_color_short_format = false
+ij_less_hex_color_upper_case = false
+ij_less_keep_blank_lines_in_code = 2
+ij_less_keep_indents_on_empty_lines = false
+ij_less_keep_single_line_blocks = false
+ij_less_line_comment_add_space = false
+ij_less_line_comment_at_first_column = false
+ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
+ij_less_space_after_colon = true
+ij_less_space_before_opening_brace = true
+ij_less_use_double_quotes = true
+ij_less_value_alignment = 0
+
+[*.proto]
+max_line_length = 80
+ij_continuation_indent_size = 2
+ij_protobuf_keep_blank_lines_in_code = 2
+ij_protobuf_keep_indents_on_empty_lines = false
+ij_protobuf_keep_line_breaks = true
+ij_protobuf_space_after_comma = true
+ij_protobuf_space_before_comma = false
+ij_protobuf_spaces_around_assignment_operators = true
+ij_protobuf_spaces_within_braces = false
+ij_protobuf_spaces_within_brackets = false
+
+[*.sass]
+ij_sass_align_closing_brace_with_properties = false
+ij_sass_blank_lines_around_nested_selector = 1
+ij_sass_blank_lines_between_blocks = 1
+ij_sass_brace_placement = 0
+ij_sass_enforce_quotes_on_format = false
+ij_sass_hex_color_long_format = false
+ij_sass_hex_color_lower_case = false
+ij_sass_hex_color_short_format = false
+ij_sass_hex_color_upper_case = false
+ij_sass_keep_blank_lines_in_code = 2
+ij_sass_keep_indents_on_empty_lines = false
+ij_sass_keep_single_line_blocks = false
+ij_sass_line_comment_add_space = false
+ij_sass_line_comment_at_first_column = false
+ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
+ij_sass_space_after_colon = true
+ij_sass_space_before_opening_brace = true
+ij_sass_use_double_quotes = true
+ij_sass_value_alignment = 0
+
+[*.scss]
+ij_scss_align_closing_brace_with_properties = false
+ij_scss_blank_lines_around_nested_selector = 1
+ij_scss_blank_lines_between_blocks = 1
+ij_scss_block_comment_add_space = false
+ij_scss_brace_placement = 0
+ij_scss_enforce_quotes_on_format = false
+ij_scss_hex_color_long_format = false
+ij_scss_hex_color_lower_case = false
+ij_scss_hex_color_short_format = false
+ij_scss_hex_color_upper_case = false
+ij_scss_keep_blank_lines_in_code = 2
+ij_scss_keep_indents_on_empty_lines = false
+ij_scss_keep_single_line_blocks = false
+ij_scss_line_comment_add_space = false
+ij_scss_line_comment_at_first_column = false
+ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
+ij_scss_space_after_colon = true
+ij_scss_space_before_opening_brace = true
+ij_scss_use_double_quotes = true
+ij_scss_value_alignment = 0
+
+[*.vue]
+ij_vue_indent_children_of_top_level = template
+ij_vue_interpolation_new_line_after_start_delimiter = true
+ij_vue_interpolation_new_line_before_end_delimiter = true
+ij_vue_interpolation_wrap = off
+ij_vue_keep_indents_on_empty_lines = false
+ij_vue_spaces_within_interpolation_expressions = true
+
+[.editorconfig]
+ij_editorconfig_align_group_field_declarations = false
+ij_editorconfig_space_after_colon = false
+ij_editorconfig_space_after_comma = true
+ij_editorconfig_space_before_colon = false
+ij_editorconfig_space_before_comma = false
+ij_editorconfig_spaces_around_assignment_operators = true
+
+[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
+ij_continuation_indent_size = 2
+ij_xml_align_attributes = false
+ij_xml_align_text = false
+ij_xml_attribute_wrap = normal
+ij_xml_block_comment_add_space = false
+ij_xml_block_comment_at_first_column = true
+ij_xml_keep_blank_lines = 2
+ij_xml_keep_indents_on_empty_lines = false
+ij_xml_keep_line_breaks = true
+ij_xml_keep_line_breaks_in_text = true
+ij_xml_keep_whitespaces = false
+ij_xml_keep_whitespaces_around_cdata = preserve
+ij_xml_keep_whitespaces_inside_cdata = false
+ij_xml_line_comment_at_first_column = true
+ij_xml_space_after_tag_name = false
+ij_xml_space_around_equals_in_attribute = false
+ij_xml_space_inside_empty_tag = false
+ij_xml_text_wrap = normal
+
+[{*.ats,*.cts,*.mts,*.ts}]
+ij_typescript_align_imports = false
+ij_typescript_align_multiline_array_initializer_expression = false
+ij_typescript_align_multiline_binary_operation = false
+ij_typescript_align_multiline_chained_methods = false
+ij_typescript_align_multiline_extends_list = false
+ij_typescript_align_multiline_for = true
+ij_typescript_align_multiline_parameters = true
+ij_typescript_align_multiline_parameters_in_calls = false
+ij_typescript_align_multiline_ternary_operation = false
+ij_typescript_align_object_properties = 0
+ij_typescript_align_union_types = false
+ij_typescript_align_var_statements = 0
+ij_typescript_array_initializer_new_line_after_left_brace = false
+ij_typescript_array_initializer_right_brace_on_new_line = false
+ij_typescript_array_initializer_wrap = off
+ij_typescript_assignment_wrap = off
+ij_typescript_binary_operation_sign_on_next_line = false
+ij_typescript_binary_operation_wrap = off
+ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/**
+ij_typescript_blank_lines_after_imports = 1
+ij_typescript_blank_lines_around_class = 1
+ij_typescript_blank_lines_around_field = 0
+ij_typescript_blank_lines_around_field_in_interface = 0
+ij_typescript_blank_lines_around_function = 1
+ij_typescript_blank_lines_around_method = 1
+ij_typescript_blank_lines_around_method_in_interface = 1
+ij_typescript_block_brace_style = end_of_line
+ij_typescript_block_comment_add_space = false
+ij_typescript_block_comment_at_first_column = true
+ij_typescript_call_parameters_new_line_after_left_paren = false
+ij_typescript_call_parameters_right_paren_on_new_line = false
+ij_typescript_call_parameters_wrap = off
+ij_typescript_catch_on_new_line = false
+ij_typescript_chained_call_dot_on_new_line = true
+ij_typescript_class_brace_style = end_of_line
+ij_typescript_comma_on_new_line = false
+ij_typescript_do_while_brace_force = never
+ij_typescript_else_on_new_line = false
+ij_typescript_enforce_trailing_comma = keep
+ij_typescript_enum_constants_wrap = on_every_item
+ij_typescript_extends_keyword_wrap = off
+ij_typescript_extends_list_wrap = off
+ij_typescript_field_prefix = _
+ij_typescript_file_name_style = relaxed
+ij_typescript_finally_on_new_line = false
+ij_typescript_for_brace_force = never
+ij_typescript_for_statement_new_line_after_left_paren = false
+ij_typescript_for_statement_right_paren_on_new_line = false
+ij_typescript_for_statement_wrap = off
+ij_typescript_force_quote_style = false
+ij_typescript_force_semicolon_style = false
+ij_typescript_function_expression_brace_style = end_of_line
+ij_typescript_if_brace_force = never
+ij_typescript_import_merge_members = global
+ij_typescript_import_prefer_absolute_path = global
+ij_typescript_import_sort_members = true
+ij_typescript_import_sort_module_name = false
+ij_typescript_import_use_node_resolution = true
+ij_typescript_imports_wrap = on_every_item
+ij_typescript_indent_case_from_switch = true
+ij_typescript_indent_chained_calls = false
+ij_typescript_indent_package_children = 0
+ij_typescript_jsdoc_include_types = false
+ij_typescript_jsx_attribute_value = braces
+ij_typescript_keep_blank_lines_in_code = 2
+ij_typescript_keep_first_column_comment = true
+ij_typescript_keep_indents_on_empty_lines = false
+ij_typescript_keep_line_breaks = true
+ij_typescript_keep_simple_blocks_in_one_line = false
+ij_typescript_keep_simple_methods_in_one_line = false
+ij_typescript_line_comment_add_space = true
+ij_typescript_line_comment_at_first_column = false
+ij_typescript_method_brace_style = end_of_line
+ij_typescript_method_call_chain_wrap = off
+ij_typescript_method_parameters_new_line_after_left_paren = false
+ij_typescript_method_parameters_right_paren_on_new_line = false
+ij_typescript_method_parameters_wrap = off
+ij_typescript_object_literal_wrap = on_every_item
+ij_typescript_object_types_wrap = on_every_item
+ij_typescript_parentheses_expression_new_line_after_left_paren = false
+ij_typescript_parentheses_expression_right_paren_on_new_line = false
+ij_typescript_place_assignment_sign_on_next_line = false
+ij_typescript_prefer_as_type_cast = false
+ij_typescript_prefer_explicit_types_function_expression_returns = false
+ij_typescript_prefer_explicit_types_function_returns = false
+ij_typescript_prefer_explicit_types_vars_fields = false
+ij_typescript_prefer_parameters_wrap = false
+ij_typescript_property_prefix =
+ij_typescript_reformat_c_style_comments = false
+ij_typescript_space_after_colon = true
+ij_typescript_space_after_comma = true
+ij_typescript_space_after_dots_in_rest_parameter = false
+ij_typescript_space_after_generator_mult = true
+ij_typescript_space_after_property_colon = true
+ij_typescript_space_after_quest = true
+ij_typescript_space_after_type_colon = true
+ij_typescript_space_after_unary_not = false
+ij_typescript_space_before_async_arrow_lparen = true
+ij_typescript_space_before_catch_keyword = true
+ij_typescript_space_before_catch_left_brace = true
+ij_typescript_space_before_catch_parentheses = true
+ij_typescript_space_before_class_lbrace = true
+ij_typescript_space_before_class_left_brace = true
+ij_typescript_space_before_colon = true
+ij_typescript_space_before_comma = false
+ij_typescript_space_before_do_left_brace = true
+ij_typescript_space_before_else_keyword = true
+ij_typescript_space_before_else_left_brace = true
+ij_typescript_space_before_finally_keyword = true
+ij_typescript_space_before_finally_left_brace = true
+ij_typescript_space_before_for_left_brace = true
+ij_typescript_space_before_for_parentheses = true
+ij_typescript_space_before_for_semicolon = false
+ij_typescript_space_before_function_left_parenth = true
+ij_typescript_space_before_generator_mult = false
+ij_typescript_space_before_if_left_brace = true
+ij_typescript_space_before_if_parentheses = true
+ij_typescript_space_before_method_call_parentheses = false
+ij_typescript_space_before_method_left_brace = true
+ij_typescript_space_before_method_parentheses = false
+ij_typescript_space_before_property_colon = false
+ij_typescript_space_before_quest = true
+ij_typescript_space_before_switch_left_brace = true
+ij_typescript_space_before_switch_parentheses = true
+ij_typescript_space_before_try_left_brace = true
+ij_typescript_space_before_type_colon = false
+ij_typescript_space_before_unary_not = false
+ij_typescript_space_before_while_keyword = true
+ij_typescript_space_before_while_left_brace = true
+ij_typescript_space_before_while_parentheses = true
+ij_typescript_spaces_around_additive_operators = true
+ij_typescript_spaces_around_arrow_function_operator = true
+ij_typescript_spaces_around_assignment_operators = true
+ij_typescript_spaces_around_bitwise_operators = true
+ij_typescript_spaces_around_equality_operators = true
+ij_typescript_spaces_around_logical_operators = true
+ij_typescript_spaces_around_multiplicative_operators = true
+ij_typescript_spaces_around_relational_operators = true
+ij_typescript_spaces_around_shift_operators = true
+ij_typescript_spaces_around_unary_operator = false
+ij_typescript_spaces_within_array_initializer_brackets = false
+ij_typescript_spaces_within_brackets = false
+ij_typescript_spaces_within_catch_parentheses = false
+ij_typescript_spaces_within_for_parentheses = false
+ij_typescript_spaces_within_if_parentheses = false
+ij_typescript_spaces_within_imports = false
+ij_typescript_spaces_within_interpolation_expressions = false
+ij_typescript_spaces_within_method_call_parentheses = false
+ij_typescript_spaces_within_method_parentheses = false
+ij_typescript_spaces_within_object_literal_braces = false
+ij_typescript_spaces_within_object_type_braces = true
+ij_typescript_spaces_within_parentheses = false
+ij_typescript_spaces_within_switch_parentheses = false
+ij_typescript_spaces_within_type_assertion = false
+ij_typescript_spaces_within_union_types = true
+ij_typescript_spaces_within_while_parentheses = false
+ij_typescript_special_else_if_treatment = true
+ij_typescript_ternary_operation_signs_on_next_line = false
+ij_typescript_ternary_operation_wrap = off
+ij_typescript_union_types_wrap = on_every_item
+ij_typescript_use_chained_calls_group_indents = false
+ij_typescript_use_double_quotes = true
+ij_typescript_use_explicit_js_extension = auto
+ij_typescript_use_import_type = auto
+ij_typescript_use_path_mapping = always
+ij_typescript_use_public_modifier = false
+ij_typescript_use_semicolon_after_statement = true
+ij_typescript_var_declaration_wrap = normal
+ij_typescript_while_brace_force = never
+ij_typescript_while_on_new_line = false
+ij_typescript_wrap_comments = false
+
+[{*.bash,*.sh,*.zsh}]
+ij_shell_binary_ops_start_line = false
+ij_shell_keep_column_alignment_padding = false
+ij_shell_minify_program = false
+ij_shell_redirect_followed_by_space = false
+ij_shell_switch_cases_indented = false
+ij_shell_use_unix_line_separator = true
+
+[{*.cjs,*.js}]
+max_line_length = 80
+ij_javascript_align_imports = false
+ij_javascript_align_multiline_array_initializer_expression = false
+ij_javascript_align_multiline_binary_operation = false
+ij_javascript_align_multiline_chained_methods = false
+ij_javascript_align_multiline_extends_list = false
+ij_javascript_align_multiline_for = false
+ij_javascript_align_multiline_parameters = false
+ij_javascript_align_multiline_parameters_in_calls = false
+ij_javascript_align_multiline_ternary_operation = false
+ij_javascript_align_object_properties = 0
+ij_javascript_align_union_types = false
+ij_javascript_align_var_statements = 0
+ij_javascript_array_initializer_new_line_after_left_brace = false
+ij_javascript_array_initializer_right_brace_on_new_line = false
+ij_javascript_array_initializer_wrap = normal
+ij_javascript_assignment_wrap = off
+ij_javascript_binary_operation_sign_on_next_line = true
+ij_javascript_binary_operation_wrap = normal
+ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/**
+ij_javascript_blank_lines_after_imports = 1
+ij_javascript_blank_lines_around_class = 1
+ij_javascript_blank_lines_around_field = 0
+ij_javascript_blank_lines_around_function = 1
+ij_javascript_blank_lines_around_method = 1
+ij_javascript_block_brace_style = end_of_line
+ij_javascript_block_comment_add_space = false
+ij_javascript_block_comment_at_first_column = true
+ij_javascript_call_parameters_new_line_after_left_paren = false
+ij_javascript_call_parameters_right_paren_on_new_line = false
+ij_javascript_call_parameters_wrap = normal
+ij_javascript_catch_on_new_line = false
+ij_javascript_chained_call_dot_on_new_line = true
+ij_javascript_class_brace_style = end_of_line
+ij_javascript_comma_on_new_line = false
+ij_javascript_do_while_brace_force = always
+ij_javascript_else_on_new_line = false
+ij_javascript_enforce_trailing_comma = keep
+ij_javascript_extends_keyword_wrap = off
+ij_javascript_extends_list_wrap = off
+ij_javascript_field_prefix = _
+ij_javascript_file_name_style = relaxed
+ij_javascript_finally_on_new_line = false
+ij_javascript_for_brace_force = always
+ij_javascript_for_statement_new_line_after_left_paren = false
+ij_javascript_for_statement_right_paren_on_new_line = false
+ij_javascript_for_statement_wrap = normal
+ij_javascript_force_quote_style = false
+ij_javascript_force_semicolon_style = false
+ij_javascript_function_expression_brace_style = end_of_line
+ij_javascript_if_brace_force = always
+ij_javascript_import_merge_members = global
+ij_javascript_import_prefer_absolute_path = global
+ij_javascript_import_sort_members = true
+ij_javascript_import_sort_module_name = false
+ij_javascript_import_use_node_resolution = true
+ij_javascript_imports_wrap = on_every_item
+ij_javascript_indent_case_from_switch = true
+ij_javascript_indent_chained_calls = false
+ij_javascript_indent_package_children = 0
+ij_javascript_jsx_attribute_value = braces
+ij_javascript_keep_blank_lines_in_code = 1
+ij_javascript_keep_first_column_comment = true
+ij_javascript_keep_indents_on_empty_lines = false
+ij_javascript_keep_line_breaks = true
+ij_javascript_keep_simple_blocks_in_one_line = false
+ij_javascript_keep_simple_methods_in_one_line = false
+ij_javascript_line_comment_add_space = true
+ij_javascript_line_comment_at_first_column = false
+ij_javascript_method_brace_style = end_of_line
+ij_javascript_method_call_chain_wrap = off
+ij_javascript_method_parameters_new_line_after_left_paren = false
+ij_javascript_method_parameters_right_paren_on_new_line = false
+ij_javascript_method_parameters_wrap = normal
+ij_javascript_object_literal_wrap = on_every_item
+ij_javascript_object_types_wrap = on_every_item
+ij_javascript_parentheses_expression_new_line_after_left_paren = false
+ij_javascript_parentheses_expression_right_paren_on_new_line = false
+ij_javascript_place_assignment_sign_on_next_line = false
+ij_javascript_prefer_as_type_cast = false
+ij_javascript_prefer_explicit_types_function_expression_returns = false
+ij_javascript_prefer_explicit_types_function_returns = false
+ij_javascript_prefer_explicit_types_vars_fields = false
+ij_javascript_prefer_parameters_wrap = false
+ij_javascript_property_prefix =
+ij_javascript_reformat_c_style_comments = false
+ij_javascript_space_after_colon = true
+ij_javascript_space_after_comma = true
+ij_javascript_space_after_dots_in_rest_parameter = false
+ij_javascript_space_after_generator_mult = true
+ij_javascript_space_after_property_colon = true
+ij_javascript_space_after_quest = true
+ij_javascript_space_after_type_colon = true
+ij_javascript_space_after_unary_not = false
+ij_javascript_space_before_async_arrow_lparen = true
+ij_javascript_space_before_catch_keyword = true
+ij_javascript_space_before_catch_left_brace = true
+ij_javascript_space_before_catch_parentheses = true
+ij_javascript_space_before_class_lbrace = true
+ij_javascript_space_before_class_left_brace = true
+ij_javascript_space_before_colon = true
+ij_javascript_space_before_comma = false
+ij_javascript_space_before_do_left_brace = true
+ij_javascript_space_before_else_keyword = true
+ij_javascript_space_before_else_left_brace = true
+ij_javascript_space_before_finally_keyword = true
+ij_javascript_space_before_finally_left_brace = true
+ij_javascript_space_before_for_left_brace = true
+ij_javascript_space_before_for_parentheses = true
+ij_javascript_space_before_for_semicolon = false
+ij_javascript_space_before_function_left_parenth = true
+ij_javascript_space_before_generator_mult = false
+ij_javascript_space_before_if_left_brace = true
+ij_javascript_space_before_if_parentheses = true
+ij_javascript_space_before_method_call_parentheses = false
+ij_javascript_space_before_method_left_brace = true
+ij_javascript_space_before_method_parentheses = false
+ij_javascript_space_before_property_colon = false
+ij_javascript_space_before_quest = true
+ij_javascript_space_before_switch_left_brace = true
+ij_javascript_space_before_switch_parentheses = true
+ij_javascript_space_before_try_left_brace = true
+ij_javascript_space_before_type_colon = false
+ij_javascript_space_before_unary_not = false
+ij_javascript_space_before_while_keyword = true
+ij_javascript_space_before_while_left_brace = true
+ij_javascript_space_before_while_parentheses = true
+ij_javascript_spaces_around_additive_operators = true
+ij_javascript_spaces_around_arrow_function_operator = true
+ij_javascript_spaces_around_assignment_operators = true
+ij_javascript_spaces_around_bitwise_operators = true
+ij_javascript_spaces_around_equality_operators = true
+ij_javascript_spaces_around_logical_operators = true
+ij_javascript_spaces_around_multiplicative_operators = true
+ij_javascript_spaces_around_relational_operators = true
+ij_javascript_spaces_around_shift_operators = true
+ij_javascript_spaces_around_unary_operator = false
+ij_javascript_spaces_within_array_initializer_brackets = false
+ij_javascript_spaces_within_brackets = false
+ij_javascript_spaces_within_catch_parentheses = false
+ij_javascript_spaces_within_for_parentheses = false
+ij_javascript_spaces_within_if_parentheses = false
+ij_javascript_spaces_within_imports = false
+ij_javascript_spaces_within_interpolation_expressions = false
+ij_javascript_spaces_within_method_call_parentheses = false
+ij_javascript_spaces_within_method_parentheses = false
+ij_javascript_spaces_within_object_literal_braces = false
+ij_javascript_spaces_within_object_type_braces = true
+ij_javascript_spaces_within_parentheses = false
+ij_javascript_spaces_within_switch_parentheses = false
+ij_javascript_spaces_within_type_assertion = false
+ij_javascript_spaces_within_union_types = true
+ij_javascript_spaces_within_while_parentheses = false
+ij_javascript_special_else_if_treatment = true
+ij_javascript_ternary_operation_signs_on_next_line = true
+ij_javascript_ternary_operation_wrap = normal
+ij_javascript_union_types_wrap = on_every_item
+ij_javascript_use_chained_calls_group_indents = false
+ij_javascript_use_double_quotes = true
+ij_javascript_use_explicit_js_extension = auto
+ij_javascript_use_import_type = auto
+ij_javascript_use_path_mapping = always
+ij_javascript_use_public_modifier = false
+ij_javascript_use_semicolon_after_statement = true
+ij_javascript_var_declaration_wrap = normal
+ij_javascript_while_brace_force = always
+ij_javascript_while_on_new_line = false
+ij_javascript_wrap_comments = false
+
+[{*.ft,*.vm,*.vsl}]
+indent_size = 4
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_vtl_keep_indents_on_empty_lines = false
+
+[{*.gant,*.groovy,*.gy}]
+indent_size = 4
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_groovy_align_group_field_declarations = false
+ij_groovy_align_multiline_array_initializer_expression = false
+ij_groovy_align_multiline_assignment = false
+ij_groovy_align_multiline_binary_operation = false
+ij_groovy_align_multiline_chained_methods = false
+ij_groovy_align_multiline_extends_list = false
+ij_groovy_align_multiline_for = true
+ij_groovy_align_multiline_list_or_map = true
+ij_groovy_align_multiline_method_parentheses = false
+ij_groovy_align_multiline_parameters = true
+ij_groovy_align_multiline_parameters_in_calls = false
+ij_groovy_align_multiline_resources = true
+ij_groovy_align_multiline_ternary_operation = false
+ij_groovy_align_multiline_throws_list = false
+ij_groovy_align_named_args_in_map = true
+ij_groovy_align_throws_keyword = false
+ij_groovy_array_initializer_new_line_after_left_brace = false
+ij_groovy_array_initializer_right_brace_on_new_line = false
+ij_groovy_array_initializer_wrap = off
+ij_groovy_assert_statement_wrap = off
+ij_groovy_assignment_wrap = off
+ij_groovy_binary_operation_wrap = off
+ij_groovy_blank_lines_after_class_header = 0
+ij_groovy_blank_lines_after_imports = 1
+ij_groovy_blank_lines_after_package = 1
+ij_groovy_blank_lines_around_class = 1
+ij_groovy_blank_lines_around_field = 0
+ij_groovy_blank_lines_around_field_in_interface = 0
+ij_groovy_blank_lines_around_method = 1
+ij_groovy_blank_lines_around_method_in_interface = 1
+ij_groovy_blank_lines_before_imports = 1
+ij_groovy_blank_lines_before_method_body = 0
+ij_groovy_blank_lines_before_package = 0
+ij_groovy_block_brace_style = end_of_line
+ij_groovy_block_comment_add_space = false
+ij_groovy_block_comment_at_first_column = true
+ij_groovy_call_parameters_new_line_after_left_paren = false
+ij_groovy_call_parameters_right_paren_on_new_line = false
+ij_groovy_call_parameters_wrap = off
+ij_groovy_catch_on_new_line = false
+ij_groovy_class_annotation_wrap = split_into_lines
+ij_groovy_class_brace_style = end_of_line
+ij_groovy_class_count_to_use_import_on_demand = 5
+ij_groovy_do_while_brace_force = never
+ij_groovy_else_on_new_line = false
+ij_groovy_enable_groovydoc_formatting = true
+ij_groovy_enum_constants_wrap = off
+ij_groovy_extends_keyword_wrap = off
+ij_groovy_extends_list_wrap = off
+ij_groovy_field_annotation_wrap = split_into_lines
+ij_groovy_finally_on_new_line = false
+ij_groovy_for_brace_force = never
+ij_groovy_for_statement_new_line_after_left_paren = false
+ij_groovy_for_statement_right_paren_on_new_line = false
+ij_groovy_for_statement_wrap = off
+ij_groovy_ginq_general_clause_wrap_policy = 2
+ij_groovy_ginq_having_wrap_policy = 1
+ij_groovy_ginq_indent_having_clause = true
+ij_groovy_ginq_indent_on_clause = true
+ij_groovy_ginq_on_wrap_policy = 1
+ij_groovy_ginq_space_after_keyword = true
+ij_groovy_if_brace_force = never
+ij_groovy_import_annotation_wrap = 2
+ij_groovy_imports_layout = *,|,javax.**,java.**,|,$*
+ij_groovy_indent_case_from_switch = true
+ij_groovy_indent_label_blocks = true
+ij_groovy_insert_inner_class_imports = false
+ij_groovy_keep_blank_lines_before_right_brace = 2
+ij_groovy_keep_blank_lines_in_code = 2
+ij_groovy_keep_blank_lines_in_declarations = 2
+ij_groovy_keep_control_statement_in_one_line = true
+ij_groovy_keep_first_column_comment = true
+ij_groovy_keep_indents_on_empty_lines = false
+ij_groovy_keep_line_breaks = true
+ij_groovy_keep_multiple_expressions_in_one_line = false
+ij_groovy_keep_simple_blocks_in_one_line = false
+ij_groovy_keep_simple_classes_in_one_line = true
+ij_groovy_keep_simple_lambdas_in_one_line = true
+ij_groovy_keep_simple_methods_in_one_line = true
+ij_groovy_label_indent_absolute = false
+ij_groovy_label_indent_size = 0
+ij_groovy_lambda_brace_style = end_of_line
+ij_groovy_layout_static_imports_separately = true
+ij_groovy_line_comment_add_space = false
+ij_groovy_line_comment_add_space_on_reformat = false
+ij_groovy_line_comment_at_first_column = true
+ij_groovy_method_annotation_wrap = split_into_lines
+ij_groovy_method_brace_style = end_of_line
+ij_groovy_method_call_chain_wrap = off
+ij_groovy_method_parameters_new_line_after_left_paren = false
+ij_groovy_method_parameters_right_paren_on_new_line = false
+ij_groovy_method_parameters_wrap = off
+ij_groovy_modifier_list_wrap = false
+ij_groovy_names_count_to_use_import_on_demand = 3
+ij_groovy_packages_to_use_import_on_demand = java.awt.*,javax.swing.*
+ij_groovy_parameter_annotation_wrap = off
+ij_groovy_parentheses_expression_new_line_after_left_paren = false
+ij_groovy_parentheses_expression_right_paren_on_new_line = false
+ij_groovy_prefer_parameters_wrap = false
+ij_groovy_resource_list_new_line_after_left_paren = false
+ij_groovy_resource_list_right_paren_on_new_line = false
+ij_groovy_resource_list_wrap = off
+ij_groovy_space_after_assert_separator = true
+ij_groovy_space_after_colon = true
+ij_groovy_space_after_comma = true
+ij_groovy_space_after_comma_in_type_arguments = true
+ij_groovy_space_after_for_semicolon = true
+ij_groovy_space_after_quest = true
+ij_groovy_space_after_type_cast = true
+ij_groovy_space_before_annotation_parameter_list = false
+ij_groovy_space_before_array_initializer_left_brace = false
+ij_groovy_space_before_assert_separator = false
+ij_groovy_space_before_catch_keyword = true
+ij_groovy_space_before_catch_left_brace = true
+ij_groovy_space_before_catch_parentheses = true
+ij_groovy_space_before_class_left_brace = true
+ij_groovy_space_before_closure_left_brace = true
+ij_groovy_space_before_colon = true
+ij_groovy_space_before_comma = false
+ij_groovy_space_before_do_left_brace = true
+ij_groovy_space_before_else_keyword = true
+ij_groovy_space_before_else_left_brace = true
+ij_groovy_space_before_finally_keyword = true
+ij_groovy_space_before_finally_left_brace = true
+ij_groovy_space_before_for_left_brace = true
+ij_groovy_space_before_for_parentheses = true
+ij_groovy_space_before_for_semicolon = false
+ij_groovy_space_before_if_left_brace = true
+ij_groovy_space_before_if_parentheses = true
+ij_groovy_space_before_method_call_parentheses = false
+ij_groovy_space_before_method_left_brace = true
+ij_groovy_space_before_method_parentheses = false
+ij_groovy_space_before_quest = true
+ij_groovy_space_before_record_parentheses = false
+ij_groovy_space_before_switch_left_brace = true
+ij_groovy_space_before_switch_parentheses = true
+ij_groovy_space_before_synchronized_left_brace = true
+ij_groovy_space_before_synchronized_parentheses = true
+ij_groovy_space_before_try_left_brace = true
+ij_groovy_space_before_try_parentheses = true
+ij_groovy_space_before_while_keyword = true
+ij_groovy_space_before_while_left_brace = true
+ij_groovy_space_before_while_parentheses = true
+ij_groovy_space_in_named_argument = true
+ij_groovy_space_in_named_argument_before_colon = false
+ij_groovy_space_within_empty_array_initializer_braces = false
+ij_groovy_space_within_empty_method_call_parentheses = false
+ij_groovy_spaces_around_additive_operators = true
+ij_groovy_spaces_around_assignment_operators = true
+ij_groovy_spaces_around_bitwise_operators = true
+ij_groovy_spaces_around_equality_operators = true
+ij_groovy_spaces_around_lambda_arrow = true
+ij_groovy_spaces_around_logical_operators = true
+ij_groovy_spaces_around_multiplicative_operators = true
+ij_groovy_spaces_around_regex_operators = true
+ij_groovy_spaces_around_relational_operators = true
+ij_groovy_spaces_around_shift_operators = true
+ij_groovy_spaces_within_annotation_parentheses = false
+ij_groovy_spaces_within_array_initializer_braces = false
+ij_groovy_spaces_within_braces = true
+ij_groovy_spaces_within_brackets = false
+ij_groovy_spaces_within_cast_parentheses = false
+ij_groovy_spaces_within_catch_parentheses = false
+ij_groovy_spaces_within_for_parentheses = false
+ij_groovy_spaces_within_gstring_injection_braces = false
+ij_groovy_spaces_within_if_parentheses = false
+ij_groovy_spaces_within_list_or_map = false
+ij_groovy_spaces_within_method_call_parentheses = false
+ij_groovy_spaces_within_method_parentheses = false
+ij_groovy_spaces_within_parentheses = false
+ij_groovy_spaces_within_switch_parentheses = false
+ij_groovy_spaces_within_synchronized_parentheses = false
+ij_groovy_spaces_within_try_parentheses = false
+ij_groovy_spaces_within_tuple_expression = false
+ij_groovy_spaces_within_while_parentheses = false
+ij_groovy_special_else_if_treatment = true
+ij_groovy_ternary_operation_wrap = off
+ij_groovy_throws_keyword_wrap = off
+ij_groovy_throws_list_wrap = off
+ij_groovy_use_flying_geese_braces = false
+ij_groovy_use_fq_class_names = false
+ij_groovy_use_fq_class_names_in_javadoc = true
+ij_groovy_use_relative_indents = false
+ij_groovy_use_single_class_imports = true
+ij_groovy_variable_annotation_wrap = off
+ij_groovy_while_brace_force = never
+ij_groovy_while_on_new_line = false
+ij_groovy_wrap_chain_calls_after_dot = false
+ij_groovy_wrap_long_lines = false
+
+[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,bowerrc,jest.config}]
+ij_json_array_wrapping = split_into_lines
+ij_json_keep_blank_lines_in_code = 0
+ij_json_keep_indents_on_empty_lines = false
+ij_json_keep_line_breaks = true
+ij_json_keep_trailing_comma = false
+ij_json_object_wrapping = split_into_lines
+ij_json_property_alignment = do_not_align
+ij_json_space_after_colon = true
+ij_json_space_after_comma = true
+ij_json_space_before_colon = false
+ij_json_space_before_comma = false
+ij_json_spaces_within_braces = false
+ij_json_spaces_within_brackets = false
+ij_json_wrap_long_lines = false
+
+[{*.htm,*.html,*.sht,*.shtm,*.shtml}]
+ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3
+ij_html_align_attributes = true
+ij_html_align_text = false
+ij_html_attribute_wrap = normal
+ij_html_block_comment_add_space = false
+ij_html_block_comment_at_first_column = true
+ij_html_do_not_align_children_of_min_lines = 0
+ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p
+ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot
+ij_html_enforce_quotes = false
+ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var
+ij_html_keep_blank_lines = 2
+ij_html_keep_indents_on_empty_lines = false
+ij_html_keep_line_breaks = true
+ij_html_keep_line_breaks_in_text = true
+ij_html_keep_whitespaces = false
+ij_html_keep_whitespaces_inside = span,pre,textarea
+ij_html_line_comment_at_first_column = true
+ij_html_new_line_after_last_attribute = never
+ij_html_new_line_before_first_attribute = never
+ij_html_quote_style = double
+ij_html_remove_new_line_before_tags = br
+ij_html_space_after_tag_name = false
+ij_html_space_around_equality_in_attribute = false
+ij_html_space_inside_empty_tag = false
+ij_html_text_wrap = normal
+
+[{*.http,*.rest}]
+indent_size = 0
+ij_http-request_call_parameters_wrap = normal
+ij_http-request_method_parameters_wrap = split_into_lines
+ij_http-request_space_before_comma = true
+ij_http-request_spaces_around_assignment_operators = true
+
+[{*.jsf,*.jsp,*.jspf,*.tag,*.tagf,*.xjsp}]
+indent_size = 4
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_jsp_jsp_prefer_comma_separated_import_list = false
+ij_jsp_keep_indents_on_empty_lines = false
+
+[{*.jspx,*.tagx}]
+indent_size = 4
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_jspx_keep_indents_on_empty_lines = false
+
+[{*.kt,*.kts}]
+indent_size = 4
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_kotlin_align_in_columns_case_branch = false
+ij_kotlin_align_multiline_binary_operation = false
+ij_kotlin_align_multiline_extends_list = false
+ij_kotlin_align_multiline_method_parentheses = false
+ij_kotlin_align_multiline_parameters = true
+ij_kotlin_align_multiline_parameters_in_calls = false
+ij_kotlin_allow_trailing_comma = false
+ij_kotlin_allow_trailing_comma_on_call_site = false
+ij_kotlin_assignment_wrap = normal
+ij_kotlin_blank_lines_after_class_header = 0
+ij_kotlin_blank_lines_around_block_when_branches = 0
+ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
+ij_kotlin_block_comment_add_space = false
+ij_kotlin_block_comment_at_first_column = true
+ij_kotlin_call_parameters_new_line_after_left_paren = true
+ij_kotlin_call_parameters_right_paren_on_new_line = true
+ij_kotlin_call_parameters_wrap = on_every_item
+ij_kotlin_catch_on_new_line = false
+ij_kotlin_class_annotation_wrap = split_into_lines
+ij_kotlin_continuation_indent_for_chained_calls = false
+ij_kotlin_continuation_indent_for_expression_bodies = false
+ij_kotlin_continuation_indent_in_argument_lists = false
+ij_kotlin_continuation_indent_in_elvis = false
+ij_kotlin_continuation_indent_in_if_conditions = false
+ij_kotlin_continuation_indent_in_parameter_lists = false
+ij_kotlin_continuation_indent_in_supertype_lists = false
+ij_kotlin_else_on_new_line = false
+ij_kotlin_enum_constants_wrap = off
+ij_kotlin_extends_list_wrap = normal
+ij_kotlin_field_annotation_wrap = split_into_lines
+ij_kotlin_finally_on_new_line = false
+ij_kotlin_if_rparen_on_new_line = true
+ij_kotlin_import_nested_classes = false
+ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^
+ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
+ij_kotlin_keep_blank_lines_before_right_brace = 2
+ij_kotlin_keep_blank_lines_in_code = 2
+ij_kotlin_keep_blank_lines_in_declarations = 2
+ij_kotlin_keep_first_column_comment = true
+ij_kotlin_keep_indents_on_empty_lines = false
+ij_kotlin_keep_line_breaks = true
+ij_kotlin_lbrace_on_next_line = false
+ij_kotlin_line_break_after_multiline_when_entry = true
+ij_kotlin_line_comment_add_space = false
+ij_kotlin_line_comment_add_space_on_reformat = false
+ij_kotlin_line_comment_at_first_column = true
+ij_kotlin_method_annotation_wrap = split_into_lines
+ij_kotlin_method_call_chain_wrap = normal
+ij_kotlin_method_parameters_new_line_after_left_paren = true
+ij_kotlin_method_parameters_right_paren_on_new_line = true
+ij_kotlin_method_parameters_wrap = on_every_item
+ij_kotlin_name_count_to_use_star_import = 5
+ij_kotlin_name_count_to_use_star_import_for_members = 3
+ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.**
+ij_kotlin_parameter_annotation_wrap = off
+ij_kotlin_space_after_comma = true
+ij_kotlin_space_after_extend_colon = true
+ij_kotlin_space_after_type_colon = true
+ij_kotlin_space_before_catch_parentheses = true
+ij_kotlin_space_before_comma = false
+ij_kotlin_space_before_extend_colon = true
+ij_kotlin_space_before_for_parentheses = true
+ij_kotlin_space_before_if_parentheses = true
+ij_kotlin_space_before_lambda_arrow = true
+ij_kotlin_space_before_type_colon = false
+ij_kotlin_space_before_when_parentheses = true
+ij_kotlin_space_before_while_parentheses = true
+ij_kotlin_spaces_around_additive_operators = true
+ij_kotlin_spaces_around_assignment_operators = true
+ij_kotlin_spaces_around_equality_operators = true
+ij_kotlin_spaces_around_function_type_arrow = true
+ij_kotlin_spaces_around_logical_operators = true
+ij_kotlin_spaces_around_multiplicative_operators = true
+ij_kotlin_spaces_around_range = false
+ij_kotlin_spaces_around_relational_operators = true
+ij_kotlin_spaces_around_unary_operator = false
+ij_kotlin_spaces_around_when_arrow = true
+ij_kotlin_variable_annotation_wrap = off
+ij_kotlin_while_on_new_line = false
+ij_kotlin_wrap_elvis_expressions = 1
+ij_kotlin_wrap_expression_body_functions = 1
+ij_kotlin_wrap_first_method_in_call_chain = false
+
+[{*.markdown,*.md}]
+indent_size = 4
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_markdown_force_one_space_after_blockquote_symbol = true
+ij_markdown_force_one_space_after_header_symbol = true
+ij_markdown_force_one_space_after_list_bullet = true
+ij_markdown_force_one_space_between_words = true
+ij_markdown_format_tables = true
+ij_markdown_insert_quote_arrows_on_wrap = true
+ij_markdown_keep_indents_on_empty_lines = false
+ij_markdown_keep_line_breaks_inside_text_blocks = true
+ij_markdown_max_lines_around_block_elements = 1
+ij_markdown_max_lines_around_header = 1
+ij_markdown_max_lines_between_paragraphs = 1
+ij_markdown_min_lines_around_block_elements = 1
+ij_markdown_min_lines_around_header = 1
+ij_markdown_min_lines_between_paragraphs = 1
+ij_markdown_wrap_text_if_long = true
+ij_markdown_wrap_text_inside_blockquotes = true
+
+[{*.pb,*.textproto,*.txtpb}]
+ij_prototext_keep_blank_lines_in_code = 2
+ij_prototext_keep_indents_on_empty_lines = false
+ij_prototext_keep_line_breaks = true
+ij_prototext_space_after_colon = true
+ij_prototext_space_after_comma = true
+ij_prototext_space_before_colon = false
+ij_prototext_space_before_comma = false
+ij_prototext_spaces_within_braces = true
+ij_prototext_spaces_within_brackets = false
+
+[{*.properties,spring.handlers,spring.schemas}]
+ij_properties_align_group_field_declarations = false
+ij_properties_keep_blank_lines = false
+ij_properties_key_value_delimiter = equals
+ij_properties_spaces_around_key_value_delimiter = false
+
+[{*.qute.htm,*.qute.html,*.qute.json,*.qute.txt,*.qute.yaml,*.qute.yml}]
+indent_size = 4
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_qute_keep_indents_on_empty_lines = false
+
+[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}]
+indent_size = 4
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_toml_keep_indents_on_empty_lines = false
+
+[{*.yaml,*.yml}]
+ij_yaml_align_values_properties = do_not_align
+ij_yaml_autoinsert_sequence_marker = true
+ij_yaml_block_mapping_on_new_line = false
+ij_yaml_indent_sequence_value = true
+ij_yaml_keep_indents_on_empty_lines = false
+ij_yaml_keep_line_breaks = true
+ij_yaml_sequence_on_new_line = false
+ij_yaml_space_before_colon = false
+ij_yaml_spaces_within_braces = true
+ij_yaml_spaces_within_brackets = true
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..a4144d0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,68 @@
+name: Bug report
+description: Create a report to help me improve
+labels:
+ - bug
+assignees: [ "vitalijr2" ]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report!
+
+ **Before opening a new issue**, make sure to search for keywords in the issues
+ filtered by the "bug" label:
+
+ * https://github.com/vitalijr2/mock-loggers/issues?q=is%3Aissue+label%3Abug+
+
+ and verify the issue you're about to submit isn't a duplicate.
+ - type: input
+ id: summary
+ validations:
+ required: true
+ attributes:
+ label: Summary
+ description: A clear and concise description of what the bug is.
+ - type: textarea
+ id: what-happened
+ validations:
+ required: true
+ attributes:
+ label: What is the current bug behavior?
+ description: Describe what actually happens.
+ placeholder: Tell us what you do and what you see!
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce
+ value: |
+ 1. ...
+ 2. ...
+ 3. ...
+ ...
+ - type: textarea
+ id: expected
+ validations:
+ required: true
+ attributes:
+ label: What is the expected correct behavior?
+ description: Describe what you should see instead.
+ placeholder: Tell us what you expected to get!
+ - type: textarea
+ id: misc
+ attributes:
+ label: Additional context
+ description: Add any other context about the problem here.
+ - type: textarea
+ id: fixes
+ attributes:
+ label: Possible fixes
+ description: If you can, link to the line of code that might be responsible for the problem.
+ - type: markdown
+ attributes:
+ value: |
+ Paste any relevant logs - please use code blocks (```)
+ to format console output, logs, and code as it's tough
+ to read otherwise.
+
+ **Do not attach ZIP files** of your code or compiled projects - instead,
+ please publish your code to a public GitHub repo & post a link to it.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..98f2b5c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,44 @@
+name: Feature request
+description: Suggest an idea for this project
+labels:
+ - enhancement
+assignees: [ "vitalijr2" ]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this feature request!
+
+ **Before opening a new issue**, make sure to search for keywords in the issues
+ filtered by the "enhancement" and "suggestion" labels:
+
+ * https://github.com/vitalijr2/mock-loggers/issues?q=is%3Aissue+label%3Aenhancement+
+ * https://github.com/vitalijr2/mock-loggers/issues?q=is%3Aissue+label%3Asuggestion+
+
+ and verify the issue you're about to submit isn't a duplicate.
+ - type: input
+ id: summary
+ validations:
+ required: true
+ attributes:
+ label: Summary
+ description: A clear and concise description of what the bug is.
+ - type: textarea
+ id: expected
+ validations:
+ required: true
+ attributes:
+ label: What is the expected correct behavior?
+ description: Describe what you should see instead.
+ placeholder: Tell us what you expected to get!
+ - type: textarea
+ id: implementation
+ attributes:
+ label: Possible implementations
+ description: If you can, link to the line of code that might be responsible for the feature.
+ - type: markdown
+ attributes:
+ value: |
+ Paste any relevant logs - please use code blocks (```)
+ to format console output, logs, and code as it's tough
+ to read otherwise.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..b276c03
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,16 @@
+version: 2
+updates:
+ # Maintain dependencies for Maven
+ - package-ecosystem: "maven"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "sunday"
+ target-branch: "development"
+ # Maintain dependencies for GitHub Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ day: "sunday"
+ target-branch: "development"
diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml
new file mode 100644
index 0000000..5e54fe9
--- /dev/null
+++ b/.github/workflows/check-release.yml
@@ -0,0 +1,26 @@
+name: "Check release"
+
+on:
+ push:
+ tags:
+ - v*
+
+permissions: read-all
+
+jobs:
+ build:
+ name: Maven build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ - name: Setup Java JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'corretto'
+ java-version: 11
+ - name: Build with Maven
+ env:
+ SIGN_KEY: ${{ secrets.SIGN_KEY }}
+ SIGN_KEY_PASS: ${{ secrets.SIGN_KEY_PASS }}
+ run: ./mvnw --batch-mode -s .mvn/ci_settings.xml -ntp -DskipTests -DskipPublishing -Prun-its,release
\ No newline at end of file
diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml
new file mode 100644
index 0000000..5ecada5
--- /dev/null
+++ b/.github/workflows/codacy.yml
@@ -0,0 +1,73 @@
+name: "Codacy"
+
+on:
+ push:
+ branches: [ "**" ]
+ tags-ignore:
+ - v*
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ "**" ]
+
+permissions: read-all
+
+jobs:
+ build:
+ name: Maven build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ - name: Setup Java JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'corretto'
+ java-version: 11
+ - name: Build with Maven
+ run: ./mvnw --batch-mode
+ - name: Temporarily save target and test requests
+ uses: actions/upload-artifact@master
+ with:
+ name: targets
+ path: |
+ core/target
+ jdk-platform-logging/targets
+ retention-days: 1
+ codacy-security-scan:
+ name: Codacy Security Scan
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ - name: Run Codacy Analysis CLI
+ uses: codacy/codacy-analysis-cli-action@09916000460adeeedc96b9704f86deba53e2ad5d
+ with:
+ project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
+ verbose: true
+ output: results.sarif
+ format: sarif
+ gh-code-scanning-compat: true
+ - name: Upload SARIF results file
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: results.sarif
+ codacy-coverage-reporter:
+ name: Codacy Coverage Reporter
+ needs: build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ - name: Retrieve saved test requests and target
+ uses: actions/download-artifact@master
+ with:
+ name: targets
+ path: .
+ - name: Run codacy-coverage-reporter
+ uses: codacy/codacy-coverage-reporter-action@a38818475bb21847788496e9f0fddaa4e84955ba
+ with:
+ coverage-reports: core/target/site/jacoco/jacoco.xml,jdk-platform-logging/target/site/jacoco/jacoco.xml
+ project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..214c5cf
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,45 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ "**" ]
+ tags-ignore:
+ - v*
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ "**" ]
+
+permissions: read-all
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ security-events: write
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - language: java-kotlin
+ build-mode: manual
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ - name: Setup Java JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'corretto'
+ java-version: 11
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{matrix.language}}
+ build-mode: ${{matrix.build-mode}}
+ - name: Build with Maven
+ run: ./mvnw --batch-mode
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/sonatype.yml b/.github/workflows/sonatype.yml
new file mode 100644
index 0000000..a289e7c
--- /dev/null
+++ b/.github/workflows/sonatype.yml
@@ -0,0 +1,28 @@
+name: "Publish artifacts"
+
+on:
+ release:
+ types: [ created ]
+
+permissions: read-all
+
+jobs:
+ publish:
+ name: Publish to Maven Central
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ - name: Setup Java JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'corretto'
+ java-version: 11
+ - name: Build and publish with Maven
+ env:
+ SIGN_KEY: ${{ secrets.SIGN_KEY }}
+ SIGN_KEY_PASS: ${{ secrets.SIGN_KEY_PASS }}
+ SONATYPE_TOKEN_USERNAME: ${{ secrets.SONATYPE_TOKEN_USERNAME }}
+ SONATYPE_TOKEN_PASSWORD: ${{ secrets.SONATYPE_TOKEN_PASSWORD }}
+ run: ./mvnw --batch-mode -s .mvn/ci_settings.xml -ntp -DskipTests -Prelease
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..58837fc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,54 @@
+#Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+release.properties
+.flattened-pom.xml
+
+# GitHub actions
+results.sarif
+
+### IntelliJ IDEA ###
+.idea
+*.iml
+*.ipr
+*.iws
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+
+### NetBeans ###
+nb-configuration.xml
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode
+
+### Mac OS ###
+.DS_Store
+
+# Vim
+*.swp
+*.swo
+
+# patch
+*.orig
+*.rej
+
+# Local environment
+.env
diff --git a/.mvn/ci_settings.xml b/.mvn/ci_settings.xml
new file mode 100644
index 0000000..26d30e8
--- /dev/null
+++ b/.mvn/ci_settings.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ central
+ ${env.SONATYPE_TOKEN_USERNAME}
+ ${env.SONATYPE_TOKEN_PASSWORD}
+
+
+
diff --git a/.mvn/maven.config b/.mvn/maven.config
new file mode 100644
index 0000000..d0f3015
--- /dev/null
+++ b/.mvn/maven.config
@@ -0,0 +1,7 @@
+--fail-at-end
+--show-version
+-Djava.awt.headless=true
+-DinstallAtEnd=true
+-DdeployAtEnd=true
+-Dhttps.protocols=TLSv1.2
+-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN
diff --git a/.mvn/wrapper/.gitignore b/.mvn/wrapper/.gitignore
new file mode 100644
index 0000000..e72f5e8
--- /dev/null
+++ b/.mvn/wrapper/.gitignore
@@ -0,0 +1 @@
+maven-wrapper.jar
diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 0000000..b9e9e77
--- /dev/null
+++ b/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.concurrent.ThreadLocalRandom;
+
+@SuppressWarnings("PMD.NoPackage")
+public final class MavenWrapperDownloader {
+ private static final String WRAPPER_VERSION = "3.3.2";
+
+ private static final boolean VERBOSE = Boolean.parseBoolean(System.getenv("MVNW_VERBOSE"));
+
+ public static void main(String[] args) {
+ log("Apache Maven Wrapper Downloader " + WRAPPER_VERSION);
+
+ if (args.length != 2) {
+ System.err.println(" - ERROR wrapperUrl or wrapperJarPath parameter missing");
+ System.exit(1);
+ }
+
+ try {
+ log(" - Downloader started");
+ final URL wrapperUrl = URI.create(args[0]).toURL();
+ final String jarPath = args[1].replace("..", ""); // Sanitize path
+ final Path wrapperJarPath = Paths.get(jarPath).toAbsolutePath().normalize();
+ downloadFileFromURL(wrapperUrl, wrapperJarPath);
+ log("Done");
+ } catch (IOException e) {
+ System.err.println("- Error downloading: " + e.getMessage());
+ if (VERBOSE) {
+ e.printStackTrace();
+ }
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(URL wrapperUrl, Path wrapperJarPath)
+ throws IOException {
+ log(" - Downloading to: " + wrapperJarPath);
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ final String username = System.getenv("MVNW_USERNAME");
+ final char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ Path temp = wrapperJarPath
+ .getParent()
+ .resolve(wrapperJarPath.getFileName() + "."
+ + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
+ try (InputStream inStream = wrapperUrl.openStream()) {
+ Files.copy(inStream, temp, StandardCopyOption.REPLACE_EXISTING);
+ Files.move(temp, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING);
+ } finally {
+ Files.deleteIfExists(temp);
+ }
+ log(" - Downloader complete");
+ }
+
+ private static void log(String msg) {
+ if (VERBOSE) {
+ System.out.println(msg);
+ }
+ }
+
+}
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..0514995
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+wrapperVersion=3.3.2
+distributionType=source
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..478fc81
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,14 @@
+mock-loggers
+Copyright 2024 Vitalij Berdinskih
+
+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.
diff --git a/changelog.md b/changelog.md
new file mode 100644
index 0000000..001f9d8
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,40 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## Unreleased
+
+- Rename to Mock Loggers
+- Add SLF4J and Apache Commons Logging modules
+
+## 1.1.3 - 2024-11-09
+
+- Improved Javadoc
+- Use BOM for jUnit and Mockito
+- Add activity badges, update other
+- Update Maven plugins: invoker, javadoc, surefire and failsafe
+- Custom annotation @MockLoggers
+
+## 1.1.2 - 2024-10-21
+
+- Update jUnit Jupiter and Platform
+- Update Mockito to 5.14
+- Update a bunch of Maven plugins: javadoc, surefire, failsafe, invoker, central-publishing, jetbrains annotations
+
+## 1.1.1 - 2024-08-20
+
+- Update jUnit to 5.11
+- Improve extension's logging
+- Clean the Maven publishing plugin's configuration.
+
+## 1.1.0 - 2024-07-19
+
+- Implement jUnit extension.
+
+## 1.0.0 - 2024-07-15
+
+- Implement a logger finder.
+- Add Javadocs.
diff --git a/commons-logging/pom.xml b/commons-logging/pom.xml
new file mode 100644
index 0000000..95a93dc
--- /dev/null
+++ b/commons-logging/pom.xml
@@ -0,0 +1,103 @@
+
+
+
+ mock-loggers-commons-logging
+
+
+
+ maven-surefire-plugin
+ org.apache.maven.plugins
+
+
+ maven-failsafe-plugin
+ org.apache.maven.plugins
+
+
+ jacoco-maven-plugin
+ org.jacoco
+
+
+
+
+
+
+ annotations
+ org.jetbrains
+ provided
+
+
+ commons-logging
+ commons-logging
+ provided
+ 1.3.4
+
+
+ mock-loggers-core
+ io.github.vitalijr2.logging
+ 1.0.0
+
+
+ mockito-core
+ org.mockito
+ provided
+
+
+
+ hamcrest
+ org.hamcrest
+ test
+
+
+ junit-jupiter-engine
+ org.junit.jupiter
+ provided
+
+
+ junit-jupiter-params
+ org.junit.jupiter
+ provided
+
+
+ mockito-junit-jupiter
+ org.mockito
+ provided
+
+
+ Mock loggers for Apache Commons Logging backed by Mockito.
+ 4.0.0
+ Mock loggers for Apache Commons Logging
+
+ mock-loggers
+ io.github.vitalijr2.logging
+ 1.0.0
+
+
+
+
+
+
+ maven-invoker-plugin
+ org.apache.maven.plugins
+
+
+
+ run-its
+
+
+
diff --git a/commons-logging/readme.md b/commons-logging/readme.md
new file mode 100644
index 0000000..5437da8
--- /dev/null
+++ b/commons-logging/readme.md
@@ -0,0 +1,159 @@
+# Mock loggers for Apache Commons Logging
+
+[Apache Commons Logging][commons-logging] factory with mock loggers backed by [Mockito][].
+
+> [!WARNING]
+> This library does not support _parallel test execution_.
+
+[![Java Version][java-version]][jdk-download]
+![Commons Logging Version][commons-logging-version]
+![Mockito Version][mockito-version]
+![Maven Central Last Update][maven-central-last-update]
+[![Maven Central][maven-central]][maven-central-link]
+[![Javadoc][javadoc]][javadoc-link]
+
+## How to use
+
+Just put a test dependency to your POM:
+```xml
+
+ mock-loggers-commons-logging
+ io.github.vitalijr2.logging
+ test
+ 1.0.0
+
+```
+
+The simplest usage example looks like this:
+```java
+@Test
+void helloWorld() {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(helloService::sayHelloWorld);
+
+ verify(LogFactory.getLog(helloService.getClass())).info("Hello World!");
+}
+```
+See more details at [HelloServiceBasicTest.java](src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceBasicTest.java)
+
+> [!IMPORTANT]
+> Keep in mind that all loggers are initialized only once during the test run.
+
+Therefore, a more complex example cleans the loggers after (or before) each test:
+```java
+// the static logger instance
+private static Log log;
+
+// initialize the mock logger once
+@BeforeAll
+static void setUpClass() {
+ log = LogFactory.getLog(HelloService.class);
+}
+
+// clean the mock logger after each test
+@AfterEach
+void tearDown() {
+ clearInvocations(log);
+}
+
+// use the mock logger in a test
+@DisplayName("Names")
+@ParameterizedTest(name = "<{0}>")
+@ValueSource(strings = {"John", "Jane"})
+void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLog = LogFactory.getLog(helloService.getClass());
+
+ verify(actualLog).info("Hello " + name + "!");
+ verifyNoMoreInteractions(actualLog);
+}
+```
+See more details at [HelloServiceFullTest.java](src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceFullTest.java)
+
+To avoid manual cleaning of mock loggers you can use the [jUnit extension][junit-extension] for automation.
+```java
+@ExtendWith(MockLoggerExtension.class)
+class HelloServiceExtensionTest {
+
+ private static Log log;
+
+ @BeforeAll
+ static void setUpClass() {
+ log = LogFactory.getLog(HelloService.class);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLog = LogFactory.getLog(helloService.getClass());
+
+ verify(actualLog).info("Hello " + name + "!");
+ verifyNoMoreInteractions(actualLog);
+ }
+
+}
+```
+See more details at [HelloServiceExtensionTest.java](src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceExtensionTest.java)
+
+Also you can use the annotation for automation.
+```java
+@MockLoggers
+class HelloServiceAnnotationTest {
+
+ private static Log log;
+
+ @BeforeAll
+ static void setUpClass() {
+ log = LogFactory.getLog(HelloService.class);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLog = LogFactory.getLog(helloService.getClass());
+
+ verify(actualLog).info("Hello " + name + "!");
+ verifyNoMoreInteractions(actualLog);
+ }
+
+}
+```
+See more details at [HelloServiceAnnotationTest.java](src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceAnnotationTest.java)
+
+[commons-logging]: https://commons.apache.org/proper/commons-logging/
+
+[Mockito]: https://site.mockito.org
+
+[java-version]: https://img.shields.io/static/v1?label=Java&message=11&color=blue&logoColor=E23D28
+
+[jdk-download]: https://www.oracle.com/java/technologies/downloads/#java11
+
+[commons-logging-version]: https://img.shields.io/static/v1?label=commons-logging&message=1.3.4&color=blue&logoColor=E23D28
+
+[mockito-version]: https://img.shields.io/static/v1?label=Mockito&message=5.14.2&color=blue&logoColor=E23D28
+
+[maven-central-last-update]: https://img.shields.io/maven-central/last-update/io.github.vitalijr2.logging/mock-loggers-commons-logging
+
+[maven-central]: https://img.shields.io/maven-central/v/io.github.vitalijr2.logging/mock-loggers-commons-logging
+
+[maven-central-link]: https://central.sonatype.com/artifact/io.github.vitalijr2.logging/mock-loggers-commons-logging?smo=true
+
+[javadoc]: https://javadoc.io/badge2/io.github.vitalijr2.logging/mock-loggers-commons-logging/javadoc.svg
+
+[javadoc-link]: https://javadoc.io/doc/io.github.vitalijr2.logging/mock-loggers-commons-logging
+
+[junit-extension]: ../core/
diff --git a/commons-logging/src/it/hello-commons-logging-world/extension-logging.properties b/commons-logging/src/it/hello-commons-logging-world/extension-logging.properties
new file mode 100644
index 0000000..cd35a2c
--- /dev/null
+++ b/commons-logging/src/it/hello-commons-logging-world/extension-logging.properties
@@ -0,0 +1,8 @@
+.level = INFO
+
+handlers = java.util.logging.ConsoleHandler
+
+java.util.logging.ConsoleHandler.level = ALL
+java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
+
+io.github.vitalijr2.mock.jdk.platform.logging.MockLoggerExtension.level = FINE
diff --git a/commons-logging/src/it/hello-commons-logging-world/pom.xml b/commons-logging/src/it/hello-commons-logging-world/pom.xml
new file mode 100644
index 0000000..a77f291
--- /dev/null
+++ b/commons-logging/src/it/hello-commons-logging-world/pom.xml
@@ -0,0 +1,91 @@
+
+
+ hello-commons-logging-world
+
+
+
+ maven-compiler-plugin
+
+ ${java.version}
+
+ ${java.version}
+
+ org.apache.maven.plugins
+ 3.13.0
+
+
+ maven-surefire-plugin
+
+
+ extension-logging.properties
+
+
+ org.apache.maven.plugins
+ 3.5.2
+
+
+
+
+
+
+ commons-logging
+ commons-logging
+ 1.3.4
+
+
+
+ @project.artifactId@
+ @project.groupId@
+ test
+ @project.version@
+
+
+ hamcrest
+ org.hamcrest
+ test
+ 3.0
+
+
+ junit-jupiter-api
+ org.junit.jupiter
+ test
+ ${junit-jupiter.version}
+
+
+ junit-jupiter-engine
+ org.junit.jupiter
+ test
+ ${junit-jupiter.version}
+
+
+ junit-jupiter-params
+ org.junit.jupiter
+ test
+ ${junit-jupiter.version}
+
+
+ mockito-core
+ org.mockito
+ test
+ ${mockito.version}
+
+
+ mockito-junit-jupiter
+ org.mockito
+ test
+ ${mockito.version}
+
+
+ Basic example
+ example.hello
+ 4.0.0
+
+ 11
+ 5.11.3
+ 5.14.2
+ UTF-8
+
+ 1.0.0
+
diff --git a/commons-logging/src/it/hello-commons-logging-world/src/main/java/example/hello/HelloService.java b/commons-logging/src/it/hello-commons-logging-world/src/main/java/example/hello/HelloService.java
new file mode 100644
index 0000000..580a076
--- /dev/null
+++ b/commons-logging/src/it/hello-commons-logging-world/src/main/java/example/hello/HelloService.java
@@ -0,0 +1,30 @@
+package example.hello;
+
+import static java.util.Objects.requireNonNull;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+public class HelloService {
+
+ private final Log log = LogFactory.getLog(HelloService.class);
+
+ public String sayHelloWorld() {
+ return sayHello("World");
+ }
+
+ public String sayHello(String name) {
+ if (requireNonNull(name, "Name is missed").isBlank()) {
+ throw new IllegalArgumentException("Name is empty");
+ }
+
+ var greeting = "Hello " + name + "!";
+
+ if (log.isInfoEnabled()) {
+ log.info(greeting);
+ }
+
+ return greeting;
+ }
+
+}
diff --git a/commons-logging/src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceAnnotationTest.java b/commons-logging/src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceAnnotationTest.java
new file mode 100644
index 0000000..9326dd7
--- /dev/null
+++ b/commons-logging/src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceAnnotationTest.java
@@ -0,0 +1,47 @@
+package example.hello;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import io.github.vitalijr2.logging.mock.MockLoggers;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+@MockLoggers
+class HelloServiceAnnotationTest {
+
+ private static Log log;
+
+ @BeforeAll
+ static void setUpClass() {
+ log = LogFactory.getLog(HelloService.class);
+ }
+
+ @BeforeEach
+ void setUp() {
+ when(log.isInfoEnabled()).thenReturn(true);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLog = LogFactory.getLog(helloService.getClass());
+
+ verify(actualLog).isInfoEnabled();
+ verify(actualLog).info("Hello " + name + "!");
+ verifyNoMoreInteractions(actualLog);
+ }
+
+}
diff --git a/commons-logging/src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceBasicTest.java b/commons-logging/src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceBasicTest.java
new file mode 100644
index 0000000..f1dd9e2
--- /dev/null
+++ b/commons-logging/src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceBasicTest.java
@@ -0,0 +1,22 @@
+package example.hello;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.verify;
+
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class HelloServiceBasicTest {
+
+ @DisplayName("Hello world")
+ @Test
+ void helloWorld() {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(helloService::sayHelloWorld);
+
+ verify(LogFactory.getLog(helloService.getClass())).isInfoEnabled();
+ }
+
+}
diff --git a/commons-logging/src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceExtensionTest.java b/commons-logging/src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceExtensionTest.java
new file mode 100644
index 0000000..54d3b82
--- /dev/null
+++ b/commons-logging/src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceExtensionTest.java
@@ -0,0 +1,48 @@
+package example.hello;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import io.github.vitalijr2.logging.mock.MockLoggerExtension;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+@ExtendWith(MockLoggerExtension.class)
+class HelloServiceExtensionTest {
+
+ private static Log log;
+
+ @BeforeAll
+ static void setUpClass() {
+ log = LogFactory.getLog(HelloService.class);
+ }
+
+ @BeforeEach
+ void setUp() {
+ when(log.isInfoEnabled()).thenReturn(true);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLog = LogFactory.getLog(helloService.getClass());
+
+ verify(actualLog).isInfoEnabled();
+ verify(actualLog).info("Hello " + name + "!");
+ verifyNoMoreInteractions(actualLog);
+ }
+
+}
diff --git a/commons-logging/src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceFullTest.java b/commons-logging/src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceFullTest.java
new file mode 100644
index 0000000..b6c7ace
--- /dev/null
+++ b/commons-logging/src/it/hello-commons-logging-world/src/test/java/example/hello/HelloServiceFullTest.java
@@ -0,0 +1,69 @@
+package example.hello;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.StringStartsWith.startsWith;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class HelloServiceFullTest {
+
+ private static Log log;
+
+ @BeforeAll
+ static void setUpClass() {
+ log = LogFactory.getLog(HelloService.class);
+ }
+
+ @BeforeEach
+ void setUp() {
+ clearInvocations(log);
+ reset(log);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ when(log.isInfoEnabled()).thenReturn(true);
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLog = LogFactory.getLog(helloService.getClass());
+
+ verify(actualLog).isInfoEnabled();
+ verify(actualLog).info("Hello " + name + "!");
+ verifyNoMoreInteractions(actualLog);
+ }
+
+ @DisplayName("Null or empty name")
+ @ParameterizedTest(name = "<{0}>")
+ @NullAndEmptySource
+ @ValueSource(strings = " ")
+ void nullOrEmptyName(String name) {
+ var helloService = new HelloService();
+
+ var exception = assertThrows(RuntimeException.class, () -> helloService.sayHello(name));
+
+ verifyNoInteractions(LogFactory.getLog(helloService.getClass()));
+
+ assertThat(exception.getMessage(), startsWith("Name is"));
+ }
+
+}
diff --git a/commons-logging/src/it/settings.xml b/commons-logging/src/it/settings.xml
new file mode 100644
index 0000000..12780df
--- /dev/null
+++ b/commons-logging/src/it/settings.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ true
+
+ it-repo
+
+
+ local.central
+
+ true
+
+
+ true
+
+ @localRepositoryUrl@
+
+
+
+
+ local.central
+
+ true
+
+
+ true
+
+ @localRepositoryUrl@
+
+
+
+
+
diff --git a/commons-logging/src/main/java/io/github/vitalijr2/logging/mock/commons/MockLoggerFactory.java b/commons-logging/src/main/java/io/github/vitalijr2/logging/mock/commons/MockLoggerFactory.java
new file mode 100644
index 0000000..c0d1673
--- /dev/null
+++ b/commons-logging/src/main/java/io/github/vitalijr2/logging/mock/commons/MockLoggerFactory.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 Vitalij Berdinskih
+ *
+ * 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.
+ */
+package io.github.vitalijr2.logging.mock.commons;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+
+import io.github.vitalijr2.logging.mock.MockLoggerCleaner;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogConfigurationException;
+import org.apache.commons.logging.LogFactory;
+import org.jetbrains.annotations.VisibleForTesting;
+
+/**
+ * Uses {@link org.mockito.Mockito#mock(Class, String)} to get a mock that is adapted for {@link System.Logger}.
+ *
+ * Example:
+ *
+ * {@literal @}Test
+ * void helloWorld() {
+ * var helloService = new HelloService();
+ *
+ * assertDoesNotThrow(helloService::sayHelloWorld);
+ *
+ * verify(LogFactory.getLog(helloService.getClass())).info("Hello World!");
+ * }
+ *
+ *
+ * @since 1.0.0
+ */
+public class MockLoggerFactory extends LogFactory implements MockLoggerCleaner {
+
+ private final Map loggers;
+
+ /**
+ * Create a map-based logger finder. The finder uses a concurrent map: a logger name is a key.
+ */
+ public MockLoggerFactory() {
+ this(new ConcurrentHashMap<>());
+ }
+
+ @VisibleForTesting
+ MockLoggerFactory(Map loggers) {
+ this.loggers = loggers;
+ subscribeToNotifications();
+ }
+
+ @Override
+ public List cleanAndReset() {
+ var processedLoggers = new ArrayList();
+
+ loggers.forEach((loggerName, logger) -> {
+ clearInvocations(logger);
+ reset(logger);
+ processedLoggers.add(loggerName);
+ });
+
+ return processedLoggers;
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return null;
+ }
+
+ @Override
+ public String[] getAttributeNames() {
+ return new String[0];
+ }
+
+ @Override
+ public Log getInstance(Class> clazz) throws LogConfigurationException {
+ return getInstance(clazz.getName());
+ }
+
+ /**
+ * Returns an instance of Logger for the given name.
+ *
+ * @param name logging name
+ * @return mock logger
+ * @throws LogConfigurationException this is never thrown
+ */
+ @Override
+ public Log getInstance(String name) throws LogConfigurationException {
+ return loggers.computeIfAbsent(name, key -> mock(Log.class, "Mock for logger " + key));
+ }
+
+ @Override
+ public void release() {
+ loggers.clear();
+ }
+
+ @Override
+ public void removeAttribute(String s) {
+ // do nothing
+ }
+
+ @Override
+ public void setAttribute(String s, Object o) {
+ // do nothing
+ }
+
+}
diff --git a/commons-logging/src/main/java/io/github/vitalijr2/logging/mock/commons/package-info.java b/commons-logging/src/main/java/io/github/vitalijr2/logging/mock/commons/package-info.java
new file mode 100644
index 0000000..00a21b2
--- /dev/null
+++ b/commons-logging/src/main/java/io/github/vitalijr2/logging/mock/commons/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Mock loggers for Apache Commons Logging backed by Mockito.
+ *
+ * @since 1.0.0
+ */
+package io.github.vitalijr2.logging.mock.commons;
diff --git a/commons-logging/src/main/resources/commons-logging.properties b/commons-logging/src/main/resources/commons-logging.properties
new file mode 100644
index 0000000..33c0387
--- /dev/null
+++ b/commons-logging/src/main/resources/commons-logging.properties
@@ -0,0 +1 @@
+org.apache.commons.logging.LogFactory=io.github.vitalijr2.logging.mock.commons.MockLoggerFactory
\ No newline at end of file
diff --git a/commons-logging/src/test/java/io/github/vitalijr2/logging/mock/commons/MockLoggerFactoryFastTest.java b/commons-logging/src/test/java/io/github/vitalijr2/logging/mock/commons/MockLoggerFactoryFastTest.java
new file mode 100644
index 0000000..7a28ca6
--- /dev/null
+++ b/commons-logging/src/test/java/io/github/vitalijr2/logging/mock/commons/MockLoggerFactoryFastTest.java
@@ -0,0 +1,87 @@
+package io.github.vitalijr2.logging.mock.commons;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsEmptyIterable.emptyIterable;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.hamcrest.collection.IsMapContaining.hasEntry;
+import static org.hamcrest.collection.IsMapWithSize.aMapWithSize;
+import static org.hamcrest.core.Is.isA;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.StringStartsWith.startsWith;
+import static org.hamcrest.object.HasToString.hasToString;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.mock;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.commons.logging.Log;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("fast")
+class MockLoggerFactoryFastTest {
+
+ @DisplayName("Create and add logger")
+ @Test
+ void createLogger() {
+ // given
+ var loggers = new HashMap();
+ var loggerFactory = new MockLoggerFactory(loggers);
+
+ // when
+ loggerFactory.getInstance("test");
+
+ // then
+ assertAll("Logger was created", () -> assertThat("size", loggers, aMapWithSize(1)),
+ () -> assertThat("entry", loggers, hasEntry(equalTo("test"), isA(Log.class))),
+ () -> assertThat(loggers.values().iterator().next(), hasToString("Mock for logger test")));
+ }
+
+ @DisplayName("Reuse existing logger")
+ @Test
+ void reuseExistingLogger() {
+ // given
+ var loggers = new HashMap<>(Map.of("test", mock(Log.class)));
+ var loggerFactory = new MockLoggerFactory(loggers);
+
+ // when
+ loggerFactory.getInstance("test");
+
+ // then
+ assertAll("Logger was created", () -> assertThat("size", loggers, aMapWithSize(1)),
+ () -> assertThat("entry", loggers, hasEntry(equalTo("test"), isA(Log.class))),
+ () -> assertThat("Logger was created outside", loggers.values().iterator().next(),
+ hasToString(startsWith("Mock for Log, hashCode:"))));
+ }
+
+ @DisplayName("Clean and reset loggers")
+ @Test
+ void cleanAndResetLoggers() {
+ // given
+ var loggers = new HashMap<>(Map.of("a", mock(Log.class), "b", mock(Log.class), "c", mock(Log.class)));
+ var loggerFactory = new MockLoggerFactory(loggers);
+
+ // when
+ var loggerNames = loggerFactory.cleanAndReset();
+
+ // then
+ assertThat(loggerNames, contains("a", "b", "c"));
+ }
+
+ @DisplayName("Clear loggers")
+ @Test
+ void clearLoggers() {
+ // given
+ var loggers = new HashMap<>(Map.of("a", mock(Log.class), "b", mock(Log.class), "c", mock(Log.class)));
+ var loggerFactory = new MockLoggerFactory(loggers);
+
+ // when
+ assertDoesNotThrow(() -> loggerFactory.release());
+
+ // then
+ assertThat(loggers.entrySet(), emptyIterable());
+ }
+
+}
diff --git a/commons-logging/src/test/java/io/github/vitalijr2/logging/mock/commons/MockLoggerFactorySlowTest.java b/commons-logging/src/test/java/io/github/vitalijr2/logging/mock/commons/MockLoggerFactorySlowTest.java
new file mode 100644
index 0000000..e341b9f
--- /dev/null
+++ b/commons-logging/src/test/java/io/github/vitalijr2/logging/mock/commons/MockLoggerFactorySlowTest.java
@@ -0,0 +1,90 @@
+package io.github.vitalijr2.logging.mock.commons;
+
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import io.github.vitalijr2.logging.mock.MockLoggerKeeper;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvFileSource;
+
+@Tag("slow")
+class MockLoggerFactorySlowTest {
+
+ private static Log log;
+
+ @BeforeAll
+ static void setUpClass() {
+ log = LogFactory.getLog(MockLoggerFactorySlowTest.class);
+ }
+
+ @AfterEach
+ void tearDown() {
+ clearInvocations(log);
+ }
+
+ @DisplayName("Test")
+ @ParameterizedTest
+ @CsvFileSource(resources = "commons-logging.csv", numLinesToSkip = 1)
+ void test(String level, String message, int traceCount, int debugCount, int infoCount, int warningCount,
+ int errorCount, int fatalCount, String logName) {
+ // given
+ verifyNoInteractions(log);
+
+ // when
+ switch (level) {
+ case "TRACE":
+ LogFactory.getLog(logName).trace(message);
+ break;
+ case "DEBUG":
+ LogFactory.getLog(logName).debug(message);
+ break;
+ case "INFO":
+ LogFactory.getLog(logName).info(message);
+ break;
+ case "WARNING":
+ LogFactory.getLog(logName).warn(message);
+ break;
+ case "ERROR":
+ LogFactory.getLog(logName).error(message);
+ break;
+ case "FATAL":
+ LogFactory.getLog(logName).fatal(message);
+ break;
+ default:
+ fail("Unknown level");
+ }
+
+ // then
+ verify(log, times(traceCount)).trace("test trace message");
+ verify(log, times(debugCount)).debug("test debug message");
+ verify(log, times(infoCount)).info("test info message");
+ verify(log, times(warningCount)).warn("test warning message");
+ verify(log, times(errorCount)).error("test error message");
+ verify(log, times(fatalCount)).fatal("test fatal message");
+ }
+
+ @DisplayName("Clean and reset mock loggers")
+ @Test
+ void cleanAndResetMockLoggers() {
+ // when
+ var loggerFactory = new MockLoggerFactory();
+
+ loggerFactory.getInstance("test").info("test message");
+
+ MockLoggerKeeper.getInstance().cleanAndReset();
+
+ // then
+ verifyNoInteractions(loggerFactory.getInstance("test"));
+ }
+
+}
diff --git a/commons-logging/src/test/resources/io/github/vitalijr2/logging/mock/commons/commons-logging.csv b/commons-logging/src/test/resources/io/github/vitalijr2/logging/mock/commons/commons-logging.csv
new file mode 100644
index 0000000..ddde649
--- /dev/null
+++ b/commons-logging/src/test/resources/io/github/vitalijr2/logging/mock/commons/commons-logging.csv
@@ -0,0 +1,7 @@
+level, message,trace count,debug count,info count,warning count,error count,fatal count,log name
+TRACE, test trace message,1,0,0,0,0,0,io.github.vitalijr2.logging.mock.commons.MockLoggerFactorySlowTest
+DEBUG, test debug message,0,1,0,0,0,0,io.github.vitalijr2.logging.mock.commons.MockLoggerFactorySlowTest
+INFO, test info message,0,0,1,0,0,0,io.github.vitalijr2.logging.mock.commons.MockLoggerFactorySlowTest
+WARNING, test warning message,0,0,0,1,0,0,io.github.vitalijr2.logging.mock.commons.MockLoggerFactorySlowTest
+ERROR, test error message,0,0,0,0,1,0,io.github.vitalijr2.logging.mock.commons.MockLoggerFactorySlowTest
+FATAL, test fatal message,0,0,0,0,0,1,io.github.vitalijr2.logging.mock.commons.MockLoggerFactorySlowTest
\ No newline at end of file
diff --git a/contributing.md b/contributing.md
new file mode 100644
index 0000000..1cede3a
--- /dev/null
+++ b/contributing.md
@@ -0,0 +1,25 @@
+# Contributing to Mock Loggers
+
+**Mock Loggers** is an open-source project and all contributions
+are welcome to assist with its development and maintenance.
+
+## Issues (bug and feature tracker)
+
+Please report any bugs found, feature requests or other issues on
+**Mock Loggers** [GitHub tracker][github-issues].
+
+When creating a new issue,
+try following [necolas's guidelines][issue-guidelines].
+
+## Fork, patch and contribute code
+
+Feel free to fork **Mock Loggers** repository
+at [GitHub][github-project] for your own use and updates.
+
+Contribute your fixes and new features back to the main codebase using
+[GitHub pull requests][github-pull-requests].
+
+[github-issues]: https://github.com/vitalijr2/mock-loggers/issues
+[issue-guidelines]: http://github.com/necolas/issue-guidelines/#readme
+[github-project]: https://github.com/vitalijr2/mock-loggers
+[github-pull-requests]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 0000000..452fb09
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+ mock-loggers-core
+
+
+
+ maven-surefire-plugin
+ org.apache.maven.plugins
+
+
+ maven-failsafe-plugin
+ org.apache.maven.plugins
+
+
+ jacoco-maven-plugin
+ org.jacoco
+
+
+
+
+
+
+ annotations
+ org.jetbrains
+ provided
+
+
+
+ hamcrest
+ org.hamcrest
+ test
+
+
+ junit-jupiter-engine
+ org.junit.jupiter
+ provided
+
+
+ mockito-junit-jupiter
+ org.mockito
+ provided
+
+
+ The observer pattern is implemented here: cleaners subscribe to notifications from a keeper, who sends
+ alerts when mock loggers need to be cleaned and reset. Logger factories should implement the cleaner interface and
+ register themselves with the keeper. The jUnit extension manages the keeper and sends alerts before and after tests.
+
+ 4.0.0
+ Logger Keeper and jUnit Extension
+
+ mock-loggers
+ io.github.vitalijr2.logging
+ 1.0.0
+
+
diff --git a/core/readme.md b/core/readme.md
new file mode 100644
index 0000000..7b7144c
--- /dev/null
+++ b/core/readme.md
@@ -0,0 +1,27 @@
+# Logger Keeper and jUnit Extension
+
+The observer pattern is implemented here: cleaners subscribe to notifications from a keeper, who sends
+alerts when mock loggers need to be cleaned and reset. Logger factories should implement the cleaner interface and
+register themselves with the keeper. The jUnit extension manages the keeper and sends alerts before and after tests.
+
+[![Java Version][java-version]][jdk-download]
+![jUnit Version][junit-version]
+![Maven Central Last Update][maven-central-last-update]
+[![Maven Central][maven-central]][maven-central-link]
+[![Javadoc][javadoc]][javadoc-link]
+
+[java-version]: https://img.shields.io/static/v1?label=Java&message=11&color=blue&logoColor=E23D28
+
+[jdk-download]: https://www.oracle.com/java/technologies/downloads/#java11
+
+[junit-version]: https://img.shields.io/static/v1?label=jUnit&message=5.11.3&color=blue&logo=junit5&logoColor=E23D28
+
+[maven-central-last-update]: https://img.shields.io/maven-central/last-update/io.github.vitalijr2.logging/mock-loggers-core
+
+[maven-central]: https://img.shields.io/maven-central/v/io.github.vitalijr2.logging/mock-loggers-core
+
+[maven-central-link]: https://central.sonatype.com/artifact/io.github.vitalijr2.logging/mock-loggers-core?smo=true
+
+[javadoc]: https://javadoc.io/badge2/io.github.vitalijr2.logging/mock-loggers-core/javadoc.svg
+
+[javadoc-link]: https://javadoc.io/doc/io.github.vitalijr2.logging/mock-loggers-core
diff --git a/core/src/main/java/io/github/vitalijr2/logging/mock/MockLoggerCleaner.java b/core/src/main/java/io/github/vitalijr2/logging/mock/MockLoggerCleaner.java
new file mode 100644
index 0000000..d8c45f8
--- /dev/null
+++ b/core/src/main/java/io/github/vitalijr2/logging/mock/MockLoggerCleaner.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 Vitalij Berdinskih
+ *
+ * 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.
+ */
+package io.github.vitalijr2.logging.mock;
+
+import java.util.List;
+
+/**
+ * An observer to listen {@linkplain MockLoggerKeeper keeper} when to clean and reset mocks. Mock logger factories
+ * should implement this.
+ *
+ * @since 1.0.0
+ */
+public interface MockLoggerCleaner {
+
+ /**
+ * Receive notification to clean and reset mock loggers.
+ *
+ * @return list of logger names that were cleaned and reset
+ */
+ List cleanAndReset();
+
+ /**
+ * Register itself with a keeper.
+ */
+ default void subscribeToNotifications() {
+ MockLoggerKeeper.getInstance().addCleaner(this);
+ }
+
+}
diff --git a/core/src/main/java/io/github/vitalijr2/logging/mock/MockLoggerExtension.java b/core/src/main/java/io/github/vitalijr2/logging/mock/MockLoggerExtension.java
new file mode 100644
index 0000000..e9a29b9
--- /dev/null
+++ b/core/src/main/java/io/github/vitalijr2/logging/mock/MockLoggerExtension.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024 Vitalij Berdinskih
+ *
+ * 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.
+ */
+package io.github.vitalijr2.logging.mock;
+
+import org.jetbrains.annotations.VisibleForTesting;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.platform.commons.logging.Logger;
+import org.junit.platform.commons.logging.LoggerFactory;
+
+/**
+ * A jUnit extension to clean and reset mock loggers.
+ *
+ * Clean and reset mock loggers before and after tests.
+ *
+ * Example:
+ *
+ * {@literal @}ExtendWith(MockLoggerExtension.class)
+ * class HelloServiceExtensionTest {
+ *
+ * private static Logger logger;
+ *
+ * {@literal @}BeforeAll
+ * static void setUpClass() {
+ * logger = System.getLogger("HelloService");
+ * }
+ *
+ * {@literal @}DisplayName("Names")
+ * {@literal @}ParameterizedTest(name = "<{0}>")
+ * {@literal @}ValueSource(strings = {"John", "Jane"})
+ * void names(String name) {
+ * var helloService = new HelloService();
+ *
+ * assertDoesNotThrow(() -< helloService.sayHello(name));
+ *
+ * var logger = System.getLogger("HelloService");
+ *
+ * verify(logger).log(Level.INFO, "Hello " + name + "!");
+ * verifyNoMoreInteractions(logger);
+ * }
+ *
+ * }
+ *
+ *
+ * @since 1.0.0
+ */
+public class MockLoggerExtension implements AfterEachCallback, BeforeEachCallback {
+
+ private final Logger extensionLogger;
+ private final MockLoggerKeeper loggerKeeper;
+
+ /**
+ * Create an extension.
+ */
+ public MockLoggerExtension() {
+ this(MockLoggerKeeper.getInstance(), LoggerFactory.getLogger(MockLoggerExtension.class));
+ }
+
+ @VisibleForTesting
+ MockLoggerExtension(MockLoggerKeeper loggerKeeper, Logger extensionLogger) {
+ this.loggerKeeper = loggerKeeper;
+ this.extensionLogger = extensionLogger;
+ }
+
+ /**
+ * Clean and reset mock loggers after tests. You are still able to run tasks in
+ * {@link org.junit.jupiter.api.AfterEach}.
+ *
+ * @param context the current extension context; never {@code null}
+ */
+ @Override
+ public void afterEach(ExtensionContext context) {
+ var loggerNames = loggerKeeper.cleanAndReset();
+
+ extensionLogger.debug(() -> "Clean and reset the loggers: " + String.join(", ", loggerNames));
+ }
+
+ /**
+ * Clean and reset mock loggers before tests. You are still able to run tasks in
+ * {@link org.junit.jupiter.api.BeforeEach}.
+ *
+ * @param context the current extension context; never {@code null}
+ */
+ @Override
+ public void beforeEach(ExtensionContext context) {
+ var loggerNames = loggerKeeper.cleanAndReset();
+
+ extensionLogger.debug(() -> "Clean and reset the loggers: " + String.join(", ", loggerNames));
+ }
+
+}
diff --git a/core/src/main/java/io/github/vitalijr2/logging/mock/MockLoggerKeeper.java b/core/src/main/java/io/github/vitalijr2/logging/mock/MockLoggerKeeper.java
new file mode 100644
index 0000000..82d7bcb
--- /dev/null
+++ b/core/src/main/java/io/github/vitalijr2/logging/mock/MockLoggerKeeper.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 Vitalij Berdinskih
+ *
+ * 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.
+ */
+package io.github.vitalijr2.logging.mock;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.VisibleForTesting;
+
+/**
+ * An observable object to notify mock logger factories to clean and reset mocks.
+ *
+ * @since 1.0.0
+ */
+public class MockLoggerKeeper {
+
+ private static final MockLoggerKeeper INSTANCE = new MockLoggerKeeper();
+
+ private final Set cleaners;
+
+ @VisibleForTesting
+ MockLoggerKeeper() {
+ cleaners = new CopyOnWriteArraySet<>();
+ }
+
+ @NotNull
+ public static MockLoggerKeeper getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Add a cleaner to the set.
+ *
+ * @param cleaner mock logger factory who wants to get notification
+ */
+ public void addCleaner(MockLoggerCleaner cleaner) {
+ cleaners.add(cleaner);
+ }
+
+ /**
+ * Send notifications to all cleaners.
+ *
+ * @return list of logger names that were cleaned and reset
+ */
+ public List cleanAndReset() {
+ var loggerNames = new ArrayList();
+
+ cleaners.forEach(cleaner -> loggerNames.addAll(cleaner.cleanAndReset()));
+
+ return loggerNames;
+ }
+
+}
diff --git a/core/src/main/java/io/github/vitalijr2/logging/mock/MockLoggers.java b/core/src/main/java/io/github/vitalijr2/logging/mock/MockLoggers.java
new file mode 100644
index 0000000..5678f7e
--- /dev/null
+++ b/core/src/main/java/io/github/vitalijr2/logging/mock/MockLoggers.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 Vitalij Berdinskih
+ *
+ * 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.
+ */
+package io.github.vitalijr2.logging.mock;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Registers {@link MockLoggerExtension} for a test class.
+ *
+ * @since 1.0.0
+ */
+@ExtendWith(MockLoggerExtension.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface MockLoggers {
+
+}
diff --git a/core/src/main/java/io/github/vitalijr2/logging/mock/package-info.java b/core/src/main/java/io/github/vitalijr2/logging/mock/package-info.java
new file mode 100644
index 0000000..1268e57
--- /dev/null
+++ b/core/src/main/java/io/github/vitalijr2/logging/mock/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * The observer pattern is implemented here: cleaners subscribe to notifications from a keeper, who sends alerts when
+ * mock loggers need to be cleaned and reset. Logger factories should implement the cleaner interface and register
+ * themselves with the keeper. The jUnit extension manages the keeper and sends alerts before and after tests.
+ */
+package io.github.vitalijr2.logging.mock;
\ No newline at end of file
diff --git a/core/src/test/java/io/github/vitalijr2/logging/mock/MockLoggerCleanerTest.java b/core/src/test/java/io/github/vitalijr2/logging/mock/MockLoggerCleanerTest.java
new file mode 100644
index 0000000..c17ed8b
--- /dev/null
+++ b/core/src/test/java/io/github/vitalijr2/logging/mock/MockLoggerCleanerTest.java
@@ -0,0 +1,34 @@
+package io.github.vitalijr2.logging.mock;
+
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.verify;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@Tag("slow")
+@ExtendWith(MockitoExtension.class)
+class MockLoggerCleanerTest {
+
+ @Mock
+ private MockLoggerCleaner cleaner;
+
+ @DisplayName("Cleaner subscribes to keeper's notifications")
+ @Test
+ void cleanerSubscribesToNotifications() {
+ // given
+ doCallRealMethod().when(cleaner).subscribeToNotifications();
+
+ // when
+ cleaner.subscribeToNotifications();
+ MockLoggerKeeper.getInstance().cleanAndReset();
+
+ // then
+ verify(cleaner).cleanAndReset();
+ }
+
+}
diff --git a/core/src/test/java/io/github/vitalijr2/logging/mock/MockLoggerExtensionFastTest.java b/core/src/test/java/io/github/vitalijr2/logging/mock/MockLoggerExtensionFastTest.java
new file mode 100644
index 0000000..dca6ed5
--- /dev/null
+++ b/core/src/test/java/io/github/vitalijr2/logging/mock/MockLoggerExtensionFastTest.java
@@ -0,0 +1,79 @@
+package io.github.vitalijr2.logging.mock;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.function.Supplier;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+@Tag("fast")
+class MockLoggerExtensionFastTest {
+
+ @Captor
+ private ArgumentCaptor> messageCaptor;
+ @Mock
+ private MockLoggerKeeper loggerKeeper;
+ @Mock
+ private ExtensionContext extensionContext;
+ @Mock
+ private org.junit.platform.commons.logging.Logger extensionLogger;
+
+
+ private MockLoggerExtension extension;
+
+ @BeforeEach
+ void setUp() {
+ extension = new MockLoggerExtension(loggerKeeper, extensionLogger);
+ }
+
+ @DisplayName("Clean and reset loggers after each test")
+ @Test
+ void resetLoggersAfterEachTest() {
+ // given
+ when(loggerKeeper.cleanAndReset()).thenReturn(List.of("a", "b", "c"));
+
+ // when
+ assertDoesNotThrow(() -> extension.afterEach(extensionContext));
+
+ // then
+ verifyNoInteractions(extensionContext);
+ verify(extensionLogger).debug(messageCaptor.capture());
+ verify(loggerKeeper).cleanAndReset();
+
+ assertEquals("Clean and reset the loggers: a, b, c", messageCaptor.getValue().get(),
+ "logging message");
+ }
+
+ @DisplayName("Clean and reset loggers before each test")
+ @Test
+ void resetLoggersBeforeEachTest() {
+ // given
+ when(loggerKeeper.cleanAndReset()).thenReturn(List.of("a", "b", "c"));
+
+ // when
+ assertDoesNotThrow(() -> extension.beforeEach(extensionContext));
+
+ // then
+ verifyNoInteractions(extensionContext);
+ verify(extensionLogger).debug(messageCaptor.capture());
+ verify(loggerKeeper).cleanAndReset();
+
+ assertEquals("Clean and reset the loggers: a, b, c", messageCaptor.getValue().get(),
+ "logging message");
+ }
+
+}
diff --git a/core/src/test/java/io/github/vitalijr2/logging/mock/MockLoggerExtensionSlowTest.java b/core/src/test/java/io/github/vitalijr2/logging/mock/MockLoggerExtensionSlowTest.java
new file mode 100644
index 0000000..4cb43bc
--- /dev/null
+++ b/core/src/test/java/io/github/vitalijr2/logging/mock/MockLoggerExtensionSlowTest.java
@@ -0,0 +1,22 @@
+package io.github.vitalijr2.logging.mock;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+@Tag("slow")
+class MockLoggerExtensionSlowTest {
+
+ @DisplayName("Initialize a logger keeper on \"before all\" step")
+ @Test
+ void initLoggerFinderOnBeforeAll() {
+ // when and then
+ assertDoesNotThrow(() -> new MockLoggerExtension());
+ }
+
+}
diff --git a/core/src/test/java/io/github/vitalijr2/logging/mock/MockLoggerKeeperTest.java b/core/src/test/java/io/github/vitalijr2/logging/mock/MockLoggerKeeperTest.java
new file mode 100644
index 0000000..4a6dd48
--- /dev/null
+++ b/core/src/test/java/io/github/vitalijr2/logging/mock/MockLoggerKeeperTest.java
@@ -0,0 +1,61 @@
+package io.github.vitalijr2.logging.mock;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@Tag("fast")
+@ExtendWith(MockitoExtension.class)
+class MockLoggerKeeperTest {
+
+ @Mock
+ private MockLoggerCleaner first;
+ @Mock
+ private MockLoggerCleaner second;
+
+ @DisplayName("Instance")
+ @Test
+ void instance() {
+ // when and them
+ assertNotNull(MockLoggerKeeper.getInstance());
+ }
+
+ @DisplayName("Happy path")
+ @Test
+ void happyPath() {
+ // given
+ var keeper = new MockLoggerKeeper();
+
+ when(first.cleanAndReset()).thenReturn(List.of("a", "b", "c"));
+ when(second.cleanAndReset()).thenReturn(List.of("x", "y", "z"));
+
+ // when
+ var loggerNames = assertDoesNotThrow(() -> {
+ keeper.addCleaner(first);
+ keeper.addCleaner(second);
+
+ return keeper.cleanAndReset();
+ });
+
+ // then
+ verify(first).cleanAndReset();
+ verify(second).cleanAndReset();
+ verifyNoMoreInteractions(first);
+ verifyNoMoreInteractions(second);
+
+ assertThat(loggerNames, contains("a", "b", "c", "x", "y", "z"));
+ }
+
+}
diff --git a/jdk-platform-logging/pom.xml b/jdk-platform-logging/pom.xml
new file mode 100644
index 0000000..b3fc10d
--- /dev/null
+++ b/jdk-platform-logging/pom.xml
@@ -0,0 +1,98 @@
+
+
+
+ mock-loggers-jdk-platform-logging
+
+
+
+ maven-surefire-plugin
+ org.apache.maven.plugins
+
+
+ maven-failsafe-plugin
+ org.apache.maven.plugins
+
+
+ jacoco-maven-plugin
+ org.jacoco
+
+
+
+
+
+
+ annotations
+ org.jetbrains
+ provided
+
+
+ mock-loggers-core
+ io.github.vitalijr2.logging
+ 1.0.0
+
+
+ mockito-core
+ org.mockito
+ provided
+
+
+
+ hamcrest
+ org.hamcrest
+ test
+
+
+ junit-jupiter-engine
+ org.junit.jupiter
+ provided
+
+
+ junit-jupiter-params
+ org.junit.jupiter
+ provided
+
+
+ mockito-junit-jupiter
+ org.mockito
+ provided
+
+
+ Mock loggers for JDK Platform Logging backed by Mockito.
+ 4.0.0
+ Mock loggers for JDK Platform Logging
+
+ mock-loggers
+ io.github.vitalijr2.logging
+ ../pom.xml
+ 1.0.0
+
+
+
+
+
+
+ maven-invoker-plugin
+ org.apache.maven.plugins
+
+
+
+ run-its
+
+
+
diff --git a/jdk-platform-logging/readme.md b/jdk-platform-logging/readme.md
new file mode 100644
index 0000000..8c58d13
--- /dev/null
+++ b/jdk-platform-logging/readme.md
@@ -0,0 +1,156 @@
+# Mock loggers for JDK Platform Logging
+
+[JDK Platform Logging][jdk-logging] service with mock loggers backed by [Mockito][].
+
+> [!WARNING]
+> This library does not support _parallel test execution_.
+
+[![Java Version][java-version]][jdk-download]
+![Mockito Version][mockito-version]
+![Maven Central Last Update][maven-central-last-update]
+[![Maven Central][maven-central]][maven-central-link]
+[![Javadoc][javadoc]][javadoc-link]
+
+## How to use
+
+Just put a test dependency to your POM:
+```xml
+
+ mock-loggers-jdk-platform-logging
+ io.github.vitalijr2.logging
+ test
+ 1.0.0
+
+```
+
+The simplest usage example looks like this:
+```java
+@Test
+void helloWorld() {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(helloService::sayHelloWorld);
+
+ verify(System.getLogger("HelloService")).log(Level.INFO, "Hello World!");
+}
+```
+See more details at [HelloServiceBasicTest.java](src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceBasicTest.java)
+
+> [!IMPORTANT]
+> Keep in mind that all loggers are initialized only once during the test run.
+
+Therefore, a more complex example cleans the loggers after (or before) each test:
+```java
+// the static logger instance
+private static Logger logger;
+
+// initialize the mock logger once
+@BeforeAll
+static void setUpClass() {
+ logger = System.getLogger("HelloService");
+}
+
+// clean the mock logger after each test
+@AfterEach
+void tearDown() {
+ clearInvocations(logger);
+}
+
+// use the mock logger in a test
+@DisplayName("Names")
+@ParameterizedTest(name = "<{0}>")
+@ValueSource(strings = {"John", "Jane"})
+void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var logger = System.getLogger("HelloService");
+
+ verify(logger).log(Level.INFO, "Hello " + name + "!");
+ verifyNoMoreInteractions(logger);
+}
+```
+See more details at [HelloServiceFullTest.java](src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceFullTest.java)
+
+To avoid manual cleaning of mock loggers you can use the [jUnit extension][junit-extension] for automation.
+```java
+@ExtendWith(MockLoggerExtension.class)
+class HelloServiceExtensionTest {
+
+ private static Logger logger;
+
+ @BeforeAll
+ static void setUpClass() {
+ logger = System.getLogger("HelloService");
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var logger = System.getLogger("HelloService");
+
+ verify(logger).log(Level.INFO, "Hello " + name + "!");
+ verifyNoMoreInteractions(logger);
+ }
+
+}
+```
+See more details at [HelloServiceExtensionTest.java](src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceExtensionTest.java)
+
+Also you can use the annotation for automation.
+```java
+@MockLoggers
+class HelloServiceAnnotationTest {
+
+ private static Logger logger;
+
+ @BeforeAll
+ static void setUpClass() {
+ logger = System.getLogger("HelloService");
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var logger = System.getLogger("HelloService");
+
+ verify(logger).log(Level.INFO, "Hello " + name + "!");
+ verifyNoMoreInteractions(logger);
+ }
+
+}
+```
+See more details at [HelloServiceAnnotationTest.java](src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceAnnotationTest.java)
+
+[jdk-logging]: https://www.baeldung.com/java-9-logging-api "Java Platform Logging API"
+
+[Mockito]: https://site.mockito.org
+
+[java-version]: https://img.shields.io/static/v1?label=Java&message=11&color=blue&logoColor=E23D28
+
+[jdk-download]: https://www.oracle.com/java/technologies/downloads/#java11
+
+[mockito-version]: https://img.shields.io/static/v1?label=Mockito&message=5.14.2&color=blue&logoColor=E23D28
+
+[maven-central-last-update]: https://img.shields.io/maven-central/last-update/io.github.vitalijr2.logging/mock-loggers-jdk-platform-logging
+
+[maven-central]: https://img.shields.io/maven-central/v/io.github.vitalijr2.logging/mock-loggers-jdk-platform-logging
+
+[maven-central-link]: https://central.sonatype.com/artifact/io.github.vitalijr2.logging/mock-loggers-jdk-platform-logging?smo=true
+
+[javadoc]: https://javadoc.io/badge2/io.github.vitalijr2.logging/mock-loggers-jdk-platform-logging/javadoc.svg
+
+[javadoc-link]: https://javadoc.io/doc/io.github.vitalijr2.logging/mock-loggers-jdk-platform-logging
+
+[junit-extension]: ../core/
diff --git a/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/extension-logging.properties b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/extension-logging.properties
new file mode 100644
index 0000000..cd35a2c
--- /dev/null
+++ b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/extension-logging.properties
@@ -0,0 +1,8 @@
+.level = INFO
+
+handlers = java.util.logging.ConsoleHandler
+
+java.util.logging.ConsoleHandler.level = ALL
+java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
+
+io.github.vitalijr2.mock.jdk.platform.logging.MockLoggerExtension.level = FINE
diff --git a/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/pom.xml b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/pom.xml
new file mode 100644
index 0000000..ebcc0be
--- /dev/null
+++ b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/pom.xml
@@ -0,0 +1,85 @@
+
+
+ hello-jdk-platform-logging-world
+
+
+
+ maven-compiler-plugin
+
+ ${java.version}
+
+ ${java.version}
+
+ org.apache.maven.plugins
+ 3.13.0
+
+
+ maven-surefire-plugin
+
+
+ extension-logging.properties
+
+
+ org.apache.maven.plugins
+ 3.5.2
+
+
+
+
+
+
+ @project.artifactId@
+ @project.groupId@
+ test
+ @project.version@
+
+
+ hamcrest
+ org.hamcrest
+ test
+ 3.0
+
+
+ junit-jupiter-api
+ org.junit.jupiter
+ test
+ ${junit-jupiter.version}
+
+
+ junit-jupiter-engine
+ org.junit.jupiter
+ test
+ ${junit-jupiter.version}
+
+
+ junit-jupiter-params
+ org.junit.jupiter
+ test
+ ${junit-jupiter.version}
+
+
+ mockito-core
+ org.mockito
+ test
+ ${mockito.version}
+
+
+ mockito-junit-jupiter
+ org.mockito
+ test
+ ${mockito.version}
+
+
+ Basic example
+ example.hello
+ 4.0.0
+
+ 11
+ 5.11.3
+ 5.14.2
+ UTF-8
+
+ 1.0.0
+
diff --git a/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/main/java/example/hello/HelloService.java b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/main/java/example/hello/HelloService.java
new file mode 100644
index 0000000..6693d81
--- /dev/null
+++ b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/main/java/example/hello/HelloService.java
@@ -0,0 +1,30 @@
+package example.hello;
+
+import static java.util.Objects.requireNonNull;
+
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+
+public class HelloService {
+
+ private final Logger logger = System.getLogger("HelloService");
+
+ public String sayHelloWorld() {
+ return sayHello("World");
+ }
+
+ public String sayHello(String name) {
+ if (requireNonNull(name, "Name is missed").isBlank()) {
+ throw new IllegalArgumentException("Name is empty");
+ }
+
+ var greeting = "Hello " + name + "!";
+
+ if (logger.isLoggable(Level.INFO)) {
+ logger.log(Level.INFO, greeting);
+ }
+
+ return greeting;
+ }
+
+}
diff --git a/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceAnnotationTest.java b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceAnnotationTest.java
new file mode 100644
index 0000000..0dee262
--- /dev/null
+++ b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceAnnotationTest.java
@@ -0,0 +1,47 @@
+package example.hello;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import io.github.vitalijr2.logging.mock.MockLoggers;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+@MockLoggers
+class HelloServiceAnnotationTest {
+
+ private static Logger logger;
+
+ @BeforeAll
+ static void setUpClass() {
+ logger = System.getLogger("HelloService");
+ }
+
+ @BeforeEach
+ void setUp() {
+ when(logger.isLoggable(Level.INFO)).thenReturn(true);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var logger = System.getLogger("HelloService");
+
+ verify(logger).isLoggable(Level.INFO);
+ verify(logger).log(Level.INFO, "Hello " + name + "!");
+ verifyNoMoreInteractions(logger);
+ }
+
+}
diff --git a/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceBasicTest.java b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceBasicTest.java
new file mode 100644
index 0000000..e896fef
--- /dev/null
+++ b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceBasicTest.java
@@ -0,0 +1,23 @@
+package example.hello;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.verify;
+
+import java.lang.System.Logger.Level;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class HelloServiceBasicTest {
+
+ @DisplayName("Hello world")
+ @Test
+ void helloWorld() {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(helloService::sayHelloWorld);
+
+ verify(System.getLogger("HelloService")).isLoggable(isA(Level.class));
+ }
+
+}
diff --git a/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceExtensionTest.java b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceExtensionTest.java
new file mode 100644
index 0000000..f2b5450
--- /dev/null
+++ b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceExtensionTest.java
@@ -0,0 +1,48 @@
+package example.hello;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import io.github.vitalijr2.logging.mock.MockLoggerExtension;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+@ExtendWith(MockLoggerExtension.class)
+class HelloServiceExtensionTest {
+
+ private static Logger logger;
+
+ @BeforeAll
+ static void setUpClass() {
+ logger = System.getLogger("HelloService");
+ }
+
+ @BeforeEach
+ void setUp() {
+ when(logger.isLoggable(Level.INFO)).thenReturn(true);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var logger = System.getLogger("HelloService");
+
+ verify(logger).isLoggable(Level.INFO);
+ verify(logger).log(Level.INFO, "Hello " + name + "!");
+ verifyNoMoreInteractions(logger);
+ }
+
+}
diff --git a/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceFullTest.java b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceFullTest.java
new file mode 100644
index 0000000..e5c99bb
--- /dev/null
+++ b/jdk-platform-logging/src/it/hello-jdk-platform-logging-world/src/test/java/example/hello/HelloServiceFullTest.java
@@ -0,0 +1,70 @@
+package example.hello;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.StringStartsWith.startsWith;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class HelloServiceFullTest {
+
+ private static Logger logger;
+
+ @BeforeAll
+ static void setUpClass() {
+ logger = System.getLogger("HelloService");
+ }
+
+ @BeforeEach
+ void setUp() {
+ clearInvocations(logger);
+ reset(logger);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ when(logger.isLoggable(Level.INFO)).thenReturn(true);
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLogger = System.getLogger("HelloService");
+
+ verify(actualLogger).isLoggable(Level.INFO);
+ verify(actualLogger).log(Level.INFO, "Hello " + name + "!");
+ verifyNoMoreInteractions(actualLogger);
+ }
+
+ @DisplayName("Null or empty name")
+ @ParameterizedTest(name = "<{0}>")
+ @NullAndEmptySource
+ @ValueSource(strings = " ")
+ void nullOrEmptyName(String name) {
+ var helloService = new HelloService();
+
+ var exception = assertThrows(RuntimeException.class, () -> helloService.sayHello(name));
+
+ verifyNoInteractions(System.getLogger("HelloService"));
+
+ assertThat(exception.getMessage(), startsWith("Name is"));
+ }
+
+
+}
diff --git a/jdk-platform-logging/src/it/settings.xml b/jdk-platform-logging/src/it/settings.xml
new file mode 100644
index 0000000..12780df
--- /dev/null
+++ b/jdk-platform-logging/src/it/settings.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ true
+
+ it-repo
+
+
+ local.central
+
+ true
+
+
+ true
+
+ @localRepositoryUrl@
+
+
+
+
+ local.central
+
+ true
+
+
+ true
+
+ @localRepositoryUrl@
+
+
+
+
+
diff --git a/jdk-platform-logging/src/main/java/io/github/vitalijr2/logging/mock/platform/MockLoggerFinder.java b/jdk-platform-logging/src/main/java/io/github/vitalijr2/logging/mock/platform/MockLoggerFinder.java
new file mode 100644
index 0000000..708246e
--- /dev/null
+++ b/jdk-platform-logging/src/main/java/io/github/vitalijr2/logging/mock/platform/MockLoggerFinder.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 Vitalij Berdinskih
+ *
+ * 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.
+ */
+package io.github.vitalijr2.logging.mock.platform;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+
+import io.github.vitalijr2.logging.mock.MockLoggerCleaner;
+import java.lang.System.Logger;
+import java.lang.System.LoggerFinder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.jetbrains.annotations.VisibleForTesting;
+
+/**
+ * Uses {@link org.mockito.Mockito#mock(Class, String)} to get a mock that is adapted for {@link Logger}.
+ *
+ * Example:
+ *
+ * {@literal @}Test
+ * void helloWorld() {
+ * var helloService = new HelloService();
+ *
+ * assertDoesNotThrow(helloService::sayHelloWorld);
+ *
+ * verify(System.getLogger("HelloService")).log(Level.INFO, "Hello World!");
+ * }
+ *
+ *
+ * @since 1.0.0
+ */
+public class MockLoggerFinder extends LoggerFinder implements MockLoggerCleaner {
+
+ private final Map loggers;
+
+ /**
+ * Create a map-based logger finder. The finder uses a concurrent map: a logger name is a key.
+ */
+ public MockLoggerFinder() {
+ this(new ConcurrentHashMap<>());
+ }
+
+ @VisibleForTesting
+ MockLoggerFinder(Map loggers) {
+ this.loggers = loggers;
+ subscribeToNotifications();
+ }
+
+ @Override
+ public List cleanAndReset() {
+ var processedLoggers = new ArrayList();
+
+ loggers.forEach((loggerName, logger) -> {
+ clearInvocations(logger);
+ reset(logger);
+ processedLoggers.add(loggerName);
+ });
+
+ return processedLoggers;
+ }
+
+ /**
+ * Returns an instance of Logger for the given name, module is ignored.
+ *
+ * @param name logging name
+ * @param module logging module
+ * @return mock logger
+ */
+ @Override
+ public Logger getLogger(String name, Module module) {
+ return loggers.computeIfAbsent(name, key -> mock(Logger.class, "Mock for logger " + key));
+ }
+
+}
diff --git a/jdk-platform-logging/src/main/java/io/github/vitalijr2/logging/mock/platform/package-info.java b/jdk-platform-logging/src/main/java/io/github/vitalijr2/logging/mock/platform/package-info.java
new file mode 100644
index 0000000..6a8debd
--- /dev/null
+++ b/jdk-platform-logging/src/main/java/io/github/vitalijr2/logging/mock/platform/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Mock loggers for JDK Platform Logging backed by Mockito.
+ *
+ * @since 1.0.0
+ */
+package io.github.vitalijr2.logging.mock.platform;
diff --git a/jdk-platform-logging/src/main/resources/META-INF/services/java.lang.System$LoggerFinder b/jdk-platform-logging/src/main/resources/META-INF/services/java.lang.System$LoggerFinder
new file mode 100644
index 0000000..2098a34
--- /dev/null
+++ b/jdk-platform-logging/src/main/resources/META-INF/services/java.lang.System$LoggerFinder
@@ -0,0 +1 @@
+io.github.vitalijr2.logging.mock.platform.MockLoggerFinder
diff --git a/jdk-platform-logging/src/test/java/io/github/vitalijr2/logging/mock/platform/MockLoggerFinderFastTest.java b/jdk-platform-logging/src/test/java/io/github/vitalijr2/logging/mock/platform/MockLoggerFinderFastTest.java
new file mode 100644
index 0000000..7bb6b12
--- /dev/null
+++ b/jdk-platform-logging/src/test/java/io/github/vitalijr2/logging/mock/platform/MockLoggerFinderFastTest.java
@@ -0,0 +1,71 @@
+package io.github.vitalijr2.logging.mock.platform;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.hamcrest.collection.IsMapContaining.hasEntry;
+import static org.hamcrest.collection.IsMapWithSize.aMapWithSize;
+import static org.hamcrest.core.Is.isA;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.StringStartsWith.startsWith;
+import static org.hamcrest.object.HasToString.hasToString;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.mockito.Mockito.mock;
+
+import java.lang.System.Logger;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("fast")
+class MockLoggerFinderFastTest {
+
+ @DisplayName("Create and add logger")
+ @Test
+ void createLogger() {
+ // given
+ var loggers = new HashMap();
+ var loggerFinder = new MockLoggerFinder(loggers);
+
+ // when
+ loggerFinder.getLogger("test", getClass().getModule());
+
+ // then
+ assertAll("Logger was created", () -> assertThat("size", loggers, aMapWithSize(1)),
+ () -> assertThat("entry", loggers, hasEntry(equalTo("test"), isA(Logger.class))),
+ () -> assertThat(loggers.values().iterator().next(), hasToString("Mock for logger test")));
+ }
+
+ @DisplayName("Reuse existing logger")
+ @Test
+ void reuseExistingLogger() {
+ // given
+ var loggers = new HashMap<>(Map.of("test", mock(Logger.class)));
+ var loggerFinder = new MockLoggerFinder(loggers);
+
+ // when
+ loggerFinder.getLogger("test", getClass().getModule());
+
+ // then
+ assertAll("Logger was reused", () -> assertThat("size", loggers, aMapWithSize(1)),
+ () -> assertThat("entry", loggers, hasEntry(equalTo("test"), isA(Logger.class))),
+ () -> assertThat("Logger was created outside", loggers.values().iterator().next(),
+ hasToString(startsWith("Mock for Logger, hashCode:"))));
+ }
+
+ @DisplayName("Clean and reset loggers")
+ @Test
+ void cleanAndResetLoggers() {
+ // given
+ var loggers = new HashMap<>(Map.of("a", mock(Logger.class), "b", mock(Logger.class), "c", mock(Logger.class)));
+ var loggerFinder = new MockLoggerFinder(loggers);
+
+ // when
+ var loggerNames = loggerFinder.cleanAndReset();
+
+ // then
+ assertThat(loggerNames, contains("a", "b", "c"));
+ }
+
+}
diff --git a/jdk-platform-logging/src/test/java/io/github/vitalijr2/logging/mock/platform/MockLoggerFinderSlowTest.java b/jdk-platform-logging/src/test/java/io/github/vitalijr2/logging/mock/platform/MockLoggerFinderSlowTest.java
new file mode 100644
index 0000000..d10e02e
--- /dev/null
+++ b/jdk-platform-logging/src/test/java/io/github/vitalijr2/logging/mock/platform/MockLoggerFinderSlowTest.java
@@ -0,0 +1,61 @@
+package io.github.vitalijr2.logging.mock.platform;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import io.github.vitalijr2.logging.mock.MockLoggerKeeper;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+@Tag("slow")
+class MockLoggerFinderSlowTest {
+
+ private static Logger logger;
+
+ @BeforeAll
+ static void setUpClass() {
+ logger = System.getLogger("test");
+ }
+
+ @AfterEach
+ void tearDown() {
+ clearInvocations(logger);
+ }
+
+ @DisplayName("Test")
+ @ParameterizedTest
+ @ValueSource(strings = {"TRACE", "DEBUG", "INFO", "WARNING", "ERROR"})
+ void test(Level level) {
+ // given
+ verifyNoInteractions(logger);
+
+ // when
+ System.getLogger("test").log(level, "test message");
+
+ // then
+ verify(logger).log(level, "test message");
+ }
+
+ @DisplayName("Clean and reset mock loggers")
+ @Test
+ void cleanAndResetMockLoggers() {
+ // when
+ var loggerFinder = new MockLoggerFinder();
+
+ loggerFinder.getLogger("test", null).log(Level.INFO, "test message");
+
+ MockLoggerKeeper.getInstance().cleanAndReset();
+
+ // then
+ verifyNoInteractions(loggerFinder.getLogger("test", null));
+ }
+
+}
diff --git a/mvnw b/mvnw
new file mode 100755
index 0000000..5e9618c
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,332 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.3.2
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ]; then
+
+ if [ -f /usr/local/etc/mavenrc ]; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ]; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ]; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false
+darwin=false
+mingw=false
+case "$(uname)" in
+CYGWIN*) cygwin=true ;;
+MINGW*) mingw=true ;;
+Darwin*)
+ darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"
+ export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"
+ export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ]; then
+ if [ -r /etc/gentoo-release ]; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] \
+ && JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] \
+ && CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \
+ && JAVA_HOME="$(
+ cd "$JAVA_HOME" || (
+ echo "cannot cd into $JAVA_HOME." >&2
+ exit 1
+ )
+ pwd
+ )"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin; then
+ javaHome="$(dirname "$javaExecutable")"
+ javaExecutable="$(cd "$javaHome" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "$javaExecutable")"
+ fi
+ javaHome="$(dirname "$javaExecutable")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ]; then
+ if [ -n "$JAVA_HOME" ]; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(
+ \unset -f command 2>/dev/null
+ \command -v java
+ )"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ]; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ echo "Warning: JAVA_HOME environment variable is not set." >&2
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]; then
+ echo "Path not specified to find_maven_basedir" >&2
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ]; do
+ if [ -d "$wdir"/.mvn ]; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(
+ cd "$wdir/.." || exit 1
+ pwd
+ )
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(
+ cd "$basedir" || exit 1
+ pwd
+ )"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' <"$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in wrapperUrl)
+ wrapperUrl="$safeValue"
+ break
+ ;;
+ esac
+ done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget >/dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl >/dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in wrapperSha256Sum)
+ wrapperSha256Sum=$value
+ break
+ ;;
+ esac
+done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum >/dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c >/dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] \
+ && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] \
+ && CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] \
+ && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 0000000..1204076
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,206 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.2
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo. >&2
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo. >&2
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo. >&2
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo. >&2
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..438f63a
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,497 @@
+
+
+
+ mock-loggers
+
+ clean verify
+
+
+
+ central-publishing-maven-plugin
+ org.sonatype.central
+ 0.6.0
+
+
+ flatten-maven-plugin
+
+ ossrh
+
+
+
+
+ flatten
+
+ flatten
+ process-resources
+
+
+
+ clean
+
+ clean
+ clean
+
+
+ org.codehaus.mojo
+ 1.6.0
+
+
+ jacoco-maven-plugin
+
+
+
+ ${project.build.directory}/jacoco-ut.exec
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+ prepare-agent-integration
+
+ prepare-agent-integration
+
+
+
+ ${project.build.directory}/jacoco.exec
+
+
+ ${project.build.directory}
+
+ *.exec
+
+
+
+
+
+ merge
+
+ merge-results
+ verify
+
+
+
+ ${project.build.directory}/jacoco.exec
+ ${project.reporting.outputDirectory}/jacoco
+
+
+ report
+
+ report
+ verify
+
+
+
+ ${project.build.directory}/jacoco.exec
+
+
+ BUNDLE
+
+
+ COMPLEXITY
+ 0.72
+ COVEREDRATIO
+
+
+
+
+
+
+ check
+
+ check-coverage
+ verify
+
+
+ org.jacoco
+ 0.8.12
+
+
+ maven-clean-plugin
+ org.apache.maven.plugins
+ 3.4.0
+
+
+ maven-compiler-plugin
+
+ ${java.version}
+
+ ${java.version}
+
+ org.apache.maven.plugins
+ 3.13.0
+
+
+ maven-enforcer-plugin
+
+
+
+ [${java.version},)
+
+
+ [3.9.0,)
+
+
+
+
+
+
+ enforce
+
+ enforce-maven
+
+
+
+ enforce
+
+ enforce-clean
+ pre-clean
+
+
+ org.apache.maven.plugins
+ 3.5.0
+
+
+ maven-failsafe-plugin
+
+ 1
+
+ **/*Test.java
+
+
+ slow
+
+ false
+
+
+
+
+ integration-test
+ verify
+
+
+
+ org.apache.maven.plugins
+ 3.5.2
+
+
+ maven-install-plugin
+ org.apache.maven.plugins
+ 3.1.3
+
+
+ maven-invoker-plugin
+
+ ${project.build.directory}/it
+
+ clean
+ verify
+
+ ${project.build.directory}/local-repo
+
+ */pom.xml
+
+ verify
+ src/it/settings.xml
+
+
+
+
+ install
+ run
+
+ integration-test
+
+
+ org.apache.maven.plugins
+ 3.8.1
+
+
+ maven-jar-plugin
+
+
+
+ ${java.vm.version} (${java.vm.vendor})
+ ${project.name}
+
+
+
+ org.apache.maven.plugins
+ 3.4.2
+
+
+ maven-javadoc-plugin
+
+
+
+ -J-Dhttp.agent=maven-javadoc-plugin_${project.groupId}:${project.artifactId}
+
+
+
+ --allow-script-in-comments
+
+
+ ]]>
+ true
+ ${java.home}/bin/javadoc
+
+ https://javadoc.io/doc/org.mockito/mockito-core/${mockito.version}/
+ https://javadoc.io/doc/org.junit.jupiter/junit-jupiter-api/${junit.version}/
+
+ false
+ public
+
+
+ ]]>
+
+ org.apache.maven.plugins
+ ${javadoc-plugin.version}
+
+
+ maven-resources-plugin
+
+ ISO-8859-1
+
+ org.apache.maven.plugins
+ 3.3.1
+
+
+ maven-source-plugin
+ org.apache.maven.plugins
+ 3.3.1
+
+
+ maven-surefire-plugin
+
+
+ fast
+
+
+ org.apache.maven.plugins
+ 3.5.2
+
+
+ sign-maven-plugin
+ org.simplify4u.plugins
+ 1.1.0
+
+
+
+
+
+ GitHub Actions
+ https://github.com/vitalijr2/mock-loggers/actions
+
+
+
+
+
+ annotations
+ org.jetbrains
+ 26.0.1
+
+
+ junit-jupiter-api
+ org.junit.jupiter
+ provided
+ ${junit.version}
+
+
+ junit-platform-commons
+ org.junit.platform
+ provided
+ 1.11.3
+
+
+ mockito-core
+ org.mockito
+ provided
+ ${mockito.version}
+
+
+
+ hamcrest
+ org.hamcrest
+ test
+ 3.0
+
+
+ junit-jupiter-engine
+ org.junit.jupiter
+ test
+ ${junit.version}
+
+
+ junit-jupiter-params
+ org.junit.jupiter
+ test
+ ${junit.version}
+
+
+ mockito-junit-jupiter
+ org.mockito
+ test
+ ${mockito.version}
+
+
+
+
+ Different logging services with mock loggers backed by Mockito.
+ Now this library implements JDK Platform Logging and Apache Commons Logging.
+
+
+
+ vitalij_r2@outlook.com
+ vitalijr2
+ Vitalij Berdinskih
+ Europe/Kyiv
+ https://github.com/vitalijr2
+
+
+ io.github.vitalijr2.logging
+ 2024
+
+ GitHub Issues
+ https://github.com/vitalijr2/mock-loggers/issues
+
+
+
+ See NOTICE for third-party licenses.
+ repo
+ Apache 2
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+
+
+ 4.0.0
+
+ commons-logging
+ core
+ jdk-platform-logging
+ slf4j
+
+ Mock Loggers
+ pom
+
+
+
+ clean deploy
+
+
+ central-publishing-maven-plugin
+
+ central
+
+ true
+ org.sonatype.central
+
+
+ maven-javadoc-plugin
+
+
+
+ jar
+
+ attach-javadocs
+
+
+ org.apache.maven.plugins
+
+
+ maven-source-plugin
+
+
+
+ jar
+
+ attach-sources
+
+
+ org.apache.maven.plugins
+
+
+ sign-maven-plugin
+
+
+
+ sign
+
+
+
+ org.simplify4u.plugins
+
+
+
+ release
+
+
+
+ clean install
+
+
+ jacoco-maven-plugin
+
+ true
+
+ org.jacoco
+
+
+ maven-failsafe-plugin
+
+ true
+
+ org.apache.maven.plugins
+
+
+ maven-install-plugin
+
+ false
+
+ org.apache.maven.plugins
+
+
+ maven-surefire-plugin
+
+ true
+
+ org.apache.maven.plugins
+
+
+
+ run-its
+
+
+
+ 11
+ 3.11.1
+ 5.11.3
+ 5.14.2
+ UTF-8
+
+
+ scm:git:https://github.com/vitalijr2/mock-loggers.git
+ scm:git:git@github.com:vitalijr2/mock-loggers.git
+ https://github.com/vitalijr2/mock-loggers
+
+ https://github.com/vitalijr2/mock-loggers
+ 1.0.0
+
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..25d98ac
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,119 @@
+# Mock Loggers
+
+Different logging services can be tested using mock loggers backed by [Mockito][].
+
+> [!WARNING]
+> This library does not support _parallel test execution_.
+
+[![Java Version][java-version]][jdk-download]
+![jUnit Version][junit-version]
+![Mockito Version][mockito-version]
+[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0.html)
+[![GitHub master check runs][github-master-check-runs]][github-master-check-runs-link]
+[![Codacy Badge][codacy-badge]][codacy-badge-link]
+[![Codacy Coverage][codacy-coverage]][codacy-coverage-link]
+![GitHub commit activity][github-commit-activity]
+[![Today's hits][today-hits]][today-hits-link]
+
+## How to use
+
+The simplest usage example looks like this:
+```java
+@Test
+void helloWorld() {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(helloService::sayHelloWorld);
+
+ verify(System.getLogger("HelloService")).log(Level.INFO, "Hello World!");
+}
+```
+
+Now this library implements services for [JDK Platform Logging][jdk-logging],
+[Apache Commons Logging][commons-logging] and [SLF4J][slf4j].
+
+See more examples in the relevant modules of this project:
+
+- for Apache Commons Logging in [mock-loggers-commons-logging](commons-logging)
+- for JDK Platform Logging in [mock-loggers-jdk-platform-logging](jdk-platform-logging)
+- for SLF4J in [mock-loggers-slf4j](slf4j)
+
+## Other logging libraries and frameworks
+
+- [Apache Log4j: Unit Testing in Maven][log4j-unit-testing-in-maven]
+- [Unit Test logback Using JUnit][logback-unit-test-using-junit], it's old but gold
+
+## Credits
+
+There are two projects which inspired me to make this library:
+
+- [s4u/slf4j-mock][slf4j-mock]
+- [ocarlsen/mock-slf4j-impl][mock-slf4j-impl]
+
+## Contributing
+
+Please read [Contributing](contributing.md).
+
+## History
+
+See [Changelog](changelog.md)
+
+## License
+
+Copyright 2024 Vitalij Berdinskih
+
+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
+
+
+
+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.
+
+[Apache License v2.0](LICENSE)
+
+[Mockito]: https://site.mockito.org
+
+[jdk-logging]: https://www.baeldung.com/java-9-logging-api "Java Platform Logging API"
+
+[commons-logging]: https://commons.apache.org/proper/commons-logging/
+
+[slf4j]: https://www.slf4j.org/
+
+[java-version]: https://img.shields.io/static/v1?label=Java&message=11&color=blue&logoColor=E23D28
+
+[jdk-download]: https://www.oracle.com/java/technologies/downloads/#java11
+
+[junit-version]: https://img.shields.io/static/v1?label=jUnit&message=5.11.3&color=blue&logo=junit5&logoColor=E23D28
+
+[mockito-version]: https://img.shields.io/static/v1?label=Mockito&message=5.14.2&color=blue&logoColor=E23D28
+
+[github-master-check-runs]: https://img.shields.io/github/check-runs/vitalijr2/mock-loggers/master
+
+[github-master-check-runs-link]: https://github.com/vitalijr2/mock-loggers/actions?query=branch%3Amaster
+
+[codacy-badge]: https://app.codacy.com/project/badge/Grade/3c0345d6db684e388deb3357362526c0
+
+[codacy-badge-link]: https://app.codacy.com/gh/vitalijr2/mock-loggers/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade
+
+[codacy-coverage]: https://app.codacy.com/project/badge/Coverage/3c0345d6db684e388deb3357362526c0
+
+[codacy-coverage-link]: https://app.codacy.com/gh/vitalijr2/mock-loggers/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage
+
+[github-commit-activity]: https://img.shields.io/github/commit-activity/y/vitalijr2/mock-loggers
+
+[today-hits]: https://hits.sh/github.com/vitalijr2/mock-loggers.svg?view=today-total&label=today's%20hits
+
+[today-hits-link]: https://hits.sh/github.com/vitalijr2/mock-loggers/
+
+[log4j-unit-testing-in-maven]: https://logging.apache.org/log4j/2.3.x/manual/configuration.html#UnitTestingInMaven
+
+[logback-unit-test-using-junit]: https://www.iamninad.com/posts/unit-test-logback-using-junit/
+
+[slf4j-mock]: https://github.com/s4u/slf4j-mock
+
+[mock-slf4j-impl]: https://github.com/ocarlsen/mock-slf4j-impl
diff --git a/security.md b/security.md
new file mode 100644
index 0000000..c4c1d09
--- /dev/null
+++ b/security.md
@@ -0,0 +1,9 @@
+# Security Policy
+
+## Supported Versions
+
+Only the last stable version at any given point.
+
+## Reporting a Vulnerability
+
+Vulnerabilities can be disclosed via email to Vitalij_R2@outlook.com.
diff --git a/slf4j/pom.xml b/slf4j/pom.xml
new file mode 100644
index 0000000..7136337
--- /dev/null
+++ b/slf4j/pom.xml
@@ -0,0 +1,103 @@
+
+
+
+ mock-loggers-slf4j
+
+
+
+ maven-surefire-plugin
+ org.apache.maven.plugins
+
+
+ maven-failsafe-plugin
+ org.apache.maven.plugins
+
+
+ jacoco-maven-plugin
+ org.jacoco
+
+
+
+
+
+
+ annotations
+ org.jetbrains
+ provided
+
+
+ mock-loggers-core
+ io.github.vitalijr2.logging
+ 1.0.0
+
+
+ mockito-core
+ org.mockito
+ provided
+
+
+ slf4j-api
+ org.slf4j
+ provided
+ 2.0.16
+
+
+
+ hamcrest
+ org.hamcrest
+ test
+
+
+ junit-jupiter-engine
+ org.junit.jupiter
+ provided
+
+
+ junit-jupiter-params
+ org.junit.jupiter
+ provided
+
+
+ mockito-junit-jupiter
+ org.mockito
+ provided
+
+
+ Mock loggers for SLF4J backed by Mockito.
+ 4.0.0
+ Mock loggers for SLF4J
+
+ mock-loggers
+ io.github.vitalijr2.logging
+ 1.0.0
+
+
+
+
+
+
+ maven-invoker-plugin
+ org.apache.maven.plugins
+
+
+
+ run-its
+
+
+
diff --git a/slf4j/readme.md b/slf4j/readme.md
new file mode 100644
index 0000000..4f03b1b
--- /dev/null
+++ b/slf4j/readme.md
@@ -0,0 +1,160 @@
+# Mock loggers for SLF4J
+
+[SLF4J][slf4j] factory with mock loggers backed by [Mockito][].
+
+> [!WARNING]
+> This library does not support _parallel test execution_.
+
+[![Java Version][java-version]][jdk-download]
+![SLF4J Version][slf4j-version]
+![Mockito Version][mockito-version]
+![Maven Central Last Update][maven-central-last-update]
+[![Maven Central][maven-central]][maven-central-link]
+[![Javadoc][javadoc]][javadoc-link]
+
+## How to use
+
+Just put a test dependency to your POM:
+```xml
+
+ mock-loggers-slf4j
+ io.github.vitalijr2.logging
+ test
+ 1.0.0
+
+```
+
+The simplest usage example looks like this:
+```java
+@Test
+void helloWorld() {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(helloService::sayHelloWorld);
+
+ verify(LoggerFactory.getLogger(helloService.getClass())).info("Hello World!");
+}
+```
+See more details at [HelloServiceBasicTest.java](src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceBasicTest.java)
+
+> [!IMPORTANT]
+> Keep in mind that all loggers are initialized only once during the test run.
+
+Therefore, a more complex example cleans the loggers after (or before) each test:
+```java
+// the static logger instance
+private static Logger logger;
+
+// initialize the mock logger once
+@BeforeAll
+static void setUpClass() {
+ logger = LoggerFactory.getLogger(HelloService.class);
+}
+
+// clean the mock logger after each test
+@AfterEach
+void tearDown() {
+ clearInvocations(logger);
+}
+
+// use the mock logger in a test
+@DisplayName("Names")
+@ParameterizedTest(name = "<{0}>")
+@ValueSource(strings = {"John", "Jane"})
+void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLogger = LoggerFactory.getLogger(helloService.getClass());
+
+ verify(actualLogger).info("Hello " + name + "!");
+ verifyNoMoreInteractions(actualLogger);
+}
+```
+See more details at [HelloServiceFullTest.java](src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceFullTest.java)
+
+To avoid manual cleaning of mock loggers you can use the [jUnit extension][junit-extension] for automation.
+
+```java
+@ExtendWith(MockLoggerExtension.class)
+class HelloServiceExtensionTest {
+
+ private static Logger logger;
+
+ @BeforeAll
+ static void setUpClass() {
+ log = LogFactory.getLog(HelloService.class);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLogger = LoggerFactory.getLogger(helloService.getClass());
+
+ verify(actualLogger).info("Hello " + name + "!");
+ verifyNoMoreInteractions(actualLogger);
+ }
+
+}
+```
+See more details at [HelloServiceExtensionTest.java](src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceExtensionTest.java)
+
+Also you can use the annotation for automation.
+```java
+@MockLoggers
+class HelloServiceAnnotationTest {
+
+ private static Logger logger;
+
+ @BeforeAll
+ static void setUpClass() {
+ logger = LogFactory.getLog(HelloService.class);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLogger = LogFactory.getLog(helloService.getClass());
+
+ verify(actualLogger).info("Hello " + name + "!");
+ verifyNoMoreInteractions(actualLogger);
+ }
+
+}
+```
+See more details at [HelloServiceAnnotationTest.java](src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceAnnotationTest.java)
+
+[slf4j]: https://www.slf4j.org/
+
+[Mockito]: https://site.mockito.org
+
+[java-version]: https://img.shields.io/static/v1?label=Java&message=11&color=blue&logoColor=E23D28
+
+[jdk-download]: https://www.oracle.com/java/technologies/downloads/#java11
+
+[slf4j-version]: https://img.shields.io/static/v1?label=SLF4J&message=2.0.16&color=blue&logoColor=E23D28
+
+[mockito-version]: https://img.shields.io/static/v1?label=Mockito&message=5.14.2&color=blue&logoColor=E23D28
+
+[maven-central-last-update]: https://img.shields.io/maven-central/last-update/io.github.vitalijr2.logging/mock-loggers-slf4j
+
+[maven-central]: https://img.shields.io/maven-central/v/io.github.vitalijr2.logging/mock-loggers-slf4j
+
+[maven-central-link]: https://central.sonatype.com/artifact/io.github.vitalijr2.logging/mock-loggers-slf4j?smo=true
+
+[javadoc]: https://javadoc.io/badge2/io.github.vitalijr2.logging/mock-loggers-slf4j/javadoc.svg
+
+[javadoc-link]: https://javadoc.io/doc/io.github.vitalijr2.logging/mock-loggers-slf4j
+
+[junit-extension]: ../core/
diff --git a/slf4j/src/it/hello-slf4j-world/extension-logging.properties b/slf4j/src/it/hello-slf4j-world/extension-logging.properties
new file mode 100644
index 0000000..cd35a2c
--- /dev/null
+++ b/slf4j/src/it/hello-slf4j-world/extension-logging.properties
@@ -0,0 +1,8 @@
+.level = INFO
+
+handlers = java.util.logging.ConsoleHandler
+
+java.util.logging.ConsoleHandler.level = ALL
+java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
+
+io.github.vitalijr2.mock.jdk.platform.logging.MockLoggerExtension.level = FINE
diff --git a/slf4j/src/it/hello-slf4j-world/pom.xml b/slf4j/src/it/hello-slf4j-world/pom.xml
new file mode 100644
index 0000000..cae231f
--- /dev/null
+++ b/slf4j/src/it/hello-slf4j-world/pom.xml
@@ -0,0 +1,91 @@
+
+
+ hello-slf4j-world
+
+
+
+ maven-compiler-plugin
+
+ ${java.version}
+
+ ${java.version}
+
+ org.apache.maven.plugins
+ 3.13.0
+
+
+ maven-surefire-plugin
+
+
+ extension-logging.properties
+
+
+ org.apache.maven.plugins
+ 3.5.2
+
+
+
+
+
+
+ slf4j-api
+ org.slf4j
+ 2.0.16
+
+
+
+ @project.artifactId@
+ @project.groupId@
+ test
+ @project.version@
+
+
+ hamcrest
+ org.hamcrest
+ test
+ 3.0
+
+
+ junit-jupiter-api
+ org.junit.jupiter
+ test
+ ${junit-jupiter.version}
+
+
+ junit-jupiter-engine
+ org.junit.jupiter
+ test
+ ${junit-jupiter.version}
+
+
+ junit-jupiter-params
+ org.junit.jupiter
+ test
+ ${junit-jupiter.version}
+
+
+ mockito-core
+ org.mockito
+ test
+ ${mockito.version}
+
+
+ mockito-junit-jupiter
+ org.mockito
+ test
+ ${mockito.version}
+
+
+ Basic example
+ example.hello
+ 4.0.0
+
+ 11
+ 5.11.3
+ 5.14.2
+ UTF-8
+
+ 1.0.0
+
diff --git a/slf4j/src/it/hello-slf4j-world/src/main/java/example/hello/HelloService.java b/slf4j/src/it/hello-slf4j-world/src/main/java/example/hello/HelloService.java
new file mode 100644
index 0000000..8d60d28
--- /dev/null
+++ b/slf4j/src/it/hello-slf4j-world/src/main/java/example/hello/HelloService.java
@@ -0,0 +1,30 @@
+package example.hello;
+
+import static java.util.Objects.requireNonNull;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class HelloService {
+
+ private final Logger logger = LoggerFactory.getLogger(HelloService.class);
+
+ public String sayHelloWorld() {
+ return sayHello("World");
+ }
+
+ public String sayHello(String name) {
+ if (requireNonNull(name, "Name is missed").isBlank()) {
+ throw new IllegalArgumentException("Name is empty");
+ }
+
+ var greeting = "Hello " + name + "!";
+
+ if (logger.isInfoEnabled()) {
+ logger.info(greeting);
+ }
+
+ return greeting;
+ }
+
+}
diff --git a/slf4j/src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceAnnotationTest.java b/slf4j/src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceAnnotationTest.java
new file mode 100644
index 0000000..3045bb3
--- /dev/null
+++ b/slf4j/src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceAnnotationTest.java
@@ -0,0 +1,47 @@
+package example.hello;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import io.github.vitalijr2.logging.mock.MockLoggers;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@MockLoggers
+class HelloServiceAnnotationTest {
+
+ private static Logger logger;
+
+ @BeforeAll
+ static void setUpClass() {
+ logger = LoggerFactory.getLogger(HelloService.class);
+ }
+
+ @BeforeEach
+ void setUp() {
+ when(logger.isInfoEnabled()).thenReturn(true);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLogger = LoggerFactory.getLogger(helloService.getClass());
+
+ verify(actualLogger).isInfoEnabled();
+ verify(actualLogger).info("Hello " + name + "!");
+ verifyNoMoreInteractions(actualLogger);
+ }
+
+}
diff --git a/slf4j/src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceBasicTest.java b/slf4j/src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceBasicTest.java
new file mode 100644
index 0000000..9f82596
--- /dev/null
+++ b/slf4j/src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceBasicTest.java
@@ -0,0 +1,22 @@
+package example.hello;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.verify;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+
+class HelloServiceBasicTest {
+
+ @DisplayName("Hello world")
+ @Test
+ void helloWorld() {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(helloService::sayHelloWorld);
+
+ verify(LoggerFactory.getLogger(helloService.getClass())).isInfoEnabled();
+ }
+
+}
diff --git a/slf4j/src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceExtensionTest.java b/slf4j/src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceExtensionTest.java
new file mode 100644
index 0000000..e9459a4
--- /dev/null
+++ b/slf4j/src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceExtensionTest.java
@@ -0,0 +1,48 @@
+package example.hello;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import io.github.vitalijr2.logging.mock.MockLoggerExtension;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(MockLoggerExtension.class)
+class HelloServiceExtensionTest {
+
+ private static Logger logger;
+
+ @BeforeAll
+ static void setUpClass() {
+ logger = LoggerFactory.getLogger(HelloService.class);
+ }
+
+ @BeforeEach
+ void setUp() {
+ when(logger.isInfoEnabled()).thenReturn(true);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLogger = LoggerFactory.getLogger(helloService.getClass());
+
+ verify(actualLogger).isInfoEnabled();
+ verify(actualLogger).info("Hello " + name + "!");
+ verifyNoMoreInteractions(actualLogger);
+ }
+
+}
diff --git a/slf4j/src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceFullTest.java b/slf4j/src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceFullTest.java
new file mode 100644
index 0000000..9226a49
--- /dev/null
+++ b/slf4j/src/it/hello-slf4j-world/src/test/java/example/hello/HelloServiceFullTest.java
@@ -0,0 +1,69 @@
+package example.hello;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.StringStartsWith.startsWith;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class HelloServiceFullTest {
+
+ private static Logger logger;
+
+ @BeforeAll
+ static void setUpClass() {
+ logger = LoggerFactory.getLogger(HelloService.class);
+ }
+
+ @BeforeEach
+ void setUp() {
+ clearInvocations(logger);
+ reset(logger);
+ }
+
+ @DisplayName("Names")
+ @ParameterizedTest(name = "<{0}>")
+ @ValueSource(strings = {"John", "Jane"})
+ void names(String name) {
+ var helloService = new HelloService();
+
+ when(logger.isInfoEnabled()).thenReturn(true);
+
+ assertDoesNotThrow(() -> helloService.sayHello(name));
+
+ var actualLogger = LoggerFactory.getLogger(helloService.getClass());
+
+ verify(actualLogger).isInfoEnabled();
+ verify(actualLogger).info("Hello " + name + "!");
+ verifyNoMoreInteractions(actualLogger);
+ }
+
+ @DisplayName("Null or empty name")
+ @ParameterizedTest(name = "<{0}>")
+ @NullAndEmptySource
+ @ValueSource(strings = " ")
+ void nullOrEmptyName(String name) {
+ var helloService = new HelloService();
+
+ var exception = assertThrows(RuntimeException.class, () -> helloService.sayHello(name));
+
+ verifyNoInteractions(LoggerFactory.getLogger(helloService.getClass()));
+
+ assertThat(exception.getMessage(), startsWith("Name is"));
+ }
+
+}
diff --git a/slf4j/src/it/settings.xml b/slf4j/src/it/settings.xml
new file mode 100644
index 0000000..12780df
--- /dev/null
+++ b/slf4j/src/it/settings.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ true
+
+ it-repo
+
+
+ local.central
+
+ true
+
+
+ true
+
+ @localRepositoryUrl@
+
+
+
+
+ local.central
+
+ true
+
+
+ true
+
+ @localRepositoryUrl@
+
+
+
+
+
diff --git a/slf4j/src/main/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerFactory.java b/slf4j/src/main/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerFactory.java
new file mode 100644
index 0000000..08d9629
--- /dev/null
+++ b/slf4j/src/main/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerFactory.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024 Vitalij Berdinskih
+ *
+ * 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.
+ */
+package io.github.vitalijr2.logging.mock.slf4j;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+
+import io.github.vitalijr2.logging.mock.MockLoggerCleaner;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.jetbrains.annotations.VisibleForTesting;
+import org.slf4j.ILoggerFactory;
+import org.slf4j.Logger;
+
+/**
+ * Uses {@link org.mockito.Mockito#mock(Class, String)} to get a mock that is adapted for {@link System.Logger}.
+ *
+ * Example:
+ *
+ * {@literal @}Test
+ * void helloWorld() {
+ * var helloService = new HelloService();
+ *
+ * assertDoesNotThrow(helloService::sayHelloWorld);
+ *
+ * verify(LoggerFactory.getLogger("HelloService")).log(Level.INFO, "Hello World!");
+ * }
+ *
+ *
+ * @since 1.0.0
+ */
+public class MockLoggerFactory implements ILoggerFactory, MockLoggerCleaner {
+
+ private final Map loggers;
+
+ /**
+ * Create a map-based logger finder. The finder uses a concurrent map: a logger name is a key.
+ */
+ public MockLoggerFactory() {
+ this(new ConcurrentHashMap<>());
+ }
+
+ @VisibleForTesting
+ public MockLoggerFactory(Map loggers) {
+ this.loggers = loggers;
+ subscribeToNotifications();
+ }
+
+ @Override
+ public List cleanAndReset() {
+ var processedLoggers = new ArrayList();
+
+ loggers.forEach((loggerName, logger) -> {
+ clearInvocations(logger);
+ reset(logger);
+ processedLoggers.add(loggerName);
+ });
+
+ return processedLoggers;
+ }
+
+ /**
+ * Returns an instance of Logger for the given name.
+ *
+ * @param name logging name
+ * @return mock logger
+ */
+ @Override
+ public Logger getLogger(String name) {
+ return loggers.computeIfAbsent(name, key -> mock(Logger.class, "Mock for logger " + key));
+ }
+
+}
diff --git a/slf4j/src/main/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerServiceProvider.java b/slf4j/src/main/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerServiceProvider.java
new file mode 100644
index 0000000..2af0af6
--- /dev/null
+++ b/slf4j/src/main/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerServiceProvider.java
@@ -0,0 +1,54 @@
+package io.github.vitalijr2.logging.mock.slf4j;
+
+import org.slf4j.ILoggerFactory;
+import org.slf4j.IMarkerFactory;
+import org.slf4j.helpers.BasicMarkerFactory;
+import org.slf4j.helpers.NOPMDCAdapter;
+import org.slf4j.spi.MDCAdapter;
+import org.slf4j.spi.SLF4JServiceProvider;
+
+/**
+ * Responsible for binding the {@link MockLoggerFactory}, the {@link BasicMarkerFactory} and the {@link NOPMDCAdapter}.
+ * This is used by the SLF4J API.
+ *
+ * @since 1.0.0
+ */
+public class MockLoggerServiceProvider implements SLF4JServiceProvider {
+
+ private static final ILoggerFactory loggerFactory = new MockLoggerFactory();
+ private static final IMarkerFactory markerFactory = new BasicMarkerFactory();
+ private static final MDCAdapter contextMapAdapter = new NOPMDCAdapter();
+
+ /**
+ * Declare the version of the SLF4J API this implementation is compiled against. The value of this field is modified
+ * with each major release.
+ */
+ // to avoid constant folding by the compiler, this field must *not* be final
+ public static String REQUESTED_API_VERSION = "2.0.99"; // !final
+
+ @Override
+ public ILoggerFactory getLoggerFactory() {
+ return loggerFactory;
+ }
+
+ @Override
+ public IMarkerFactory getMarkerFactory() {
+ return markerFactory;
+ }
+
+ @Override
+ public MDCAdapter getMDCAdapter() {
+ return contextMapAdapter;
+ }
+
+ @Override
+ public String getRequestedApiVersion() {
+ return REQUESTED_API_VERSION;
+ }
+
+ @Override
+ public void initialize() {
+ // initialized by static fields
+ }
+
+}
diff --git a/slf4j/src/main/java/io/github/vitalijr2/logging/mock/slf4j/package-info.java b/slf4j/src/main/java/io/github/vitalijr2/logging/mock/slf4j/package-info.java
new file mode 100644
index 0000000..2acad17
--- /dev/null
+++ b/slf4j/src/main/java/io/github/vitalijr2/logging/mock/slf4j/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Mock loggers for SLF4J backed by Mockito.
+ *
+ * @since 1.0.0
+ */
+package io.github.vitalijr2.logging.mock.slf4j;
diff --git a/slf4j/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider b/slf4j/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider
new file mode 100644
index 0000000..d92fa64
--- /dev/null
+++ b/slf4j/src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider
@@ -0,0 +1 @@
+io.github.vitalijr2.logging.mock.slf4j.MockLoggerServiceProvider
\ No newline at end of file
diff --git a/slf4j/src/test/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerFactoryFastTest.java b/slf4j/src/test/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerFactoryFastTest.java
new file mode 100644
index 0000000..38ba4c8
--- /dev/null
+++ b/slf4j/src/test/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerFactoryFastTest.java
@@ -0,0 +1,71 @@
+package io.github.vitalijr2.logging.mock.slf4j;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.hamcrest.collection.IsMapContaining.hasEntry;
+import static org.hamcrest.collection.IsMapWithSize.aMapWithSize;
+import static org.hamcrest.core.Is.isA;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.StringStartsWith.startsWith;
+import static org.hamcrest.object.HasToString.hasToString;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.mockito.Mockito.mock;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+
+@Tag("fast")
+class MockLoggerFactoryFastTest {
+
+ @DisplayName("Create and add logger")
+ @Test
+ void createLogger() {
+ // given
+ var loggers = new HashMap();
+ var loggerFactory = new MockLoggerFactory(loggers);
+
+ // when
+ loggerFactory.getLogger("test");
+
+ // then
+ assertAll("Logger was created", () -> assertThat("size", loggers, aMapWithSize(1)),
+ () -> assertThat("entry", loggers, hasEntry(equalTo("test"), isA(Logger.class))),
+ () -> assertThat(loggers.values().iterator().next(), hasToString("Mock for logger test")));
+ }
+
+ @DisplayName("Reuse existing logger")
+ @Test
+ void reuseExistingLogger() {
+ // given
+ var loggers = new HashMap<>(Map.of("test", mock(Logger.class)));
+ var loggerFactory = new MockLoggerFactory(loggers);
+
+ // when
+ loggerFactory.getLogger("test");
+
+ // then
+ assertAll("Logger was created", () -> assertThat("size", loggers, aMapWithSize(1)),
+ () -> assertThat("entry", loggers, hasEntry(equalTo("test"), isA(Logger.class))),
+ () -> assertThat("Logger was created outside", loggers.values().iterator().next(),
+ hasToString(startsWith("Mock for Logger, hashCode:"))));
+ }
+
+ @DisplayName("Clean and reset loggers")
+ @Test
+ void cleanAndResetLoggers() {
+ // given
+ var loggers = new HashMap<>(Map.of("a", mock(Logger.class), "b", mock(Logger.class), "c", mock(Logger.class)));
+ var loggerFactory = new MockLoggerFactory(loggers);
+
+ // when
+ var loggerNames = loggerFactory.cleanAndReset();
+
+ // then
+ assertThat(loggerNames, contains("a", "b", "c"));
+ }
+
+}
diff --git a/slf4j/src/test/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerFactorySlowTest.java b/slf4j/src/test/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerFactorySlowTest.java
new file mode 100644
index 0000000..583f38a
--- /dev/null
+++ b/slf4j/src/test/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerFactorySlowTest.java
@@ -0,0 +1,70 @@
+package io.github.vitalijr2.logging.mock.slf4j;
+
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvFileSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Tag("slow")
+class MockLoggerFactorySlowTest {
+
+ private static Logger logger;
+
+ @BeforeAll
+ static void beforeAll() {
+ logger = LoggerFactory.getLogger("test");
+ }
+
+ @AfterEach
+ void tearDown() {
+ clearInvocations(logger);
+ }
+
+ @DisplayName("Test")
+ @ParameterizedTest
+ @CsvFileSource(resources = "slf4j.csv", numLinesToSkip = 1)
+ void test(String level, String message, int traceCount, int debugCount, int infoCount, int warningCount,
+ int errorCount) {
+ // given
+ verifyNoInteractions(logger);
+
+ // when
+ switch (level) {
+ case "TRACE":
+ LoggerFactory.getLogger("test").trace(message);
+ break;
+ case "DEBUG":
+ LoggerFactory.getLogger("test").debug(message);
+ break;
+ case "INFO":
+ LoggerFactory.getLogger("test").info(message);
+ break;
+ case "WARNING":
+ LoggerFactory.getLogger("test").warn(message);
+ break;
+ case "ERROR":
+ LoggerFactory.getLogger("test").error(message);
+ break;
+ default:
+ fail("Unknown level");
+ }
+
+ // then
+ verify(logger, times(traceCount)).trace("test trace message");
+ verify(logger, times(debugCount)).debug("test debug message");
+ verify(logger, times(infoCount)).info("test info message");
+ verify(logger, times(warningCount)).warn("test warning message");
+ verify(logger, times(errorCount)).error("test error message");
+ }
+
+}
diff --git a/slf4j/src/test/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerServiceProviderTest.java b/slf4j/src/test/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerServiceProviderTest.java
new file mode 100644
index 0000000..b285d8f
--- /dev/null
+++ b/slf4j/src/test/java/io/github/vitalijr2/logging/mock/slf4j/MockLoggerServiceProviderTest.java
@@ -0,0 +1,34 @@
+package io.github.vitalijr2.logging.mock.slf4j;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("fast")
+class MockLoggerServiceProviderTest {
+
+ private MockLoggerServiceProvider provider;
+
+ @BeforeEach
+ void setUp() {
+ provider = new MockLoggerServiceProvider();
+ }
+
+ @DisplayName("Initialize")
+ @Test
+ void initialize() {
+ // when
+ provider.initialize();
+
+ // then
+ assertAll("After initialization",
+ () -> assertNotNull(provider.getLoggerFactory(), "logger factory"),
+ () -> assertNotNull(provider.getMarkerFactory(), "marker factory"),
+ () -> assertNotNull(provider.getMDCAdapter(), "context map adapter"));
+ }
+
+}
diff --git a/slf4j/src/test/resources/io/github/vitalijr2/logging/mock/slf4j/slf4j.csv b/slf4j/src/test/resources/io/github/vitalijr2/logging/mock/slf4j/slf4j.csv
new file mode 100644
index 0000000..d314b35
--- /dev/null
+++ b/slf4j/src/test/resources/io/github/vitalijr2/logging/mock/slf4j/slf4j.csv
@@ -0,0 +1,6 @@
+level, message,trace count,debug count,info count,warning count,error count
+TRACE, test trace message,1,0,0,0,0
+DEBUG, test debug message,0,1,0,0,0
+INFO, test info message,0,0,1,0,0
+WARNING, test warning message,0,0,0,1,0
+ERROR, test error message,0,0,0,0,1
\ No newline at end of file