diff --git a/.editorconfig b/.editorconfig
index c6c8b362..322b027f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,9 +1,1380 @@
-root = true
-
[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
indent_style = space
+insert_final_newline = false
+max_line_length = 120
+tab_width = 4
+ij_continuation_indent_size = 8
+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
+
+[*.feature]
indent_size = 2
-end_of_line = lf
-charset = utf-8
-trim_trailing_whitespace = true
-insert_final_newline = true
+ij_gherkin_keep_indents_on_empty_lines = false
+
+[*.java]
+indent_size = 2
+max_line_length = 100
+ij_continuation_indent_size = 4
+ij_wrap_on_typing = true
+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 = true
+ij_java_align_multiline_assignment = false
+ij_java_align_multiline_binary_operation = false
+ij_java_align_multiline_chained_methods = true
+ij_java_align_multiline_deconstruction_list_components = false
+ij_java_align_multiline_extends_list = false
+ij_java_align_multiline_for = true
+ij_java_align_multiline_method_parentheses = false
+ij_java_align_multiline_parameters = false
+ij_java_align_multiline_parameters_in_calls = false
+ij_java_align_multiline_parenthesized_expression = false
+ij_java_align_multiline_records = false
+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 = true
+ij_java_align_types_in_multi_catch = true
+ij_java_annotation_parameter_wrap = split_into_lines
+ij_java_array_initializer_new_line_after_left_brace = true
+ij_java_array_initializer_right_brace_on_new_line = true
+ij_java_array_initializer_wrap = on_every_item
+ij_java_assert_statement_colon_on_next_line = false
+ij_java_assert_statement_wrap = normal
+ij_java_assignment_wrap = normal
+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 = 0
+ij_java_blank_lines_after_imports = 1
+ij_java_blank_lines_after_package = 1
+ij_java_blank_lines_around_class = 1
+ij_java_blank_lines_around_field = 0
+ij_java_blank_lines_around_field_in_interface = 0
+ij_java_blank_lines_around_initializer = 1
+ij_java_blank_lines_around_method = 1
+ij_java_blank_lines_around_method_in_interface = 1
+ij_java_blank_lines_before_class_end = 0
+ij_java_blank_lines_before_imports = 1
+ij_java_blank_lines_before_method_body = 0
+ij_java_blank_lines_before_package = 1
+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 = true
+ij_java_call_parameters_right_paren_on_new_line = true
+ij_java_call_parameters_wrap = on_every_item
+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 = on_every_item
+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 = true
+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 = false
+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 = split_into_lines
+ij_java_extends_keyword_wrap = normal
+ij_java_extends_list_wrap = on_every_item
+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 = on_every_item
+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 = false
+ij_java_insert_override_annotation = true
+ij_java_keep_blank_lines_before_right_brace = 2
+ij_java_keep_blank_lines_between_package_declaration_and_header = 2
+ij_java_keep_blank_lines_in_code = 2
+ij_java_keep_blank_lines_in_declarations = 2
+ij_java_keep_builder_methods_indents = true
+ij_java_keep_control_statement_in_one_line = false
+ij_java_keep_first_column_comment = false
+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 = on_every_item
+ij_java_method_parameters_new_line_after_left_paren = true
+ij_java_method_parameters_right_paren_on_new_line = true
+ij_java_method_parameters_wrap = on_every_item
+ij_java_modifier_list_wrap = false
+ij_java_multi_catch_types_wrap = on_every_item
+ij_java_names_count_to_use_import_on_demand = 999
+ij_java_new_line_after_lparen_in_annotation = true
+ij_java_new_line_after_lparen_in_deconstruction_pattern = true
+ij_java_new_line_after_lparen_in_record_header = true
+ij_java_packages_to_use_import_on_demand =
+ij_java_parameter_annotation_wrap = normal
+ij_java_parameter_name_prefix =
+ij_java_parameter_name_suffix =
+ij_java_parentheses_expression_new_line_after_left_paren = true
+ij_java_parentheses_expression_right_paren_on_new_line = true
+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 = on_every_item
+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 = true
+ij_java_resource_list_right_paren_on_new_line = true
+ij_java_resource_list_wrap = on_every_item
+ij_java_rparen_on_new_line_in_annotation = true
+ij_java_rparen_on_new_line_in_deconstruction_pattern = true
+ij_java_rparen_on_new_line_in_record_header = true
+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 = true
+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_ternary_operation_signs_on_next_line = false
+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 = on_every_item
+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 = split_into_lines
+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
+
+[*.less]
+indent_size = 2
+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]
+indent_size = 2
+tab_width = 2
+ij_continuation_indent_size = 4
+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
+
+[*.vue]
+indent_size = 2
+tab_width = 2
+ij_continuation_indent_size = 4
+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,*.wsdd,*.wsdl,*.xjb,*.xml,*.xsd,*.xsl,*.xslt,*.xul,phpunit.xml.dist}]
+ij_xml_align_attributes = true
+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_continuation_indent_size = 4
+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 = true
+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_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}]
+indent_size = 2
+tab_width = 2
+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}]
+ij_continuation_indent_size = 4
+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 = true
+ij_javascript_align_multiline_parameters = true
+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 = off
+ij_javascript_assignment_wrap = off
+ij_javascript_binary_operation_sign_on_next_line = false
+ij_javascript_binary_operation_wrap = off
+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 = off
+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 = never
+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 = never
+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 = off
+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 = never
+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 = true
+ij_javascript_indent_package_children = 0
+ij_javascript_jsx_attribute_value = braces
+ij_javascript_keep_blank_lines_in_code = 2
+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 = off
+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 = false
+ij_javascript_ternary_operation_wrap = off
+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_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 = never
+ij_javascript_while_on_new_line = false
+ij_javascript_wrap_comments = false
+
+[{*.ctp,*.hphp,*.inc,*.module,*.php,*.php4,*.php5,*.phtml}]
+ij_continuation_indent_size = 4
+ij_php_align_assignments = false
+ij_php_align_class_constants = false
+ij_php_align_enum_cases = false
+ij_php_align_group_field_declarations = false
+ij_php_align_inline_comments = false
+ij_php_align_key_value_pairs = false
+ij_php_align_match_arm_bodies = false
+ij_php_align_multiline_array_initializer_expression = false
+ij_php_align_multiline_binary_operation = false
+ij_php_align_multiline_chained_methods = false
+ij_php_align_multiline_extends_list = false
+ij_php_align_multiline_for = true
+ij_php_align_multiline_parameters = true
+ij_php_align_multiline_parameters_in_calls = false
+ij_php_align_multiline_ternary_operation = false
+ij_php_align_named_arguments = false
+ij_php_align_phpdoc_comments = false
+ij_php_align_phpdoc_param_names = false
+ij_php_anonymous_brace_style = end_of_line
+ij_php_api_weight = 28
+ij_php_array_initializer_new_line_after_left_brace = false
+ij_php_array_initializer_right_brace_on_new_line = false
+ij_php_array_initializer_wrap = off
+ij_php_assignment_wrap = off
+ij_php_attributes_wrap = off
+ij_php_author_weight = 28
+ij_php_binary_operation_sign_on_next_line = false
+ij_php_binary_operation_wrap = off
+ij_php_blank_lines_after_class_header = 0
+ij_php_blank_lines_after_function = 1
+ij_php_blank_lines_after_imports = 1
+ij_php_blank_lines_after_opening_tag = 0
+ij_php_blank_lines_after_package = 0
+ij_php_blank_lines_around_class = 1
+ij_php_blank_lines_around_constants = 0
+ij_php_blank_lines_around_enum_cases = 0
+ij_php_blank_lines_around_field = 0
+ij_php_blank_lines_around_method = 1
+ij_php_blank_lines_before_class_end = 0
+ij_php_blank_lines_before_imports = 1
+ij_php_blank_lines_before_method_body = 0
+ij_php_blank_lines_before_package = 1
+ij_php_blank_lines_before_return_statement = 0
+ij_php_blank_lines_between_imports = 0
+ij_php_block_brace_style = end_of_line
+ij_php_call_parameters_new_line_after_left_paren = false
+ij_php_call_parameters_right_paren_on_new_line = false
+ij_php_call_parameters_wrap = off
+ij_php_catch_on_new_line = false
+ij_php_category_weight = 28
+ij_php_class_brace_style = next_line
+ij_php_comma_after_last_argument = false
+ij_php_comma_after_last_array_element = false
+ij_php_comma_after_last_closure_use_var = false
+ij_php_comma_after_last_match_arm = false
+ij_php_comma_after_last_parameter = false
+ij_php_concat_spaces = true
+ij_php_copyright_weight = 28
+ij_php_deprecated_weight = 28
+ij_php_do_while_brace_force = never
+ij_php_else_if_style = as_is
+ij_php_else_on_new_line = false
+ij_php_example_weight = 28
+ij_php_extends_keyword_wrap = off
+ij_php_extends_list_wrap = off
+ij_php_fields_default_visibility = private
+ij_php_filesource_weight = 28
+ij_php_finally_on_new_line = false
+ij_php_for_brace_force = never
+ij_php_for_statement_new_line_after_left_paren = false
+ij_php_for_statement_right_paren_on_new_line = false
+ij_php_for_statement_wrap = off
+ij_php_force_empty_methods_in_one_line = false
+ij_php_force_short_declaration_array_style = false
+ij_php_getters_setters_naming_style = camel_case
+ij_php_getters_setters_order_style = getters_first
+ij_php_global_weight = 28
+ij_php_group_use_wrap = on_every_item
+ij_php_if_brace_force = never
+ij_php_if_lparen_on_next_line = false
+ij_php_if_rparen_on_next_line = false
+ij_php_ignore_weight = 28
+ij_php_import_sorting = alphabetic
+ij_php_indent_break_from_case = true
+ij_php_indent_case_from_switch = true
+ij_php_indent_code_in_php_tags = false
+ij_php_internal_weight = 28
+ij_php_keep_blank_lines_after_lbrace = 2
+ij_php_keep_blank_lines_before_right_brace = 2
+ij_php_keep_blank_lines_in_code = 2
+ij_php_keep_blank_lines_in_declarations = 2
+ij_php_keep_control_statement_in_one_line = true
+ij_php_keep_first_column_comment = true
+ij_php_keep_indents_on_empty_lines = false
+ij_php_keep_line_breaks = true
+ij_php_keep_rparen_and_lbrace_on_one_line = false
+ij_php_keep_simple_classes_in_one_line = false
+ij_php_keep_simple_methods_in_one_line = false
+ij_php_lambda_brace_style = end_of_line
+ij_php_license_weight = 28
+ij_php_line_comment_add_space = false
+ij_php_line_comment_at_first_column = true
+ij_php_link_weight = 28
+ij_php_lower_case_boolean_const = false
+ij_php_lower_case_keywords = true
+ij_php_lower_case_null_const = false
+ij_php_method_brace_style = next_line
+ij_php_method_call_chain_wrap = off
+ij_php_method_parameters_new_line_after_left_paren = false
+ij_php_method_parameters_right_paren_on_new_line = false
+ij_php_method_parameters_wrap = off
+ij_php_method_weight = 28
+ij_php_modifier_list_wrap = false
+ij_php_multiline_chained_calls_semicolon_on_new_line = false
+ij_php_namespace_brace_style = 1
+ij_php_new_line_after_php_opening_tag = false
+ij_php_null_type_position = in_the_end
+ij_php_package_weight = 28
+ij_php_param_weight = 0
+ij_php_parameters_attributes_wrap = off
+ij_php_parentheses_expression_new_line_after_left_paren = false
+ij_php_parentheses_expression_right_paren_on_new_line = false
+ij_php_phpdoc_blank_line_before_tags = false
+ij_php_phpdoc_blank_lines_around_parameters = false
+ij_php_phpdoc_keep_blank_lines = true
+ij_php_phpdoc_param_spaces_between_name_and_description = 1
+ij_php_phpdoc_param_spaces_between_tag_and_type = 1
+ij_php_phpdoc_param_spaces_between_type_and_name = 1
+ij_php_phpdoc_use_fqcn = false
+ij_php_phpdoc_wrap_long_lines = false
+ij_php_place_assignment_sign_on_next_line = false
+ij_php_place_parens_for_constructor = 0
+ij_php_property_read_weight = 28
+ij_php_property_weight = 28
+ij_php_property_write_weight = 28
+ij_php_return_type_on_new_line = false
+ij_php_return_weight = 1
+ij_php_see_weight = 28
+ij_php_since_weight = 28
+ij_php_sort_phpdoc_elements = true
+ij_php_space_after_colon = true
+ij_php_space_after_colon_in_enum_backed_type = true
+ij_php_space_after_colon_in_named_argument = true
+ij_php_space_after_colon_in_return_type = true
+ij_php_space_after_comma = true
+ij_php_space_after_for_semicolon = true
+ij_php_space_after_quest = true
+ij_php_space_after_type_cast = false
+ij_php_space_after_unary_not = false
+ij_php_space_before_array_initializer_left_brace = false
+ij_php_space_before_catch_keyword = true
+ij_php_space_before_catch_left_brace = true
+ij_php_space_before_catch_parentheses = true
+ij_php_space_before_class_left_brace = true
+ij_php_space_before_closure_left_parenthesis = true
+ij_php_space_before_colon = true
+ij_php_space_before_colon_in_enum_backed_type = false
+ij_php_space_before_colon_in_named_argument = false
+ij_php_space_before_colon_in_return_type = false
+ij_php_space_before_comma = false
+ij_php_space_before_do_left_brace = true
+ij_php_space_before_else_keyword = true
+ij_php_space_before_else_left_brace = true
+ij_php_space_before_finally_keyword = true
+ij_php_space_before_finally_left_brace = true
+ij_php_space_before_for_left_brace = true
+ij_php_space_before_for_parentheses = true
+ij_php_space_before_for_semicolon = false
+ij_php_space_before_if_left_brace = true
+ij_php_space_before_if_parentheses = true
+ij_php_space_before_method_call_parentheses = false
+ij_php_space_before_method_left_brace = true
+ij_php_space_before_method_parentheses = false
+ij_php_space_before_quest = true
+ij_php_space_before_short_closure_left_parenthesis = false
+ij_php_space_before_switch_left_brace = true
+ij_php_space_before_switch_parentheses = true
+ij_php_space_before_try_left_brace = true
+ij_php_space_before_unary_not = false
+ij_php_space_before_while_keyword = true
+ij_php_space_before_while_left_brace = true
+ij_php_space_before_while_parentheses = true
+ij_php_space_between_ternary_quest_and_colon = false
+ij_php_spaces_around_additive_operators = true
+ij_php_spaces_around_arrow = false
+ij_php_spaces_around_assignment_in_declare = false
+ij_php_spaces_around_assignment_operators = true
+ij_php_spaces_around_bitwise_operators = true
+ij_php_spaces_around_equality_operators = true
+ij_php_spaces_around_logical_operators = true
+ij_php_spaces_around_multiplicative_operators = true
+ij_php_spaces_around_null_coalesce_operator = true
+ij_php_spaces_around_pipe_in_union_type = false
+ij_php_spaces_around_relational_operators = true
+ij_php_spaces_around_shift_operators = true
+ij_php_spaces_around_unary_operator = false
+ij_php_spaces_around_var_within_brackets = false
+ij_php_spaces_within_array_initializer_braces = false
+ij_php_spaces_within_brackets = false
+ij_php_spaces_within_catch_parentheses = false
+ij_php_spaces_within_for_parentheses = false
+ij_php_spaces_within_if_parentheses = false
+ij_php_spaces_within_method_call_parentheses = false
+ij_php_spaces_within_method_parentheses = false
+ij_php_spaces_within_parentheses = false
+ij_php_spaces_within_short_echo_tags = true
+ij_php_spaces_within_switch_parentheses = false
+ij_php_spaces_within_while_parentheses = false
+ij_php_special_else_if_treatment = false
+ij_php_subpackage_weight = 28
+ij_php_ternary_operation_signs_on_next_line = false
+ij_php_ternary_operation_wrap = off
+ij_php_throws_weight = 2
+ij_php_todo_weight = 28
+ij_php_treat_multiline_arrays_and_lambdas_multiline = false
+ij_php_unknown_tag_weight = 28
+ij_php_upper_case_boolean_const = false
+ij_php_upper_case_null_const = false
+ij_php_uses_weight = 28
+ij_php_var_weight = 28
+ij_php_variable_naming_style = mixed
+ij_php_version_weight = 28
+ij_php_while_brace_force = never
+ij_php_while_on_new_line = false
+
+[{*.gant,*.groovy,*.gy}]
+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,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,composer.lock,jest.config}]
+indent_size = 2
+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,*.ng,*.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_continuation_indent_size = 4
+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
+
+[{*.kt,*.kts}]
+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 = off
+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 = false
+ij_kotlin_call_parameters_right_paren_on_new_line = false
+ij_kotlin_call_parameters_wrap = off
+ij_kotlin_catch_on_new_line = false
+ij_kotlin_class_annotation_wrap = split_into_lines
+ij_kotlin_continuation_indent_for_chained_calls = true
+ij_kotlin_continuation_indent_for_expression_bodies = true
+ij_kotlin_continuation_indent_in_argument_lists = true
+ij_kotlin_continuation_indent_in_elvis = true
+ij_kotlin_continuation_indent_in_if_conditions = true
+ij_kotlin_continuation_indent_in_parameter_lists = true
+ij_kotlin_continuation_indent_in_supertype_lists = true
+ij_kotlin_else_on_new_line = false
+ij_kotlin_enum_constants_wrap = off
+ij_kotlin_extends_list_wrap = off
+ij_kotlin_field_annotation_wrap = split_into_lines
+ij_kotlin_finally_on_new_line = false
+ij_kotlin_if_rparen_on_new_line = false
+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 = off
+ij_kotlin_method_parameters_new_line_after_left_paren = false
+ij_kotlin_method_parameters_right_paren_on_new_line = false
+ij_kotlin_method_parameters_wrap = off
+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 = 0
+ij_kotlin_wrap_first_method_in_call_chain = false
+
+[{*.markdown,*.md}]
+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}]
+indent_size = 2
+tab_width = 2
+ij_continuation_indent_size = 4
+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
+
+[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}]
+ij_toml_keep_indents_on_empty_lines = false
+
+[{*.yaml,*.yml}]
+indent_size = 2
+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.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..dfe985ab
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,21 @@
+---
+
+name: Bug report
+about: Create a report to help us improve
+title: "[BUG]"
+labels: bug
+assignees: dheid
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**Code snippets**
+Code to reproduce the behavior
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..71529ebf
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,21 @@
+---
+
+name: Feature request
+about: Suggest an idea for this project
+title: "[REQUEST]"
+labels: enhancement
+assignees: dheid
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 04ceb6e8..0e775aff 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,6 +1,14 @@
version: 2
updates:
- - package-ecosystem: "maven"
- directory: "/"
+ - package-ecosystem: maven
+ directory: /
schedule:
- interval: "weekly"
+ interval: weekly
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ - package-ecosystem: docker
+ directory: /
+ schedule:
+ interval: weekly
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 8b137891..d05f593c 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1 +1,10 @@
+
+- [ ] Ensure that the pull request title represents the desired changelog entry
+- [ ] Please describe what you did
+- [ ] Link to relevant issues in GitHub
+- [ ] Link to relevant pull requests, esp. upstream and downstream changes
+- [ ] Ensure you have provided tests - that demonstrates feature works or fixes the issue
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..ca29d2df
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,38 @@
+name: Java CI with Maven
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ checks: write
+ contents: read
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-java@v5
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: maven
+ - run: mvn -B verify
+ - uses: dorny/test-reporter@v3
+ if: always()
+ with:
+ name: JUnit Tests
+ path: '**/target/surefire-reports/TEST-*.xml'
+ reporter: java-junit
+ fail-on-error: false
+ - uses: madrapps/jacoco-report@v1.7.2
+ with:
+ paths: ${{ github.workspace }}/**/target/site/jacoco/jacoco.xml
+ token: ${{ secrets.GITHUB_TOKEN }}
+ min-coverage-overall: 80
+ min-coverage-changed-files: 80
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000..c00ed329
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,36 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+ schedule:
+ - cron: '27 11 * * 5'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'java' ]
+
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-java@v5
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: maven
+ - uses: github/codeql-action/init@v4
+ with:
+ languages: ${{ matrix.language }}
+ - uses: github/codeql-action/autobuild@v4
+ - uses: github/codeql-action/analyze@v4
diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml
new file mode 100644
index 00000000..57750289
--- /dev/null
+++ b/.github/workflows/dependabot-automerge.yml
@@ -0,0 +1,28 @@
+name: Dependabot auto-merge
+
+on: pull_request
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ dependabot:
+ runs-on: ubuntu-latest
+ if: github.actor == 'dependabot[bot]'
+ steps:
+ - name: Fetch Dependabot metadata
+ id: metadata
+ uses: dependabot/fetch-metadata@v3.1.0
+ with:
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
+ - name: Approve pull request
+ run: gh pr review --approve "$PR_URL"
+ env:
+ PR_URL: ${{ github.event.pull_request.html_url }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Enable auto-merge
+ run: gh pr merge --auto --squash "$PR_URL"
+ env:
+ PR_URL: ${{ github.event.pull_request.html_url }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml
new file mode 100644
index 00000000..9876fb61
--- /dev/null
+++ b/.github/workflows/gh-pages.yaml
@@ -0,0 +1,36 @@
+name: Deploy GitHub Pages
+
+on:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/configure-pages@v6
+ - uses: actions/jekyll-build-pages@v1
+ with:
+ source: ./
+ destination: ./_site
+ - uses: actions/upload-pages-artifact@v5
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..6ef2406c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,32 @@
+name: Release with Maven
+on:
+ workflow_dispatch:
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-java@v5
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: maven
+ server-id: central
+ server-username: OSSRH_USERNAME
+ server-password: OSSRH_PASSWORD
+ gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
+ gpg-passphrase: GPG_PASSPHRASE
+ - id: version
+ run: |
+ VERSION=$( mvn -B help:evaluate -Dexpression=project.version -q -DforceStdout )
+ echo "version=${VERSION%-SNAPSHOT}" >> "$GITHUB_OUTPUT"
+ - run: mvn -B versions:set -DnewVersion=${{ steps.version.outputs.version }} -DgenerateBackupPoms=false
+ - run: mvn -B deploy -Prelease
+ env:
+ OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
+ OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
+ GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
+ - uses: release-drafter/release-drafter@v7
+ with:
+ version: ${{ steps.version.outputs.version }}
+ publish: true
diff --git a/.gitignore b/.gitignore
index 74c79135..60bd73bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,20 +1,13 @@
-# General System Files #
*~
*\.DS_Store
*#*#
*.swp
info
-
-# Package Files #
**/target/
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
-
-# IDEA files
*.iml
.idea
-
-# Eclipse files
.classpath
.project
+test/logs/*
+!test/logs/.gitkeep
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 13a50d11..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-install: mvn install -DskipTests=true -Dgpg.skip=true
-jdk:
- - openjdk8
-language: java
-after_success:
- - mvn clean test jacoco:report coveralls:report
-
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..494c2390
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+mail@daniel-heid.de.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..3cd587b1
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,93 @@
+# Contributing
+
+When contributing to this repository, please first discuss the change you wish to make via issue,
+email, or any other method with the owners of this repository before making a change.
+
+Please note we have a code of conduct, please follow it in all your interactions with the project.
+
+## Pull Request Process
+
+1. Ensure any install or build dependencies are removed before the end of the layer when doing a
+ build.
+2. Update the README.md with details of changes to the interface, this includes new environment
+ variables, exposed ports, useful file locations and container parameters.
+3. Increase the version numbers in any examples files and the README.md to the new version that this
+ Pull Request would represent. The versioning scheme we use is [SemVer](https://semver.org/).
+4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
+ do not have permission to do that, you may request the second reviewer to merge it for you.
+
+## Code of Conduct
+
+### Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of experience,
+nationality, personal appearance, race, religion, or sexual identity and
+orientation.
+
+### Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+### Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+### Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+### Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at mail@daniel-heid.de. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+### Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at [https://contributor-covenant.org/version/1/4][version]
+
+[homepage]: https://contributor-covenant.org
+[version]: https://contributor-covenant.org/version/1/4/
+
diff --git a/README.md b/README.md
index 7b325aa1..7b1121d6 100644
--- a/README.md
+++ b/README.md
@@ -1,283 +1,741 @@
-# Matomo Java Tracker
+# Official Matomo Java Tracker
-[](https://maven-badges.herokuapp.com/maven-central/org.piwik.java.tracking/matomo-java-tracker)
-[](https://travis-ci.org/matomo-org/matomo-java-tracker)
+
+[](https://github.com/matomo-org/matomo-java-tracker/actions/workflows/build.yml)
[](https://isitmaintained.com/project/matomo-org/matomo-java-tracker "Average time to resolve an issue")
[](https://isitmaintained.com/project/matomo-org/matomo-java-tracker "Percentage of issues still open")
-Official Java implementation of the [Matomo Tracking HTTP API](https://developer.matomo.org/api-reference/tracking-api).
+The Matomo Java Tracker functions as the official Java implementation for
+the [Matomo Tracking HTTP API](https://developer.matomo.org/api-reference/tracking-api). This versatile tracker empowers
+you to monitor visits, goals, and ecommerce transactions and items. Specifically designed for integration into
+server-side applications, it seamlessly integrates with Java-based web applications or web services.
+
+Key features:
+
+* Comprehensive tracking capabilities: Monitor page views, goals, ecommerce transactions, and items.
+* Customization options: Support for custom dimensions and variables.
+* Extensive tracking parameters: Capture data on campaigns, events, downloads, outlinks, site searches, devices, and
+ visitors.
+* Java compatibility: Supports Java 8 and higher, with a dedicated artifact (matomo-java-tracker-java11) for Java 11 or newer.
+* SSL certificate flexibility: Option to skip SSL certificate validation (caution: not recommended for production).
+* Minimal runtime dependencies: Relies solely on SLF4J.
+* Asynchronous request support: Permits non-blocking requests.
+* Compatibility with Matomo versions 4 and 5.
+* Versatile request handling: Send both single and multiple requests.
+* Robust documentation: Thoroughly documented with Javadoc for easy reference.
+* Data accuracy assurance: Ensures correct values are transmitted to the Matomo Tracking API.
+* Logging capabilities: Include debug and error logging for effective troubleshooting.
+* Seamless integration: Easily integrates into frameworks such as Spring by creating the MatomoTracker Spring bean for
+ use in other beans.
+
+Please prefer the Java 11 or newer version as the Java 8 will become obsolete in the future.
+
+You can find our [Developer Guide here](https://developer.matomo.org/api-reference/tracking-java)
+
+Further information on Matomo and Matomo HTTP tracking:
+
+* [Matomo PHP Tracker](https://github.com/matomo-org/matomo-php-tracker)
+* [Matomo Tracking HTTP API](https://developer.matomo.org/api-reference/tracking-api)
+* [Introducing the Matomo Java Tracker](https://matomo.org/blog/2015/11/introducing-piwik-java-tracker/)
+* [Tracking API User Guide](https://matomo.org/guide/apis/tracking-api/)
+* [Matomo Developer](https://developer.matomo.org/)
+* [The Matomo project](https://matomo.org/)
+
+Projects that use Matomo Java Tracker:
+
+* [Box-c - supports the UNC Libraries' Digital Collections Repository](https://github.com/UNC-Libraries/box-c)
+* [DSpace - provide durable access to digital resources](https://github.com/thanvanlong/dspace)
+* [Identifiers.org satellite Web SPA](https://github.com/identifiers-org/cloud-satellite-web-spa)
+* [Cloud native Resolver Web Service for identifiers.org](https://github.com/identifiers-org/cloud-ws-resolver)
+* [Resource Catalogue](https://github.com/madgeek-arc/resource-catalogue)
+* [INCEpTION - A semantic annotation platform offering intelligent assistance and knowledge management](https://github.com/inception-project/inception)
+* [QualiChain Analytics Intelligent Profiling](https://github.com/JoaoCabrita95/IP)
+* [Digitale Ehrenamtskarte](https://github.com/digitalfabrik/entitlementcard)
+* [skidfuscator-java-obfuscator](https://github.com/skidfuscatordev/skidfuscator-java-obfuscator)
+* [DnA](https://github.com/mercedes-benz/DnA)
+* And many closed source projects that we are not aware of :smile:
+
+## Table of Contents
+
+* [What Is New?](#what-is-new)
+* [Javadoc](#javadoc)
+* [Need help?](#need-help)
+* [Using this API](#using-this-api)
+* [Migration from Version 2 to 3](#migration-from-version-2-to-3)
+* [Building and Testing](#building-and-testing)
+* [Versioning](#versioning)
+* [Contribute](#contribute)
+* [License](#license)
+
+## What Is New?
+
+### Version 4.0.0
+
+Added more tracking parameters for user agent data, ecommerce product, bot recording mode, HTTP status, bandwidth,
+source label and media attributes.
+
+The Spring Boot Starter now requires Spring Boot 4. The `@NonNull` annotation has been migrated from
+`org.springframework.lang.NonNull` to `org.jspecify.annotations.NonNull` across the Spring module. The
+`PropertyMapper` usage was updated to align with the Spring Boot 4 API (`alwaysApplyingWhenNonNull()` was removed).
+
+Fixed a Java 8 compatibility issue in `JavaxHttpServletWrapper` where `Enumeration.asIterator()` (introduced in
+Java 9) was used to iterate over HTTP header names. It has been replaced with a standard `while` loop.
+
+Zero is now accepted as a valid value for numeric tracking parameters such as `idGoal`, `revenue`, and ecommerce
+fields, where it was previously treated as absent and omitted from requests.
+
+Several performance improvements were made to `Java11Sender`: URL construction no longer uses `String.format`,
+the case-insensitive `User-Agent` header lookup no longer allocates a `TreeMap`, `.trim().isEmpty()` checks were
+replaced with the Java 11 `.isBlank()` method, and debug log statements are now guarded to avoid allocating
+cookie list copies when debug logging is disabled.
+
+Several performance improvements were also made to the core module: hex encoding in `VisitorId` now uses a
+lookup table instead of `String.format` per byte, `String.format` calls in `DeviceResolution`, `AcceptLanguage`,
+`EcommerceItem`, and `DaemonThreadFactory` were replaced with string concatenation, `ServletMatomoRequest` now
+computes `cookieName.toLowerCase()` once per cookie instead of six times, `QueryCreator` caches the
+`value.toString()` result to avoid calling it twice, and redundant double-check patterns such as
+`isEmpty() || trim().isEmpty()` were simplified throughout.
+
+Spotless now automatically removes unused imports during the build.
+
+Dependency updates: Spring Boot 3.4.2 → 4.0.5, JUnit Jupiter 5.11.4 → 6.0.3, SLF4J 2.0.16 → 2.0.17,
+Jetty EE10 12.0.16 → 12.1.8, Jetty (javax) 10.0.24 → 10.0.26.
+
+The local testing Docker setup now uses MariaDB 12 and Matomo 5.
+
+All previously deprecated API has been removed as part of this major release. The following is a
+summary of the breaking changes and the recommended replacements:
+
+- The `org.piwik.java.tracking` compatibility package has been removed entirely (`PiwikRequest`,
+ `PiwikTracker`, `PiwikDate`, `PiwikLocale`, `CustomVariable`, `EcommerceItem`). Use the
+ corresponding classes in `org.matomo.java.tracking` instead.
+- The deprecated `MatomoDate` and `MatomoLocale` classes have been removed. Use `java.time.Instant`
+ and `org.matomo.java.tracking.parameters.Country` respectively.
+- The top-level `MatomoRequestBuilder` class has been removed. Use `MatomoRequest.request()` or
+ `MatomoRequest.MatomoRequestBuilder` instead.
+- The deprecated `MatomoRequest(int siteId, String actionUrl)` constructor has been removed. Use
+ `MatomoRequest.request()` builder instead.
+- Deprecated mutator methods on `MatomoRequest` have been removed: `setCustomTrackingParameter`,
+ `addCustomTrackingParameter`, `clearCustomTrackingParameter`, `enableEcommerce`,
+ `getEcommerceItem`, `addEcommerceItem`, `clearEcommerceItems`, `getPageCustomVariable`,
+ `setPageCustomVariable`, `getUserCustomVariable`, `getVisitCustomVariable`,
+ `setUserCustomVariable`, `setVisitCustomVariable`, `getRequestDatetime`, `setRequestDatetime`,
+ `setParameter`, `builder()`, and `setDeviceResolution(String)`. Use the builder API
+ (`MatomoRequest.request()...build()`) instead.
+- Deprecated constructors on `MatomoTracker` taking `hostUrl`, `proxyHost`, `proxyPort`, and
+ `timeout` as individual arguments have been removed. Use
+ `MatomoTracker(TrackerConfiguration)` instead.
+- The `sendRequestAsync(MatomoRequest, Function)` overload has been removed. Chain
+ `CompletableFuture.thenApply()` on `sendRequestAsync(MatomoRequest)` instead.
+- The `sendBulkRequest(Iterable, String authToken)` and
+ `sendBulkRequestAsync(Collection, String, Consumer)` / `sendBulkRequestAsync(Collection, String)`
+ overloads have been removed. Set the auth token in `TrackerConfiguration` or on the requests
+ directly, and chain `CompletableFuture.thenAccept()` for callbacks.
+- Deprecated `Country(Locale)` constructor and `Country.getLocale()` / `Country.setLocale(Locale)`
+ methods have been removed. Construct `Country` from a country code string instead.
+
+### Version 3.4.0
+
+We fixed a synchronization issue in the Java 8 sender (https://github.com/matomo-org/matomo-java-tracker/issues/168).
+To consume the exact amount of space needed for the queries to send to Matomo, we need the collection size of the incoming
+requests. So we changed `Iterable` to `Collection` in some `MatomoTracker`. This could affect users, that use parameters
+of type `Iterable` in the tracker. Please use `Collection` instead.
+
+### Version 3.3.1
+
+Do you still use Matomo Java Tracker 2.x? We created version 3, that is compatible with Matomo 4 and 5 and contains
+fewer
+dependencies. Release notes can be found here: https://github.com/matomo-org/matomo-java-tracker/releases
+
+Here are the most important changes:
+
+* Matomo Java Tracker is compatible with Matomo 4 and 5
+* less dependencies
+* new dimension parameter
+* special types allow to provide valid parameters now
+* a new implementation for Java 11 or newer uses the HttpClient available since Java 11
+
+See also the [Developer Guide here](https://developer.matomo.org/api-reference/tracking-java)
## Javadoc
-The Javadoc for this project is hosted as a Github page for this repo. The latest Javadoc can be
-found [here](https://matomo-org.github.io/matomo-java-tracker/javadoc/HEAD/index.html). Javadoc for the latest and all
-releases can be found [here](https://matomo-org.github.io/matomo-java-tracker/javadoc/index.html).
+The Javadoc for all versions can be found
+[at javadoc.io](https://javadoc.io/doc/org.piwik.java.tracking/matomo-java-tracker-core/latest/index.html). Thanks to
+[javadoc.io](https://javadoc.io) for hosting it.
+
+## Need help?
+
+* Check the [Developer Guide](https://developer.matomo.org/api-reference/tracking-java)
+* Open an issue in the [Issue Tracker](https://github.com/matomo-org/matomo-java-tracker/issues)
+* Use [our GitHub discussions](https://github.com/matomo-org/matomo-java-tracker/discussions)
+* Ask your question on [Stackoverflow with the tag `matomo`](https://stackoverflow.com/questions/tagged/matomo)
+* Create a thread in the [Matomo Forum](https://forum.matomo.org/)
+* Contact [Matomo Support](https://matomo.org/support/)
## Using this API
+See the following sections for information on how to use this API. For more information, see the Javadoc. We also
+recommend to read the [Tracking API User Guide](https://matomo.org/guide/apis/tracking-api/).
+The [Matomo Tracking HTTP API](https://developer.matomo.org/api-reference/tracking-api) is well
+documented and contains many examples.
+
+This project contains the following Maven artifacts:
+
+1. **matomo-java-tracker-core**: This artifact is the core module of the Matomo Java Tracker project. It provides the
+ main functionality of the Matomo Java Tracker, which is a Java implementation for the Matomo Tracking HTTP API. This
+ module is designed to be used as a base for other modules in the project that provide additional functionality or
+ integrations.
+2. **matomo-java-tracker**: This is a specific implementation of the core module designed for Java 8. It provides the
+ main functionality of the Matomo Java Tracker and is built upon the core. This artifact is
+ specifically designed for applications running on Java 8.
+3. **matomo-java-tracker-java11**: This artifact is a Java 11 or newer implementation of the Matomo Java Tracker. It uses the
+ HttpClient available since Java 11. It is recommended to use this version if you are using Java 11 or newer.
+4. **matomo-java-tracker-spring-boot-starter**: This artifact is a Spring Boot Starter for the Matomo Java Tracker. It
+ provides auto-configuration for the Matomo Java Tracker in a Spring Boot application. By including this artifact in
+ your project, you can take advantage of Spring Boot's auto-configuration features to automatically set up and
+ configure the Matomo Java Tracker.
+5. **matomo-java-tracker-servlet-jakarta**: This artifact is specifically designed for applications using the Jakarta
+ Servlet API (part of Jakarta EE).
+6. **matomo-java-tracker-servlet-javax**: This artifact is specifically designed for applications using the older Java
+ Servlet API (part of Java EE).
+7. **matomo-java-tracker-test**: This artifact contains tools for manual testing against a local Matomo instance created
+ with Docker. It contains a tester class that sends randomized requests to a local Matomo instance and a servlet that
+ can be used to test the servlet integration.
+
+Each of these artifacts serves a different purpose and can be used depending on the specific needs of your project and
+the Java version you are using.
+
### Add library to your build
-Add a dependency on Matomo Java Tracker using Maven:
+Add a dependency on Matomo Java Tracker using Maven. For Java 8:
```xml
- org.piwik.java.tracking
- matomo-java-tracker
- 2.0
+ org.piwik.java.tracking
+ matomo-java-tracker
+ 4.0.0
```
-or Gradle:
+For Java 11 or newer:
+
+```xml
+
+
+ org.piwik.java.tracking
+ matomo-java-tracker-java11
+ 4.0.0
+
+```
+
+or Gradle (Java 8):
```groovy
dependencies {
- implementation("org.piwik.java.tracking:matomo-java-tracker:2.0")
+ implementation("org.piwik.java.tracking:matomo-java-tracker:4.0.0")
}
```
-### Create a Request
-
-Each MatomoRequest represents an action the user has taken that you want tracked by your Matomo server. Create a
-MatomoRequest through
-
-```java
-
-import org.matomo.java.tracking.MatomoRequest;
-
-public class YourImplementation {
-
- public void yourMethod() {
- MatomoRequest request = MatomoRequest.builder()
- .siteId(42)
- .actionUrl("https://www.mydomain.com/signup")
- .actionName("Signup")
- .build();
- }
+or Gradle (Java 11 or newer):
+```groovy
+dependencies {
+ implementation("org.piwik.java.tracking:matomo-java-tracker-java11:4.0.0")
}
-
```
-Per default every request has the following default parameters:
-
-| Parameter Name | Default Value |
-|------------------|--------------------------------|
-| required | true |
-| visitorId | random 16 character hex string |
-| randomValue | random 20 character hex string |
-| apiVersion | 1 |
-| responseAsImage | false |
+or Gradle with Kotlin DSL (Java 8)
-Overwrite these properties as desired.
+```kotlin
+implementation("org.piwik.java.tracking:matomo-java-tracker:4.0.0")
+```
-Note that if you want to be able to track campaigns using *Referrers > Campaigns*, you must add the correct
-URL parameters to your actionUrl. For example,
+or Gradle with Kotlin DSL (Java 11 or newer)
-```java
+```kotlin
+implementation("org.piwik.java.tracking:matomo-java-tracker-java11:4.0.0")
+```
-package example;
+### Spring Boot Module
-import org.matomo.java.tracking.MatomoRequest;
+If you use Spring Boot 4, you can use the Spring Boot Starter artifact. It will create a MatomoTracker bean for you
+and allows you to configure the tracker via application properties. Add the following dependency to your build:
-public class YourImplementation {
+```xml
- public void yourMethod() {
+
+ org.piwik.java.tracking
+ matomo-java-tracker-spring-boot-starter
+ 4.0.0
+
+```
- MatomoRequest request = MatomoRequest.builder()
- .siteId(42)
- .actionUrl("http://example.org/landing.html?pk_campaign=Email-Nov2011&pk_kwd=LearnMore") // include the query parameters to the url
- .actionName("LearnMore")
- .build();
- }
+or Gradle:
+```groovy
+dependencies {
+ implementation("org.piwik.java.tracking:matomo-java-tracker-spring-boot-starter:4.0.0")
}
```
-See [Tracking Campaigns](https://matomo.org/docs/tracking-campaigns/) for more information.
+or Gradle with Kotlin DSL
-All HTTP query parameters denoted on
-the [Matomo Tracking HTTP API](https://developer.matomo.org/api-reference/tracking-api) can be set using the appropriate
-getters and setters. See _MatomoRequest.java_ for the mappings of the parameters to their corresponding
-Java getters/setters.
+```kotlin
+implementation("org.piwik.java.tracking:matomo-java-tracker-spring-boot-starter:4.0.0")
+```
-Some parameters are dependent on the state of other parameters:
-_EcommerceEnabled_ must be called before the following parameters are set: *EcommerceId* and *
-EcommerceRevenue*.
+The following properties are supported:
+
+| Property Name | Description |
+|----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
+| matomo.tracker.api-endpoint (required) | The URL to the Matomo Tracking API endpoint. Must be set. |
+| matomo.tracker.default-site-id | If you provide a default site id, it will be taken if the action does not contain a site id. |
+| matomo.tracker.default-token-auth | If you provide a default token auth, it will be taken if the action does not contain a token auth. |
+| matomo.tracker.enabled | The tracker is enabled per default. You can disable it per configuration with this flag. |
+| matomo.tracker.log-failed-tracking | Will send errors to the log if the Matomo Tracking API responds with an erroneous HTTP code |
+| matomo.tracker.connect-timeout | allows you to change the default connection timeout of 10 seconds. 0 is interpreted as infinite, null uses the system default |
+| matomo.tracker.socket-timeout | allows you to change the default socket timeout of 10 seconds. 0 is interpreted as infinite, null uses the system default |
+| matomo.tracker.user-agent | The user agent used by the request made to the endpoint. Default: `MatomoJavaClient` |
+| matomo.tracker.proxy-host | The hostname or IP address of an optional HTTP proxy. `proxyPort` must be configured as well |
+| matomo.tracker.proxy-port | The port of an HTTP proxy. `proxyHost` must be configured as well. |
+| matomo.tracker.proxy-username | If the HTTP proxy requires a username for basic authentication, it can be configured with this method. Proxy host, port and password must also be set. |
+| matomo.tracker.proxy-password | The corresponding password for the basic auth proxy user. The proxy host, port and username must be set as well. |
+| matomo.tracker.disable-ssl-cert-validation | If set to true, the SSL certificate of the Matomo server will not be validated. This should only be used for testing purposes. Default: false |
+| matomo.tracker.disable-ssl-host-verification | If set to true, the SSL host of the Matomo server will not be validated. This should only be used for testing purposes. Default: false |
+| matomo.tracker.thread-pool-size | The number of threads that will be used to asynchronously send requests. Default: 2 |
+| matomo.tracker.filter.enabled | Enables a servlet filter that tracks every request within the application |
+
+To ensure the `MatomoTracker` bean is created by the auto configuration, you have to add the following property to
+your `application.properties` file:
+
+```properties
+matomo.tracker.api-endpoint=https://your-matomo-domain.tld/matomo.php
+```
-_EcommerceId_ and _EcommerceRevenue_ must be set before the following parameters are
-set: *EcommerceDiscount*, *EcommerceItem*, *EcommerceLastOrderTimestamp*, *
-EcommerceShippingCost*, *EcommerceSubtotal*, and *EcommerceTax*.
+Or if you use YAML:
-_AuthToken_ must be set before the following parameters are set: *VisitorCity*, *
-VisitorCountry*, *VisitorIp*, *VisitorLatitude*, *VisitorLongitude*, and *VisitorRegion*
-.
+```yaml
+matomo:
+ tracker:
+ api-endpoint: https://your-matomo-domain.tld/matomo.php
+```
-### Sending Requests
+You can automatically add the `MatomoTrackerFilter` to your Spring Boot application if you add the following property:
-Create a MatomoTracker using the constructor
+```properties
+matomo.tracker.filter.enabled=true
+```
-```java
-package example;
+Or if you use YAML:
-import org.matomo.java.tracking.MatomoTracker;
+```yaml
+matomo:
+ tracker:
+ filter:
+ enabled: true
+```
-public class YourImplementation {
+The filter uses `ServletMatomoRequest` to create a `MatomoRequest` from a `HttpServletRequest` on every filter call.
- public void yourMethod() {
+### Sending a Tracking Request
- MatomoTracker tracker = new MatomoTracker("https://your-matomo-domain.tld/matomo.php");
+To let the Matomo Java Tracker send a request to the Matomo instance, you need the following minimal code:
- }
+```java
+import java.net.URI;
+import org.matomo.java.tracking.MatomoRequests;
+import org.matomo.java.tracking.MatomoTracker;
+import org.matomo.java.tracking.TrackerConfiguration;
+import org.matomo.java.tracking.parameters.VisitorId;
+
+/**
+ * Example for sending a request.
+ */
+public class SendExample {
+
+ /**
+ * Example for sending a request.
+ *
+ * @param args ignored
+ */
+ public static void main(String[] args) {
+
+ TrackerConfiguration configuration = TrackerConfiguration
+ .builder()
+ .apiEndpoint(URI.create("https://www.yourdomain.com/matomo.php"))
+ .defaultSiteId(1)
+ .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71")
+ .logFailedTracking(true)
+ .build();
+
+ try (MatomoTracker tracker = new MatomoTracker(configuration)) {
+ tracker.sendRequestAsync(MatomoRequests
+ .event("Training", "Workout completed", "Bench press", 60.0)
+ .visitorId(VisitorId.fromString("customer@mail.com"))
+ .build()
+ );
+ } catch (Exception e) {
+ throw new RuntimeException("Could not close tracker", e);
+ }
+ }
}
+
```
-using the Matomo Endpoint URL as the first parameter.
+This will send a request to the Matomo instance at https://www.yourdomain.com/matomo.php and track an event in the
+category "Training" with action "Workout completed" for the visitor customer@mail.com for the site with id 1. The request will be sent asynchronously, that means the method will return immediately and your
+application will not wait for the response of the Matomo server. In the configuration we set the default site id to 1
+and configure the default auth token. With `logFailedTracking` we enable logging of failed tracking requests.
-To send a single request, call
+If you want to perform an operation after a successful asynchronous call to Matomo, you can use the completable future
+result like this:
```java
-package example;
-
-import org.apache.http.HttpResponse;
+import java.net.URI;
import org.matomo.java.tracking.MatomoRequest;
+import org.matomo.java.tracking.MatomoRequests;
import org.matomo.java.tracking.MatomoTracker;
+import org.matomo.java.tracking.TrackerConfiguration;
+import org.matomo.java.tracking.parameters.VisitorId;
+
+/**
+ * Example for sending a request and performing an action when the request was sent successfully.
+ */
+public class ConsumerExample {
+
+ /**
+ * Example for sending a request and performing an action when the request was sent successfully.
+ *
+ * @param args ignored
+ */
+ public static void main(String[] args) {
+
+ TrackerConfiguration configuration = TrackerConfiguration
+ .builder()
+ .apiEndpoint(URI.create("https://www.yourdomain.com/matomo.php"))
+ .defaultSiteId(1)
+ .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71")
+ .logFailedTracking(true)
+ .build();
+
+ try (MatomoTracker tracker = new MatomoTracker(configuration)) {
+ MatomoRequest request = MatomoRequests
+ .event("Training", "Workout completed", "Bench press", 60.0)
+ .visitorId(VisitorId.fromString("customer@mail.com"))
+ .build();
+
+ tracker.sendRequestAsync(request)
+ .thenAccept(req -> System.out.printf("Sent request %s%n", req))
+ .exceptionally(throwable -> {
+ System.err.printf("Failed to send request: %s%n", throwable.getMessage());
+ return null;
+ });
+ } catch (Exception e) {
+ throw new RuntimeException("Could not close tracker", e);
+ }
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-
-public class YourImplementation {
-
- public void yourMethod() {
-
- MatomoRequest request = MatomoRequest.builder().siteId(42).actionUrl("https://www.mydomain.com/some/page").actionName("Signup").build();
-
- MatomoTracker tracker = new MatomoTracker("https://your-matomo-domain.tld/matomo.php");
- try {
- Future response = tracker.sendRequestAsync(request);
- // usually not needed:
- HttpResponse httpResponse = response.get();
- int statusCode = httpResponse.getStatusLine().getStatusCode();
- if (statusCode > 399) {
- // problem
- }
- } catch (IOException e) {
- throw new UncheckedIOException("Could not send request to Matomo", e);
- } catch (ExecutionException | InterruptedException e) {
- throw new RuntimeException("Error while getting response", e);
}
- }
-
}
+
+
```
-If you have multiple requests to wish to track, it may be more efficient to send them in a single HTTP call. To do this,
+If you have multiple requests you wish to track, it may be more efficient to send them in a single HTTP call. To do this,
send a bulk request. Place your requests in an _Iterable_ data structure and call
```java
-package example;
-
-import org.apache.http.HttpResponse;
-import org.matomo.java.tracking.MatomoRequest;
-import org.matomo.java.tracking.MatomoRequestBuilder;
+import java.net.URI;
+import org.matomo.java.tracking.MatomoRequests;
import org.matomo.java.tracking.MatomoTracker;
+import org.matomo.java.tracking.TrackerConfiguration;
+import org.matomo.java.tracking.parameters.VisitorId;
+
+/**
+ * Example for sending multiple requests in one bulk request.
+ */
+public class BulkExample {
+
+ /**
+ * Example for sending multiple requests in one bulk request.
+ *
+ * @param args ignored
+ */
+ public static void main(String[] args) {
+
+ TrackerConfiguration configuration = TrackerConfiguration
+ .builder()
+ .apiEndpoint(URI.create("https://www.yourdomain.com/matomo.php"))
+ .defaultSiteId(1)
+ .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71")
+ .logFailedTracking(true)
+ .build();
+
+ try (MatomoTracker tracker = new MatomoTracker(configuration)) {
+ VisitorId visitorId = VisitorId.fromString("customer@mail.com");
+ tracker.sendBulkRequestAsync(
+ MatomoRequests.siteSearch("Running shoes", "Running", 120L)
+ .visitorId(visitorId).build(),
+ MatomoRequests.pageView("VelocityStride ProX Running Shoes")
+ .visitorId(visitorId).build(),
+ MatomoRequests.ecommerceOrder("QXZ-789LMP", 100.0, 124.0, 19.0, 10.0, 5.0)
+ .visitorId(visitorId)
+ .build()
+ );
+ } catch (Exception e) {
+ throw new RuntimeException("Could not close tracker", e);
+ }
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-
-public class YourImplementation {
-
- public void yourMethod() {
-
- Collection requests = new ArrayList<>();
- MatomoRequestBuilder builder = MatomoRequest.builder().siteId(42);
- requests.add(builder.actionUrl("https://www.mydomain.com/some/page").actionName("Some Page").build());
- requests.add(builder.actionUrl("https://www.mydomain.com/another/page").actionName("Another Page").build());
-
- MatomoTracker tracker = new MatomoTracker("https://your-matomo-domain.tld/matomo.php");
- try {
- Future response = tracker.sendBulkRequestAsync(requests);
- // usually not needed:
- HttpResponse httpResponse = response.get();
- int statusCode = httpResponse.getStatusLine().getStatusCode();
- if (statusCode > 399) {
- // problem
- }
- } catch (IOException e) {
- throw new UncheckedIOException("Could not send request to Matomo", e);
- } catch (ExecutionException | InterruptedException e) {
- throw new RuntimeException("Error while getting response", e);
}
- }
-
}
-
```
-If some of the parameters that you've specified in the bulk request require AuthToken to be set, this can also be set in
-the bulk request through
+This will send three requests in a single HTTP call. The requests will be sent asynchronously.
+
+Per default every request has the following default parameters:
+
+| Parameter Name | Default Value |
+|-----------------|--------------------------------|
+| required | true |
+| visitorId | random 16 character hex string |
+| randomValue | random 20 character hex string |
+| apiVersion | 1 |
+| responseAsImage | false |
+
+Overwrite these properties as desired. We strongly recommend you to determine the visitor id for every user using
+a unique identifier, e.g. an email address. If you do not provide a visitor id, a random visitor id will be generated.
+
+Ecommerce requests contain ecommerce items, that can be fluently build:
```java
-package example;
+import java.net.URI;
+import org.matomo.java.tracking.MatomoRequests;
+import org.matomo.java.tracking.MatomoTracker;
+import org.matomo.java.tracking.TrackerConfiguration;
+import org.matomo.java.tracking.parameters.EcommerceItem;
+import org.matomo.java.tracking.parameters.EcommerceItems;
+import org.matomo.java.tracking.parameters.VisitorId;
+
+/**
+ * Example for sending an ecommerce request.
+ */
+public class EcommerceExample {
+
+ /**
+ * Example for sending an ecommerce request.
+ *
+ * @param args ignored
+ */
+ public static void main(String[] args) {
+
+ TrackerConfiguration configuration = TrackerConfiguration
+ .builder()
+ .apiEndpoint(URI.create("https://www.yourdomain.com/matomo.php"))
+ .defaultSiteId(1)
+ .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71")
+ .logFailedTracking(true)
+ .build();
+
+ try (MatomoTracker tracker = new MatomoTracker(configuration)) {
+ tracker.sendBulkRequestAsync(MatomoRequests
+ .ecommerceCartUpdate(50.0)
+ .ecommerceItems(EcommerceItems
+ .builder()
+ .item(EcommerceItem
+ .builder()
+ .sku("XYZ12345")
+ .name("Matomo - The big book about web analytics")
+ .category("Education & Teaching")
+ .price(23.1)
+ .quantity(2)
+ .build())
+ .item(EcommerceItem
+ .builder()
+ .sku("B0C2WV3MRJ")
+ .name("Matomo for data visualization")
+ .category("Education & Teaching")
+ .price(15.0)
+ .quantity(1)
+ .build())
+ .build())
+ .visitorId(VisitorId.fromString("customer@mail.com"))
+ .build()
+ );
+ } catch (Exception e) {
+ throw new RuntimeException("Could not close tracker", e);
+ }
+
+ }
+
+}
+```
+
+Note that if you want to be able to track campaigns using *Referrers > Campaigns*, you must add the correct
+URL parameters to your actionUrl. See [Tracking Campaigns](https://matomo.org/docs/tracking-campaigns/) for more
+information. All HTTP query parameters
+denoted on the [Matomo Tracking HTTP API](https://developer.matomo.org/api-reference/tracking-api) can be set using the
+appropriate getters and setters. See
+[MatomoRequest](core/src/main/java/org/matomo/java/tracking/MatomoRequest.java) for the mappings of the parameters to
+their corresponding attributes.
+
+Requests are validated prior to sending. If a request is invalid, a `MatomoException` will be thrown.
-import org.apache.http.HttpResponse;
-import org.matomo.java.tracking.MatomoLocale;
+In a Servlet environment, it might be easier to use the `ServletMatomoRequest` class to create a `MatomoRequest` from a
+`HttpServletRequest`:
+
+```java
+import jakarta.servlet.http.HttpServletRequest;
import org.matomo.java.tracking.MatomoRequest;
-import org.matomo.java.tracking.MatomoRequestBuilder;
+import org.matomo.java.tracking.MatomoRequests;
import org.matomo.java.tracking.MatomoTracker;
+import org.matomo.java.tracking.parameters.VisitorId;
+import org.matomo.java.tracking.servlet.JakartaHttpServletWrapper;
+import org.matomo.java.tracking.servlet.ServletMatomoRequest;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Locale;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-
-public class YourImplementation {
-
- public void yourMethod() {
-
- Collection requests = new ArrayList<>();
- MatomoRequestBuilder builder = MatomoRequest.builder().siteId(42);
- requests.add(builder.actionUrl("https://www.mydomain.com/some/page").actionName("Some Page").build());
- requests.add(builder.actionUrl("https://www.mydomain.com/another/page").actionName("Another Page").visitorCountry(new MatomoLocale(Locale.GERMANY)).build());
-
- MatomoTracker tracker = new MatomoTracker("https://your-matomo-domain.tld/matomo.php");
- try {
- Future response = tracker.sendBulkRequestAsync(requests, "33dc3f2536d3025974cccb4b4d2d98f4"); // second parameter is authentication token need for country override
- // usually not needed:
- HttpResponse httpResponse = response.get();
- int statusCode = httpResponse.getStatusLine().getStatusCode();
- if (statusCode > 399) {
- // problem
- }
- } catch (IOException e) {
- throw new UncheckedIOException("Could not send request to Matomo", e);
- } catch (ExecutionException | InterruptedException e) {
- throw new RuntimeException("Error while getting response", e);
- }
+public class ServletMatomoRequestExample {
- }
+ private final MatomoTracker tracker;
-}
+ public ServletMatomoRequestExample(MatomoTracker tracker) {
+ this.tracker = tracker;
+ }
+ public void someControllerMethod(HttpServletRequest request) {
+ MatomoRequest matomoRequest = ServletMatomoRequest
+ .addServletRequestHeaders(
+ MatomoRequests.contentImpression(
+ "Latest Product Announced",
+ "Main Blog Text",
+ "https://www.yourdomain.com/blog/2018/10/01/new-product-launches"
+ ),
+ JakartaHttpServletWrapper.fromHttpServletRequest(request)
+ ).visitorId(VisitorId.fromString("customer@mail.com"))
+ // ...
+ .build();
+ tracker.sendRequestAsync(matomoRequest);
+ // ...
+ }
+}
```
-## Building
-
-You need a GPG signing key on your machine. Please follow these
-instructions: https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key
+The `ServletMatomoRequest` automatically sets the action URL, applies browser request headers, corresponding Matomo
+cookies and the visitor IP address. It sets the visitor ID, Matomo session ID, custom variables and heatmap
+if Matomo cookies are present. Since there was a renaming from Java EE (javax) to Jakarta EE (jakarta), we provide a
+wrapper class `JakartaHttpServletWrapper` for Jakarta and `JavaxHttpServletWrapper` for javax.
+
+### Tracking Configuration
+
+The `MatomoTracker` can be configured using the `TrackerConfiguration` object. The following configuration options are
+available:
+
+* `.apiEndpoint(...)` An `URI` object that points to the Matomo Tracking API endpoint of your Matomo installation. Must
+ be set.
+* `.defaultSiteId(...)` If you provide a default site id, it will be taken if the action does not contain a site id.
+* `.defaultTokenAuth(...)` If you provide a default token auth, it will be taken if the action does not contain a token
+ auth.
+* `.enabled(...)` The tracker is enabled per default. You can disable it per configuration with this flag.
+* `.logFailedTracking(...)` Will send errors to the log if the Matomo Tracking API responds with an erroneous HTTP code
+* `.connectTimeout(...)` allows you to change the default connection timeout of 10 seconds. 0 is
+ interpreted as infinite, null uses the system default
+* `.socketTimeout(...)` allows you to change the default socket timeout of 10 seconds. 0 is
+ interpreted as infinite, null uses the system default
+* `.userAgent(...)` used by the request made to the endpoint is `MatomoJavaClient` per default. You can change it by
+ using this builder method.
+* `.proxyHost(...)` The hostname or IP address of an optional HTTP proxy. `proxyPort` must be
+ configured as well
+* `.proxyPort(...)` The port of an HTTP proxy. `proxyHost` must be configured as well.
+* `.proxyUsername(...)` If the HTTP proxy requires a username for basic authentication, it can be
+ configured with this method. Proxy host, port and password must also be set.
+* `.proxyPassword(...)` The corresponding password for the basic auth proxy user. The proxy host,
+ port and username must be set as well.
+* `.disableSslCertValidation(...)` If set to true, the SSL certificate of the Matomo server will not be validated. This
+ should only be used for testing purposes. Default: false
+* `.disableSslHostVerification(...)` If set to true, the SSL host of the Matomo server will not be validated. This
+ should only be used for testing purposes. Default: false
+* `.threadPoolSize(...)` The number of threads that will be used to asynchronously send requests. Default: 2
+
+## Migration from Version 2 to 3
+
+We improved this library by adding the dimension parameter and removing outdated parameters in Matomo version 5,
+removing some dependencies (that even contained vulnerabilities) and increasing maintainability. Sadly this includes the
+following breaking changes:
+
+### Removals
+
+* The parameter `actionTime` (`gt_ms`) is no longer supported by Matomo 5 and was removed.
+* Many methods marked as deprecated in version 2 were removed. Please see the
+ former [Javadoc](https://javadoc.io/doc/org.piwik.java.tracking/matomo-java-tracker/2.1/index.html) of version 2 to
+ get the
+ deprecated methods.
+* We removed the vulnerable dependency to the Apache HTTP client. Callbacks are no longer of
+ type `FutureCallback`, but
+ `Consumer` instead.
+* The `send...` methods of `MatomoTracker` no longer return a value (usually Matomo always returns an HTTP 204 response
+ without a body). If the request fails, an exception will be thrown.
+* Since there are several ways on how to set the auth token, `verifyAuthTokenSet` was removed. Just check yourself,
+ whether your auth token is null. However, the tracker checks, whether an auth token is either set by parameter, by
+ request or per configuration.
+* Due to a major refactoring on how the queries are created, we no longer use a large map instead of concrete attributes
+ to collect the Matomo parameters. Therefore `getParameters()` of class `MatomoRequest` no longer exists. Please use
+ getters and setters instead.
+* The methods `verifyEcommerceEnabled()` and `verifyEcommerceState()` were removed from `MatomoRequest`. The request
+ will be validated prior to sending and not during construction.
+* `getRandomHexString` was removed. Use `RandomValue.random()` or `VisitorId.random()` instead.
+
+### Type Changes and Renaming
+
+* `requestDatetime`, `visitorPreviousVisitTimestamp`, `visitorFirstVisitTimestamp`, `ecommerceLastOrderTimestamp` are
+ of type `Instant`. You can use `Instant.ofEpochSecond()` to create
+ them from epoch seconds.
+* `requestDatetime` was renamed to `requestTimestamp` due to setter collision and downwards compatibility
+* `goalRevenue` is the same parameter as `ecommerceRevenue` and was removed to prevent duplication.
+ Use `ecommerceRevenue` instead.
+* `setEventValue` requires a double parameter
+* `setEcommerceLastOrderTimestamp` requires an `Instant` parameter
+* `headerAcceptLanguage` is of type `AcceptLanguage`. You can build it easily
+ using `AcceptLanguage.fromHeader("de")`
+* `visitorCountry` is of type `Country`. You can build it easily using `Country.fromCode("fr")`
+* `deviceResolution` is of type `DeviceResolution`. You can build it easily
+ using `DeviceResolution.builder.width(...).height(...).build()`. To ease the migration, we added a constructor
+ method `DeviceResolution.fromString()` that accepts inputs of kind _width_x_height_, e.g. `100x200`
+* `pageViewId` is of type `UniqueId`. You can build it easily using `UniqueId.random()`
+* `randomValue` is of type `RandomValue`. You can build it easily using `RandomValue.random()`. However, if you
+ really
+ want to insert a custom string here, use `RandomValue.fromString()` construction method.
+* URL was removed due to performance and complicated exception handling and problems with parsing of complex
+ URLs. `actionUrl`, `referrerUrl`, `outlinkUrl`, `contentTarget` and `downloadUrl` are strings.
+* `getCustomTrackingParameter()` of `MatomoRequest` returns an unmodifiable list.
+* Instead of `IllegalStateException` the tracker throws `MatomoException`
+* In former versions the goal id had always to be zero or null. You can define higher numbers than zero.
+* For more type changes see the sections below.
+
+### Visitor ID
+
+* `visitorId` and `visitorCustomId` are of type `VisitorId`. You can build them easily
+ using `VisitorId.fromHash(...)`.
+* You can use `VisitorId.fromHex()` to create a `VisitorId` from a string that contains only hexadecimal characters.
+* Or simply use `VisitorId.fromUUID()` to create a `VisitorId` from a `UUID` object.
+* VisitorId.fromHex() supports less than 16 hexadecimal characters. If the string is shorter than 16 characters,
+ the remaining characters will be filled with zeros.
+
+### Custom Variables
+
+* According to Matomo, custom variables should no longer be used. Please use dimensions instead. Dimension support has
+ been introduced.
+* `CustomVariable` is in package `org.matomo.java.tracking.parameters`.
+* `customTrackingParameters` in `MatomoRequestBuilder` requires a `Map>` instead
+ of `Map`
+* `pageCustomVariables` and `visitCustomVariables` are of type `CustomVariables` instead of collections. Create them
+ with `new CustomVariables().add(customVariable)`
+* `setPageCustomVariable` and `getPageCustomVariable` no longer accept a string as an index. Please use integers
+ instead.
+* Custom variables will be sent URL encoded
+
+## Building and testing
This project can be tested and built by calling
@@ -285,32 +743,60 @@ This project can be tested and built by calling
mvn install
```
-The built jars and javadoc can be found in `target`. By using the install Maven goal, the snapshot
-version can be used using your local Maven repository for testing purposes, e.g.
+The built jars and javadoc can be found in `target`. By using
+the Maven goal `install`, a snapshot
+version can be used in your local Maven repository for testing purposes, e.g.
```xml
-
- org.piwik.java.tracking
- matomo-java-tracker
- 2.1-SNAPSHOT
+ org.piwik.java.tracking
+ matomo-java-tracker
+ 4.0.1-SNAPSHOT
```
-This project also supports [Pitest](http://pitest.org/) mutation testing. This report can be generated by calling
+## Testing on a local Matomo instance
+
+To start a local Matomo instance for testing, you can use the docker-compose file in the root directory of this project.
+Start the docker containers with
```shell
-mvn org.pitest:pitest-maven:mutationCoverage
+docker-compose up -d
```
-and will produce an HTML report at `target/pit-reports/YYYYMMDDHHMI`
+After that you can access Matomo at http://localhost:8080. You have to set up Matomo first. The database credentials are
+`matomo` and `matomo`. The database name is `matomo`. The (internal) database host address is `database`. The database
+port is `3306`. Set the URL to http://localhost and enable ecommerce. Configure an auth token using the user interface
+without HTTPS encryption enabled.
+
+After the installation you can run `MatomoTrackerTester` in the module `test` to test the tracker. Configure a valid
+auth token before running the tests. The tester will send multiple randomized requests to the local Matomo instance.
-Clean this project using
+To enable debug logging, you append the following line to the `config.ini.php` file:
+
+```ini
+[Tracker]
+debug = 1
+```
+
+Use the following snippet to do this:
```shell
-mvn clean
+docker-compose exec matomo sh -c 'echo -e "\n\n[Tracker]\ndebug = 1\n" >> /var/www/html/config/config.ini.php'
```
+To test the servlet integration, run `MatomoServletTester` in your favorite IDE. It starts an embedded Jetty server
+that serves a simple servlet. The servlet sends a request to the local Matomo instance if you call the URL
+http://localhost:8090/track.html. Maybe you need to disable support for the Do Not Track preference in Matomo to get the
+request tracked: Go to _Administration > Privacy > Do Not Track_ and disable the checkbox _Respect Do Not Track_.
+We also recommend to install the Custom Variables plugin from Marketplace to the test custom variables feature and
+setup some dimensions.
+
+## Versioning
+
+We use [SemVer](https://semver.org/) for versioning. For the versions available, see
+the [tags on this repository](https://github.com/matomo-org/matomo-java-tracker/tags).
+
## Contribute
Have a fantastic feature idea? Spot a bug? We would absolutely love for you to contribute to this project! Please feel
@@ -319,14 +805,29 @@ free to:
* Fork this project
* Create a feature branch from the _master_ branch
* Write awesome code that does awesome things
-* Write awesome test to test your awesome code
+* Write awesome tests to test your awesome code
* Verify that everything is working as it should by running _mvn test_. If everything passes, you may
want to make sure that your tests are covering everything you think they are!
- Run `mvn org.pitest:pitest-maven:mutationCoverage` to find out!
+ Run `mvn verify` to find out!
* Commit this code to your repository
* Submit a pull request from your branch to our dev branch and let us know why you made the changes you did
* We'll take a look at your request and work to get it integrated with the repo!
+Please read [the contribution document](CONTRIBUTING.md) for details on our code of conduct, and the
+process for submitting pull requests to us.
+
+We use Checkstyle and JaCoCo to ensure code quality. Please run `mvn verify` before submitting a pull request. Please
+provide tests for your changes. We use JUnit 5 for testing. Coverage should be at least 80%.
+
+## Other Java Matomo Tracker Implementations
+
+* [The original Piwik Java Tracker Implementation](https://github.com/summitsystemsinc/piwik-java-tracking)
+* [Matomo SDK for Android](https://github.com/matomo-org/matomo-sdk-android)
+* [Piwik SDK Android](https://github.com/lkogler/piwik-sdk-android)
+* [piwik-tracking](https://github.com/ralscha/piwik-tracking)
+* [Matomo Tracking API Java Client](https://github.com/dheid/matomo-tracker) -> Most of the code was integrated in the
+ official Matomo Java Tracker
+
## License
This software is released under the BSD 3-Clause license. See [LICENSE](LICENSE).
@@ -334,3 +835,4 @@ This software is released under the BSD 3-Clause license. See [LICENSE](LICENSE)
## Copyright
Copyright (c) 2015 General Electric Company. All rights reserved.
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..8f6e49f6
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,17 @@
+# Security Policy
+
+## Supported Versions
+
+The following versions of this library are
+currently being supported with security updates.
+
+| Version | Supported |
+|---------|------------------------|
+| >4 | :white_check_mark: yes |
+| <=3 | ✖️ no |
+
+## Reporting a Vulnerability
+
+If you found a security vulerability please don't hesitate to send me a message,
+open a new [discussion](https://github.com/matomo-org/matomo-java-tracker/discussions) or
+open a new [issue](https://github.com/matomo-org/matomo-java-tracker/issues).
diff --git a/checkstyle.xml b/checkstyle.xml
new file mode 100644
index 00000000..8a41bc88
--- /dev/null
+++ b/checkstyle.xml
@@ -0,0 +1,358 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 00000000..78690ada
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+
+ org.piwik.java.tracking
+ matomo-java-tracker-parent
+ 4.0.1-SNAPSHOT
+ ../pom.xml
+
+
+ matomo-java-tracker-core
+ jar
+
+ Matomo Java Tracker Core
+
+
+
+ com.github.spotbugs
+ spotbugs-annotations
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ test
+
+
+
diff --git a/core/src/main/java/lombok.config b/core/src/main/java/lombok.config
new file mode 100644
index 00000000..7a21e880
--- /dev/null
+++ b/core/src/main/java/lombok.config
@@ -0,0 +1 @@
+lombok.addLombokGeneratedAnnotation = true
diff --git a/core/src/main/java/org/matomo/java/tracking/ActionType.java b/core/src/main/java/org/matomo/java/tracking/ActionType.java
new file mode 100644
index 00000000..f6431e68
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/ActionType.java
@@ -0,0 +1,27 @@
+package org.matomo.java.tracking;
+
+import java.util.function.BiConsumer;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+/** The type of action performed (download or outlink). */
+@RequiredArgsConstructor
+public enum ActionType {
+ DOWNLOAD(MatomoRequest.MatomoRequestBuilder::downloadUrl),
+ LINK(MatomoRequest.MatomoRequestBuilder::outlinkUrl);
+
+ @NonNull private final BiConsumer consumer;
+
+ /**
+ * Applies the action URL to the given builder.
+ *
+ * @param builder The builder to apply the action URL to.
+ * @param actionUrl The action URL to apply.
+ * @return The builder with the action URL applied.
+ */
+ public MatomoRequest.MatomoRequestBuilder applyUrl(
+ @NonNull MatomoRequest.MatomoRequestBuilder builder, @NonNull String actionUrl) {
+ consumer.accept(builder, actionUrl);
+ return builder;
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/AuthToken.java b/core/src/main/java/org/matomo/java/tracking/AuthToken.java
new file mode 100644
index 00000000..5a90f026
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/AuthToken.java
@@ -0,0 +1,38 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+final class AuthToken {
+
+ private AuthToken() {
+ // utility
+ }
+
+ @Nullable
+ static String determineAuthToken(
+ @Nullable Iterable extends MatomoRequest> requests,
+ @Nullable TrackerConfiguration trackerConfiguration) {
+ if (requests != null) {
+ for (MatomoRequest request : requests) {
+ if (request != null && isNotBlank(request.getAuthToken())) {
+ return request.getAuthToken();
+ }
+ }
+ }
+ if (trackerConfiguration != null && isNotBlank(trackerConfiguration.getDefaultAuthToken())) {
+ return trackerConfiguration.getDefaultAuthToken();
+ }
+ return null;
+ }
+
+ private static boolean isNotBlank(@Nullable String str) {
+ return str != null && !str.isEmpty() && !str.trim().isEmpty();
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/BulkRequest.java b/core/src/main/java/org/matomo/java/tracking/BulkRequest.java
new file mode 100644
index 00000000..1c894ddf
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/BulkRequest.java
@@ -0,0 +1,39 @@
+package org.matomo.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Iterator;
+import lombok.Builder;
+import lombok.NonNull;
+import lombok.Value;
+
+@Builder
+@Value
+class BulkRequest {
+
+ @NonNull Collection queries;
+
+ @Nullable String authToken;
+
+ byte[] toBytes() {
+
+ if (queries.isEmpty()) {
+ throw new IllegalArgumentException("Queries must not be empty");
+ }
+ StringBuilder payload = new StringBuilder("{\"requests\":[");
+ Iterator iterator = queries.iterator();
+ while (iterator.hasNext()) {
+ String query = iterator.next();
+ payload.append("\"?").append(query).append('"');
+ if (iterator.hasNext()) {
+ payload.append(',');
+ }
+ }
+ payload.append(']');
+ if (authToken != null) {
+ payload.append(",\"token_auth\":\"").append(authToken).append('"');
+ }
+ return payload.append('}').toString().getBytes(StandardCharsets.UTF_8);
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/DaemonThreadFactory.java b/core/src/main/java/org/matomo/java/tracking/DaemonThreadFactory.java
new file mode 100644
index 00000000..9b0a9cd2
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/DaemonThreadFactory.java
@@ -0,0 +1,17 @@
+package org.matomo.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+class DaemonThreadFactory implements ThreadFactory {
+
+ private final AtomicInteger count = new AtomicInteger();
+
+ @Override
+ public Thread newThread(@NonNull Runnable r) {
+ Thread thread = new Thread(null, r, "MatomoJavaTracker-" + count.getAndIncrement());
+ thread.setDaemon(true);
+ return thread;
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/ExecutorServiceCloser.java b/core/src/main/java/org/matomo/java/tracking/ExecutorServiceCloser.java
new file mode 100644
index 00000000..b1f09920
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/ExecutorServiceCloser.java
@@ -0,0 +1,39 @@
+package org.matomo.java.tracking;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import lombok.NonNull;
+
+/** Helps to close an executor service. */
+public class ExecutorServiceCloser {
+
+ /**
+ * Closes the given executor service.
+ *
+ *
This will check whether the executor service is already terminated, and if not, it initiates
+ * a shutdown and waits a minute. If the minute expires, the executor service is shutdown
+ * immediately.
+ *
+ * @param executorService The executor service to close
+ */
+ public static void close(@NonNull ExecutorService executorService) {
+ boolean terminated = executorService.isTerminated();
+ if (!terminated) {
+ executorService.shutdown();
+ boolean interrupted = false;
+ while (!terminated) {
+ try {
+ terminated = executorService.awaitTermination(1L, TimeUnit.MINUTES);
+ } catch (InterruptedException e) {
+ if (!interrupted) {
+ executorService.shutdownNow();
+ interrupted = true;
+ }
+ }
+ }
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/InvalidUrlException.java b/core/src/main/java/org/matomo/java/tracking/InvalidUrlException.java
new file mode 100644
index 00000000..2f4c56e9
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/InvalidUrlException.java
@@ -0,0 +1,16 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking;
+
+/** Thrown when an invalid URL is passed to the tracker. */
+public class InvalidUrlException extends RuntimeException {
+
+ InvalidUrlException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/MatomoException.java b/core/src/main/java/org/matomo/java/tracking/MatomoException.java
new file mode 100644
index 00000000..99a3e90b
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/MatomoException.java
@@ -0,0 +1,25 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking;
+
+/**
+ * Thrown when an error occurs while communicating with the Matomo server or when the request is
+ * invalid.
+ */
+public class MatomoException extends RuntimeException {
+
+ private static final long serialVersionUID = 4592083764365938934L;
+
+ MatomoException(String message) {
+ super(message);
+ }
+
+ MatomoException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/MatomoRequest.java b/core/src/main/java/org/matomo/java/tracking/MatomoRequest.java
new file mode 100644
index 00000000..da1097bd
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/MatomoRequest.java
@@ -0,0 +1,776 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking;
+
+import java.nio.charset.Charset;
+import java.time.Instant;
+import java.util.Map;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Builder.Default;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+import org.matomo.java.tracking.parameters.AcceptLanguage;
+import org.matomo.java.tracking.parameters.Country;
+import org.matomo.java.tracking.parameters.CustomVariables;
+import org.matomo.java.tracking.parameters.DeviceResolution;
+import org.matomo.java.tracking.parameters.EcommerceItems;
+import org.matomo.java.tracking.parameters.RandomValue;
+import org.matomo.java.tracking.parameters.UniqueId;
+import org.matomo.java.tracking.parameters.VisitorId;
+
+/**
+ * A class that implements the
+ * Matomo Tracking HTTP API. These requests can be sent using {@link MatomoTracker}.
+ *
+ * @author brettcsorba
+ */
+@Builder(builderMethodName = "request")
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString
+public class MatomoRequest {
+
+ /**
+ * The ID of the website we're tracking a visit/action for. Only needed, if no default site id is
+ * configured.
+ */
+ @TrackingParameter(name = "rec")
+ @Default
+ private Boolean required = true;
+
+ /**
+ * The ID of the website we're tracking a visit/action for. Only needed, if no default site id is
+ * configured.
+ */
+ @TrackingParameter(name = "idsite", min = 1)
+ private Integer siteId;
+
+ /**
+ * The title of the action being tracked. For page tracks this is used as page title. If enabled
+ * in your installation you may use the category tree structure in this field. For example, "game
+ * / register new user" would then create a group "game" and add the item "register new user" in
+ * it.
+ */
+ @TrackingParameter(name = "action_name")
+ private String actionName;
+
+ /** The full URL for the current action. */
+ @TrackingParameter(name = "url")
+ private String actionUrl;
+
+ /** Defines the API version to use (default: 1). */
+ @TrackingParameter(name = "apiv")
+ @Default
+ private String apiVersion = "1";
+
+ /**
+ * The unique visitor ID. See {@link VisitorId}. Default is {@link VisitorId#random()}
+ *
+ *
Since version 3.0.0 this parameter is of type {@link VisitorId} and not a String anymore.
+ * Use {@link VisitorId#fromHex(String)} to create a VisitorId from a hex string, {@link
+ * VisitorId#fromUUID(UUID)} to create it from a UUID or {@link VisitorId#fromHash(long)} to
+ * create it from a long value.
+ */
+ @TrackingParameter(name = "_id")
+ @Default
+ private VisitorId visitorId = VisitorId.random();
+
+ /**
+ * Tracks if the visitor is a returning visitor.
+ *
+ *
This is done by storing a visitor ID in a 1st party cookie.
+ */
+ @TrackingParameter(name = "_idn")
+ private Boolean newVisitor;
+
+ /**
+ * The full HTTP Referrer URL. This value is used to determine how someone got to your website
+ * (ie, through a website, search engine or campaign)
+ */
+ @TrackingParameter(name = "urlref")
+ private String referrerUrl;
+
+ /**
+ * Custom variables are custom name-value pairs that you can assign to your visitors (or page
+ * views).
+ */
+ @TrackingParameter(name = "_cvar")
+ private CustomVariables visitCustomVariables;
+
+ /**
+ * The current count of visits for this visitor. To set this value correctly, it would be required
+ * to store the value for each visitor in your application (using sessions or persisting in a
+ * database). Then you would manually increment the counts by one on each new visit or "session",
+ * depending on how you choose to define a visit.
+ */
+ @TrackingParameter(name = "_idvc", min = 0)
+ private Integer visitorVisitCount;
+
+ /**
+ * The UNIX timestamp of this visitor's previous visit. This parameter is used to populate the
+ * report Visitors > Engagement > Visits by days since last visit.
+ */
+ @TrackingParameter(name = "_viewts")
+ private Instant visitorPreviousVisitTimestamp;
+
+ /**
+ * The UNIX timestamp of this visitor's first visit. This could be set to the date where the user
+ * first started using your software/app, or when he/she created an account.
+ */
+ @TrackingParameter(name = "_idts")
+ private Instant visitorFirstVisitTimestamp;
+
+ /** The campaign name. This parameter will only be used for the first pageview of a visit. */
+ @TrackingParameter(name = "_rcn")
+ private String campaignName;
+
+ /**
+ * The campaign keyword (see Tracking
+ * Campaigns). Used to populate the Referrers > Campaigns report (clicking on a
+ * campaign loads all keywords for this campaign). This parameter will only be used for the first
+ * pageview of a visit.
+ */
+ @TrackingParameter(name = "_rck")
+ private String campaignKeyword;
+
+ /** The resolution of the device the visitor is using. */
+ @TrackingParameter(name = "res")
+ private DeviceResolution deviceResolution;
+
+ /** The current hour (local time). */
+ @TrackingParameter(name = "h", min = 0, max = 23)
+ private Integer currentHour;
+
+ /** The current minute (local time). */
+ @TrackingParameter(name = "m", min = 0, max = 59)
+ private Integer currentMinute;
+
+ /** The current second (local time). */
+ @TrackingParameter(name = "s", min = 0, max = 59)
+ private Integer currentSecond;
+
+ /** Does the visitor use the Adobe Flash Plugin. */
+ @TrackingParameter(name = "fla")
+ private Boolean pluginFlash;
+
+ /** Does the visitor use the Java plugin. */
+ @TrackingParameter(name = "java")
+ private Boolean pluginJava;
+
+ /** Does the visitor use Director plugin. */
+ @TrackingParameter(name = "dir")
+ private Boolean pluginDirector;
+
+ /** Does the visitor use Quicktime plugin. */
+ @TrackingParameter(name = "qt")
+ private Boolean pluginQuicktime;
+
+ /** Does the visitor use Realplayer plugin. */
+ @TrackingParameter(name = "realp")
+ private Boolean pluginRealPlayer;
+
+ /** Does the visitor use a PDF plugin. */
+ @TrackingParameter(name = "pdf")
+ private Boolean pluginPDF;
+
+ /** Does the visitor use a Windows Media plugin. */
+ @TrackingParameter(name = "wma")
+ private Boolean pluginWindowsMedia;
+
+ /** Does the visitor use a Gears plugin. */
+ @TrackingParameter(name = "gears")
+ private Boolean pluginGears;
+
+ /** Does the visitor use a Silverlight plugin. */
+ @TrackingParameter(name = "ag")
+ private Boolean pluginSilverlight;
+
+ /** Does the visitor's client is known to support cookies. */
+ @TrackingParameter(name = "cookie")
+ private Boolean supportsCookies;
+
+ /** An override value for the User-Agent HTTP header field. */
+ @TrackingParameter(name = "ua")
+ private String headerUserAgent;
+
+ /**
+ * JSON-encoded User Agent
+ * Client Hints collected by JavaScript. Used to enrich the detected user agent data.
+ *
+ *
Example: {@code {"brands":[{"brand":"Chromium","version":"110"}],"mobile":false}}
+ */
+ @TrackingParameter(name = "uadata")
+ private String clientHints;
+
+ /**
+ * An override value for the Accept-Language HTTP header field. This value is used to detect the
+ * visitor's country if GeoIP is not enabled.
+ */
+ @TrackingParameter(name = "lang")
+ private AcceptLanguage headerAcceptLanguage;
+
+ /**
+ * Defines the User ID for this request. User ID is any non-empty unique string identifying the
+ * user (such as an email address or a username). When specified, the User ID will be "enforced".
+ * This means that if there is no recent visit with this User ID, a new one will be created. If a
+ * visit is found in the last 30 minutes with your specified User ID, then the new action will be
+ * recorded to this existing visit.
+ */
+ @TrackingParameter(name = "uid")
+ private String userId;
+
+ /** defines the visitor ID for this request. */
+ @TrackingParameter(name = "cid")
+ private VisitorId visitorCustomId;
+
+ /** will force a new visit to be created for this action. */
+ @TrackingParameter(name = "new_visit")
+ private Boolean newVisit;
+
+ /**
+ * Custom variables are custom name-value pairs that you can assign to your visitors (or page
+ * views).
+ */
+ @TrackingParameter(name = "cvar")
+ private CustomVariables pageCustomVariables;
+
+ /**
+ * An external URL the user has opened. Used for tracking outlink clicks. We recommend to also set
+ * the url parameter to this same value.
+ */
+ @TrackingParameter(name = "link")
+ private String outlinkUrl;
+
+ /**
+ * URL of a file the user has downloaded. Used for tracking downloads. We recommend to also set
+ * the url parameter to this same value.
+ */
+ @TrackingParameter(name = "download")
+ private String downloadUrl;
+
+ /**
+ * The Site Search keyword. When specified, the request will not be tracked as a normal pageview
+ * but will instead be tracked as a Site Search request
+ */
+ @TrackingParameter(name = "search")
+ private String searchQuery;
+
+ /** When search is specified, you can optionally specify a search category with this parameter. */
+ @TrackingParameter(name = "search_cat")
+ private String searchCategory;
+
+ /**
+ * When search is specified, we also recommend setting the search_count to the number of search
+ * results displayed on the results page. When keywords are tracked with &search_count=0 they will
+ * appear in the "No Result Search Keyword" report.
+ */
+ @TrackingParameter(name = "search_count", min = 0)
+ private Long searchResultsCount;
+
+ /**
+ * Accepts a six character unique ID that identifies which actions were performed on a specific
+ * page view. When a page was viewed, all following tracking requests (such as events) during that
+ * page view should use the same pageview ID. Once another page was viewed a new unique ID should
+ * be generated. Use [0-9a-Z] as possible characters for the unique ID.
+ */
+ @TrackingParameter(name = "pv_id")
+ private UniqueId pageViewId;
+
+ /**
+ * If specified, the tracking request will trigger a conversion for the goal of the website being
+ * tracked with this ID. The value 0 tracks an ecommerce interaction.
+ */
+ @TrackingParameter(name = "idgoal", min = 0)
+ private Integer goalId;
+
+ /** The grand total for the ecommerce order (required when tracking an ecommerce order). */
+ @TrackingParameter(name = "revenue", min = 0)
+ private Double ecommerceRevenue;
+
+ /**
+ * The charset of the page being tracked. Specify the charset if the data you send to Matomo is
+ * encoded in a different character set than the default utf-8
+ */
+ @TrackingParameter(name = "cs")
+ private Charset characterSet;
+
+ /**
+ * can be optionally sent along any tracking request that isn't a page view. For example, it can
+ * be sent together with an event tracking request. The advantage being that should you ever
+ * disable the event plugin, then the event tracking requests will be ignored vs if the parameter
+ * is not set, a page view would be tracked even though it isn't a page view.
+ */
+ @TrackingParameter(name = "ca")
+ private Boolean customAction;
+
+ /** How long it took to connect to server. */
+ @TrackingParameter(name = "pf_net", min = 0)
+ private Long networkTime;
+
+ /** How long it took the server to generate page. */
+ @TrackingParameter(name = "pf_srv", min = 0)
+ private Long serverTime;
+
+ /** How long it takes the browser to download the response from the server. */
+ @TrackingParameter(name = "pf_tfr", min = 0)
+ private Long transferTime;
+
+ /**
+ * How long the browser spends loading the webpage after the response was fully received until the
+ * user can start interacting with it.
+ */
+ @TrackingParameter(name = "pf_dm1", min = 0)
+ private Long domProcessingTime;
+
+ /**
+ * How long it takes for the browser to load media and execute any Javascript code listening for
+ * the DOMContentLoaded event.
+ */
+ @TrackingParameter(name = "pf_dm2", min = 0)
+ private Long domCompletionTime;
+
+ /** How long it takes the browser to execute Javascript code waiting for the window.load event. */
+ @TrackingParameter(name = "pf_onl", min = 0)
+ private Long onloadTime;
+
+ /** eg. Videos, Music, Games... */
+ @TrackingParameter(name = "e_c")
+ private String eventCategory;
+
+ /** An event action like Play, Pause, Duration, Add Playlist, Downloaded, Clicked... */
+ @TrackingParameter(name = "e_a")
+ private String eventAction;
+
+ /** The event name for example a Movie name, or Song name, or File name... */
+ @TrackingParameter(name = "e_n")
+ private String eventName;
+
+ /** Some numeric value that represents the event value. */
+ @TrackingParameter(name = "e_v", min = 0)
+ private Double eventValue;
+
+ /** The name of the content. For instance 'Ad Foo Bar' */
+ @TrackingParameter(name = "c_n")
+ private String contentName;
+
+ /** The actual content piece. For instance the path to an image, video, audio, any text */
+ @TrackingParameter(name = "c_p")
+ private String contentPiece;
+
+ /** The target of the content. For instance the URL of a landing page */
+ @TrackingParameter(name = "c_t")
+ private String contentTarget;
+
+ /** The name of the interaction with the content. For instance a 'click' */
+ @TrackingParameter(name = "c_i")
+ private String contentInteraction;
+
+ /**
+ * The unique string identifier for the ecommerce order (required when tracking an ecommerce
+ * order).
+ */
+ @TrackingParameter(name = "ec_id")
+ private String ecommerceId;
+
+ /** Items in the Ecommerce order. */
+ @TrackingParameter(name = "ec_items")
+ private EcommerceItems ecommerceItems;
+
+ /** The subtotal of the order; excludes shipping. */
+ @TrackingParameter(name = "ec_st", min = 0)
+ private Double ecommerceSubtotal;
+
+ /** Tax amount of the order. */
+ @TrackingParameter(name = "ec_tx", min = 0)
+ private Double ecommerceTax;
+
+ /** Shipping cost of the order. */
+ @TrackingParameter(name = "ec_sh", min = 0)
+ private Double ecommerceShippingCost;
+
+ /** Discount offered. */
+ @TrackingParameter(name = "ec_dt", min = 0)
+ private Double ecommerceDiscount;
+
+ /**
+ * The UNIX timestamp of this customer's last ecommerce order. This value is used to process the
+ * "Days since last order" report.
+ */
+ @TrackingParameter(name = "_ects")
+ private Instant ecommerceLastOrderTimestamp;
+
+ /**
+ * The SKU of the product being viewed. Used for ecommerce product page tracking.
+ *
+ *
Requires {@code idgoal=0} and {@code _pks} to be set.
+ */
+ @TrackingParameter(name = "_pks")
+ private String ecommerceProductSku;
+
+ /**
+ * The name of the product being viewed. Used for ecommerce product page tracking.
+ *
+ *
Requires {@code idgoal=0} and {@code _pks} to be set.
+ */
+ @TrackingParameter(name = "_pkn")
+ private String ecommerceProductName;
+
+ /**
+ * The category of the product being viewed. Used for ecommerce product page tracking.
+ *
+ *
Can be a string or a JSON-encoded array of up to five category names.
+ *
+ *
Requires {@code idgoal=0} and {@code _pks} to be set.
+ */
+ @TrackingParameter(name = "_pkc")
+ private String ecommerceProductCategory;
+
+ /**
+ * The price of the product being viewed. Used for ecommerce product page tracking.
+ *
+ *
Requires {@code idgoal=0} and {@code _pks} to be set.
+ */
+ @TrackingParameter(name = "_pkp", min = 0)
+ private Double ecommerceProductPrice;
+
+ /**
+ * 32 character authorization key used to authenticate the API request. We recommend to create a
+ * user specifically for accessing the Tracking API, and give the user only write permission on
+ * the website(s).
+ */
+ @TrackingParameter(name = "token_auth", regex = "[a-z0-9]{32}")
+ private String authToken;
+
+ /** Override value for the visitor IP (both IPv4 and IPv6 notations supported). */
+ @TrackingParameter(name = "cip")
+ private String visitorIp;
+
+ /**
+ * Override for the datetime of the request (normally the current time is used). This can be used
+ * to record visits and page views in the past.
+ */
+ @TrackingParameter(name = "cdt")
+ private Instant requestTimestamp;
+
+ /** An override value for the country. Must be a two-letter ISO 3166 Alpha-2 country code. */
+ @TrackingParameter(name = "country", maxLength = 2)
+ private Country visitorCountry;
+
+ /**
+ * An override value for the region. Should be set to a ISO 3166-2 region code, which are used by
+ * MaxMind's and DB-IP's GeoIP2 databases. See here for a list of them for every country.
+ */
+ @TrackingParameter(name = "region", maxLength = 2)
+ private String visitorRegion;
+
+ /** An override value for the city. The name of the city the visitor is located in, eg, Tokyo. */
+ @TrackingParameter(name = "city")
+ private String visitorCity;
+
+ /** An override value for the visitor's latitude, eg 22.456. */
+ @TrackingParameter(name = "lat", min = -90, max = 90)
+ private Double visitorLatitude;
+
+ /** An override value for the visitor's longitude, eg 22.456. */
+ @TrackingParameter(name = "long", min = -180, max = 180)
+ private Double visitorLongitude;
+
+ /**
+ * When set to false, the queued tracking handler won't be used and instead the tracking request
+ * will be executed directly. This can be useful when you need to debug a tracking problem or want
+ * to test that the tracking works in general.
+ */
+ @TrackingParameter(name = "queuedtracking")
+ private Boolean queuedTracking;
+
+ /**
+ * If set to 0 (send_image=0) Matomo will respond with an HTTP 204 response code instead of a GIF
+ * image. This improves performance and can fix errors if images are not allowed to be obtained
+ * directly (like Chrome Apps). Available since Matomo 2.10.0
+ *
+ *
Default is {@code false}
+ */
+ @TrackingParameter(name = "send_image")
+ @Default
+ private Boolean responseAsImage = false;
+
+ /**
+ * If set to true, the request will be a Heartbeat request which will not track any new activity
+ * (such as a new visit, new action or new goal). The heartbeat request will only update the
+ * visit's total time to provide accurate "Visit duration" metric when this parameter is set. It
+ * won't record any other data. This means by sending an additional tracking request when the user
+ * leaves your site or app with &ping=1, you fix the issue where the time spent of the last page
+ * visited is reported as 0 seconds.
+ */
+ @TrackingParameter(name = "ping")
+ private Boolean ping;
+
+ /**
+ * By default, Matomo does not track bots. If you use the Tracking HTTP API directly, you may be
+ * interested in tracking bot requests.
+ */
+ @TrackingParameter(name = "bots")
+ private Boolean trackBotRequests;
+
+ /**
+ * When {@code bots=1} is set, this specifies the recording mode for bot requests.
+ *
+ *
Set to {@code 1} to record bot requests without triggering any goals, events or actions.
+ */
+ @TrackingParameter(name = "recMode")
+ private Integer botRecordingMode;
+
+ /**
+ * The HTTP status code of the tracked request. Used with bot tracking.
+ *
+ *
When tracking a bot visit, this can be set to the HTTP status code of the bot's request.
+ */
+ @TrackingParameter(name = "http_status")
+ private Integer httpStatusCode;
+
+ /** The bandwidth used for the tracked request in bytes. Used with bot tracking. */
+ @TrackingParameter(name = "bw_bytes", min = 0)
+ private Long bandwidthBytes;
+
+ /**
+ * Defines the source of the tracking request (e.g., {@code "backend"} or {@code "mobile-app"}).
+ *
+ *
Used to classify where the tracking hit originated.
+ */
+ @TrackingParameter(name = "source")
+ private String sourceLabel;
+
+ /**
+ * Meant to hold a random value that is generated before each request. Using it helps avoid the
+ * tracking request being cached by the browser or a proxy.
+ */
+ @TrackingParameter(name = "rand")
+ @Default
+ private RandomValue randomValue = RandomValue.random();
+
+ /**
+ * Meant to hold a random value that is generated before each request. Using it helps avoid the
+ * tracking request being cached by the browser or a proxy.
+ */
+ @TrackingParameter(name = "debug")
+ private Boolean debug;
+
+ /**
+ * A unique ID used to identify the media. Part of the Media Analytics plugin.
+ */
+ @TrackingParameter(name = "ma_id")
+ private String mediaId;
+
+ /**
+ * The title of the media resource. Part of the Media Analytics plugin.
+ */
+ @TrackingParameter(name = "ma_ti")
+ private String mediaTitle;
+
+ /**
+ * The URL of the media resource. Part of the Media Analytics plugin.
+ */
+ @TrackingParameter(name = "ma_re")
+ private String mediaResource;
+
+ /**
+ * The type of media, e.g. {@code "video"} or {@code "audio"}. Part of the Media Analytics plugin.
+ */
+ @TrackingParameter(name = "ma_mt")
+ private String mediaType;
+
+ /**
+ * The name of the media player used to play the media, e.g. {@code "html5"}. Part of the Media Analytics plugin.
+ */
+ @TrackingParameter(name = "ma_pn")
+ private String mediaPlayerName;
+
+ /**
+ * The number of seconds the visitor has spent playing/watching the media resource so far. Part of
+ * the Media Analytics plugin.
+ */
+ @TrackingParameter(name = "ma_st", min = 0)
+ private Integer mediaTimeSpent;
+
+ /**
+ * The total duration / length of the media resource in seconds. Part of the Media Analytics plugin.
+ */
+ @TrackingParameter(name = "ma_le", min = 0)
+ private Integer mediaLength;
+
+ /**
+ * The current progress of the media in percent (0–100). Part of the Media Analytics plugin.
+ */
+ @TrackingParameter(name = "ma_ps", min = 0, max = 100)
+ private Integer mediaProgressPercent;
+
+ /**
+ * How many seconds it took before the media started playing. Part of the Media Analytics plugin.
+ */
+ @TrackingParameter(name = "ma_ttp", min = 0)
+ private Integer mediaTimeToPlay;
+
+ /**
+ * The width of the media player in pixels. Part of the Media Analytics plugin.
+ */
+ @TrackingParameter(name = "ma_w", min = 0)
+ private Integer mediaWidth;
+
+ /**
+ * The height of the media player in pixels. Part of the Media Analytics plugin.
+ */
+ @TrackingParameter(name = "ma_h", min = 0)
+ private Integer mediaHeight;
+
+ /**
+ * Whether the media is currently displayed in fullscreen. Part of the Media Analytics plugin.
+ */
+ @TrackingParameter(name = "ma_fs")
+ private Boolean mediaFullscreen;
+
+ /**
+ * A JSON-encoded array of which positions in the media were viewed by the visitor. Part of the Media Analytics plugin.
+ *
+ *
Example: {@code [[0,15],[30,44]]} means the visitor watched segments 0–15s and 30–44s.
+ */
+ @TrackingParameter(name = "ma_se")
+ private String mediaSegmentsViewed;
+
+ /**
+ * Contains an error message describing the error that occurred during the last tracking request.
+ *
+ *
Custom action must be enabled for this.
+ *
+ *
Required for crash analytics
+ */
+ @TrackingParameter(name = "cra")
+ private String crashMessage;
+
+ /**
+ * The type of exception that occurred during the last tracking request.
+ *
+ *
Custom action must be enabled for this.
+ *
+ *
Typically a fully qualified class name of the exception, e.g. {@code
+ * java.lang.NullPointerException}.
+ *
+ *
Optional for crash analytics
+ */
+ @TrackingParameter(name = "cra_tp")
+ private String crashType;
+
+ /**
+ * Category of a crash to group crashes by.
+ *
+ *
Custom action must be enabled for this.
+ *
+ *
Optional for crash analytics
+ */
+ @TrackingParameter(name = "cra_ct")
+ private String crashCategory;
+
+ /**
+ * A stack trace of the exception that occurred during the last tracking request.
+ *
+ *
Custom action must be enabled for this.
+ *
+ *
Optional for crash analytics
+ */
+ @TrackingParameter(name = "cra_st")
+ private String crashStackTrace;
+
+ /**
+ * The originating source of the crash.
+ *
+ *
Could be a source file URI or something similar
+ *
+ *
Custom action must be enabled for this.
+ *
+ *
Optional for crash analytics
+ */
+ @TrackingParameter(name = "cra_ru")
+ private String crashLocation;
+
+ /**
+ * The line number of the crash source, where the crash occurred.
+ *
+ *
Custom action must be enabled for this.
+ *
+ *
Optional for crash analytics
+ */
+ @TrackingParameter(name = "cra_rl", min = 0)
+ private Integer crashLine;
+
+ /**
+ * The column within the line where the crash occurred.
+ *
+ *
Optional for crash analytics
+ */
+ @TrackingParameter(name = "cra_rc", min = 0)
+ private Integer crashColumn;
+
+ /**
+ * The Matomo session ID sent as a cookie {@code MATOMO_SESSID}.
+ *
+ *
If not null a cookie with the name {@code MATOMO_SESSID} will be sent with the value of this
+ * parameter.
+ */
+ private String sessionId;
+
+ /**
+ * Custom Dimension values for specific Custom Dimension IDs.
+ *
+ *
Custom Dimensions plugin must be
+ * installed. See the Custom Dimensions
+ * guide. Requires Matomo at least 2.15.1
+ */
+ private Map dimensions;
+
+ /**
+ * Allows you to specify additional HTTP request parameters that will be sent to Matomo.
+ *
+ *
For example, you can use this to set the Accept-Language header, or to set the
+ * Content-Type.
+ */
+ private Map additionalParameters;
+
+ /**
+ * You can set additional HTTP headers for the request sent to Matomo.
+ *
+ *
For example, you can use this to set the Accept-Language header, or to set the
+ * Content-Type.
+ */
+ private Map headers;
+
+ /**
+ * Appends additional cookies to the request.
+ *
+ *
This allows you to add Matomo specific cookies, like {@code _pk_id} or {@code _pk_sess}
+ * coming from Matomo responses to the request.
+ */
+ private Map cookies;
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/MatomoRequests.java b/core/src/main/java/org/matomo/java/tracking/MatomoRequests.java
new file mode 100644
index 00000000..8fce9560
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/MatomoRequests.java
@@ -0,0 +1,325 @@
+package org.matomo.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Optional;
+import lombok.NonNull;
+
+/**
+ * This class contains static methods for common tracking items to create {@link MatomoRequest}
+ * objects.
+ *
+ *
The intention of this class is to bundle common tracking items in a single place to make
+ * tracking easier. The methods contain the typical parameters for the tracking item and return a
+ * {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters, like the visitor
+ * ID, a user ID or custom dimensions.
+ */
+public class MatomoRequests {
+
+ /**
+ * Creates a {@link MatomoRequest} object for a download or a link action.
+ *
+ * @param url The URL of the download or link. Must not be null.
+ * @param type The type of the action. Either {@link ActionType#DOWNLOAD} or {@link
+ * ActionType#LINK}.
+ * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters.
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder action(
+ @NonNull String url, @NonNull ActionType type) {
+ return type.applyUrl(MatomoRequest.request(), url);
+ }
+
+ /**
+ * Creates a {@link MatomoRequest} object for a content impression.
+ *
+ *
A content impression is a view of a content piece. The content piece can be a product, an
+ * article, a video, a banner, etc. The content piece can be specified by the parameters {@code
+ * piece} and {@code target}. The {@code name} parameter is required and should be a descriptive
+ * name of the content piece.
+ *
+ * @param name The name of the content piece, like the name of a product or an article. Must not
+ * be null. Example: "SuperPhone".
+ * @param piece The content piece. Can be null. Example: "Smartphone".
+ * @param target The target of the content piece, like the URL of a product or an article. Can be
+ * null. Example: "https://example.com/superphone".
+ * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters.
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder contentImpression(
+ @NonNull String name, @Nullable String piece, @Nullable String target) {
+ return MatomoRequest.request().contentName(name).contentPiece(piece).contentTarget(target);
+ }
+
+ /**
+ * Creates a {@link MatomoRequest} object for a content interaction.
+ *
+ *
Make sure you have tracked a content impression using the same content name and content
+ * piece, otherwise it will not count.
+ *
+ *
A content interaction is an interaction with a content piece. The content piece can be a
+ * product, an article, a video, a banner, etc. The content piece can be specified by the
+ * parameters {@code piece} and {@code target}. The {@code name} parameter is required and should
+ * be a descriptive name of the content piece. The {@code interaction} parameter is required and
+ * should be the type of the interaction, like "click" or "add-to-cart".
+ *
+ * @param interaction The type of the interaction. Must not be null. Example: "click".
+ * @param name The name of the content piece, like the name of a product or an article.
+ * @param piece The content piece. Can be null. Example: "Blog Article XYZ".
+ * @param target The target of the content piece, like the URL of a product or an article. Can be
+ * null. Example: "https://example.com/blog/article-xyz".
+ * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters.
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder contentInteraction(
+ @NonNull String interaction,
+ @NonNull String name,
+ @Nullable String piece,
+ @Nullable String target) {
+ return MatomoRequest.request()
+ .contentInteraction(interaction)
+ .contentName(name)
+ .contentPiece(piece)
+ .contentTarget(target);
+ }
+
+ /**
+ * Creates a {@link MatomoRequest} object for a crash.
+ *
+ *
Requires Crash Analytics plugin to be enabled in the target Matomo instance.
+ *
+ *
A crash is an error that causes the application to stop working. The parameters {@code
+ * message} and {@code stackTrace} are required. The other parameters are optional. The {@code
+ * type} parameter can be used to specify the type of the crash, like {@code
+ * NullPointerException}. The {@code category} parameter can be used to specify the category of
+ * the crash, like payment failure. The {@code location}, {@code line} and {@code column} can be
+ * used to specify the location of the crash. The {@code location} parameter should be the name of
+ * the file where the crash occurred. The {@code line} and {@code column} parameters should be the
+ * line and column number of the crash.
+ *
+ * @param message The message of the crash. Must not be null.
+ * @param type The type of the crash. Can be null. Example: {@code java.lang.NullPointerException}
+ * @param category The category of the crash. Can be null. Example: "payment failure".
+ * @param stackTrace The stack trace of the crash. Must not be null.
+ * @param location The location of the crash. Can be null. Example: "MainActivity.java".
+ * @param line The line number of the crash. Can be null. Example: 42.
+ * @param column The column number of the crash. Can be null. Example: 23.
+ * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters.
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder crash(
+ @NonNull String message,
+ @Nullable String type,
+ @Nullable String category,
+ @Nullable String stackTrace,
+ @Nullable String location,
+ @Nullable Integer line,
+ @Nullable Integer column) {
+ return MatomoRequest.request()
+ .crashMessage(message)
+ .crashType(type)
+ .crashCategory(category)
+ .crashStackTrace(stackTrace)
+ .crashLocation(location)
+ .crashLine(line)
+ .crashColumn(column);
+ }
+
+ /**
+ * Creates a {@link MatomoRequest} object for a crash with information from a {@link Throwable}.
+ *
+ *
Requires Crash Analytics plugin to be enabled in the target Matomo instance.
+ *
+ *
The {@code category} parameter can be used to specify the category of the crash, like
+ * payment failure.
+ *
+ * @param throwable The throwable that caused the crash. Must not be null.
+ * @param category The category of the crash. Can be null. Example: "payment failure".
+ * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters.
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder crash(
+ @NonNull Throwable throwable, @Nullable String category) {
+ return MatomoRequest.request()
+ .crashMessage(throwable.getMessage())
+ .crashCategory(category)
+ .crashStackTrace(formatStackTrace(throwable))
+ .crashType(throwable.getClass().getName())
+ .crashLocation(
+ getFirstStackTraceElement(throwable).map(StackTraceElement::getFileName).orElse(null))
+ .crashLine(
+ getFirstStackTraceElement(throwable)
+ .map(StackTraceElement::getLineNumber)
+ .orElse(null));
+ }
+
+ @edu.umd.cs.findbugs.annotations.NonNull
+ private static String formatStackTrace(@Nullable Throwable throwable) {
+ StringWriter writer = new StringWriter();
+ throwable.printStackTrace(new PrintWriter(writer));
+ return writer.toString().trim();
+ }
+
+ @edu.umd.cs.findbugs.annotations.NonNull
+ private static Optional getFirstStackTraceElement(
+ @edu.umd.cs.findbugs.annotations.NonNull Throwable throwable) {
+ StackTraceElement[] stackTrace = throwable.getStackTrace();
+ if (stackTrace == null || stackTrace.length == 0) {
+ return Optional.empty();
+ }
+ return Optional.of(stackTrace[0]);
+ }
+
+ /**
+ * Creates a {@link MatomoRequest} object for a ecommerce cart update (add item, remove item,
+ * update item).
+ *
+ *
The {@code revenue} parameter is required and should be the total revenue of the cart.
+ *
+ * @param revenue The total revenue of the cart. Must not be null.
+ * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters.
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder ecommerceCartUpdate(@NonNull Double revenue) {
+ return MatomoRequest.request().ecommerceRevenue(revenue);
+ }
+
+ /**
+ * Creates a {@link MatomoRequest} object for a ecommerce order.
+ *
+ *
All revenues (revenue, subtotal, tax, shippingCost, discount) will be individually summed
+ * and reported in Matomo reports.
+ *
+ *
The {@code id} and {@code revenue} parameters are required and should be the order ID and
+ * the total revenue of the order. The other parameters are optional. The {@code subtotal}, {@code
+ * tax}, {@code shippingCost} and {@code discount} parameters should be the subtotal, tax,
+ * shipping cost and discount of the order.
+ *
+ *
If the Ecommerce order contains items (products), you must call {@link
+ * MatomoRequest.MatomoRequestBuilder#ecommerceItems(EcommerceItems)} to add the items to the
+ * request.
+ *
+ * @param id An order ID. Can be a stock keeping unit (SKU) or a unique ID. Must not be null.
+ * @param revenue The total revenue of the order. Must not be null.
+ * @param subtotal The subtotal of the order. Can be null.
+ * @param tax The tax of the order. Can be null.
+ * @param shippingCost The shipping cost of the order. Can be null.
+ * @param discount The discount of the order. Can be null.
+ * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters.
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder ecommerceOrder(
+ @NonNull String id,
+ @NonNull Double revenue,
+ @Nullable Double subtotal,
+ @Nullable Double tax,
+ @Nullable Double shippingCost,
+ @Nullable Double discount) {
+ return MatomoRequest.request()
+ .ecommerceId(id)
+ .ecommerceRevenue(revenue)
+ .ecommerceSubtotal(subtotal)
+ .ecommerceTax(tax)
+ .ecommerceShippingCost(shippingCost)
+ .ecommerceDiscount(discount);
+ }
+
+ /**
+ * Creates a {@link MatomoRequest} object for an event.
+ *
+ *
The {@code category} and {@code action} parameters are required and should be the category
+ * and action of the event. The {@code name} and {@code value} parameters are optional. The {@code
+ * category} parameter should be a category of the event, like "Travel". The {@code action}
+ * parameter should be an action of the event, like "Book flight". The {@code name} parameter
+ * should be the name of the event, like "Flight to Berlin". The {@code value} parameter should be
+ * the value of the event, like the price of the flight.
+ *
+ * @param category The category of the event. Must not be null. Example: "Music"
+ * @param action The action of the event. Must not be null. Example: "Play"
+ * @param name The name of the event. Can be null. Example: "Edvard Grieg - The Death of Ase"
+ * @param value The value of the event. Can be null. Example: 9.99
+ * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters.
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder event(
+ @NonNull String category,
+ @NonNull String action,
+ @Nullable String name,
+ @Nullable Double value) {
+ return MatomoRequest.request()
+ .eventCategory(category)
+ .eventAction(action)
+ .eventName(name)
+ .eventValue(value);
+ }
+
+ /**
+ * Creates a {@link MatomoRequest} object for a conversion of a goal of the website.
+ *
+ *
The {@code id} parameter is required and should be the ID of the goal. The {@code revenue},
+ * {@code name} and {@code value} parameters are optional. The {@code revenue} parameter should be
+ * the revenue of the conversion. The {@code name} parameter should be the name of the conversion.
+ * The {@code value} parameter should be the value of the conversion.
+ *
+ * @param id The ID of the goal. Must not be null. Example: 1
+ * @param revenue The revenue of the conversion. Can be null. Example: 9.99
+ * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters.
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder goal(int id, @Nullable Double revenue) {
+ return MatomoRequest.request().goalId(id).ecommerceRevenue(revenue);
+ }
+
+ /**
+ * Creates a {@link MatomoRequest} object for a page view.
+ *
+ *
The {@code name} parameter is required and should be the name of the page.
+ *
+ * @param name The name of the page. Must not be null. Example: "Home"
+ * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters.
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder pageView(@NonNull String name) {
+ return MatomoRequest.request().actionName(name);
+ }
+
+ /**
+ * Creates a {@link MatomoRequest} object for a search.
+ *
+ *
These are used to populate reports in Actions > Site Search.
+ *
+ *
The {@code query} parameter is required and should be the search query. The {@code category}
+ * and {@code resultsCount} parameters are optional. The {@code category} parameter should be the
+ * category of the search, like "Music". The {@code resultsCount} parameter should be the number
+ * of results of the search.
+ *
+ * @param query The search query. Must not be null. Example: "Edvard Grieg"
+ * @param category The category of the search. Can be null. Example: "Music"
+ * @param resultsCount The number of results of the search. Can be null. Example: 42
+ * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters.
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder siteSearch(
+ @NonNull String query, @Nullable String category, @Nullable Long resultsCount) {
+ return MatomoRequest.request()
+ .searchQuery(query)
+ .searchCategory(category)
+ .searchResultsCount(resultsCount);
+ }
+
+ /**
+ * Creates a {@link MatomoRequest} object for a ping.
+ *
+ *
Ping requests do not track new actions. If they are sent within the standard visit length
+ * (see global.ini.php), they will extend the existing visit and the current last action for the
+ * visit. If after the standard visit length, ping requests will create a new visit using the last
+ * action in the last known visit.
+ *
+ * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters.
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder ping() {
+ return MatomoRequest.request().ping(true);
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/MatomoTracker.java b/core/src/main/java/org/matomo/java/tracking/MatomoTracker.java
new file mode 100644
index 00000000..2d9c0cad
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/MatomoTracker.java
@@ -0,0 +1,194 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import lombok.AccessLevel;
+import lombok.NonNull;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * The main class that sends {@link MatomoRequest}s to a specified Matomo server.
+ *
+ *
Contains several methods to send requests synchronously and asynchronously. The asynchronous
+ * methods return a {@link CompletableFuture} that can be used to wait for the request to finish.
+ * The synchronous methods block until the request is finished. The asynchronous methods are more
+ * efficient if you want to send multiple requests at once.
+ *
+ *
Configure this tracker using the {@link TrackerConfiguration} class. You can use the {@link
+ * TrackerConfiguration#builder()} to create a new configuration. The configuration is immutable and
+ * can be reused for multiple trackers.
+ *
+ *
The tracker is thread-safe and can be used by multiple threads at once.
+ *
+ * @author brettcsorba
+ */
+@Slf4j
+public class MatomoTracker implements AutoCloseable {
+
+ private final TrackerConfiguration trackerConfiguration;
+
+ @Setter(AccessLevel.PROTECTED)
+ private SenderFactory senderFactory = new ServiceLoaderSenderFactory();
+
+ private Sender sender;
+
+ /**
+ * Creates a new Matomo Tracker instance.
+ *
+ * @param trackerConfiguration Configurations parameters (you can use a builder)
+ */
+ public MatomoTracker(@NonNull TrackerConfiguration trackerConfiguration) {
+ trackerConfiguration.validate();
+ this.trackerConfiguration = trackerConfiguration;
+ }
+
+ /**
+ * Sends a tracking request to Matomo using the HTTP GET method.
+ *
+ *
Use this method if you want to send a single request. If you want to send multiple requests
+ * at once, use {@link #sendBulkRequest(Iterable)} instead. If you want to send multiple requests
+ * asynchronously, use {@link #sendRequestAsync(MatomoRequest)} or {@link
+ * #sendBulkRequestAsync(Collection)} instead.
+ *
+ * @param request request to send. must not be null
+ */
+ public void sendRequest(@NonNull MatomoRequest request) {
+ if (trackerConfiguration.isEnabled()) {
+ log.debug("Sending request via GET: {}", request);
+ applyGoalIdAndCheckSiteId(request);
+ initializeSender();
+ sender.sendSingle(request);
+ } else {
+ log.warn("Not sending request, because tracker is disabled");
+ }
+ }
+
+ private void initializeSender() {
+ if (sender == null) {
+ sender =
+ senderFactory.createSender(trackerConfiguration, new QueryCreator(trackerConfiguration));
+ }
+ }
+
+ /**
+ * Send a request asynchronously via HTTP GET.
+ *
+ *
Use this method if you want to send a single request. If you want to send multiple requests
+ * at once, use {@link #sendBulkRequestAsync(Collection)} instead. If you want to send multiple
+ * requests synchronously, use {@link #sendRequest(MatomoRequest)} or {@link
+ * #sendBulkRequest(Iterable)} instead.
+ *
+ * @param request request to send
+ * @return completable future to let you know when the request is done. Contains the request.
+ */
+ public CompletableFuture sendRequestAsync(@NonNull MatomoRequest request) {
+ if (trackerConfiguration.isEnabled()) {
+ applyGoalIdAndCheckSiteId(request);
+ log.debug("Sending async request via GET: {}", request);
+ initializeSender();
+ return sender.sendSingleAsync(request);
+ }
+ log.warn("Not sending request, because tracker is disabled");
+ return CompletableFuture.completedFuture(null);
+ }
+
+ private void applyGoalIdAndCheckSiteId(@NonNull MatomoRequest request) {
+ if (request.getGoalId() == null
+ && (request.getEcommerceId() != null
+ || request.getEcommerceRevenue() != null
+ || request.getEcommerceDiscount() != null
+ || request.getEcommerceItems() != null
+ || request.getEcommerceLastOrderTimestamp() != null
+ || request.getEcommerceShippingCost() != null
+ || request.getEcommerceSubtotal() != null
+ || request.getEcommerceTax() != null)) {
+ request.setGoalId(0);
+ }
+ if (trackerConfiguration.getDefaultSiteId() == null && request.getSiteId() == null) {
+ throw new IllegalArgumentException("No default site ID and no request site ID is given");
+ }
+ }
+
+ /**
+ * Send multiple requests in a single HTTP POST call.
+ *
+ *
More efficient than sending several individual requests. If you want to send a single
+ * request, use {@link #sendRequest(MatomoRequest)} instead. If you want to send multiple requests
+ * asynchronously, use {@link #sendBulkRequestAsync(Collection)} instead.
+ *
+ * @param requests the requests to send
+ */
+ public void sendBulkRequest(MatomoRequest... requests) {
+ sendBulkRequest(Arrays.asList(requests));
+ }
+
+ /**
+ * Send multiple requests in a single HTTP POST call.
+ *
+ *
More efficient than sending several individual requests. If you want to send a single
+ * request, use {@link #sendRequest(MatomoRequest)} instead. If you want to send multiple requests
+ * asynchronously, use {@link #sendBulkRequestAsync(Collection)} instead.
+ *
+ * @param requests the requests to send
+ */
+ public void sendBulkRequest(@NonNull Iterable extends MatomoRequest> requests) {
+ if (trackerConfiguration.isEnabled()) {
+ for (MatomoRequest request : requests) {
+ applyGoalIdAndCheckSiteId(request);
+ }
+ log.debug("Sending requests via POST: {}", requests);
+ initializeSender();
+ sender.sendBulk(requests);
+ } else {
+ log.warn("Not sending request, because tracker is disabled");
+ }
+ }
+
+ /**
+ * Send multiple requests in a single HTTP call. More efficient than sending several individual
+ * requests.
+ *
+ * @param requests the requests to send
+ * @return completable future to let you know when the request is done
+ */
+ public CompletableFuture sendBulkRequestAsync(MatomoRequest... requests) {
+ return sendBulkRequestAsync(Arrays.asList(requests));
+ }
+
+ /**
+ * Send multiple requests in a single HTTP call. More efficient than sending several individual
+ * requests.
+ *
+ * @param requests the requests to send
+ * @return completable future to let you know when the request is done
+ */
+ public CompletableFuture sendBulkRequestAsync(
+ @NonNull Collection extends MatomoRequest> requests) {
+ if (trackerConfiguration.isEnabled()) {
+ for (MatomoRequest request : requests) {
+ applyGoalIdAndCheckSiteId(request);
+ }
+ log.debug("Sending async requests via POST: {}", requests);
+ initializeSender();
+ return sender.sendBulkAsync(requests);
+ }
+ log.warn("Tracker is disabled");
+ return CompletableFuture.completedFuture(null);
+ }
+
+ @Override
+ public void close() throws Exception {
+ if (sender != null) {
+ sender.close();
+ }
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/ProxyAuthenticator.java b/core/src/main/java/org/matomo/java/tracking/ProxyAuthenticator.java
new file mode 100644
index 00000000..c237bcb9
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/ProxyAuthenticator.java
@@ -0,0 +1,31 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+class ProxyAuthenticator extends Authenticator {
+
+ @NonNull private final String user;
+
+ @NonNull private final String password;
+
+ @Nullable
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ if (getRequestorType() == RequestorType.PROXY) {
+ return new PasswordAuthentication(user, password.toCharArray());
+ }
+ return null;
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/QueryCreator.java b/core/src/main/java/org/matomo/java/tracking/QueryCreator.java
new file mode 100644
index 00000000..a824dedb
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/QueryCreator.java
@@ -0,0 +1,158 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Member;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+class QueryCreator {
+
+ private static final TrackingParameterMethod[] TRACKING_PARAMETER_METHODS =
+ initializeTrackingParameterMethods();
+
+ private final TrackerConfiguration trackerConfiguration;
+
+ private static TrackingParameterMethod[] initializeTrackingParameterMethods() {
+ Field[] declaredFields = MatomoRequest.class.getDeclaredFields();
+ List methods = new ArrayList<>(declaredFields.length);
+ for (Field field : declaredFields) {
+ if (field.isAnnotationPresent(TrackingParameter.class)) {
+ addMethods(methods, field, field.getAnnotation(TrackingParameter.class));
+ }
+ }
+ return methods.toArray(new TrackingParameterMethod[0]);
+ }
+
+ private static void addMethods(
+ Collection methods,
+ Member member,
+ TrackingParameter trackingParameter) {
+ try {
+ for (PropertyDescriptor pd :
+ Introspector.getBeanInfo(MatomoRequest.class).getPropertyDescriptors()) {
+ if (member.getName().equals(pd.getName())) {
+ String regex = trackingParameter.regex();
+ methods.add(
+ TrackingParameterMethod.builder()
+ .parameterName(trackingParameter.name())
+ .min(trackingParameter.min())
+ .max(trackingParameter.max())
+ .maxLength(trackingParameter.maxLength())
+ .method(pd.getReadMethod())
+ .pattern(
+ regex == null || regex.isEmpty() || regex.trim().isEmpty()
+ ? null
+ : Pattern.compile(trackingParameter.regex()))
+ .build());
+ }
+ }
+ } catch (IntrospectionException e) {
+ throw new MatomoException("Could not initialize read methods", e);
+ }
+ }
+
+ String createQuery(@NonNull MatomoRequest request, @Nullable String authToken) {
+ StringBuilder query = new StringBuilder(100);
+ if (request.getSiteId() == null) {
+ appendAmpersand(query);
+ query.append("idsite=").append(trackerConfiguration.getDefaultSiteId());
+ }
+ if (authToken != null) {
+ if (authToken.length() != 32) {
+ throw new IllegalArgumentException("Auth token must be exactly 32 characters long");
+ }
+ appendAmpersand(query);
+ query.append("token_auth=").append(authToken);
+ }
+ for (TrackingParameterMethod method : TRACKING_PARAMETER_METHODS) {
+ appendParameter(method, request, query);
+ }
+ if (request.getAdditionalParameters() != null) {
+ for (Entry entry : request.getAdditionalParameters().entrySet()) {
+ Object value = entry.getValue();
+ if (value != null) {
+ String valueString = value.toString();
+ if (!valueString.isEmpty() && !valueString.trim().isEmpty()) {
+ appendAmpersand(query);
+ query.append(encode(entry.getKey())).append('=').append(encode(valueString));
+ }
+ }
+ }
+ }
+ if (request.getDimensions() != null) {
+ for (Entry entry : request.getDimensions().entrySet()) {
+ if (entry.getKey() != null && entry.getValue() != null) {
+ appendAmpersand(query);
+ query
+ .append("dimension")
+ .append(entry.getKey())
+ .append('=')
+ .append(encode(entry.getValue().toString()));
+ }
+ }
+ }
+ return query.toString();
+ }
+
+ private static void appendAmpersand(StringBuilder query) {
+ if (query.length() != 0) {
+ query.append('&');
+ }
+ }
+
+ private static void appendParameter(
+ TrackingParameterMethod method, MatomoRequest request, StringBuilder query) {
+ try {
+ Object parameterValue = method.getMethod().invoke(request);
+ if (parameterValue != null) {
+ method.validateParameterValue(parameterValue);
+ appendAmpersand(query);
+ query.append(method.getParameterName()).append('=');
+ if (parameterValue instanceof Boolean) {
+ query.append((boolean) parameterValue ? '1' : '0');
+ } else if (parameterValue instanceof Charset) {
+ query.append(((Charset) parameterValue).name());
+ } else if (parameterValue instanceof Instant) {
+ query.append(((Instant) parameterValue).getEpochSecond());
+ } else {
+ String parameterValueString = parameterValue.toString();
+ if (!parameterValueString.trim().isEmpty()) {
+ query.append(encode(parameterValueString));
+ }
+ }
+ }
+ } catch (Exception e) {
+ throw new MatomoException("Could not append parameter", e);
+ }
+ }
+
+ @NonNull
+ private static String encode(@NonNull String parameterValue) {
+ try {
+ return URLEncoder.encode(parameterValue, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new MatomoException("Could not encode parameter", e);
+ }
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/RequestValidator.java b/core/src/main/java/org/matomo/java/tracking/RequestValidator.java
new file mode 100644
index 00000000..5df2cae8
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/RequestValidator.java
@@ -0,0 +1,48 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import lombok.NonNull;
+
+final class RequestValidator {
+
+ private RequestValidator() {
+ // utility
+ }
+
+ static void validate(@NonNull MatomoRequest request, @Nullable CharSequence authToken) {
+
+ if (request.getSearchResultsCount() != null && request.getSearchQuery() == null) {
+ throw new MatomoException("Search query must be set if search results count is set");
+ }
+ if (authToken == null) {
+ if (request.getVisitorLongitude() != null
+ || request.getVisitorLatitude() != null
+ || request.getVisitorRegion() != null
+ || request.getVisitorCity() != null
+ || request.getVisitorCountry() != null
+ || request.getVisitorIp() != null) {
+ throw new MatomoException(
+ "Auth token must be present if visitor longitude, latitude, region, city, country or IP"
+ + " are set");
+ }
+ if (request.getRequestTimestamp() != null
+ && request.getRequestTimestamp().isBefore(Instant.now().minus(4, ChronoUnit.HOURS))) {
+ throw new MatomoException(
+ "Auth token must be present if request timestamp is more than four hours ago");
+ }
+ } else {
+ if (authToken.length() != 32) {
+ throw new IllegalArgumentException("Auth token must be exactly 32 characters long");
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/Sender.java b/core/src/main/java/org/matomo/java/tracking/Sender.java
new file mode 100644
index 00000000..75c2243b
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/Sender.java
@@ -0,0 +1,17 @@
+package org.matomo.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+
+interface Sender extends AutoCloseable {
+ @NonNull
+ CompletableFuture sendSingleAsync(@NonNull MatomoRequest request);
+
+ void sendSingle(@NonNull MatomoRequest request);
+
+ void sendBulk(@NonNull Iterable extends MatomoRequest> requests);
+
+ @NonNull
+ CompletableFuture sendBulkAsync(@NonNull Collection extends MatomoRequest> requests);
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/SenderFactory.java b/core/src/main/java/org/matomo/java/tracking/SenderFactory.java
new file mode 100644
index 00000000..11c36f42
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/SenderFactory.java
@@ -0,0 +1,7 @@
+package org.matomo.java.tracking;
+
+/** A factory for {@link Sender} instances. */
+public interface SenderFactory {
+
+ Sender createSender(TrackerConfiguration trackerConfiguration, QueryCreator queryCreator);
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/SenderProvider.java b/core/src/main/java/org/matomo/java/tracking/SenderProvider.java
new file mode 100644
index 00000000..14f57e92
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/SenderProvider.java
@@ -0,0 +1,6 @@
+package org.matomo.java.tracking;
+
+interface SenderProvider {
+
+ Sender provideSender(TrackerConfiguration trackerConfiguration, QueryCreator queryCreator);
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/ServiceLoaderSenderFactory.java b/core/src/main/java/org/matomo/java/tracking/ServiceLoaderSenderFactory.java
new file mode 100644
index 00000000..958bcb6d
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/ServiceLoaderSenderFactory.java
@@ -0,0 +1,30 @@
+package org.matomo.java.tracking;
+
+import static java.util.stream.Collectors.toMap;
+
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.function.Function;
+import java.util.stream.StreamSupport;
+
+class ServiceLoaderSenderFactory implements SenderFactory {
+
+ @Override
+ public Sender createSender(TrackerConfiguration trackerConfiguration, QueryCreator queryCreator) {
+ ServiceLoader serviceLoader = ServiceLoader.load(SenderProvider.class);
+ Map senderProviders =
+ StreamSupport.stream(serviceLoader.spliterator(), false)
+ .collect(
+ toMap(senderProvider -> senderProvider.getClass().getName(), Function.identity()));
+ SenderProvider senderProvider =
+ senderProviders.get("org.matomo.java.tracking.Java11SenderProvider");
+ if (senderProvider == null) {
+ senderProvider = senderProviders.get("org.matomo.java.tracking.Java8SenderProvider");
+ }
+ if (senderProvider == null) {
+ throw new MatomoException("No SenderProvider found");
+ }
+ return senderProvider.provideSender(
+ trackerConfiguration, new QueryCreator(trackerConfiguration));
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/TrackerConfiguration.java b/core/src/main/java/org/matomo/java/tracking/TrackerConfiguration.java
new file mode 100644
index 00000000..110ae027
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/TrackerConfiguration.java
@@ -0,0 +1,166 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.net.URI;
+import java.time.Duration;
+import java.util.regex.Pattern;
+import lombok.Builder;
+import lombok.Value;
+
+/** Defines configuration settings for the Matomo tracking. */
+@Builder
+@Value
+public class TrackerConfiguration {
+
+ private static final Pattern AUTH_TOKEN_PATTERN = Pattern.compile("[a-z0-9]+");
+
+ /**
+ * The Matomo Tracking HTTP API endpoint. Example: https://your-matomo-domain.example/matomo.php
+ */
+ URI apiEndpoint;
+
+ /** The default ID of the website that will be used if not specified explicitly. */
+ Integer defaultSiteId;
+
+ /** The authorization token (parameter token_auth) to use if not specified explicitly. */
+ String defaultAuthToken;
+
+ /** Allows to stop the tracker to send requests to the Matomo endpoint. */
+ @Builder.Default boolean enabled = true;
+
+ /**
+ * The timeout until a connection is established.
+ *
+ *
A timeout value of zero is interpreted as an infinite timeout. A `null` value is interpreted
+ * as undefined (system default if applicable).
+ *
+ *
Default: 5 seconds
+ */
+ @Builder.Default Duration connectTimeout = Duration.ofSeconds(5L);
+
+ /**
+ * The socket timeout ({@code SO_TIMEOUT}), which is the timeout for waiting for data or, put
+ * differently, a maximum period inactivity between two consecutive data packets.
+ *
+ *
A timeout value of zero is interpreted as an infinite timeout. A `null value is interpreted
+ * as undefined (system default if applicable).
+ *
+ *
Default: 5 seconds
+ */
+ @Builder.Default Duration socketTimeout = Duration.ofSeconds(5L);
+
+ /**
+ * The hostname or IP address of an optional HTTP proxy. {@code proxyPort} must be configured as
+ * well
+ */
+ @Nullable String proxyHost;
+
+ /** The port of an HTTP proxy. {@code proxyHost} must be configured as well. */
+ int proxyPort;
+
+ /**
+ * If the HTTP proxy requires a username for basic authentication, it can be configured here.
+ * Proxy host, port and password must also be set.
+ */
+ @Nullable String proxyUsername;
+
+ /**
+ * The corresponding password for the basic auth proxy user. The proxy host, port and username
+ * must be set as well.
+ */
+ @Nullable String proxyPassword;
+
+ /** A custom user agent to be set. Defaults to "MatomoJavaClient" */
+ @Builder.Default String userAgent = "MatomoJavaClient";
+
+ /**
+ * Logs if the Matomo Tracking API endpoint responds with an erroneous HTTP code. Defaults to
+ * false.
+ */
+ boolean logFailedTracking;
+
+ /**
+ * Disables SSL certificate validation. This is useful for testing with self-signed certificates.
+ * Do not use in production environments. Defaults to false.
+ *
+ *
Attention: This slows down performance
+ *
+ * @see #disableSslHostVerification
+ */
+ boolean disableSslCertValidation;
+
+ /**
+ * Disables SSL host verification. This is useful for testing with self-signed certificates. Do
+ * not use in production environments. Defaults to false.
+ *
+ *
If you use the Java 11 of the Matomo Java Tracker, this setting is ignored. Instead, you
+ * have to set the system property {@code jdk.internal.httpclient.disableHostnameVerification} as
+ * described in the Module
+ * java.net.http.
+ *
+ * @see #disableSslCertValidation
+ */
+ boolean disableSslHostVerification;
+
+ /**
+ * The thread pool size for the async sender. Defaults to 2.
+ *
+ *
Attention: If you use this library in a web application, make sure that this thread pool
+ * does not exceed the thread pool of the web application. Otherwise, you might run into problems.
+ */
+ @Builder.Default int threadPoolSize = 2;
+
+ /** Validates the auth token. The auth token must be exactly 32 characters long. */
+ public void validate() {
+ if (apiEndpoint == null) {
+ throw new IllegalArgumentException("API endpoint must not be null");
+ }
+ if (defaultAuthToken != null) {
+ if (defaultAuthToken.length() != 32) {
+ throw new IllegalArgumentException("Auth token must be exactly 32 characters long");
+ }
+ if (!AUTH_TOKEN_PATTERN.matcher(defaultAuthToken).matches()) {
+ throw new IllegalArgumentException(
+ "Auth token must contain only lowercase letters and numbers");
+ }
+ }
+ if (defaultSiteId != null && defaultSiteId < 0) {
+ throw new IllegalArgumentException("Default site ID must not be negative");
+ }
+ if (proxyHost != null && proxyPort < 1) {
+ throw new IllegalArgumentException("Proxy port must be greater than 0");
+ }
+ if (proxyPort > 0 && proxyHost == null) {
+ throw new IllegalArgumentException("Proxy host must be set if port is set");
+ }
+ if (proxyUsername != null && proxyHost == null) {
+ throw new IllegalArgumentException("Proxy host must be set if username is set");
+ }
+ if (proxyPassword != null && proxyHost == null) {
+ throw new IllegalArgumentException("Proxy host must be set if password is set");
+ }
+ if (proxyUsername != null && proxyPassword == null) {
+ throw new IllegalArgumentException("Proxy password must be set if username is set");
+ }
+ if (proxyPassword != null && proxyUsername == null) {
+ throw new IllegalArgumentException("Proxy username must be set if password is set");
+ }
+ if (socketTimeout != null && socketTimeout.isNegative()) {
+ throw new IllegalArgumentException("Socket timeout must not be negative");
+ }
+ if (connectTimeout != null && connectTimeout.isNegative()) {
+ throw new IllegalArgumentException("Connect timeout must not be negative");
+ }
+ if (threadPoolSize < 1) {
+ throw new IllegalArgumentException("Thread pool size must be greater than 0");
+ }
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/TrackingParameter.java b/core/src/main/java/org/matomo/java/tracking/TrackingParameter.java
new file mode 100644
index 00000000..1e11181f
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/TrackingParameter.java
@@ -0,0 +1,28 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@interface TrackingParameter {
+
+ String name();
+
+ String regex() default "";
+
+ double min() default Double.MIN_VALUE;
+
+ double max() default Double.MAX_VALUE;
+
+ int maxLength() default Integer.MAX_VALUE;
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/TrackingParameterMethod.java b/core/src/main/java/org/matomo/java/tracking/TrackingParameterMethod.java
new file mode 100644
index 00000000..82556dba
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/TrackingParameterMethod.java
@@ -0,0 +1,61 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking;
+
+import java.lang.reflect.Method;
+import java.util.regex.Pattern;
+import lombok.Builder;
+import lombok.NonNull;
+import lombok.Value;
+
+@Builder
+@Value
+class TrackingParameterMethod {
+
+ String parameterName;
+
+ Method method;
+
+ Pattern pattern;
+
+ double min;
+
+ double max;
+
+ int maxLength;
+
+ void validateParameterValue(@NonNull Object parameterValue) {
+ if (pattern != null
+ && parameterValue instanceof CharSequence
+ && !pattern.matcher((CharSequence) parameterValue).matches()) {
+ throw new MatomoException(
+ String.format("Invalid value for %s. Must match regex %s", parameterName, pattern));
+ }
+ if (maxLength != 0 && parameterValue.toString().length() > maxLength) {
+ throw new MatomoException(
+ String.format(
+ "Invalid value for %s. Must be less or equal than %d characters",
+ parameterName, maxLength));
+ }
+ if (parameterValue instanceof Number) {
+ Number number = (Number) parameterValue;
+ if (number.doubleValue() < min) {
+ throw new MatomoException(
+ String.format(
+ "Invalid value for %s. Must be greater or equal than %s",
+ parameterName, min % 1 == 0 ? Long.toString((long) min) : min));
+ }
+ if (number.doubleValue() > max) {
+ throw new MatomoException(
+ String.format(
+ "Invalid value for %s. Must be less or equal than %s",
+ parameterName, max % 1 == 0 ? Long.toString((long) max) : max));
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/TrustingX509TrustManager.java b/core/src/main/java/org/matomo/java/tracking/TrustingX509TrustManager.java
new file mode 100644
index 00000000..3e742f9f
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/TrustingX509TrustManager.java
@@ -0,0 +1,24 @@
+package org.matomo.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.X509TrustManager;
+
+class TrustingX509TrustManager implements X509TrustManager {
+
+ @Override
+ @Nullable
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+
+ @Override
+ public void checkClientTrusted(@Nullable X509Certificate[] chain, @Nullable String authType) {
+ // no operation
+ }
+
+ @Override
+ public void checkServerTrusted(@Nullable X509Certificate[] chain, @Nullable String authType) {
+ // no operation
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/package-info.java b/core/src/main/java/org/matomo/java/tracking/package-info.java
new file mode 100644
index 00000000..3a56acd9
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/package-info.java
@@ -0,0 +1,12 @@
+/**
+ * This package contains classes that allow you to specify a {@link
+ * org.matomo.java.tracking.MatomoTracker} with the corresponding {@link
+ * org.matomo.java.tracking.TrackerConfiguration}. You can then send a {@link
+ * org.matomo.java.tracking.MatomoRequest} as a single HTTP GET request or multiple requests as a
+ * bulk HTTP POST request synchronously or asynchronously. If an exception occurs, {@link
+ * org.matomo.java.tracking.MatomoException} will be thrown.
+ *
+ *
For more information about the Matomo Tracking HTTP API, see the Matomo Tracking HTTP API.
+ */
+package org.matomo.java.tracking;
diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/AcceptLanguage.java b/core/src/main/java/org/matomo/java/tracking/parameters/AcceptLanguage.java
new file mode 100644
index 00000000..d30aadaa
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/AcceptLanguage.java
@@ -0,0 +1,69 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.parameters;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.List;
+import java.util.Locale.LanguageRange;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import lombok.Builder;
+import lombok.Singular;
+import lombok.Value;
+
+/**
+ * Describes the content for the Accept-Language header field that can be overridden by a custom
+ * parameter. The format is specified in the corresponding RFC 4647 Matching of Language Tags
+ *
+ *
Example: "en-US,en;q=0.8,de;q=0.6"
+ */
+@Builder
+@Value
+public class AcceptLanguage {
+
+ @Singular List languageRanges;
+
+ /**
+ * Creates the Accept-Language definition for a given header.
+ *
+ *
Please see {@link LanguageRange#parse(String)} for more information. Example:
+ * "en-US,en;q=0.8,de;q=0.6"
+ *
+ * @param header A header that can be null
+ * @return The parsed header (probably reformatted). null if the header is null.
+ * @see LanguageRange#parse(String)
+ */
+ @Nullable
+ public static AcceptLanguage fromHeader(@Nullable String header) {
+ if (header == null || header.trim().isEmpty()) {
+ return null;
+ }
+ return new AcceptLanguage(LanguageRange.parse(header));
+ }
+
+ /**
+ * Returns the Accept Language header value.
+ *
+ * @return The header value, e.g. "en-US,en;q=0.8,de;q=0.6"
+ */
+ @NonNull
+ public String toString() {
+ return languageRanges.stream()
+ .filter(Objects::nonNull)
+ .map(AcceptLanguage::format)
+ .collect(Collectors.joining(","));
+ }
+
+ private static String format(@NonNull LanguageRange languageRange) {
+ return languageRange.getWeight() == LanguageRange.MAX_WEIGHT
+ ? languageRange.getRange()
+ : languageRange.getRange() + ";q=" + languageRange.getWeight();
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/Country.java b/core/src/main/java/org/matomo/java/tracking/parameters/Country.java
new file mode 100644
index 00000000..a86090cb
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/Country.java
@@ -0,0 +1,72 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.parameters;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.List;
+import java.util.Locale;
+import java.util.Locale.LanguageRange;
+import lombok.AccessLevel;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * A two-letter country code representing a country.
+ *
+ *
See ISO 3166-1 alpha-2 for a
+ * list of valid codes.
+ */
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+public class Country {
+
+ @NonNull private String code;
+
+ /**
+ * Creates a country from a given code.
+ *
+ * @param code Must consist of two lower letters or simply null. Case is ignored
+ * @return The country or null if code was null
+ */
+ @Nullable
+ public static Country fromCode(@Nullable String code) {
+ if (code == null || code.isEmpty() || code.trim().isEmpty()) {
+ return null;
+ }
+ if (code.length() == 2) {
+ return new Country(code.toLowerCase(Locale.ROOT));
+ }
+ throw new IllegalArgumentException("Invalid country code");
+ }
+
+ /**
+ * Extracts the country from the given accept language header.
+ *
+ * @param ranges A language range list. See {@link LanguageRange#parse(String)}
+ * @return The country or null if ranges was null
+ */
+ @Nullable
+ public static Country fromLanguageRanges(@Nullable String ranges) {
+ if (ranges == null || ranges.isEmpty() || ranges.trim().isEmpty()) {
+ return null;
+ }
+ List languageRanges = LanguageRange.parse(ranges);
+ for (LanguageRange languageRange : languageRanges) {
+ String range = languageRange.getRange();
+ String[] split = range.split("-");
+ if (split.length == 2 && split[1].length() == 2) {
+ return new Country(split[1].toLowerCase(Locale.ROOT));
+ }
+ }
+ throw new IllegalArgumentException("Invalid country code");
+ }
+
+ @Override
+ public String toString() {
+ return code;
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/CustomVariable.java b/core/src/main/java/org/matomo/java/tracking/parameters/CustomVariable.java
new file mode 100644
index 00000000..45cbf58a
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/CustomVariable.java
@@ -0,0 +1,55 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.parameters;
+
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.Setter;
+import lombok.ToString;
+
+/**
+ * A key-value pair that represents custom information. See How do I use
+ * Custom Variables?
+ *
+ *
If you are not already using Custom Variables to measure your custom data, Matomo recommends
+ * to use the Custom Dimensions feature
+ * instead. There are many advantages of Custom
+ * Dimensions over Custom variables. Custom variables will be deprecated in the future.
+ *
+ * @deprecated Should not be used according to the Matomo FAQ: How do I use
+ * Custom Variables?
+ */
+@Getter
+@Setter
+@AllArgsConstructor
+@ToString
+@EqualsAndHashCode(exclude = "index")
+@Deprecated
+public class CustomVariable {
+
+ private int index;
+
+ @NonNull private String key;
+
+ @NonNull private String value;
+
+ /**
+ * Instantiates a new custom variable.
+ *
+ * @param key the key of the custom variable (required)
+ * @param value the value of the custom variable (required)
+ */
+ public CustomVariable(@NonNull String key, @NonNull String value) {
+ this.key = key;
+ this.value = value;
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/CustomVariables.java b/core/src/main/java/org/matomo/java/tracking/parameters/CustomVariables.java
new file mode 100644
index 00000000..acd2e041
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/CustomVariables.java
@@ -0,0 +1,213 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.parameters;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.StringTokenizer;
+import lombok.EqualsAndHashCode;
+import lombok.NonNull;
+
+/**
+ * A bunch of key-value pairs that represent custom information. See How do I use
+ * Custom Variables?
+ *
+ * @deprecated Should not be used according to the Matomo FAQ: How do I use
+ * Custom Variables?
+ */
+@EqualsAndHashCode
+@Deprecated
+public class CustomVariables {
+
+ private final Map variables = new LinkedHashMap<>();
+
+ /**
+ * Adds a custom variable to the list with the next available index.
+ *
+ * @param variable The custom variable to add
+ * @return This object for method chaining
+ */
+ public CustomVariables add(@NonNull CustomVariable variable) {
+ if (variable.getKey().isEmpty()) {
+ throw new IllegalArgumentException("Custom variable key must not be null or empty");
+ }
+ if (variable.getValue().isEmpty()) {
+ throw new IllegalArgumentException("Custom variable value must not be null or empty");
+ }
+ boolean found = false;
+ for (Entry entry : variables.entrySet()) {
+ CustomVariable customVariable = entry.getValue();
+ if (customVariable.getKey().equals(variable.getKey())) {
+ variables.put(entry.getKey(), variable);
+ found = true;
+ }
+ }
+ if (!found) {
+ int i = 1;
+ while (variables.putIfAbsent(i, variable) != null) {
+ i++;
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Adds a custom variable to the list with the given index.
+ *
+ * @param cv The custom variable to add
+ * @param index The index to add the custom variable at
+ * @return This object for method chaining
+ */
+ public CustomVariables add(@NonNull CustomVariable cv, int index) {
+ validateIndex(index);
+ variables.put(index, cv);
+ return this;
+ }
+
+ private static void validateIndex(int index) {
+ if (index <= 0) {
+ throw new IllegalArgumentException("Index must be greater than 0");
+ }
+ }
+
+ /**
+ * Returns the custom variable at the given index.
+ *
+ * @param index The index of the custom variable
+ * @return The custom variable at the given index
+ */
+ @Nullable
+ public CustomVariable get(int index) {
+ validateIndex(index);
+ return variables.get(index);
+ }
+
+ /**
+ * Returns the value of the custom variable with the given key. If there are multiple custom
+ * variables with the same key, the first one is returned. If there is no custom variable with the
+ * given key, null is returned.
+ *
+ * @param key The key of the custom variable. Must not be null.
+ * @return The value of the custom variable with the given key. null if there is no variable with
+ * the given key.
+ */
+ @Nullable
+ public String get(@NonNull String key) {
+ if (key.isEmpty()) {
+ throw new IllegalArgumentException("key must not be null or empty");
+ }
+ return variables.values().stream()
+ .filter(variable -> variable.getKey().equals(key))
+ .findFirst()
+ .map(CustomVariable::getValue)
+ .orElse(null);
+ }
+
+ /**
+ * Removes the custom variable at the given index. If there is no custom variable at the given
+ * index, nothing happens.
+ *
+ * @param index The index of the custom variable to remove. Must be greater than 0.
+ */
+ public void remove(int index) {
+ validateIndex(index);
+ variables.remove(index);
+ }
+
+ /**
+ * Removes the custom variable with the given key. If there is no custom variable with the given
+ * key, nothing happens.
+ *
+ * @param key The key of the custom variable to remove. Must not be null.
+ */
+ public void remove(@NonNull String key) {
+ variables.entrySet().removeIf(entry -> entry.getValue().getKey().equals(key));
+ }
+
+ boolean isEmpty() {
+ return variables.isEmpty();
+ }
+
+ /**
+ * Parses a JSON representation of custom variables.
+ *
+ *
The format is as follows: {@code {"1":["key1","value1"],"2":["key2","value2"]}}
+ *
+ *
This is mainly used to parse the custom variables from the cookie.
+ *
+ * @param value The JSON representation of the custom variables to parse or null
+ * @return The parsed custom variables or null if the given value is null or empty
+ */
+ @Nullable
+ public static CustomVariables parse(@Nullable String value) {
+ if (value == null || value.isEmpty()) {
+ return null;
+ }
+
+ CustomVariables customVariables = new CustomVariables();
+ StringTokenizer tokenizer = new StringTokenizer(value, ":{}\"");
+
+ Integer key = null;
+ String customVariableKey = null;
+ String customVariableValue = null;
+ while (tokenizer.hasMoreTokens()) {
+ String token = tokenizer.nextToken().trim();
+ if (!token.isEmpty()) {
+ if (token.matches("\\d+")) {
+ key = Integer.parseInt(token);
+ } else if (token.startsWith("[") && key != null) {
+ customVariableKey = tokenizer.nextToken();
+ tokenizer.nextToken();
+ customVariableValue = tokenizer.nextToken();
+ } else if (key != null && customVariableKey != null && customVariableValue != null) {
+ customVariables.add(new CustomVariable(customVariableKey, customVariableValue), key);
+ } else if (token.equals(",")) {
+ key = null;
+ customVariableKey = null;
+ customVariableValue = null;
+ }
+ }
+ }
+ return customVariables;
+ }
+
+ /**
+ * Creates a JSON representation of the custom variables. The format is as follows: {@code
+ * {"1":["key1","value1"],"2":["key2","value2"]}}
+ *
+ * @return A JSON representation of the custom variables
+ */
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder("{");
+ Iterator> iterator = variables.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Entry entry = iterator.next();
+ stringBuilder
+ .append('"')
+ .append(entry.getKey())
+ .append("\":[\"")
+ .append(entry.getValue().getKey())
+ .append("\",\"")
+ .append(entry.getValue().getValue())
+ .append("\"]");
+ if (iterator.hasNext()) {
+ stringBuilder.append(',');
+ }
+ }
+ stringBuilder.append('}');
+ return stringBuilder.toString();
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/DeviceResolution.java b/core/src/main/java/org/matomo/java/tracking/parameters/DeviceResolution.java
new file mode 100644
index 00000000..684b054f
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/DeviceResolution.java
@@ -0,0 +1,53 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.parameters;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import lombok.Builder;
+import lombok.RequiredArgsConstructor;
+
+/** The resolution (width and height) of the user's output device (monitor / phone). */
+@Builder
+@RequiredArgsConstructor
+public class DeviceResolution {
+
+ private final int width;
+
+ private final int height;
+
+ /**
+ * Creates a device resolution from a string representation.
+ *
+ *
The string must be in the format "widthxheight", e.g. "1920x1080".
+ *
+ * @param deviceResolution The string representation of the device resolution, e.g. "1920x1080"
+ * @return The device resolution representation
+ */
+ @Nullable
+ public static DeviceResolution fromString(@Nullable String deviceResolution) {
+ if (deviceResolution == null || deviceResolution.trim().isEmpty()) {
+ return null;
+ }
+ if (deviceResolution.length() < 3) {
+ throw new IllegalArgumentException("Wrong device resolution size");
+ }
+ String[] dimensions = deviceResolution.split("x");
+ if (dimensions.length != 2) {
+ throw new IllegalArgumentException("Wrong dimension size");
+ }
+ return builder()
+ .width(Integer.parseInt(dimensions[0]))
+ .height(Integer.parseInt(dimensions[1]))
+ .build();
+ }
+
+ @Override
+ public String toString() {
+ return width + "x" + height;
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java
new file mode 100644
index 00000000..59821849
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java
@@ -0,0 +1,35 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.parameters;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+/** Represents an item in an ecommerce order. */
+@Builder
+@AllArgsConstructor
+@Getter
+@Setter
+public class EcommerceItem {
+
+ private String sku;
+
+ @Builder.Default private String name = "";
+
+ @Builder.Default private String category = "";
+
+ @Builder.Default private Double price = 0.0;
+
+ @Builder.Default private Integer quantity = 0;
+
+ public String toString() {
+ return "[\"" + sku + "\",\"" + name + "\",\"" + category + "\"," + price + "," + quantity + "]";
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItems.java b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItems.java
new file mode 100644
index 00000000..8cf47357
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItems.java
@@ -0,0 +1,36 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.parameters;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.Singular;
+import lombok.experimental.Delegate;
+
+/** Multiple things that you can buy online. */
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode
+@Getter
+@Setter
+public class EcommerceItems {
+
+ @Delegate @Singular private List items = new ArrayList<>();
+
+ public String toString() {
+ return items.stream().map(String::valueOf).collect(Collectors.joining(",", "[", "]"));
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/Hex.java b/core/src/main/java/org/matomo/java/tracking/parameters/Hex.java
new file mode 100644
index 00000000..cc007506
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/Hex.java
@@ -0,0 +1,27 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.parameters;
+
+import lombok.NonNull;
+
+final class Hex {
+
+ private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
+
+ private Hex() {
+ // utility class
+ }
+
+ static String fromBytes(@NonNull byte[] bytes) {
+ StringBuilder result = new StringBuilder(bytes.length * 2);
+ for (byte b : bytes) {
+ result.append(HEX_CHARS[(b >> 4) & 0xF]).append(HEX_CHARS[b & 0xF]);
+ }
+ return result.toString();
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/RandomValue.java b/core/src/main/java/org/matomo/java/tracking/parameters/RandomValue.java
new file mode 100644
index 00000000..5a066bb5
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/RandomValue.java
@@ -0,0 +1,53 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.parameters;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+/** A random value to avoid the tracking request being cached by the browser or a proxy. */
+public class RandomValue {
+
+ private static final Random RANDOM = new SecureRandom();
+
+ private final byte[] representation = new byte[10];
+
+ private String override;
+
+ /**
+ * Static factory to generate a random value.
+ *
+ * @return A randomly generated value
+ */
+ public static RandomValue random() {
+ RandomValue randomValue = new RandomValue();
+ RANDOM.nextBytes(randomValue.representation);
+ return randomValue;
+ }
+
+ /**
+ * Static factory to generate a random value from a given string. The string will be used as is
+ * and not hashed.
+ *
+ * @param override The string to use as random value
+ * @return A random value from the given string
+ */
+ public static RandomValue fromString(String override) {
+ RandomValue randomValue = new RandomValue();
+ randomValue.override = override;
+ return randomValue;
+ }
+
+ @Override
+ public String toString() {
+ if (override != null) {
+ return override;
+ }
+ return Hex.fromBytes(representation);
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/UniqueId.java b/core/src/main/java/org/matomo/java/tracking/parameters/UniqueId.java
new file mode 100644
index 00000000..0f0f95e8
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/UniqueId.java
@@ -0,0 +1,54 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.parameters;
+
+import java.security.SecureRandom;
+import java.util.Random;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+
+/** A six character unique ID consisting of the characters [0-9a-Z]. */
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+public final class UniqueId {
+
+ private static final String CHARS =
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+ private static final Random RANDOM = new SecureRandom();
+
+ private final long value;
+
+ /**
+ * Static factory to generate a random unique id.
+ *
+ * @return A randomly generated unique id
+ */
+ public static UniqueId random() {
+ return fromValue(RANDOM.nextLong());
+ }
+
+ /**
+ * Creates a unique id from a number.
+ *
+ * @param value A number to create this unique id from
+ * @return The unique id for the given value
+ */
+ public static UniqueId fromValue(long value) {
+ return new UniqueId(value);
+ }
+
+ @Override
+ public String toString() {
+ return IntStream.range(0, 6)
+ .map(i -> (int) (value >> i * 8))
+ .mapToObj(codePoint -> String.valueOf(CHARS.charAt(Math.abs(codePoint % CHARS.length()))))
+ .collect(Collectors.joining());
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/VisitorId.java b/core/src/main/java/org/matomo/java/tracking/parameters/VisitorId.java
new file mode 100644
index 00000000..70ab8087
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/VisitorId.java
@@ -0,0 +1,132 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.parameters;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.security.SecureRandom;
+import java.util.Random;
+import java.util.UUID;
+import java.util.regex.Pattern;
+import lombok.NonNull;
+
+/**
+ * The unique visitor ID, must be a 16 characters hexadecimal string. Every unique visitor must be
+ * assigned a different ID and this ID must not change after it is assigned. If this value is not
+ * set Matomo will still track visits, but the unique visitors metric might be less accurate.
+ */
+public class VisitorId {
+
+ private static final Random RANDOM = new SecureRandom();
+ private static final Pattern HEX_DIGITS = Pattern.compile("[0-9a-fA-F]+");
+
+ private final byte[] representation = new byte[8];
+
+ /**
+ * Static factory to generate a random visitor id.
+ *
+ *
Please consider creating a fixed id for each visitor by getting a hash code from e.g. the
+ * username and using {@link #fromHash(long)} or {@link #fromString(String)} instead of using this
+ * method.
+ *
+ * @return A randomly generated visitor id
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static VisitorId random() {
+ VisitorId visitorId = new VisitorId();
+ RANDOM.nextBytes(visitorId.representation);
+ return visitorId;
+ }
+
+ /**
+ * Creates always the same visitor id for the given input.
+ *
+ *
You can use e.g. {@link Object#hashCode()} to generate a hash code for an object, e.g. a
+ * username string as input.
+ *
+ * @param hash A number (a hash code) to create the visitor id from
+ * @return Always the same visitor id for the same input
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static VisitorId fromHash(long hash) {
+ VisitorId visitorId = new VisitorId();
+ long remainingHash = hash;
+ for (int i = visitorId.representation.length - 1; i >= 0; i--) {
+ visitorId.representation[i] = (byte) (remainingHash & 0xFF);
+ remainingHash >>= Byte.SIZE;
+ }
+ return visitorId;
+ }
+
+ /**
+ * Creates a visitor id from a UUID.
+ *
+ *
Uses the most significant bits of the UUID to create the visitor id.
+ *
+ * @param uuid A UUID to create the visitor id from
+ * @return The visitor id for the given UUID
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static VisitorId fromUUID(@NonNull UUID uuid) {
+ return fromHash(uuid.getMostSignificantBits());
+ }
+
+ /**
+ * Creates a visitor id from a hexadecimal string.
+ *
+ *
The input must be a valid hexadecimal string with a maximum length of 16 characters. If the
+ * input is shorter than 16 characters it will be padded with zeros.
+ *
+ * @param inputHex A hexadecimal string to create the visitor id from
+ * @return The visitor id for the given input
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static VisitorId fromHex(@NonNull String inputHex) {
+ if (inputHex.trim().isEmpty()) {
+ throw new IllegalArgumentException("Hex string must not be null or empty");
+ }
+ if (inputHex.length() > 16) {
+ throw new IllegalArgumentException("Hex string must not be longer than 16 characters");
+ }
+ if (!HEX_DIGITS.matcher(inputHex).matches()) {
+ throw new IllegalArgumentException("Input must be a valid hex string");
+ }
+ VisitorId visitorId = new VisitorId();
+ for (int charIndex = inputHex.length() - 1,
+ representationIndex = visitorId.representation.length - 1;
+ charIndex >= 0;
+ charIndex -= 2, representationIndex--) {
+ String hex = inputHex.substring(Math.max(0, charIndex - 1), charIndex + 1);
+ try {
+ visitorId.representation[representationIndex] = (byte) Integer.parseInt(hex, 16);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Input must be a valid hex string", e);
+ }
+ }
+ return visitorId;
+ }
+
+ /**
+ * Creates a visitor id from a string. The string will be hashed to create the visitor id.
+ *
+ * @param str A string to create the visitor id from
+ * @return The visitor id for the given string or null if the string is null or empty
+ */
+ @Nullable
+ public static VisitorId fromString(@Nullable String str) {
+ if (str == null || str.trim().isEmpty()) {
+ return null;
+ }
+ return fromHash(str.hashCode());
+ }
+
+ @Override
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public String toString() {
+ return Hex.fromBytes(representation);
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/package-info.java b/core/src/main/java/org/matomo/java/tracking/parameters/package-info.java
new file mode 100644
index 00000000..89f96f81
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * Contains types for Matomo Tracking Parameters according to the Matomo Tracking HTTP API.
+ *
+ *
The types help you to use the correct format for the tracking parameters. The package was
+ * introduced in Matomo Java Tracker version 3 to let the tracker be more self-explanatory and
+ * better maintainable.
+ */
+package org.matomo.java.tracking.parameters;
diff --git a/core/src/main/java/org/matomo/java/tracking/servlet/CookieWrapper.java b/core/src/main/java/org/matomo/java/tracking/servlet/CookieWrapper.java
new file mode 100644
index 00000000..a4debe13
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/servlet/CookieWrapper.java
@@ -0,0 +1,12 @@
+package org.matomo.java.tracking.servlet;
+
+import lombok.Value;
+
+/** Wrapper for the cookie name and value. */
+@Value
+public class CookieWrapper {
+
+ String name;
+
+ String value;
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/servlet/HttpServletRequestWrapper.java b/core/src/main/java/org/matomo/java/tracking/servlet/HttpServletRequestWrapper.java
new file mode 100644
index 00000000..15a7dc2f
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/servlet/HttpServletRequestWrapper.java
@@ -0,0 +1,53 @@
+package org.matomo.java.tracking.servlet;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.Map;
+import lombok.Builder;
+import lombok.NonNull;
+import lombok.Value;
+
+/** Wraps a HttpServletRequest to be compatible with both the Jakarta and the Java EE API. */
+@Builder
+@Value
+public class HttpServletRequestWrapper {
+
+ @Nullable StringBuffer requestURL;
+
+ @Nullable String remoteAddr;
+
+ @Nullable String remoteUser;
+
+ @Nullable Map headers;
+
+ @Nullable CookieWrapper[] cookies;
+
+ /**
+ * Returns an enumeration of all the header names this request contains. If the request has no
+ * headers, this method returns an empty enumeration.
+ *
+ * @return an enumeration of all the header names sent with this request
+ */
+ public Enumeration getHeaderNames() {
+ return headers == null
+ ? Collections.emptyEnumeration()
+ : Collections.enumeration(headers.keySet());
+ }
+
+ /**
+ * Returns the value of the specified request header as a String. If the request did not include a
+ * header of the specified name, this method returns null. If there are multiple headers with the
+ * same name, this method returns the last header in the request. The header name is case
+ * insensitive. You can use this method with any request header.
+ *
+ * @param name a String specifying the header name (case insensitive) - must not be {@code null}.
+ * @return a String containing the value of the requested header, or null if the request does not
+ * have a header of that name
+ */
+ @Nullable
+ public String getHeader(@NonNull String name) {
+ return headers == null ? null : headers.get(name.toLowerCase(Locale.ROOT));
+ }
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/servlet/ServletMatomoRequest.java b/core/src/main/java/org/matomo/java/tracking/servlet/ServletMatomoRequest.java
new file mode 100644
index 00000000..9dbb4e7c
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/servlet/ServletMatomoRequest.java
@@ -0,0 +1,157 @@
+package org.matomo.java.tracking.servlet;
+
+import static java.util.Arrays.asList;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import lombok.NonNull;
+import org.matomo.java.tracking.MatomoRequest;
+import org.matomo.java.tracking.parameters.CustomVariables;
+import org.matomo.java.tracking.parameters.VisitorId;
+
+/**
+ * Adds the headers from a {@link HttpServletRequestWrapper} to a {@link
+ * MatomoRequest.MatomoRequestBuilder}.
+ *
+ *
Use #fromServletRequest(HttpServletRequestWrapper) to create a new builder with the headers
+ * from the request or #addServletRequestHeaders(MatomoRequest.MatomoRequestBuilder,
+ * HttpServletRequestWrapper) to add the headers to an existing builder.
+ */
+public final class ServletMatomoRequest {
+
+ /** Please ensure these values are always lower case. */
+ private static final Set RESTRICTED_HEADERS =
+ Collections.unmodifiableSet(
+ new HashSet<>(asList("connection", "content-length", "expect", "host", "upgrade")));
+
+ private ServletMatomoRequest() {
+ // should not be instantiated
+ }
+
+ /**
+ * Creates a new builder with the headers from the request.
+ *
+ *
Use #addServletRequestHeaders(MatomoRequest.MatomoRequestBuilder, HttpServletRequestWrapper)
+ * to add the headers to an existing builder.
+ *
+ * @param request the request to get the headers from (must not be null)
+ * @return a new builder with the headers from the request (never null)
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder fromServletRequest(
+ @NonNull HttpServletRequestWrapper request) {
+ return addServletRequestHeaders(MatomoRequest.request(), request);
+ }
+
+ /**
+ * Adds the headers from the request to an existing builder.
+ *
+ *
This implementation uses a thread pool to send requests asynchronously. The thread pool is
+ * configured using {@link TrackerConfiguration#getThreadPoolSize()}. The thread pool uses daemon
+ * threads. This means that the JVM will exit even if the thread pool is not shut down.
+ *
+ *
+ *
+ * @see MatomoTrackerAutoConfiguration
+ * @see TrackerConfiguration
+ */
+@ConfigurationProperties(prefix = "matomo.tracker")
+@Getter
+@Setter
+public class MatomoTrackerProperties {
+
+ /**
+ * The Matomo Tracking HTTP API endpoint. Example: https://your-matomo-domain.example/matomo.php
+ */
+ private String apiEndpoint;
+
+ /** The default ID of the website that will be used if not specified explicitly. */
+ private Integer defaultSiteId;
+
+ /** The authorization token (parameter token_auth) to use if not specified explicitly. */
+ private String defaultAuthToken;
+
+ /** Allows to stop the tracker to send requests to the Matomo endpoint. */
+ private Boolean enabled = true;
+
+ /**
+ * The timeout until a connection is established.
+ *
+ *
A timeout value of zero is interpreted as an infinite timeout. A `null` value is interpreted
+ * as undefined (system default if applicable).
+ *
+ *
Default: 10 seconds
+ */
+ private Duration connectTimeout = Duration.ofSeconds(5L);
+
+ /**
+ * The socket timeout ({@code SO_TIMEOUT}), which is the timeout for waiting for data or, put
+ * differently, a maximum period inactivity between two consecutive data packets.
+ *
+ *
A timeout value of zero is interpreted as an infinite timeout. A `null value is interpreted
+ * as undefined (system default if applicable).
+ *
+ *
Default: 30 seconds
+ */
+ private Duration socketTimeout = Duration.ofSeconds(5L);
+
+ /**
+ * The hostname or IP address of an optional HTTP proxy. {@code proxyPort} must be configured as
+ * well
+ */
+ private String proxyHost;
+
+ /** The port of an HTTP proxy. {@code proxyHost} must be configured as well. */
+ private Integer proxyPort;
+
+ /**
+ * If the HTTP proxy requires a username for basic authentication, it can be configured here.
+ * Proxy host, port and password must also be set.
+ */
+ private String proxyUsername;
+
+ /**
+ * The corresponding password for the basic auth proxy user. The proxy host, port and username
+ * must be set as well.
+ */
+ private String proxyPassword;
+
+ /** A custom user agent to be set. Defaults to "MatomoJavaClient" */
+ private String userAgent = "MatomoJavaClient";
+
+ /**
+ * Logs if the Matomo Tracking API endpoint responds with an erroneous HTTP code. Defaults to
+ * false.
+ */
+ private Boolean logFailedTracking;
+
+ /**
+ * Disables SSL certificate validation. This is useful for testing with self-signed certificates.
+ * Do not use in production environments. Defaults to false.
+ *
+ *
Attention: This slows down performance
+ *
+ * @see #disableSslHostVerification
+ */
+ private Boolean disableSslCertValidation;
+
+ /**
+ * Disables SSL host verification. This is useful for testing with self-signed certificates. Do
+ * not use in production environments. Defaults to false.
+ *
+ *
Attention: This slows down performance
+ *
+ * @see #disableSslCertValidation
+ */
+ private Boolean disableSslHostVerification;
+
+ /**
+ * The thread pool size for the async sender. Defaults to 2.
+ *
+ *
Attention: If you use this library in a web application, make sure that this thread pool
+ * does not exceed the thread pool of the web application. Otherwise, you might run into problems.
+ */
+ private Integer threadPoolSize = 2;
+}
diff --git a/spring/src/main/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizer.java b/spring/src/main/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizer.java
new file mode 100644
index 00000000..afd00d33
--- /dev/null
+++ b/spring/src/main/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizer.java
@@ -0,0 +1,49 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.spring;
+
+import java.net.URI;
+import org.jspecify.annotations.NonNull;
+import org.matomo.java.tracking.TrackerConfiguration;
+import org.springframework.boot.context.properties.PropertyMapper;
+import org.springframework.core.Ordered;
+
+class StandardTrackerConfigurationBuilderCustomizer
+ implements TrackerConfigurationBuilderCustomizer, Ordered {
+
+ private final MatomoTrackerProperties properties;
+
+ StandardTrackerConfigurationBuilderCustomizer(MatomoTrackerProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public int getOrder() {
+ return 0;
+ }
+
+ @Override
+ public void customize(TrackerConfiguration.@NonNull TrackerConfigurationBuilder builder) {
+ PropertyMapper map = PropertyMapper.get();
+ map.from(properties::getApiEndpoint).as(URI::create).to(builder::apiEndpoint);
+ map.from(properties::getDefaultSiteId).to(builder::defaultSiteId);
+ map.from(properties::getDefaultAuthToken).to(builder::defaultAuthToken);
+ map.from(properties::getEnabled).to(builder::enabled);
+ map.from(properties::getConnectTimeout).to(builder::connectTimeout);
+ map.from(properties::getSocketTimeout).to(builder::socketTimeout);
+ map.from(properties::getProxyHost).to(builder::proxyHost);
+ map.from(properties::getProxyPort).to(builder::proxyPort);
+ map.from(properties::getProxyUsername).to(builder::proxyUsername);
+ map.from(properties::getProxyPassword).to(builder::proxyPassword);
+ map.from(properties::getUserAgent).to(builder::userAgent);
+ map.from(properties::getLogFailedTracking).to(builder::logFailedTracking);
+ map.from(properties::getDisableSslCertValidation).to(builder::disableSslCertValidation);
+ map.from(properties::getDisableSslHostVerification).to(builder::disableSslHostVerification);
+ map.from(properties::getThreadPoolSize).to(builder::threadPoolSize);
+ }
+}
diff --git a/spring/src/main/java/org/matomo/java/tracking/spring/TrackerConfigurationBuilderCustomizer.java b/spring/src/main/java/org/matomo/java/tracking/spring/TrackerConfigurationBuilderCustomizer.java
new file mode 100644
index 00000000..621cc4b1
--- /dev/null
+++ b/spring/src/main/java/org/matomo/java/tracking/spring/TrackerConfigurationBuilderCustomizer.java
@@ -0,0 +1,36 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking.spring;
+
+import org.jspecify.annotations.NonNull;
+import org.matomo.java.tracking.TrackerConfiguration;
+
+/**
+ * Allows to customize the {@link TrackerConfiguration.TrackerConfigurationBuilder} with additional
+ * properties.
+ *
+ *
Implementations of this interface are detected automatically by the {@link
+ * MatomoTrackerAutoConfiguration}.
+ *
+ * @see MatomoTrackerAutoConfiguration
+ * @see TrackerConfiguration
+ * @see TrackerConfiguration.TrackerConfigurationBuilder
+ */
+@FunctionalInterface
+public interface TrackerConfigurationBuilderCustomizer {
+
+ /**
+ * Customize the {@link TrackerConfiguration.TrackerConfigurationBuilder}.
+ *
+ * @param builder the {@link TrackerConfiguration.TrackerConfigurationBuilder} instance (never
+ * {@code null})
+ * @see TrackerConfiguration#builder()
+ * @see MatomoTrackerProperties
+ */
+ void customize(TrackerConfiguration.@NonNull TrackerConfigurationBuilder builder);
+}
diff --git a/spring/src/main/java/org/matomo/java/tracking/spring/package-info.java b/spring/src/main/java/org/matomo/java/tracking/spring/package-info.java
new file mode 100644
index 00000000..b7db4c90
--- /dev/null
+++ b/spring/src/main/java/org/matomo/java/tracking/spring/package-info.java
@@ -0,0 +1,10 @@
+/**
+ * Provides Spring specific classes to integrate Matomo tracking into Spring applications.
+ *
+ *
See {@link org.matomo.java.tracking.spring.MatomoTrackerProperties} for the available
+ * configuration properties.
+ *
+ *
See {@link org.matomo.java.tracking.spring.MatomoTrackerAutoConfiguration} for the available
+ * configuration options.
+ */
+package org.matomo.java.tracking.spring;
diff --git a/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000..dd6d4697
--- /dev/null
+++ b/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.matomo.java.tracking.spring.MatomoTrackerAutoConfiguration
diff --git a/spring/src/test/java/org/matomo/java/tracking/spring/MatomoTrackerAutoConfigurationIT.java b/spring/src/test/java/org/matomo/java/tracking/spring/MatomoTrackerAutoConfigurationIT.java
new file mode 100644
index 00000000..d185ea0c
--- /dev/null
+++ b/spring/src/test/java/org/matomo/java/tracking/spring/MatomoTrackerAutoConfigurationIT.java
@@ -0,0 +1,77 @@
+package org.matomo.java.tracking.spring;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.URI;
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import org.matomo.java.tracking.MatomoTracker;
+import org.matomo.java.tracking.TrackerConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+class MatomoTrackerAutoConfigurationIT {
+
+ private final ApplicationContextRunner contextRunner =
+ new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(MatomoTrackerAutoConfiguration.class));
+
+ @Test
+ void matomoTrackerRegistration() {
+ contextRunner
+ .withPropertyValues("matomo.tracker.api-endpoint:https://test.com/matomo.php")
+ .run(
+ context -> {
+ assertThat(context).hasSingleBean(MatomoTracker.class).hasBean("matomoTracker");
+ });
+ }
+
+ @Test
+ void additionalTrackerConfigurationBuilderCustomization() {
+ this.contextRunner
+ .withPropertyValues("matomo.tracker.api-endpoint:https://test.com/matomo.php")
+ .withUserConfiguration(TrackerConfigurationBuilderCustomizerConfig.class)
+ .run(
+ context -> {
+ TrackerConfiguration trackerConfiguration =
+ context.getBean(TrackerConfiguration.class);
+ assertThat(trackerConfiguration.getConnectTimeout())
+ .isEqualTo(Duration.ofMinutes(1L));
+ });
+ }
+
+ @Test
+ void customTrackerConfigurationBuilder() {
+ this.contextRunner
+ .withPropertyValues("matomo.tracker.api-endpoint:https://test.com/matomo.php")
+ .withUserConfiguration(TrackerConfigurationBuilderConfig.class)
+ .run(
+ context -> {
+ TrackerConfiguration trackerConfiguration =
+ context.getBean(TrackerConfiguration.class);
+ assertThat(trackerConfiguration.isDisableSslHostVerification()).isTrue();
+ });
+ }
+
+ @Configuration
+ static class TrackerConfigurationBuilderCustomizerConfig {
+
+ @Bean
+ TrackerConfigurationBuilderCustomizer customConnectTimeout() {
+ return configurationBuilder -> configurationBuilder.connectTimeout(Duration.ofMinutes(1L));
+ }
+ }
+
+ @Configuration
+ static class TrackerConfigurationBuilderConfig {
+
+ @Bean
+ TrackerConfiguration.TrackerConfigurationBuilder customTrackerConfigurationBuilder() {
+ return TrackerConfiguration.builder()
+ .apiEndpoint(URI.create("https://test.com/matomo.php"))
+ .disableSslHostVerification(true);
+ }
+ }
+}
diff --git a/spring/src/test/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizerIT.java b/spring/src/test/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizerIT.java
new file mode 100644
index 00000000..22052c8a
--- /dev/null
+++ b/spring/src/test/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizerIT.java
@@ -0,0 +1,54 @@
+package org.matomo.java.tracking.spring;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+import org.matomo.java.tracking.TrackerConfiguration;
+
+class StandardTrackerConfigurationBuilderCustomizerIT {
+
+ @Test
+ void createsStandardTrackerConfigurationBuilderCustomizer() {
+ MatomoTrackerProperties properties = new MatomoTrackerProperties();
+ properties.setApiEndpoint("https://test.com/matomo.php");
+ properties.setDefaultSiteId(1);
+ properties.setDefaultAuthToken("abc123def4563123abc123def4563123");
+ properties.setEnabled(true);
+ properties.setConnectTimeout(Duration.ofMinutes(1L));
+ properties.setSocketTimeout(Duration.ofMinutes(2L));
+ properties.setProxyHost("proxy.example.com");
+ properties.setProxyPort(8080);
+ properties.setProxyUsername("user");
+ properties.setProxyPassword("password");
+ properties.setUserAgent("Mozilla/5.0 (compatible; AcmeInc/1.0; +https://example.com/bot.html)");
+ properties.setLogFailedTracking(true);
+ properties.setDisableSslCertValidation(true);
+ properties.setDisableSslHostVerification(true);
+ properties.setThreadPoolSize(10);
+ StandardTrackerConfigurationBuilderCustomizer customizer =
+ new StandardTrackerConfigurationBuilderCustomizer(properties);
+ TrackerConfiguration.TrackerConfigurationBuilder builder = TrackerConfiguration.builder();
+
+ customizer.customize(builder);
+
+ assertThat(customizer.getOrder()).isZero();
+ TrackerConfiguration configuration = builder.build();
+ assertThat(configuration.getApiEndpoint()).hasToString("https://test.com/matomo.php");
+ assertThat(configuration.getDefaultSiteId()).isEqualTo(1);
+ assertThat(configuration.getDefaultAuthToken()).isEqualTo("abc123def4563123abc123def4563123");
+ assertThat(configuration.isEnabled()).isTrue();
+ assertThat(configuration.getConnectTimeout()).hasSeconds(60L);
+ assertThat(configuration.getSocketTimeout()).hasSeconds(120L);
+ assertThat(configuration.getProxyHost()).isEqualTo("proxy.example.com");
+ assertThat(configuration.getProxyPort()).isEqualTo(8080);
+ assertThat(configuration.getProxyUsername()).isEqualTo("user");
+ assertThat(configuration.getProxyPassword()).isEqualTo("password");
+ assertThat(configuration.getUserAgent())
+ .isEqualTo("Mozilla/5.0 (compatible; AcmeInc/1.0; +https://example.com/bot.html)");
+ assertThat(configuration.isLogFailedTracking()).isTrue();
+ assertThat(configuration.isDisableSslCertValidation()).isTrue();
+ assertThat(configuration.isDisableSslHostVerification()).isTrue();
+ assertThat(configuration.getThreadPoolSize()).isEqualTo(10);
+ }
+}
diff --git a/src/main/java/org/matomo/java/tracking/CustomVariable.java b/src/main/java/org/matomo/java/tracking/CustomVariable.java
deleted file mode 100644
index 2d52e4d4..00000000
--- a/src/main/java/org/matomo/java/tracking/CustomVariable.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Matomo Java Tracker
- *
- * @link https://github.com/matomo/matomo-java-tracker
- * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.matomo.java.tracking;
-
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.NonNull;
-import lombok.ToString;
-import lombok.experimental.FieldDefaults;
-
-/**
- * A user defined custom variable.
- *
- * @author brettcsorba
- */
-@Getter
-@FieldDefaults(level = AccessLevel.PRIVATE)
-@AllArgsConstructor
-@ToString
-@EqualsAndHashCode
-public class CustomVariable {
-
- @NonNull
- String key;
-
- @NonNull
- String value;
-
-}
diff --git a/src/main/java/org/matomo/java/tracking/CustomVariables.java b/src/main/java/org/matomo/java/tracking/CustomVariables.java
deleted file mode 100644
index 0bbe94df..00000000
--- a/src/main/java/org/matomo/java/tracking/CustomVariables.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Matomo Java Tracker
- *
- * @link https://github.com/matomo/matomo-java-tracker
- * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.matomo.java.tracking;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import lombok.EqualsAndHashCode;
-import lombok.NonNull;
-
-import javax.annotation.Nullable;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * @author brettcsorba
- */
-@EqualsAndHashCode
-class CustomVariables {
-
- private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
- private final Map variables = new HashMap<>();
-
- void add(@NonNull CustomVariable variable) {
- boolean found = false;
- for (Map.Entry entry : variables.entrySet()) {
- CustomVariable customVariable = entry.getValue();
- if (customVariable.getKey().equals(variable.getKey())) {
- variables.put(entry.getKey(), variable);
- found = true;
- }
- }
- if (!found) {
- int i = 1;
- while (variables.putIfAbsent(i, variable) != null) {
- i++;
- }
- }
- }
-
- void add(@NonNull CustomVariable cv, int index) {
- if (index <= 0) {
- throw new IllegalArgumentException("Index must be greater than 0.");
- }
- variables.put(index, cv);
- }
-
- @Nullable
- CustomVariable get(int index) {
- if (index <= 0) {
- throw new IllegalArgumentException("Index must be greater than 0.");
- }
- return variables.get(index);
- }
-
- @Nullable
- String get(@NonNull String key) {
- return variables.values().stream().filter(variable -> variable.getKey().equals(key)).findFirst().map(CustomVariable::getValue).orElse(null);
- }
-
- void remove(int index) {
- variables.remove(index);
- }
-
- void remove(@NonNull String key) {
- variables.entrySet().removeIf(entry -> entry.getValue().getKey().equals(key));
- }
-
- boolean isEmpty() {
- return variables.isEmpty();
- }
-
- @Override
- public String toString() {
- ObjectNode objectNode = OBJECT_MAPPER.createObjectNode();
- for (Map.Entry entry : variables.entrySet()) {
- CustomVariable variable = entry.getValue();
- objectNode.putArray(entry.getKey().toString()).add(variable.getKey()).add(variable.getValue());
- }
- return objectNode.toString();
- }
-}
diff --git a/src/main/java/org/matomo/java/tracking/EcommerceItem.java b/src/main/java/org/matomo/java/tracking/EcommerceItem.java
deleted file mode 100644
index f7b5bf23..00000000
--- a/src/main/java/org/matomo/java/tracking/EcommerceItem.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Matomo Java Tracker
- *
- * @link https://github.com/matomo/matomo-java-tracker
- * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.matomo.java.tracking;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.Setter;
-
-/**
- * Represents an item in an ecommerce order.
- *
- * @author brettcsorba
- */
-@Setter
-@Getter
-@AllArgsConstructor
-@Builder
-public class EcommerceItem {
-
- private String sku;
- private String name;
- private String category;
- private Double price;
- private Integer quantity;
-
-}
diff --git a/src/main/java/org/matomo/java/tracking/EcommerceItems.java b/src/main/java/org/matomo/java/tracking/EcommerceItems.java
deleted file mode 100644
index a27ae2f2..00000000
--- a/src/main/java/org/matomo/java/tracking/EcommerceItems.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Matomo Java Tracker
- *
- * @link https://github.com/matomo/matomo-java-tracker
- * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.matomo.java.tracking;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import lombok.EqualsAndHashCode;
-import lombok.NonNull;
-
-import javax.annotation.Nonnull;
-import java.util.ArrayList;
-import java.util.List;
-
-@EqualsAndHashCode
-class EcommerceItems {
-
- private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
- private final List ecommerceItems = new ArrayList<>();
-
- public void add(@NonNull EcommerceItem ecommerceItem) {
- ecommerceItems.add(ecommerceItem);
- }
-
- @Nonnull
- public EcommerceItem get(int index) {
- return ecommerceItems.get(index);
- }
-
- @Override
- public String toString() {
- ArrayNode arrayNode = OBJECT_MAPPER.createArrayNode();
- for (EcommerceItem ecommerceItem : ecommerceItems) {
- arrayNode.add(OBJECT_MAPPER.createArrayNode()
- .add(ecommerceItem.getSku())
- .add(ecommerceItem.getName())
- .add(ecommerceItem.getCategory())
- .add(ecommerceItem.getPrice())
- .add(ecommerceItem.getQuantity())
- );
- }
- return arrayNode.toString();
- }
-}
diff --git a/src/main/java/org/matomo/java/tracking/HttpClientFactory.java b/src/main/java/org/matomo/java/tracking/HttpClientFactory.java
deleted file mode 100644
index 86dca94f..00000000
--- a/src/main/java/org/matomo/java/tracking/HttpClientFactory.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package org.matomo.java.tracking;
-
-import lombok.AllArgsConstructor;
-import lombok.EqualsAndHashCode;
-import org.apache.http.HttpHost;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.impl.client.HttpClientBuilder;
-import org.apache.http.impl.conn.DefaultProxyRoutePlanner;
-import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
-import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
-
-import javax.annotation.Nullable;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Internal factory for providing instances of HTTP clients.
- * Especially {@link org.apache.http.nio.client.HttpAsyncClient} instances are intended to be global resources that share the same lifecycle as the application.
- * For details see Apache documentation.
- *
- * @author norbertroamsys
- */
-final class HttpClientFactory {
-
- private HttpClientFactory() {
- // utility
- }
-
- /**
- * Internal key class for caching {@link CloseableHttpAsyncClient} instances.
- */
- @EqualsAndHashCode
- @AllArgsConstructor
- private static final class KeyEntry {
-
- private final String proxyHost;
- private final int proxyPort;
- private final int timeout;
-
- }
-
- private static final Map ASYNC_INSTANCES = new HashMap<>();
-
- /**
- * Factory for getting a synchronous client by proxy and timeout configuration.
- * The clients will be created on each call.
- *
- * @param proxyHost the proxy host
- * @param proxyPort the proxy port
- * @param timeout the timeout
- * @return the created client
- */
- public static HttpClient getInstanceFor(final String proxyHost, final int proxyPort, final int timeout) {
- return HttpClientBuilder.create().setRoutePlanner(createRoutePlanner(proxyHost, proxyPort)).setDefaultRequestConfig(createRequestConfig(timeout)).build();
- }
-
- /**
- * Factory for getting a asynchronous client by proxy and timeout configuration.
- * The clients will be created and cached as a singleton instance.
- *
- * @param proxyHost the proxy host
- * @param proxyPort the proxy port
- * @param timeout the timeout
- * @return the created client
- */
- public static synchronized CloseableHttpAsyncClient getAsyncInstanceFor(final String proxyHost, final int proxyPort, final int timeout) {
- return ASYNC_INSTANCES.computeIfAbsent(new KeyEntry(proxyHost, proxyPort, timeout), key ->
- HttpAsyncClientBuilder.create().setRoutePlanner(createRoutePlanner(key.proxyHost, key.proxyPort)).setDefaultRequestConfig(createRequestConfig(key.timeout)).build());
- }
-
- @Nullable
- private static DefaultProxyRoutePlanner createRoutePlanner(final String proxyHost, final int proxyPort) {
- if (proxyHost != null && proxyPort != 0) {
- final HttpHost proxy = new HttpHost(proxyHost, proxyPort);
- return new DefaultProxyRoutePlanner(proxy);
- }
- return null;
- }
-
- private static RequestConfig createRequestConfig(final int timeout) {
- final RequestConfig.Builder config = RequestConfig.custom()
- .setConnectTimeout(timeout)
- .setConnectionRequestTimeout(timeout)
- .setSocketTimeout(timeout);
- return config.build();
- }
-
-}
diff --git a/src/main/java/org/matomo/java/tracking/InvalidUrlException.java b/src/main/java/org/matomo/java/tracking/InvalidUrlException.java
deleted file mode 100644
index 01d4fbcb..00000000
--- a/src/main/java/org/matomo/java/tracking/InvalidUrlException.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.matomo.java.tracking;
-
-public class InvalidUrlException extends RuntimeException {
-
- public InvalidUrlException(Throwable cause) {
- super(cause);
- }
-}
diff --git a/src/main/java/org/matomo/java/tracking/MatomoBoolean.java b/src/main/java/org/matomo/java/tracking/MatomoBoolean.java
deleted file mode 100644
index faa3ee92..00000000
--- a/src/main/java/org/matomo/java/tracking/MatomoBoolean.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Matomo Java Tracker
- *
- * @link https://github.com/matomo/matomo-java-tracker
- * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.matomo.java.tracking;
-
-import lombok.Value;
-
-/**
- * Object representing a locale required by some Matomo query parameters.
- *
- * @author brettcsorba
- */
-@Value
-public class MatomoBoolean {
- boolean value;
-
- /**
- * Returns the locale's lowercase country code.
- *
- * @return the locale's lowercase country code
- */
- @Override
- public String toString() {
- return value ? "1" : "0";
- }
-}
diff --git a/src/main/java/org/matomo/java/tracking/MatomoDate.java b/src/main/java/org/matomo/java/tracking/MatomoDate.java
deleted file mode 100644
index 25706998..00000000
--- a/src/main/java/org/matomo/java/tracking/MatomoDate.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Matomo Java Tracker
- *
- * @link https://github.com/matomo/matomo-java-tracker
- * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.matomo.java.tracking;
-
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.Locale;
-
-/**
- * A datetime object that will return the datetime in the format {@code yyyy-MM-dd hh:mm:ss}.
- *
- * @author brettcsorba
- */
-public class MatomoDate {
- private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
-
- private ZonedDateTime zonedDateTime;
-
- /**
- * Allocates a Date object and initializes it so that it represents the time
- * at which it was allocated, measured to the nearest millisecond.
- */
- public MatomoDate() {
- zonedDateTime = ZonedDateTime.now(ZoneOffset.UTC);
- }
-
- /**
- * Allocates a Date object and initializes it to represent the specified number
- * of milliseconds since the standard base time known as "the epoch", namely
- * January 1, 1970, 00:00:00 GMT.
- *
- * @param epochMilli the milliseconds since January 1, 1970, 00:00:00 GMT.
- */
- public MatomoDate(long epochMilli) {
- zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), ZoneOffset.UTC);
- }
-
- /**
- * Sets the time zone of the String that will be returned by {@link #toString()}.
- * Defaults to UTC.
- *
- * @param zone the TimeZone to set
- */
- public void setTimeZone(ZoneId zone) {
- zonedDateTime = zonedDateTime.withZoneSameInstant(zone);
- }
-
- /**
- * Converts this MatomoDate object to a String of the form:
- *
- * {@code yyyy-MM-dd hh:mm:ss}.
- *
- * @return a string representation of this MatomoDate
- */
- @Override
- public String toString() {
- return DATE_TIME_FORMATTER.format(zonedDateTime);
- }
-
- /**
- * Converts this datetime to the number of milliseconds from the epoch
- * of 1970-01-01T00:00:00Z.
- *
- * @return the number of milliseconds since the epoch of 1970-01-01T00:00:00Z
- * @throws ArithmeticException if numeric overflow occurs
- */
- public long getTime() {
- return zonedDateTime.toInstant().toEpochMilli();
- }
-}
diff --git a/src/main/java/org/matomo/java/tracking/MatomoException.java b/src/main/java/org/matomo/java/tracking/MatomoException.java
deleted file mode 100644
index 39ea492e..00000000
--- a/src/main/java/org/matomo/java/tracking/MatomoException.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.matomo.java.tracking;
-
-public class MatomoException extends RuntimeException {
-
- private static final long serialVersionUID = 4592083764365938934L;
-
- public MatomoException(String message, Throwable cause) {
- super(message, cause);
- }
-}
diff --git a/src/main/java/org/matomo/java/tracking/MatomoLocale.java b/src/main/java/org/matomo/java/tracking/MatomoLocale.java
deleted file mode 100644
index a561a17a..00000000
--- a/src/main/java/org/matomo/java/tracking/MatomoLocale.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Matomo Java Tracker
- *
- * @link https://github.com/matomo/matomo-java-tracker
- * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.matomo.java.tracking;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.Setter;
-
-import java.util.Locale;
-
-/**
- * Object representing a locale required by some Matomo query parameters.
- *
- * @author brettcsorba
- */
-@Setter
-@Getter
-@AllArgsConstructor
-public class MatomoLocale {
- private Locale locale;
-
- /**
- * Returns the locale's lowercase country code.
- *
- * @return the locale's lowercase country code
- */
- @Override
- public String toString() {
- return locale.getCountry().toLowerCase(Locale.ENGLISH);
- }
-}
diff --git a/src/main/java/org/matomo/java/tracking/MatomoRequest.java b/src/main/java/org/matomo/java/tracking/MatomoRequest.java
deleted file mode 100644
index f3681161..00000000
--- a/src/main/java/org/matomo/java/tracking/MatomoRequest.java
+++ /dev/null
@@ -1,2172 +0,0 @@
-/*
- * Matomo Java Tracker
- *
- * @link https://github.com/matomo/matomo-java-tracker
- * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.matomo.java.tracking;
-
-import com.google.common.collect.LinkedHashMultimap;
-import com.google.common.collect.Multimap;
-import com.google.common.io.BaseEncoding;
-import lombok.NonNull;
-import lombok.ToString;
-import org.apache.http.client.utils.URIBuilder;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.nio.charset.Charset;
-import java.security.SecureRandom;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-/**
- * A class that implements the
- * Matomo Tracking HTTP API. These requests can be sent using {@link MatomoTracker}.
- *
- * @author brettcsorba
- */
-@ToString
-public class MatomoRequest {
- public static final int ID_LENGTH = 16;
- public static final int AUTH_TOKEN_LENGTH = 32;
-
- private static final String ACTION_NAME = "action_name";
- private static final String ACTION_TIME = "gt_ms";
- private static final String ACTION_URL = "url";
- private static final String API_VERSION = "apiv";
- private static final String AUTH_TOKEN = "token_auth";
- private static final String CAMPAIGN_KEYWORD = "_rck";
- private static final String CAMPAIGN_NAME = "_rcn";
- private static final String CHARACTER_SET = "cs";
- private static final String CONTENT_INTERACTION = "c_i";
- private static final String CONTENT_NAME = "c_n";
- private static final String CONTENT_PIECE = "c_p";
- private static final String CONTENT_TARGET = "c_t";
- private static final String CURRENT_HOUR = "h";
- private static final String CURRENT_MINUTE = "m";
- private static final String CURRENT_SECOND = "s";
-
- private static final String CUSTOM_ACTION = "ca";
- private static final String DEVICE_RESOLUTION = "res";
- private static final String DOWNLOAD_URL = "download";
- private static final String ECOMMERCE_DISCOUNT = "ec_dt";
- private static final String ECOMMERCE_ID = "ec_id";
- private static final String ECOMMERCE_ITEMS = "ec_items";
- private static final String ECOMMERCE_LAST_ORDER_TIMESTAMP = "_ects";
- private static final String ECOMMERCE_REVENUE = "revenue";
- private static final String ECOMMERCE_SHIPPING_COST = "ec_sh";
- private static final String ECOMMERCE_SUBTOTAL = "ec_st";
- private static final String ECOMMERCE_TAX = "ec_tx";
- private static final String EVENT_ACTION = "e_a";
- private static final String EVENT_CATEGORY = "e_c";
- private static final String EVENT_NAME = "e_n";
- private static final String EVENT_VALUE = "e_v";
- private static final String HEADER_ACCEPT_LANGUAGE = "lang";
- private static final String GOAL_ID = "idgoal";
- private static final String GOAL_REVENUE = "revenue";
- private static final String HEADER_USER_AGENT = "ua";
- private static final String NEW_VISIT = "new_visit";
- private static final String OUTLINK_URL = "link";
- private static final String PAGE_CUSTOM_VARIABLE = "cvar";
- private static final String PLUGIN_DIRECTOR = "dir";
- private static final String PLUGIN_FLASH = "fla";
- private static final String PLUGIN_GEARS = "gears";
- private static final String PLUGIN_JAVA = "java";
- private static final String PLUGIN_PDF = "pdf";
- private static final String PLUGIN_QUICKTIME = "qt";
- private static final String PLUGIN_REAL_PLAYER = "realp";
- private static final String PLUGIN_SILVERLIGHT = "ag";
- private static final String PLUGIN_WINDOWS_MEDIA = "wma";
- private static final String RANDOM_VALUE = "rand";
- private static final String REFERRER_URL = "urlref";
- private static final String REQUEST_DATETIME = "cdt";
- private static final String REQUIRED = "rec";
- private static final String RESPONSE_AS_IMAGE = "send_image";
- private static final String SEARCH_CATEGORY = "search_cat";
- private static final String SEARCH_QUERY = "search";
- private static final String SEARCH_RESULTS_COUNT = "search_count";
- private static final String SITE_ID = "idsite";
- private static final String TRACK_BOT_REQUESTS = "bots";
- private static final String VISIT_CUSTOM_VARIABLE = "_cvar";
- private static final String USER_ID = "uid";
- private static final String VISITOR_CITY = "city";
- private static final String VISITOR_COUNTRY = "country";
- private static final String VISITOR_CUSTOM_ID = "cid";
- private static final String VISITOR_FIRST_VISIT_TIMESTAMP = "_idts";
- private static final String VISITOR_ID = "_id";
- private static final String VISITOR_IP = "cip";
- private static final String VISITOR_LATITUDE = "lat";
- private static final String VISITOR_LONGITUDE = "long";
- private static final String VISITOR_PREVIOUS_VISIT_TIMESTAMP = "_viewts";
- private static final String VISITOR_REGION = "region";
- private static final String VISITOR_VISIT_COUNT = "_idvc";
-
- private static final int RANDOM_VALUE_LENGTH = 20;
- private static final long REQUEST_DATETIME_AUTH_LIMIT = 14400000L;
- private static final Pattern VISITOR_ID_PATTERN = Pattern.compile("[0-9A-Fa-f]+");
-
- private final Multimap parameters = LinkedHashMultimap.create(8, 1);
-
- private final Set customTrackingParameterNames = new HashSet<>(2);
-
- /**
- * Create a new request from the id of the site being tracked and the full
- * url for the current action. This constructor also sets:
- *
- * {@code
- * Required = true
- * Visior Id = random 16 character hex string
- * Random Value = random 20 character hex string
- * API version = 1
- * Response as Image = false
- * }
- *
- * Overwrite these values yourself as desired.
- *
- * @param siteId the id of the website we're tracking a visit/action for
- * @param actionUrl the full URL for the current action
- */
- public MatomoRequest(int siteId, String actionUrl) {
- setParameter(SITE_ID, siteId);
- setBooleanParameter(REQUIRED, true);
- setParameter(ACTION_URL, actionUrl);
- setParameter(VISITOR_ID, getRandomHexString(ID_LENGTH));
- setParameter(RANDOM_VALUE, getRandomHexString(RANDOM_VALUE_LENGTH));
- setParameter(API_VERSION, "1");
- setBooleanParameter(RESPONSE_AS_IMAGE, false);
- }
-
- public static MatomoRequestBuilder builder() {
- return new MatomoRequestBuilder();
- }
-
- /**
- * Get the title of the action being tracked
- *
- * @return the title of the action being tracked
- */
- @Nullable
- public String getActionName() {
- return castOrNull(ACTION_NAME);
- }
-
- /**
- * Set the title of the action being tracked. It is possible to
- * use slashes /
- * to set one or several categories for this action.
- * For example, Help / Feedback
- * will create the Action Feedback in the category Help.
- *
- * @param actionName the title of the action to set. A null value will remove this parameter
- */
- public void setActionName(String actionName) {
- setParameter(ACTION_NAME, actionName);
- }
-
- /**
- * Get the amount of time it took the server to generate this action, in milliseconds.
- *
- * @return the amount of time
- */
- @Nullable
- public Long getActionTime() {
- return castOrNull(ACTION_TIME);
- }
-
- /**
- * Set the amount of time it took the server to generate this action, in milliseconds.
- * This value is used to process the
- * Page speed report
- * Avg. generation time column in the Page URL and Page Title reports,
- * as well as a site wide running average of the speed of your server.
- *
- * @param actionTime the amount of time to set. A null value will remove this parameter
- */
- public void setActionTime(Long actionTime) {
- setParameter(ACTION_TIME, actionTime);
- }
-
- /**
- * Get the full URL for the current action.
- *
- * @return the full URL
- * @deprecated Please use {@link #getActionUrlAsString}
- */
- @Nullable
- public URL getActionUrl() {
- return castToUrlOrNull(ACTION_URL);
- }
-
- /**
- * Get the full URL for the current action.
- *
- * @return the full URL
- */
- @Deprecated
- @Nullable
- public String getActionUrlAsString() {
- return castOrNull(ACTION_URL);
- }
-
-
- /**
- * Set the full URL for the current action.
- *
- * @param actionUrl the full URL to set. A null value will remove this parameter
- * @deprecated Please use {@link #setActionUrl(String)}
- */
- @Deprecated
- public void setActionUrl(@NonNull URL actionUrl) {
- setActionUrl(actionUrl.toString());
- }
-
- /**
- * Set the full URL for the current action.
- *
- * @param actionUrl the full URL to set. A null value will remove this parameter
- */
- public void setActionUrl(String actionUrl) {
- setParameter(ACTION_URL, actionUrl);
- }
-
- /**
- * Set the full URL for the current action.
- *
- * @param actionUrl the full URL to set. A null value will remove this parameter
- * @deprecated Please use {@link #setActionUrl(String)}
- */
- @Deprecated
- public void setActionUrlWithString(String actionUrl) {
- setActionUrl(actionUrl);
- }
-
- /**
- * Get the api version
- *
- * @return the api version
- */
- @Nullable
- public String getApiVersion() {
- return castOrNull(API_VERSION);
- }
-
- /**
- * Set the api version to use (currently always set to 1)
- *
- * @param apiVersion the api version to set. A null value will remove this parameter
- */
- public void setApiVersion(String apiVersion) {
- setParameter(API_VERSION, apiVersion);
- }
-
- /**
- * Get the authorization key.
- *
- * @return the authorization key
- */
- @Nullable
- public String getAuthToken() {
- return castOrNull(AUTH_TOKEN);
- }
-
- /**
- * Set the {@value #AUTH_TOKEN_LENGTH} character authorization key used to authenticate the API request.
- *
- * @param authToken the authorization key to set. A null value will remove this parameter
- */
- public void setAuthToken(String authToken) {
- if (authToken != null && authToken.length() != AUTH_TOKEN_LENGTH) {
- throw new IllegalArgumentException(authToken + " is not " + AUTH_TOKEN_LENGTH + " characters long.");
- }
- setParameter(AUTH_TOKEN, authToken);
- }
-
- /**
- * Verifies that AuthToken has been set for this request. Will throw an
- * {@link IllegalStateException} if not.
- */
- public void verifyAuthTokenSet() {
- if (getAuthToken() == null) {
- throw new IllegalStateException("AuthToken must be set before this value can be set.");
- }
- }
-
- /**
- * Get the campaign keyword
- *
- * @return the campaign keyword
- */
- @Nullable
- public String getCampaignKeyword() {
- return castOrNull(CAMPAIGN_KEYWORD);
- }
-
- /**
- * Set the Campaign Keyword (see
- * Tracking Campaigns).
- * Used to populate the Referrers > Campaigns report (clicking on a
- * campaign loads all keywords for this campaign). Note: this parameter
- * will only be used for the first pageview of a visit.
- *
- * @param campaignKeyword the campaign keyword to set. A null value will remove this parameter
- */
- public void setCampaignKeyword(String campaignKeyword) {
- setParameter(CAMPAIGN_KEYWORD, campaignKeyword);
- }
-
- /**
- * Get the campaign name
- *
- * @return the campaign name
- */
- @Nullable
- public String getCampaignName() {
- return castOrNull(CAMPAIGN_NAME);
- }
-
- /**
- * Set the Campaign Name (see
- * Tracking Campaigns).
- * Used to populate the Referrers > Campaigns report. Note: this parameter
- * will only be used for the first pageview of a visit.
- *
- * @param campaignName the campaign name to set. A null value will remove this parameter
- */
- public void setCampaignName(String campaignName) {
- setParameter(CAMPAIGN_NAME, campaignName);
- }
-
- /**
- * Get the charset of the page being tracked
- *
- * @return the charset
- */
- @Nullable
- public Charset getCharacterSet() {
- return castOrNull(CHARACTER_SET);
- }
-
- /**
- * The charset of the page being tracked. Specify the charset if the data
- * you send to Matomo is encoded in a different character set than the default
- * utf-8.
- *
- * @param characterSet the charset to set. A null value will remove this parameter
- */
- public void setCharacterSet(Charset characterSet) {
- setParameter(CHARACTER_SET, characterSet);
- }
-
- /**
- * Get the name of the interaction with the content
- *
- * @return the name of the interaction
- */
- @Nullable
- public String getContentInteraction() {
- return castOrNull(CONTENT_INTERACTION);
- }
-
- /**
- * Set the name of the interaction with the content. For instance a 'click'.
- *
- * @param contentInteraction the name of the interaction to set. A null value will remove this parameter
- */
- public void setContentInteraction(String contentInteraction) {
- setParameter(CONTENT_INTERACTION, contentInteraction);
- }
-
- /**
- * Get the name of the content
- *
- * @return the name
- */
- @Nullable
- public String getContentName() {
- return castOrNull(CONTENT_NAME);
- }
-
- /**
- * Set the name of the content. For instance 'Ad Foo Bar'.
- *
- * @param contentName the name to set. A null value will remove this parameter
- */
- public void setContentName(String contentName) {
- setParameter(CONTENT_NAME, contentName);
- }
-
- /**
- * Get the content piece.
- *
- * @return the content piece.
- */
- @Nullable
- public String getContentPiece() {
- return castOrNull(CONTENT_PIECE);
- }
-
- /**
- * Set the actual content piece. For instance the path to an image, video, audio, any text.
- *
- * @param contentPiece the content piece to set. A null value will remove this parameter
- */
- public void setContentPiece(String contentPiece) {
- setParameter(CONTENT_PIECE, contentPiece);
- }
-
- /**
- * Get the content target
- *
- * @return the target
- */
- @Nullable
- public URL getContentTarget() {
- return castToUrlOrNull(CONTENT_TARGET);
- }
-
- @Nullable
- private URL castToUrlOrNull(@NonNull String key) {
- String url = castOrNull(key);
- if (url == null) {
- return null;
- }
- try {
- return new URL(url);
- } catch (MalformedURLException e) {
- throw new InvalidUrlException(e);
- }
- }
-
- /**
- * Get the content target
- *
- * @return the target
- */
- @Nullable
- public String getContentTargetAsString() {
- return castOrNull(CONTENT_TARGET);
- }
-
- /**
- * Set the target of the content. For instance the URL of a landing page.
- *
- * @param contentTarget the target to set. A null value will remove this parameter
- * @deprecated Please use {@link #setContentTarget(String)}
- */
- @Deprecated
- public void setContentTarget(@NonNull URL contentTarget) {
- setContentTarget(contentTarget.toString());
- }
-
- /**
- * Set the target of the content. For instance the URL of a landing page.
- *
- * @param contentTarget the target to set. A null value will remove this parameter
- */
- public void setContentTarget(String contentTarget) {
- setParameter(CONTENT_TARGET, contentTarget);
- }
-
- /**
- * Set the target of the content. For instance the URL of a landing page.
- *
- * @param contentTarget the target to set. A null value will remove this parameter
- * @deprecated Please use {@link #setContentTarget(String)}
- */
- @Deprecated
- public void setContentTargetWithString(String contentTarget) {
- setContentTarget(contentTarget);
- }
-
- /**
- * Get the current hour.
- *
- * @return the current hour
- */
- @Nullable
- public Integer getCurrentHour() {
- return castOrNull(CURRENT_HOUR);
- }
-
- /**
- * Set the current hour (local time).
- *
- * @param currentHour the hour to set. A null value will remove this parameter
- */
- public void setCurrentHour(Integer currentHour) {
- setParameter(CURRENT_HOUR, currentHour);
- }
-
- /**
- * Get the current minute.
- *
- * @return the current minute
- */
- @Nullable
- public Integer getCurrentMinute() {
- return castOrNull(CURRENT_MINUTE);
- }
-
- /**
- * Set the current minute (local time).
- *
- * @param currentMinute the minute to set. A null value will remove this parameter
- */
- public void setCurrentMinute(Integer currentMinute) {
- setParameter(CURRENT_MINUTE, currentMinute);
- }
-
- /**
- * Get the current second
- *
- * @return the current second
- */
- @Nullable
- public Integer getCurrentSecond() {
- return castOrNull(CURRENT_SECOND);
- }
-
- /**
- * Set the current second (local time).
- *
- * @param currentSecond the second to set. A null value will remove this parameter
- */
- public void setCurrentSecond(Integer currentSecond) {
- setParameter(CURRENT_SECOND, currentSecond);
- }
-
- /**
- * Get the custom action
- *
- * @return the custom action
- */
- @Nullable
- public Boolean getCustomAction() {
- return getBooleanParameter(CUSTOM_ACTION);
- }
-
- /**
- * Set the custom action
- *
- * @param customAction the second to set. A null value will remove this parameter
- */
- public void setCustomAction(Boolean customAction) {
- setBooleanParameter(CUSTOM_ACTION, customAction);
- }
-
- /**
- * Gets the list of objects currently stored at the specified custom tracking
- * parameter. An empty list will be returned if there are no objects set at
- * that key.
- *
- * @param key the key of the parameter whose list of objects to get. Cannot be null
- * @return the list of objects currently stored at the specified key
- */
- public List