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..adbb253e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,20 @@
+---
+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..d5a45c1a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+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..cd85bf2d 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
+ assignees:
+ - dheid
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ assignees:
+ - dheid
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 8b137891..42d2b4f0 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1 +1,11 @@
+
+- [ ] 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/release-drafter.yml b/.github/release-drafter.yml
new file mode 100644
index 00000000..6830782a
--- /dev/null
+++ b/.github/release-drafter.yml
@@ -0,0 +1,81 @@
+name-template: 'v$RESOLVED_VERSION'
+tag-template: 'v$RESOLVED_VERSION'
+
+categories:
+ - title: 💥 Breaking changes
+ labels:
+ - breaking
+ - title: 🚨 Removed
+ labels:
+ - removed
+ - title: 🎉 Major features and improvements
+ labels:
+ - major-enhancement
+ - major-rfe
+ - title: 🐛 Major bug fixes
+ labels:
+ - major-bug
+ - title: ⚠️ Deprecated
+ labels:
+ - deprecated
+ - title: 🚀 New features and improvements
+ labels:
+ - enhancement
+ - feature
+ - rfe
+ - title: 🐛 Bug fixes
+ labels:
+ - bug
+ - fix
+ - bugfix
+ - regression
+ - regression-fix
+ - title: 🌐 Localization and translation
+ labels:
+ - localization
+ - title: 👷 Changes for developers
+ labels:
+ - developer
+ - title: 📝 Documentation updates
+ labels:
+ - documentation
+ - title: 👻 Maintenance
+ labels:
+ - chore
+ - internal
+ - maintenance
+ - title: 🚦 Tests
+ labels:
+ - test
+ - tests
+ - title: ✍ Other changes
+ - title: 📦 Dependency updates
+ labels:
+ - dependencies
+ collapse-after: 15
+
+exclude-labels:
+ - reverted
+ - no-changelog
+ - skip-changelog
+ - invalid
+
+template: |
+ ## Changes
+
+ $CHANGES
+
+autolabeler:
+ - label: 'documentation'
+ files:
+ - '*.md'
+ branch:
+ - '/docs{0,1}\/.+/'
+ - label: 'bug'
+ branch:
+ - '/fix\/.+/'
+ title:
+ - '/fix/i'
+ - label: 'enhancement'
+ branch:
+ - '/feature\/.+/'
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..1711ae4b
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,30 @@
+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: 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
+ - uses: scacap/action-surefire-report@v1.9.1
\ No newline at end of file
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/gh-pages.yml b/.github/workflows/gh-pages.yml
new file mode 100644
index 00000000..ace69696
--- /dev/null
+++ b/.github/workflows/gh-pages.yml
@@ -0,0 +1,32 @@
+name: Deploy GitHub Pages
+on:
+ push:
+ branches: ["main"]
+ workflow_dispatch:
+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@v5
+ - uses: actions/jekyll-build-pages@v1
+ with:
+ source: ./
+ destination: ./_site
+ - uses: actions/upload-pages-artifact@v4
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..3ab20fb0
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,36 @@
+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: ossrh
+ server-username: OSSRH_USERNAME
+ server-password: OSSRH_PASSWORD
+ gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
+ gpg-passphrase: GPG_PASSPHRASE
+ - run: |
+ git config user.email "matomo-java-tracker@daniel-heid.de"
+ git config user.name "Matomo Java Tracker"
+ - id: version
+ run: |
+ VERSION=$( mvn -B help:evaluate -Dexpression=project.version -q -DforceStdout )
+ echo "::set-output name=version::${VERSION%-SNAPSHOT}"
+ - run: mvn -B release:prepare release:perform
+ 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
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
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..7dfdf721
--- /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..db941103 100644
--- a/README.md
+++ b/README.md
@@ -1,283 +1,677 @@
-# 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.
+* 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 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 3.4.x
+
+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.x
+
+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 3.4.0 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 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 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 higher.
+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
+ 3.4.0
```
-or Gradle:
+For Java 11:
+
+```xml
+
+
+ org.piwik.java.tracking
+ matomo-java-tracker-java11
+ 3.4.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:3.4.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):
+```groovy
+dependencies {
+ implementation("org.piwik.java.tracking:matomo-java-tracker-java11:3.4.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:3.4.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)
-```java
+```kotlin
+implementation("org.piwik.java.tracking:matomo-java-tracker-java11:3.4.0")
+```
-package example;
+### Spring Boot Module
-import org.matomo.java.tracking.MatomoRequest;
+If you use Spring Boot 3, 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
+ 3.4.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:3.4.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:3.4.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 | used by the request made to the endpoint is `MatomoJavaClient` per default. You can change it by using this builder method. |
+| 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 a page view for the
+visitor customer@mail.com with the action name "Checkout" and action URL "https://www.yourdomain.com/checkout" 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,
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 two 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 your 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);
+ }
+
+ }
-import org.apache.http.HttpResponse;
-import org.matomo.java.tracking.MatomoLocale;
+}
+```
+
+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.
+
+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 `AcceptLanguage.fromCode("fr")`
+* `deviceResolution` is of type `DeviceResolution`. You can build it easily
+ using `DeviceResolution.builder.width(...).height(...).build()`. To easy 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 +679,81 @@ 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
+ 3.4.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`
+You need to adapt your config.ini.php file and change
+the following line:
+
+```ini
+[General]
+trusted_hosts[] = "localhost:8080"
+```
+
+to
+
+```ini
+[General]
+trusted_hosts[] = "localhost:8080"
+```
-Clean this project using
+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.
+
+The following snippets helps you to do this quickly:
```shell
-mvn clean
+docker-compose exec matomo sed -i 's/localhost/localhost:8080/g' /var/www/html/config/config.ini.php
+```
+
+After the installation you can run `MatomoTrackerTester` in the module `test` to test the tracker. It will send
+multiple randomized
+requests to the local Matomo instance.
+
+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
+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
@@ -322,11 +765,26 @@ free to:
* Write awesome test 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 +792,5 @@ 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..2ebd49e5
--- /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 |
+|---------|------------------------|
+| >3 | :white_check_mark: yes |
+| <=2 | ✖️ 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..a716cbbb
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,46 @@
+
+ 4.0.0
+
+
+ org.piwik.java.tracking
+ matomo-java-tracker-parent
+ 3.4.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..5033a776
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/ActionType.java
@@ -0,0 +1,33 @@
+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..1c775234
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/AuthToken.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;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+final class AuthToken {
+
+ private AuthToken() {
+ // utility
+ }
+
+ @Nullable
+ static String determineAuthToken(
+ @Nullable
+ String overrideAuthToken,
+ @Nullable
+ Iterable extends MatomoRequest> requests,
+ @Nullable
+ TrackerConfiguration trackerConfiguration
+ ) {
+ if (isNotBlank(overrideAuthToken)) {
+ return overrideAuthToken;
+ }
+ 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..10483dc5
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/BulkRequest.java
@@ -0,0 +1,43 @@
+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/CustomVariable.java b/core/src/main/java/org/matomo/java/tracking/CustomVariable.java
new file mode 100644
index 00000000..04cbbee5
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/CustomVariable.java
@@ -0,0 +1,30 @@
+/*
+ * 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.NonNull;
+
+/**
+ * A user defined custom variable.
+ *
+ * @author brettcsorba
+ * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead.
+ */
+@Deprecated
+public class CustomVariable extends org.matomo.java.tracking.parameters.CustomVariable {
+
+ /**
+ * 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) {
+ super(key, value);
+ }
+}
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..9dc93b45
--- /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, String.format("MatomoJavaTracker-%d", count.getAndIncrement()));
+ thread.setDaemon(true);
+ return thread;
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/matomo/java/tracking/EcommerceItem.java b/core/src/main/java/org/matomo/java/tracking/EcommerceItem.java
new file mode 100644
index 00000000..64ac6037
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/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;
+
+
+/**
+ * A user defined custom variable.
+ *
+ * @author brettcsorba
+ * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead.
+ */
+@Deprecated
+public class EcommerceItem extends org.matomo.java.tracking.parameters.EcommerceItem {
+
+
+ /**
+ * Instantiates a new ecommerce item.
+ *
+ * @param sku the sku (Stock Keeping Unit) of the item
+ * @param name the name of the item
+ * @param category the category of the item
+ * @param price the price of the item
+ * @param quantity the quantity of the item
+ */
+ public EcommerceItem(
+ String sku, String name, String category, Double price, Integer quantity
+ ) {
+ super(sku, name, category, price, quantity);
+ }
+}
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..1ef11c97
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/ExecutorServiceCloser.java
@@ -0,0 +1,42 @@
+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..0b243ab0
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/InvalidUrlException.java
@@ -0,0 +1,18 @@
+/*
+ * 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/src/main/java/org/matomo/java/tracking/MatomoDate.java b/core/src/main/java/org/matomo/java/tracking/MatomoDate.java
similarity index 78%
rename from src/main/java/org/matomo/java/tracking/MatomoDate.java
rename to core/src/main/java/org/matomo/java/tracking/MatomoDate.java
index 25706998..331f4923 100644
--- a/src/main/java/org/matomo/java/tracking/MatomoDate.java
+++ b/core/src/main/java/org/matomo/java/tracking/MatomoDate.java
@@ -4,22 +4,24 @@
* @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;
+import lombok.Getter;
/**
* A datetime object that will return the datetime in the format {@code yyyy-MM-dd hh:mm:ss}.
*
* @author brettcsorba
+ * @deprecated Please use {@link Instant}
*/
+@Deprecated
+@Getter
public class MatomoDate {
- private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
private ZonedDateTime zonedDateTime;
@@ -27,6 +29,7 @@ public class MatomoDate {
* Allocates a Date object and initializes it so that it represents the time
* at which it was allocated, measured to the nearest millisecond.
*/
+ @Deprecated
public MatomoDate() {
zonedDateTime = ZonedDateTime.now(ZoneOffset.UTC);
}
@@ -38,6 +41,7 @@ public MatomoDate() {
*
* @param epochMilli the milliseconds since January 1, 1970, 00:00:00 GMT.
*/
+ @Deprecated
public MatomoDate(long epochMilli) {
zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), ZoneOffset.UTC);
}
@@ -52,18 +56,6 @@ 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.
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..1f94a08f
--- /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/MatomoLocale.java b/core/src/main/java/org/matomo/java/tracking/MatomoLocale.java
new file mode 100644
index 00000000..d0dad67d
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/MatomoLocale.java
@@ -0,0 +1,43 @@
+/*
+ * 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 static java.util.Objects.requireNonNull;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.Locale;
+import lombok.Getter;
+import lombok.Setter;
+import org.matomo.java.tracking.parameters.Country;
+
+/**
+ * Object representing a locale required by some Matomo query parameters.
+ *
+ * @author brettcsorba
+ * @deprecated Use {@link Country} instead
+ */
+@Setter
+@Getter
+@Deprecated
+public class MatomoLocale extends Country {
+
+ /**
+ * Constructs a new MatomoLocale.
+ *
+ * @param locale The locale to get the country code from
+ * @deprecated Please use {@link Country}
+ */
+ @Deprecated
+ public MatomoLocale(
+ @NonNull
+ Locale locale
+ ) {
+ super(requireNonNull(locale, "Locale must not be null"));
+ }
+
+}
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..42ef0c29
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/MatomoRequest.java
@@ -0,0 +1,1171 @@
+/*
+ * 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.nio.charset.Charset;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+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.NonNull;
+import lombok.Setter;
+import lombok.ToString;
+import lombok.experimental.Tolerate;
+import org.matomo.java.tracking.parameters.AcceptLanguage;
+import org.matomo.java.tracking.parameters.Country;
+import org.matomo.java.tracking.parameters.CustomVariable;
+import org.matomo.java.tracking.parameters.CustomVariables;
+import org.matomo.java.tracking.parameters.DeviceResolution;
+import org.matomo.java.tracking.parameters.EcommerceItem;
+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;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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
+ *
+ * @deprecated Please use {@link MatomoRequest#request()}
+ */
+ @Deprecated
+ public MatomoRequest(int siteId, String actionUrl) {
+ this.siteId = siteId;
+ this.actionUrl = actionUrl;
+ required = true;
+ visitorId = VisitorId.random();
+ randomValue = RandomValue.random();
+ apiVersion = "1";
+ responseAsImage = false;
+ }
+
+ /**
+ * 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 parameter at the specified key, null if nothing at this key
+ */
+ @Nullable
+ public Object getCustomTrackingParameter(@NonNull String key) {
+ if (additionalParameters == null || additionalParameters.isEmpty()) {
+ return null;
+ }
+ return additionalParameters.get(key);
+ }
+
+ /**
+ * Set a custom tracking parameter whose toString() value will be sent to the Matomo server. These
+ * parameters are stored separately from named Matomo parameters, meaning it is not possible to
+ * overwrite or clear named Matomo parameters with this method. A custom parameter that has the
+ * same name as a named Matomo parameter will be sent in addition to that named parameter.
+ *
+ * @param key the parameter's key. Cannot be null
+ * @param value the parameter's value. Removes the parameter if null
+ *
+ * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder#additionalParameters(Map)} instead.
+ */
+ @Deprecated
+ public void setCustomTrackingParameter(
+ @NonNull String key, @Nullable Object value
+ ) {
+
+ if (value == null) {
+ if (additionalParameters != null) {
+ additionalParameters.remove(key);
+ }
+ } else {
+ if (additionalParameters == null) {
+ additionalParameters = new LinkedHashMap<>();
+ }
+ additionalParameters.put(key, value);
+ }
+ }
+
+ /**
+ * Add a custom tracking parameter to the specified key. If there is already a parameter at this
+ * key, the new value replaces the old value.
+ *
+ * @param key the parameter's key. Cannot be null
+ * @param value the parameter's value. May be null
+ *
+ * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder#additionalParameters(Map)} instead.
+ */
+ @Deprecated
+ public void addCustomTrackingParameter(@NonNull String key, @Nullable Object value) {
+ if (additionalParameters == null) {
+ additionalParameters = new LinkedHashMap<>();
+ }
+ additionalParameters.put(key, value);
+ }
+
+ /**
+ * Removes all custom tracking parameters.
+ *
+ * @deprecated Please use {@link MatomoRequest.MatomoRequestBuilder#additionalParameters(Map)}
+ * instead so that you can manage the map yourself.
+ */
+ @Deprecated
+ public void clearCustomTrackingParameter() {
+ additionalParameters.clear();
+ }
+
+ /**
+ * Sets idgoal=0 in the request to track an ecommerce interaction: cart update or an
+ * ecommerce order.
+ *
+ * @deprecated Please use {@link MatomoRequest#setGoalId(Integer)} instead
+ */
+ @Deprecated
+ public void enableEcommerce() {
+ setGoalId(0);
+ }
+
+ /**
+ * Get the {@link EcommerceItem} at the specified index.
+ *
+ * @param index the index of the {@link EcommerceItem} to return
+ *
+ * @return the {@link EcommerceItem} at the specified index
+ * @deprecated Use @link {@link MatomoRequest.MatomoRequestBuilder#ecommerceItems(EcommerceItems)}
+ * instead
+ */
+ @Nullable
+ @Deprecated
+ public EcommerceItem getEcommerceItem(int index) {
+ if (ecommerceItems == null || ecommerceItems.isEmpty()) {
+ return null;
+ }
+ return ecommerceItems.get(index);
+ }
+
+ /**
+ * Add an {@link EcommerceItem} to this order. Ecommerce must be enabled, and EcommerceId and
+ * EcommerceRevenue must first be set.
+ *
+ * @param item the {@link EcommerceItem} to add. Cannot be null
+ *
+ * @deprecated Use @link {@link MatomoRequest.MatomoRequestBuilder#ecommerceItems(EcommerceItems)}
+ * instead
+ */
+ @Deprecated
+ public void addEcommerceItem(@NonNull EcommerceItem item) {
+ if (ecommerceItems == null) {
+ ecommerceItems = new EcommerceItems();
+ }
+ ecommerceItems.add(item);
+ }
+
+ /**
+ * Clears all {@link EcommerceItem} from this order.
+ *
+ * @deprecated Use @link {@link MatomoRequest.MatomoRequestBuilder#ecommerceItems(EcommerceItems)}
+ * instead
+ */
+ @Deprecated
+ public void clearEcommerceItems() {
+ ecommerceItems.clear();
+ }
+
+ /**
+ * Get the page custom variable at the specified key.
+ *
+ * @param key the key of the variable to get
+ *
+ * @return the variable at the specified key, null if key is not present
+ * @deprecated Use the {@link #getPageCustomVariables()} method instead.
+ */
+ @Nullable
+ @Deprecated
+ public String getPageCustomVariable(String key) {
+ if (pageCustomVariables == null) {
+ return null;
+ }
+ return pageCustomVariables.get(key);
+ }
+
+ /**
+ * Get the page custom variable at the specified index.
+ *
+ * @param index the index of the variable to get. Must be greater than 0
+ *
+ * @return the variable at the specified key, null if nothing at this index
+ * @deprecated Use {@link MatomoRequest#getPageCustomVariables()} instead
+ */
+ @Deprecated
+ @Nullable
+ public CustomVariable getPageCustomVariable(int index) {
+ return getCustomVariable(pageCustomVariables, index);
+ }
+
+ @Nullable
+ @Deprecated
+ private static CustomVariable getCustomVariable(CustomVariables customVariables, int index) {
+ if (customVariables == null) {
+ return null;
+ }
+ return customVariables.get(index);
+ }
+
+ /**
+ * Set a page custom variable with the specified key and value at the first available index. All
+ * page custom variables with this key will be overwritten or deleted
+ *
+ * @param key the key of the variable to set
+ * @param value the value of the variable to set at the specified key. A null value will remove
+ * this custom variable
+ *
+ * @deprecated Use {@link MatomoRequest#getPageCustomVariables()} instead
+ */
+ @Deprecated
+ public void setPageCustomVariable(
+ @NonNull String key, @Nullable String value
+ ) {
+ if (value == null) {
+ if (pageCustomVariables == null) {
+ return;
+ }
+ pageCustomVariables.remove(key);
+ } else {
+ CustomVariable variable = new CustomVariable(key, value);
+ if (pageCustomVariables == null) {
+ pageCustomVariables = new CustomVariables();
+ }
+ pageCustomVariables.add(variable);
+ }
+ }
+
+ /**
+ * Set a page custom variable at the specified index.
+ *
+ * @param customVariable the CustomVariable to set. A null value will remove the CustomVariable
+ * at the specified index
+ * @param index the index of he CustomVariable to set
+ *
+ * @deprecated Use {@link #getPageCustomVariables()} instead
+ */
+ @Deprecated
+ public void setPageCustomVariable(
+ @Nullable CustomVariable customVariable, int index
+ ) {
+ if (pageCustomVariables == null) {
+ if (customVariable == null) {
+ return;
+ }
+ pageCustomVariables = new CustomVariables();
+ }
+ setCustomVariable(pageCustomVariables, customVariable, index);
+ }
+
+ @Deprecated
+ private static void setCustomVariable(
+ CustomVariables customVariables, @Nullable CustomVariable customVariable, int index
+ ) {
+ if (customVariable == null) {
+ customVariables.remove(index);
+ } else {
+ customVariables.add(customVariable, index);
+ }
+ }
+
+ /**
+ * Get the datetime of the request.
+ *
+ * @return the datetime of the request
+ * @deprecated Use {@link #getRequestTimestamp()} instead
+ */
+ @Deprecated
+ @Nullable
+ public MatomoDate getRequestDatetime() {
+ return requestTimestamp == null ? null : new MatomoDate(requestTimestamp.toEpochMilli());
+ }
+
+ /**
+ * Set the datetime of the request (normally the current time is used). This can be used to record
+ * visits and page views in the past. The datetime must be sent in UTC timezone. Note: if you
+ * record data in the past, you will need to force
+ * Matomo to re-process reports for the past dates. If you set the Request
+ * Datetime to a datetime older than four hours then Auth Token must be set. If you
+ * set
+ * Request Datetime with a datetime in the last four hours then you
+ * don't need to pass Auth Token.
+ *
+ * @param matomoDate the datetime of the request to set. A null value will remove this parameter
+ *
+ * @deprecated Use {@link #setRequestTimestamp(Instant)} instead
+ */
+ @Deprecated
+ public void setRequestDatetime(MatomoDate matomoDate) {
+ if (matomoDate == null) {
+ requestTimestamp = null;
+ } else {
+ setRequestTimestamp(matomoDate.getZonedDateTime().toInstant());
+ }
+ }
+
+
+ /**
+ * Get the visit custom variable at the specified key.
+ *
+ * @param key the key of the variable to get
+ *
+ * @return the variable at the specified key, null if key is not present
+ * @deprecated Use the {@link #getVisitCustomVariables()} method instead.
+ */
+ @Nullable
+ @Deprecated
+ public String getUserCustomVariable(String key) {
+ if (visitCustomVariables == null) {
+ return null;
+ }
+ return visitCustomVariables.get(key);
+ }
+
+ /**
+ * Get the visit custom variable at the specified index.
+ *
+ * @param index the index of the variable to get
+ *
+ * @return the variable at the specified index, null if nothing at this index
+ * @deprecated Use {@link #getVisitCustomVariables()} instead
+ */
+ @Nullable
+ @Deprecated
+ public CustomVariable getVisitCustomVariable(int index) {
+ return getCustomVariable(visitCustomVariables, index);
+ }
+
+ /**
+ * Set a visit custom variable with the specified key and value at the first available index. All
+ * visit custom variables with this key will be overwritten or deleted
+ *
+ * @param key the key of the variable to set
+ * @param value the value of the variable to set at the specified key. A null value will remove
+ * this parameter
+ *
+ * @deprecated Use {@link #setVisitCustomVariables(CustomVariables)} instead
+ */
+ @Deprecated
+ public void setUserCustomVariable(
+ @NonNull String key, @Nullable String value
+ ) {
+ if (value == null) {
+ if (visitCustomVariables == null) {
+ return;
+ }
+ visitCustomVariables.remove(key);
+ } else {
+ CustomVariable variable = new CustomVariable(key, value);
+ if (visitCustomVariables == null) {
+ visitCustomVariables = new CustomVariables();
+ }
+ visitCustomVariables.add(variable);
+ }
+ }
+
+ /**
+ * Set a user custom variable at the specified key.
+ *
+ * @param customVariable the CustomVariable to set. A null value will remove the custom variable
+ * at the specified index
+ * @param index the index to set the customVariable at.
+ *
+ * @deprecated Use {@link #setVisitCustomVariables(CustomVariables)} instead
+ */
+ @Deprecated
+ public void setVisitCustomVariable(
+ @Nullable CustomVariable customVariable, int index
+ ) {
+ if (visitCustomVariables == null) {
+ if (customVariable == null) {
+ return;
+ }
+ visitCustomVariables = new CustomVariables();
+ }
+ setCustomVariable(visitCustomVariables, customVariable, index);
+ }
+
+ /**
+ * Sets a custom parameter to append to the Matomo tracking parameters.
+ *
+ *
Attention: If a parameter with the same name already exists, it will be appended twice!
+ *
+ * @param parameterName The name of the query parameter to append. Must not be null or empty.
+ * @param value The value of the query parameter to append. To remove the parameter, pass
+ * null.
+ *
+ * @deprecated Use @link {@link MatomoRequest.MatomoRequestBuilder#additionalParameters(Map)}
+ * instead
+ */
+ @Deprecated
+ public void setParameter(@NonNull String parameterName, Object value) {
+ if (parameterName.trim().isEmpty()) {
+ throw new IllegalArgumentException("Parameter name must not be empty");
+ }
+ if (additionalParameters == null) {
+ if (value == null) {
+ return;
+ }
+ additionalParameters = new LinkedHashMap<>();
+ }
+ if (value == null) {
+ additionalParameters.remove(parameterName);
+ } else {
+ additionalParameters.put(parameterName, value);
+ }
+ }
+
+ /**
+ * Creates a new {@link MatomoRequestBuilder} instance. Only here for backwards compatibility.
+ *
+ * @deprecated Use {@link MatomoRequest#request()} instead.
+ */
+ @Deprecated
+ public static org.matomo.java.tracking.MatomoRequestBuilder builder() {
+ return new org.matomo.java.tracking.MatomoRequestBuilder();
+ }
+
+ /**
+ * Parses the given device resolution string and sets the {@link #deviceResolution} field.
+ *
+ * @param deviceResolution the device resolution string to parse. Format: "WIDTHxHEIGHT"
+ *
+ * @deprecated Use {@link #setDeviceResolution(DeviceResolution)} instead.
+ */
+ @Tolerate
+ @Deprecated
+ public void setDeviceResolution(@Nullable String deviceResolution) {
+ if (deviceResolution == null || deviceResolution.trim().isEmpty()) {
+ this.deviceResolution = null;
+ } else {
+ this.deviceResolution = DeviceResolution.fromString(deviceResolution);
+ }
+ }
+
+}
diff --git a/core/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java b/core/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java
new file mode 100644
index 00000000..01c43cd2
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java
@@ -0,0 +1,49 @@
+package org.matomo.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Map;
+import org.matomo.java.tracking.parameters.AcceptLanguage;
+
+/**
+ * The former MatomoRequestBuilder class has been moved to MatomoRequest.MatomoRequestBuilder.
+ * This class is only here for backwards compatibility.
+ *
+ * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder} instead.
+ */
+@Deprecated
+public class MatomoRequestBuilder extends MatomoRequest.MatomoRequestBuilder {
+
+
+ /**
+ * Sets the tracking parameter for the accept languages of a user. Only here for backwards
+ * compatibility.
+ *
+ * @param headerAcceptLanguage The accept language header of a user. Must be in the format
+ * specified in RFC 2616.
+ * @return This builder
+ * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder#headerAcceptLanguage(AcceptLanguage)}
+ * in combination with {@link AcceptLanguage#fromHeader(String)} instead.
+ */
+ @Deprecated
+ public MatomoRequestBuilder headerAcceptLanguage(@Nullable String headerAcceptLanguage) {
+ headerAcceptLanguage(AcceptLanguage.fromHeader(headerAcceptLanguage));
+ return this;
+ }
+
+ /**
+ * Sets the custom tracking parameters to the given parameters.
+ *
+ *
This converts the given map to a map of collections. Only included for backwards
+ * compatibility.
+ *
+ * @param parameters The custom tracking parameters to set
+ * @return This builder
+ * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder#additionalParameters(Map)}}
+ */
+ @Deprecated
+ public MatomoRequestBuilder customTrackingParameters(@Nullable Map parameters) {
+ additionalParameters(parameters);
+ return this;
+ }
+
+}
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..c5b57c09
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/MatomoRequests.java
@@ -0,0 +1,359 @@
+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..3c38d017
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/MatomoTracker.java
@@ -0,0 +1,376 @@
+/*
+ * 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.Arrays;
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+import java.util.function.Function;
+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 tracker that will send {@link MatomoRequest}s to the specified
+ * Tracking HTTP API endpoint.
+ *
+ * @param hostUrl url endpoint to send requests to. Usually in the format
+ * https://your-matomo-domain.tld/matomo.php. Must not be null
+ * @deprecated Please use {@link MatomoTracker#MatomoTracker(TrackerConfiguration)}
+ */
+ @Deprecated
+ public MatomoTracker(
+ @NonNull String hostUrl
+ ) {
+ this(hostUrl, 0);
+ }
+
+ /**
+ * Creates a tracker that will send {@link MatomoRequest}s to the specified
+ * Tracking HTTP API endpoint.
+ *
+ * @param hostUrl url endpoint to send requests to. Usually in the format
+ * https://your-matomo-domain.tld/matomo.php.
+ * @param timeout the timeout of the sent request in milliseconds or -1 if not set
+ * @deprecated Please use {@link MatomoTracker#MatomoTracker(TrackerConfiguration)}
+ */
+ @Deprecated
+ public MatomoTracker(
+ @NonNull String hostUrl, int timeout
+ ) {
+ this(hostUrl, null, 0, timeout);
+ }
+
+ /**
+ * Creates a tracker that will send {@link MatomoRequest}s to the specified
+ * Tracking HTTP API endpoint.
+ *
+ * @param hostUrl url endpoint to send requests to. Usually in the format
+ * https://your-matomo-domain.tld/matomo.php.
+ * @param proxyHost The hostname or IP address of an optional HTTP proxy, null allowed
+ * @param proxyPort The port of an HTTP proxy or -1 if not set
+ * @param timeout the timeout of the request in milliseconds or -1 if not set
+ * @deprecated Please use {@link MatomoTracker#MatomoTracker(TrackerConfiguration)}
+ */
+ @Deprecated
+ public MatomoTracker(
+ @NonNull String hostUrl, @Nullable String proxyHost, int proxyPort, int timeout
+ ) {
+ this(TrackerConfiguration
+ .builder()
+ .enabled(true)
+ .apiEndpoint(URI.create(hostUrl))
+ .proxyHost(proxyHost)
+ .proxyPort(proxyPort)
+ .connectTimeout(timeout == -1 ? Duration.ofSeconds(5L) : Duration.ofSeconds(timeout))
+ .socketTimeout(timeout == -1 ? Duration.ofSeconds(5L) : Duration.ofSeconds(timeout))
+ .build());
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Creates a tracker that will send {@link MatomoRequest}s to the specified
+ * Tracking HTTP API endpoint via the provided proxy.
+ *
+ * @param hostUrl url endpoint to send requests to. Usually in the format
+ * https://your-matomo-domain.tld/matomo.php.
+ * @param proxyHost url endpoint for the proxy, null allowed
+ * @param proxyPort proxy server port number or -1 if not set
+ * @deprecated Please use {@link MatomoTracker#MatomoTracker(TrackerConfiguration)}
+ */
+ @Deprecated
+ public MatomoTracker(
+ @NonNull String hostUrl, @Nullable String proxyHost, int proxyPort
+ ) {
+ this(hostUrl, proxyHost, proxyPort, -1);
+ }
+
+ /**
+ * 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
+ ) {
+ return sendRequestAsync(request, Function.identity());
+ }
+
+ /**
+ * Send a request asynchronously via HTTP GET and specify a callback that gets executed when the response arrives.
+ *
+ *
Use this method if you want to send a single request. If you want to send multiple requests at once, use
+ * {@link #sendBulkRequestAsync(Collection, Consumer)} instead. If you want to send multiple requests synchronously,
+ * use {@link #sendRequest(MatomoRequest)} or {@link #sendBulkRequest(Iterable)} instead.
+ *
+ * @param request request to send
+ * @param callback callback that gets executed when response arrives, must not be null
+ * @return a completable future to let you know when the request is done. The future contains
+ * the callback result.
+ * @deprecated Please use {@link MatomoTracker#sendRequestAsync(MatomoRequest)} in combination with
+ * {@link CompletableFuture#thenAccept(Consumer)} instead
+ */
+ @Deprecated
+ public CompletableFuture sendRequestAsync(
+ @NonNull MatomoRequest request,
+ @NonNull Function callback
+ ) {
+ if (trackerConfiguration.isEnabled()) {
+ applyGoalIdAndCheckSiteId(request);
+ log.debug("Sending async request via GET: {}", request);
+ initializeSender();
+ CompletableFuture future = sender.sendSingleAsync(request);
+ return future.thenApply(callback);
+ }
+ 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), null);
+ }
+
+ /**
+ * 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) {
+ sendBulkRequest(requests, null);
+ }
+
+ /**
+ * Send multiple requests in a single HTTP POST call. More efficient than sending
+ * several individual requests.
+ *
+ *
Specify the AuthToken if parameters that require an auth token is used. 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
+ * @param authToken specify if any of the parameters use require AuthToken, if null the default auth token from the
+ * request or the tracker configuration is used.
+ * @deprecated use {@link #sendBulkRequest(Iterable)} instead and set the auth token in the tracker configuration or
+ * the requests directly.
+ */
+ @Deprecated
+ public void sendBulkRequest(
+ @NonNull Iterable extends MatomoRequest> requests, @Nullable String authToken
+ ) {
+ if (trackerConfiguration.isEnabled()) {
+ for (MatomoRequest request : requests) {
+ applyGoalIdAndCheckSiteId(request);
+ }
+ log.debug("Sending requests via POST: {}", requests);
+ initializeSender();
+ sender.sendBulk(requests, authToken);
+ } 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), null, null);
+ }
+
+ /**
+ * 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
+ ) {
+ return sendBulkRequestAsync(requests, null, null);
+ }
+
+ /**
+ * Send multiple requests in a single HTTP call. More efficient than sending
+ * several individual requests. Specify the AuthToken if parameters that require
+ * an auth token is used.
+ *
+ * @param requests the requests to send
+ * @param authToken specify if any of the parameters use require AuthToken, if null the default auth token from the
+ * request or the tracker configuration is used
+ * @param callback callback that gets executed when response arrives, null allowed
+ * @return a completable future to let you know when the request is done
+ * @deprecated Please set the auth token in the tracker configuration or the requests directly and use
+ * {@link CompletableFuture#thenAccept(Consumer)} instead for the callback.
+ */
+ @Deprecated
+ public CompletableFuture sendBulkRequestAsync(
+ @NonNull Collection extends MatomoRequest> requests,
+ @Nullable String authToken,
+ @Nullable Consumer callback
+ ) {
+ if (trackerConfiguration.isEnabled()) {
+ for (MatomoRequest request : requests) {
+ applyGoalIdAndCheckSiteId(request);
+ }
+ log.debug("Sending async requests via POST: {}", requests);
+ initializeSender();
+ CompletableFuture future = sender.sendBulkAsync(requests, authToken);
+ if (callback != null) {
+ return future.thenAccept(callback);
+ }
+ return future;
+ }
+ log.warn("Tracker is disabled");
+ return CompletableFuture.completedFuture(null);
+ }
+
+ /**
+ * Send multiple requests in a single HTTP call. More efficient than sending
+ * several individual requests.
+ *
+ * @param requests the requests to send
+ * @param callback callback that gets executed when response arrives, null allowed
+ * @return completable future to let you know when the request is done
+ */
+ public CompletableFuture sendBulkRequestAsync(
+ @NonNull Collection extends MatomoRequest> requests,
+ @Nullable Consumer callback
+ ) {
+ return sendBulkRequestAsync(requests, null, callback);
+ }
+
+ /**
+ * Send multiple requests in a single HTTP call. More efficient than sending
+ * several individual requests. Specify the AuthToken if parameters that require
+ * an auth token is used.
+ *
+ * @param requests the requests to send
+ * @param authToken specify if any of the parameters use require AuthToken, null allowed
+ * @return completable future to let you know when the request is done
+ * @deprecated Please set the auth token in the tracker configuration or the requests directly and use
+ * {@link #sendBulkRequestAsync(Collection)} instead.
+ */
+ public CompletableFuture sendBulkRequestAsync(
+ @NonNull Collection extends MatomoRequest> requests, @Nullable String authToken
+ ) {
+ return sendBulkRequestAsync(requests, authToken, 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..ec314cde
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/ProxyAuthenticator.java
@@ -0,0 +1,34 @@
+/*
+ * 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..63a2b9d2
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/QueryCreator.java
@@ -0,0 +1,160 @@
+/*
+ * 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 && !value.toString().trim().isEmpty()) {
+ appendAmpersand(query);
+ query.append(encode(entry.getKey())).append('=').append(encode(value.toString()));
+ }
+ }
+ }
+ 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..f604e1c5
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/RequestValidator.java
@@ -0,0 +1,52 @@
+/*
+ * 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..7b1df985
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/Sender.java
@@ -0,0 +1,26 @@
+package org.matomo.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+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, @Nullable String overrideAuthToken
+ );
+
+ @NonNull
+ CompletableFuture sendBulkAsync(
+ @NonNull Collection extends MatomoRequest> requests, @Nullable String overrideAuthToken
+ );
+}
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..c4bfa561
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/SenderFactory.java
@@ -0,0 +1,10 @@
+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..12b80291
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/SenderProvider.java
@@ -0,0 +1,7 @@
+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..7cafe302
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/ServiceLoaderSenderFactory.java
@@ -0,0 +1,28 @@
+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..424ce82d
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/TrackerConfiguration.java
@@ -0,0 +1,187 @@
+/*
+ * 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, for 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.trim().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..0b9d82bc
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/TrackingParameter.java
@@ -0,0 +1,29 @@
+/*
+ * 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..2540ea0c
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/TrackingParameterMethod.java
@@ -0,0 +1,67 @@
+/*
+ * 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..790db746
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/TrustingX509TrustManager.java
@@ -0,0 +1,28 @@
+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..5c08c6dc
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * 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..0ecee82d
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/AcceptLanguage.java
@@ -0,0 +1,75 @@
+/*
+ * 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() :
+ String.format("%s;q=%s", languageRange.getRange(), 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..ab51e14f
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/Country.java
@@ -0,0 +1,121 @@
+/*
+ * 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;
+
+ /**
+ * Only for internal use to grant downwards compatibility to {@link org.matomo.java.tracking.MatomoLocale}.
+ *
+ * @param locale A locale that must contain a country code
+ */
+ @Deprecated
+ protected Country(
+ @edu.umd.cs.findbugs.annotations.NonNull
+ Locale locale
+ ) {
+ setLocale(locale);
+ }
+
+ /**
+ * 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");
+ }
+
+ /**
+ * Returns the locale for this country.
+ *
+ * @return The locale for this country
+ * @see Locale#forLanguageTag(String)
+ * @deprecated Since you instantiate this class, you can determine the language on your own
+ * using {@link Locale#forLanguageTag(String)}
+ */
+ @Deprecated
+ public Locale getLocale() {
+ return Locale.forLanguageTag(code);
+ }
+
+ /**
+ * Sets the locale for this country.
+ *
+ * @param locale A locale that must contain a country code
+ * @see Locale#getCountry()
+ * @deprecated Since you instantiate this class, you can determine the language on your own
+ * using {@link Locale#getCountry()}
+ */
+ @Deprecated
+ public final void setLocale(Locale locale) {
+ if (locale == null || locale.getCountry() == null || locale.getCountry().isEmpty()) {
+ throw new IllegalArgumentException("Invalid locale");
+ }
+ code = locale.getCountry().toLowerCase(Locale.ENGLISH);
+ }
+
+ @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..0f6a3fa9
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/CustomVariable.java
@@ -0,0 +1,57 @@
+/*
+ * 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..4fee72df
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/CustomVariables.java
@@ -0,0 +1,209 @@
+/*
+ * 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..6729443c
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/DeviceResolution.java
@@ -0,0 +1,59 @@
+/*
+ * 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 String.format("%dx%d", width, height);
+ }
+
+}
diff --git a/src/main/java/org/matomo/java/tracking/EcommerceItem.java b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java
similarity index 53%
rename from src/main/java/org/matomo/java/tracking/EcommerceItem.java
rename to core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java
index f7b5bf23..7f5ba051 100644
--- a/src/main/java/org/matomo/java/tracking/EcommerceItem.java
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java
@@ -4,7 +4,8 @@
* @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;
+
+package org.matomo.java.tracking.parameters;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -13,19 +14,28 @@
/**
* Represents an item in an ecommerce order.
- *
- * @author brettcsorba
*/
-@Setter
-@Getter
-@AllArgsConstructor
@Builder
+@AllArgsConstructor
+@Getter
+@Setter
public class EcommerceItem {
private String sku;
- private String name;
- private String category;
- private Double price;
- private Integer quantity;
+ @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 String.format("[\"%s\",\"%s\",\"%s\",%s,%d]", 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..7000ded7
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItems.java
@@ -0,0 +1,41 @@
+/*
+ * 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..13ab4d98
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/Hex.java
@@ -0,0 +1,26 @@
+/*
+ * 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 Hex() {
+ // utility class
+ }
+
+ static String fromBytes(@NonNull byte[] bytes) {
+ StringBuilder result = new StringBuilder(bytes.length * 2);
+ for (byte b : bytes) {
+ result.append(String.format("%02x", b));
+ }
+ 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..2b000b6e
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/RandomValue.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 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..06a82be6
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/UniqueId.java
@@ -0,0 +1,58 @@
+/*
+ * 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..39b54e30
--- /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..f709edce
--- /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..4c29ae3d
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/servlet/CookieWrapper.java
@@ -0,0 +1,15 @@
+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..b72c087b
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/servlet/HttpServletRequestWrapper.java
@@ -0,0 +1,60 @@
+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..ef81608f
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/servlet/ServletMatomoRequest.java
@@ -0,0 +1,170 @@
+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.
+ *
+ *
Use #fromServletRequest(HttpServletRequestWrapper) to create a new builder with the headers from
+ * the request.
+ *
+ * @param builder the builder to add the headers to (must not be null)
+ * @param request the request to get the headers from (must not be null)
+ *
+ * @return the builder with the headers added (never null)
+ */
+ @edu.umd.cs.findbugs.annotations.NonNull
+ public static MatomoRequest.MatomoRequestBuilder addServletRequestHeaders(
+ @NonNull MatomoRequest.MatomoRequestBuilder builder, @NonNull HttpServletRequestWrapper request
+ ) {
+ return builder
+ .actionUrl(request.getRequestURL() == null ? null : request.getRequestURL().toString())
+ .headers(collectHeaders(request))
+ .visitorIp(determineVisitorIp(request))
+ .userId(request.getRemoteUser())
+ .cookies(processCookies(builder, request));
+ }
+
+ @edu.umd.cs.findbugs.annotations.NonNull
+ private static Map collectHeaders(
+ @edu.umd.cs.findbugs.annotations.NonNull HttpServletRequestWrapper request
+ ) {
+ Map headers = new HashMap<>(10);
+ Enumeration headerNames = request.getHeaderNames();
+ while (headerNames.hasMoreElements()) {
+ String headerName = headerNames.nextElement();
+ if (headerName != null && !headerName.trim().isEmpty() && !RESTRICTED_HEADERS.contains(
+ headerName.toLowerCase(
+ Locale.ROOT))) {
+ headers.put(headerName, request.getHeader(headerName));
+ }
+ }
+ return headers;
+ }
+
+ @Nullable
+ private static String determineVisitorIp(
+ @edu.umd.cs.findbugs.annotations.NonNull HttpServletRequestWrapper request
+ ) {
+ String forwardedForHeader = request.getHeader("X-Forwarded-For");
+ if (isNotEmpty(forwardedForHeader)) {
+ return forwardedForHeader;
+ }
+ if (isNotEmpty(request.getRemoteAddr())) {
+ return request.getRemoteAddr();
+ }
+ return null;
+ }
+
+ @edu.umd.cs.findbugs.annotations.NonNull
+ private static Map processCookies(
+ @edu.umd.cs.findbugs.annotations.NonNull MatomoRequest.MatomoRequestBuilder builder,
+ @edu.umd.cs.findbugs.annotations.NonNull HttpServletRequestWrapper request
+ ) {
+ Map cookies = new LinkedHashMap<>(3);
+ if (request.getCookies() != null) {
+ builder.supportsCookies(Boolean.TRUE);
+ for (CookieWrapper cookie : request.getCookies()) {
+ if (isNotEmpty(cookie.getValue())) {
+ processCookie(builder, cookies, cookie.getName(), cookie.getValue());
+ }
+ }
+ }
+ return cookies;
+ }
+
+ private static boolean isNotEmpty(String forwardedForHeader) {
+ return forwardedForHeader != null && !forwardedForHeader.trim().isEmpty();
+ }
+
+ private static void processCookie(
+ @edu.umd.cs.findbugs.annotations.NonNull MatomoRequest.MatomoRequestBuilder builder,
+ @edu.umd.cs.findbugs.annotations.NonNull Map cookies,
+ @edu.umd.cs.findbugs.annotations.NonNull String cookieName,
+ @edu.umd.cs.findbugs.annotations.NonNull String cookieValue
+ ) {
+ if (cookieName.toLowerCase(Locale.ROOT).startsWith("_pk_id")) {
+ extractVisitorId(builder, cookies, cookieValue, cookieName);
+ }
+ if (cookieName.toLowerCase(Locale.ROOT).equalsIgnoreCase("MATOMO_SESSID")) {
+ builder.sessionId(cookieValue);
+ }
+ if (cookieName.toLowerCase(Locale.ROOT).startsWith("_pk_ses")
+ || cookieName.toLowerCase(Locale.ROOT).startsWith("_pk_ref")
+ || cookieName.toLowerCase(Locale.ROOT).startsWith("_pk_hsr")) {
+ cookies.put(cookieName, cookieValue);
+ }
+ if (cookieName.toLowerCase(Locale.ROOT).startsWith("_pk_cvar")) {
+ builder.visitCustomVariables(CustomVariables.parse(cookieValue));
+ }
+ }
+
+ private static void extractVisitorId(
+ @edu.umd.cs.findbugs.annotations.NonNull MatomoRequest.MatomoRequestBuilder builder,
+ @edu.umd.cs.findbugs.annotations.NonNull Map cookies,
+ @edu.umd.cs.findbugs.annotations.NonNull String cookieValue,
+ @edu.umd.cs.findbugs.annotations.NonNull String cookieName
+ ) {
+ String[] cookieValues = cookieValue.split("\\.");
+ if (cookieValues.length > 0) {
+ builder.visitorId(VisitorId.fromHex(cookieValues[0])).newVisitor(Boolean.FALSE);
+ cookies.put(cookieName, cookieValue);
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/piwik/java/tracking/CustomVariable.java b/core/src/main/java/org/piwik/java/tracking/CustomVariable.java
new file mode 100644
index 00000000..20845bed
--- /dev/null
+++ b/core/src/main/java/org/piwik/java/tracking/CustomVariable.java
@@ -0,0 +1,32 @@
+/*
+ * 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.piwik.java.tracking;
+
+/**
+ * A user defined custom variable.
+ *
+ *
Renamed to {@link org.matomo.java.tracking.parameters.CustomVariable} in 3.0.0.
+ *
+ * @author brettcsorba
+ * @deprecated Use {@link org.matomo.java.tracking.parameters.CustomVariable} instead.
+ */
+@Deprecated
+public class CustomVariable extends org.matomo.java.tracking.parameters.CustomVariable {
+
+ /**
+ * Instantiates a new custom variable.
+ *
+ * @param key the key of the custom variable (required)
+ * @param value the value of the custom variable (required)
+ * @deprecated Use {@link org.matomo.java.tracking.parameters.CustomVariable} instead.
+ */
+ @Deprecated
+ public CustomVariable(String key, String value) {
+ super(key, value);
+ }
+}
diff --git a/core/src/main/java/org/piwik/java/tracking/EcommerceItem.java b/core/src/main/java/org/piwik/java/tracking/EcommerceItem.java
new file mode 100644
index 00000000..49c3a83c
--- /dev/null
+++ b/core/src/main/java/org/piwik/java/tracking/EcommerceItem.java
@@ -0,0 +1,34 @@
+/*
+ * 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.piwik.java.tracking;
+
+/**
+ * Describes an item in an ecommerce transaction.
+ *
+ * @author brettcsorba
+ * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead.
+ */
+@Deprecated
+public class EcommerceItem extends org.matomo.java.tracking.parameters.EcommerceItem {
+
+ /**
+ * Creates a new ecommerce item.
+ *
+ * @param sku the sku (Stock Keeping Unit) of the item.
+ * @param name the name of the item.
+ * @param category the category of the item.
+ * @param price the price of the item.
+ * @param quantity the quantity of the item.
+ * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead.
+ */
+ @Deprecated
+ public EcommerceItem(String sku, String name, String category, Double price, Integer quantity) {
+ super(sku, name, category, price, quantity);
+ }
+
+}
diff --git a/core/src/main/java/org/piwik/java/tracking/PiwikDate.java b/core/src/main/java/org/piwik/java/tracking/PiwikDate.java
new file mode 100644
index 00000000..d8029269
--- /dev/null
+++ b/core/src/main/java/org/piwik/java/tracking/PiwikDate.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.piwik.java.tracking;
+
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.util.TimeZone;
+import org.matomo.java.tracking.MatomoDate;
+
+/**
+ * A date object that can be used to send dates to Matomo. This class is deprecated and will be removed in a future.
+ *
+ * @author brettcsorba
+ * @deprecated Please use {@link Instant}
+ */
+@Deprecated
+public class PiwikDate extends MatomoDate {
+
+ /**
+ * Creates a new date object with the current time.
+ *
+ * @deprecated Use {@link Instant} instead.
+ */
+ @Deprecated
+ public PiwikDate() {
+ }
+
+ /**
+ * Creates a new date object with the specified time. The time is specified in milliseconds since the epoch.
+ *
+ * @param epochMilli The time in milliseconds since the epoch
+ * @deprecated Use {@link Instant} instead.
+ */
+ @Deprecated
+ public PiwikDate(long epochMilli) {
+ super(epochMilli);
+ }
+
+ /**
+ * Sets the time zone for this date object. This is used to convert the date to UTC before sending it to Matomo.
+ *
+ * @param zone the time zone to use
+ * @deprecated Use {@link ZonedDateTime#toInstant()} instead.
+ */
+ @Deprecated
+ public void setTimeZone(TimeZone zone) {
+ setTimeZone(zone.toZoneId());
+ }
+
+}
diff --git a/core/src/main/java/org/piwik/java/tracking/PiwikLocale.java b/core/src/main/java/org/piwik/java/tracking/PiwikLocale.java
new file mode 100644
index 00000000..34f156d6
--- /dev/null
+++ b/core/src/main/java/org/piwik/java/tracking/PiwikLocale.java
@@ -0,0 +1,33 @@
+/*
+ * 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.piwik.java.tracking;
+
+import java.util.Locale;
+import org.matomo.java.tracking.parameters.Country;
+
+/**
+ * A locale object that can be used to send visitor country to Matomo. This class is deprecated and will be removed in
+ * the future.
+ *
+ * @author brettcsorba
+ * @deprecated Use {@link Country} instead.
+ */
+@Deprecated
+public class PiwikLocale extends Country {
+
+ /**
+ * Creates a new Piwik locale object with the specified locale.
+ *
+ * @param locale the locale to use
+ * @deprecated Use {@link Country} instead.
+ */
+ @Deprecated
+ public PiwikLocale(Locale locale) {
+ super(locale);
+ }
+}
diff --git a/src/main/java/org/piwik/java/tracking/PiwikRequest.java b/core/src/main/java/org/piwik/java/tracking/PiwikRequest.java
similarity index 50%
rename from src/main/java/org/piwik/java/tracking/PiwikRequest.java
rename to core/src/main/java/org/piwik/java/tracking/PiwikRequest.java
index bab72b01..e4d64fe4 100644
--- a/src/main/java/org/piwik/java/tracking/PiwikRequest.java
+++ b/core/src/main/java/org/piwik/java/tracking/PiwikRequest.java
@@ -4,14 +4,18 @@
* @link https://github.com/matomo/matomo-java-tracker
* @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
*/
+
package org.piwik.java.tracking;
-import lombok.NonNull;
-import org.matomo.java.tracking.MatomoRequest;
+import static java.util.Objects.requireNonNull;
import java.net.URL;
+import org.matomo.java.tracking.MatomoRequest;
/**
+ * A request object that can be used to send requests to Matomo. This class is deprecated and will be removed in the
+ * future.
+ *
* @author brettcsorba
* @deprecated Use {@link MatomoRequest} instead.
*/
@@ -19,10 +23,14 @@
public class PiwikRequest extends MatomoRequest {
/**
+ * Creates a new request object with the specified site ID and action URL.
+ *
+ * @param siteId the site ID
+ * @param actionUrl the action URL. Must not be null.
* @deprecated Use {@link MatomoRequest} instead.
*/
@Deprecated
- public PiwikRequest(int siteId, @NonNull URL actionUrl) {
- super(siteId, actionUrl.toString());
+ public PiwikRequest(int siteId, URL actionUrl) {
+ super(siteId, requireNonNull(actionUrl, "Action URL must not be null").toString());
}
}
diff --git a/core/src/main/java/org/piwik/java/tracking/PiwikTracker.java b/core/src/main/java/org/piwik/java/tracking/PiwikTracker.java
new file mode 100644
index 00000000..02e18b13
--- /dev/null
+++ b/core/src/main/java/org/piwik/java/tracking/PiwikTracker.java
@@ -0,0 +1,74 @@
+/*
+ * 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.piwik.java.tracking;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import org.matomo.java.tracking.MatomoTracker;
+
+/**
+ * Creates a new PiwikTracker instance. This class is deprecated and will be removed in the future.
+ *
+ * @author brettcsorba
+ * @deprecated Use {@link MatomoTracker} instead.
+ */
+@Deprecated
+public class PiwikTracker extends MatomoTracker {
+
+ /**
+ * Creates a new PiwikTracker instance with the given host URL.
+ *
+ * @param hostUrl the host URL of the Matomo server
+ * @deprecated Use {@link MatomoTracker} instead.
+ */
+ @Deprecated
+ public PiwikTracker(@NonNull String hostUrl) {
+ super(hostUrl);
+ }
+
+ /**
+ * Creates a new PiwikTracker instance with the given host URL and timeout in milliseconds. Use -1 for no timeout.
+ *
+ * @param hostUrl the host URL of the Matomo server
+ * @param timeout the timeout in milliseconds or -1 for no timeout
+ * @deprecated Use {@link MatomoTracker} instead.
+ */
+ @Deprecated
+ public PiwikTracker(@NonNull String hostUrl, int timeout) {
+ super(hostUrl, timeout);
+ }
+
+ /**
+ * Creates a new PiwikTracker instance with the given host URL and proxy settings.
+ *
+ * @param hostUrl the host URL of the Matomo server
+ * @param proxyHost the proxy host
+ * @param proxyPort the proxy port
+ * @deprecated Use {@link MatomoTracker} instead.
+ */
+ @Deprecated
+ public PiwikTracker(@NonNull String hostUrl, @Nullable String proxyHost, int proxyPort) {
+ super(hostUrl, proxyHost, proxyPort);
+ }
+
+ /**
+ * Creates a new PiwikTracker instance with the given host URL, proxy settings and timeout in milliseconds. Use -1 for
+ * no timeout.
+ *
+ * @param hostUrl the host URL of the Matomo server
+ * @param proxyHost the proxy host
+ * @param proxyPort the proxy port
+ * @param timeout the timeout in milliseconds or -1 for no timeout
+ * @deprecated Use {@link MatomoTracker} instead.
+ */
+ @Deprecated
+ public PiwikTracker(@NonNull String hostUrl, @Nullable String proxyHost, int proxyPort, int timeout) {
+ super(hostUrl, proxyHost, proxyPort, timeout);
+ }
+
+}
diff --git a/core/src/main/java/org/piwik/java/tracking/package-info.java b/core/src/main/java/org/piwik/java/tracking/package-info.java
new file mode 100644
index 00000000..eb647ae7
--- /dev/null
+++ b/core/src/main/java/org/piwik/java/tracking/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Piwik Java Tracking API. Renamed to {@link org.matomo.java.tracking} in 3.0.0.
+ */
+
+package org.piwik.java.tracking;
diff --git a/core/src/test/java/org/matomo/java/tracking/AuthTokenTest.java b/core/src/test/java/org/matomo/java/tracking/AuthTokenTest.java
new file mode 100644
index 00000000..169a83e6
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/AuthTokenTest.java
@@ -0,0 +1,73 @@
+package org.matomo.java.tracking;
+
+import static java.util.Collections.singleton;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.URI;
+import org.junit.jupiter.api.Test;
+
+class AuthTokenTest {
+
+ @Test
+ void determineAuthTokenReturnsAuthTokenFromRequest() {
+
+ MatomoRequest request =
+ MatomoRequests
+ .event("Inbox", "Open", null, null)
+ .authToken("bdeca231a312ab12cde124131bedfa23").build();
+
+ String authToken = AuthToken.determineAuthToken(null, singleton(request), null);
+
+ assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23");
+
+ }
+
+ @Test
+ void determineAuthTokenReturnsAuthTokenFromTrackerConfiguration() {
+
+ TrackerConfiguration trackerConfiguration = TrackerConfiguration
+ .builder()
+ .apiEndpoint(URI.create("https://your-matomo-domain.example/matomo."))
+ .defaultAuthToken("bdeca231a312ab12cde124131bedfa23")
+ .build();
+
+ String authToken = AuthToken.determineAuthToken(null, null, trackerConfiguration);
+
+ assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23");
+ }
+
+ @Test
+ void determineAuthTokenFromTrackerConfigurationIfRequestTokenIsEmpty() {
+
+ MatomoRequest request = MatomoRequests.ping().authToken("").build();
+
+ TrackerConfiguration trackerConfiguration = TrackerConfiguration
+ .builder()
+ .apiEndpoint(URI.create("https://your-matomo-domain.example/matomo"))
+ .defaultAuthToken("bdeca231a312ab12cde124131bedfa23")
+ .build();
+
+ String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration);
+
+ assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23");
+
+ }
+
+ @Test
+ void determineAuthTokenFromTrackerConfigurationIfRequestTokenIsBlank() {
+
+ MatomoRequest request = MatomoRequests.pageView("Help").authToken(" ").build();
+
+ TrackerConfiguration trackerConfiguration = TrackerConfiguration
+ .builder()
+ .apiEndpoint(URI.create("https://your-matomo-domain.example/matomo"))
+ .defaultAuthToken("bdeca231a312ab12cde124131bedfa23")
+ .build();
+
+ String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration);
+
+ assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23");
+
+ }
+
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/BulkRequestTest.java b/core/src/test/java/org/matomo/java/tracking/BulkRequestTest.java
new file mode 100644
index 00000000..535986a0
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/BulkRequestTest.java
@@ -0,0 +1,35 @@
+package org.matomo.java.tracking;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singleton;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.jupiter.api.Test;
+
+class BulkRequestTest {
+
+ @Test
+ void formatsQueriesAsJson() {
+ BulkRequest bulkRequest = BulkRequest.builder()
+ .queries(singleton("idsite=1&rec=1&action_name=TestBulkRequest"))
+ .authToken("token")
+ .build();
+
+ byte[] bytes = bulkRequest.toBytes();
+
+ assertThat(new String(bytes)).isEqualTo("{\"requests\":[\"?idsite=1&rec=1&action_name=TestBulkRequest\"],\"token_auth\":\"token\"}");
+ }
+
+ @Test
+ void failsIfQueriesAreEmpty() {
+
+ BulkRequest bulkRequest = BulkRequest.builder().queries(emptyList()).build();
+
+ assertThatThrownBy(bulkRequest::toBytes)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Queries must not be empty");
+
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/matomo/java/tracking/CustomVariableTest.java b/core/src/test/java/org/matomo/java/tracking/CustomVariableTest.java
new file mode 100644
index 00000000..e58a10dd
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/CustomVariableTest.java
@@ -0,0 +1,43 @@
+package org.matomo.java.tracking;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.jupiter.api.Test;
+
+class CustomVariableTest {
+
+ @Test
+ void createsCustomVariable() {
+ CustomVariable customVariable = new CustomVariable("key", "value");
+
+ assertThat(customVariable.getKey()).isEqualTo("key");
+ assertThat(customVariable.getValue()).isEqualTo("value");
+ }
+
+ @Test
+ void failsOnNullKey() {
+ assertThatThrownBy(() -> new CustomVariable(
+ null,
+ "value"
+ )).isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void failsOnNullValue() {
+ assertThatThrownBy(() -> new CustomVariable(
+ "key",
+ null
+ )).isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void failsOnNullKeyAndValue() {
+ assertThatThrownBy(() -> new CustomVariable(
+ null,
+ null
+ )).isInstanceOf(NullPointerException.class);
+ }
+
+
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/DaemonThreadFactoryTest.java b/core/src/test/java/org/matomo/java/tracking/DaemonThreadFactoryTest.java
new file mode 100644
index 00000000..787693db
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/DaemonThreadFactoryTest.java
@@ -0,0 +1,27 @@
+package org.matomo.java.tracking;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+class DaemonThreadFactoryTest {
+
+ private final DaemonThreadFactory daemonThreadFactory = new DaemonThreadFactory();
+
+ @Test
+ void createsNewThreadAsDaemonThread() {
+ Thread thread = daemonThreadFactory.newThread(() -> {
+ // do nothing
+ });
+ assertThat(thread.isDaemon()).isTrue();
+ }
+
+ @Test
+ void createsNewThreadWithMatomoJavaTrackerName() {
+ Thread thread = daemonThreadFactory.newThread(() -> {
+ // do nothing
+ });
+ assertThat(thread.getName()).startsWith("MatomoJavaTracker-");
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java b/core/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java
new file mode 100644
index 00000000..71e9fcc0
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java
@@ -0,0 +1,68 @@
+package org.matomo.java.tracking;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+class EcommerceItemTest {
+
+ private EcommerceItem ecommerceItem = new EcommerceItem(null, null, null, null, null);
+
+ /**
+ * Test of constructor, of class EcommerceItem.
+ */
+ @Test
+ void testConstructor() {
+ EcommerceItem ecommerceItem = new EcommerceItem("sku", "name", "category", 2.0, 2);
+ assertThat(ecommerceItem.getSku()).isEqualTo("sku");
+ assertThat(ecommerceItem.getName()).isEqualTo("name");
+ assertThat(ecommerceItem.getCategory()).isEqualTo("category");
+ assertThat(ecommerceItem.getPrice()).isEqualTo(2.0);
+ assertThat(ecommerceItem.getQuantity()).isEqualTo(2);
+ }
+
+ /**
+ * Test of getSku method, of class EcommerceItem.
+ */
+ @Test
+ void testGetSku() {
+ ecommerceItem.setSku("sku");
+ assertThat(ecommerceItem.getSku()).isEqualTo("sku");
+ }
+
+ /**
+ * Test of getName method, of class EcommerceItem.
+ */
+ @Test
+ void testGetName() {
+ ecommerceItem.setName("name");
+ assertThat(ecommerceItem.getName()).isEqualTo("name");
+ }
+
+ /**
+ * Test of getCategory method, of class EcommerceItem.
+ */
+ @Test
+ void testGetCategory() {
+ ecommerceItem.setCategory("category");
+ assertThat(ecommerceItem.getCategory()).isEqualTo("category");
+ }
+
+ /**
+ * Test of getPrice method, of class EcommerceItem.
+ */
+ @Test
+ void testGetPrice() {
+ ecommerceItem.setPrice(2.0);
+ assertThat(ecommerceItem.getPrice()).isEqualTo(2.0);
+ }
+
+ /**
+ * Test of getQuantity method, of class EcommerceItem.
+ */
+ @Test
+ void testGetQuantity() {
+ ecommerceItem.setQuantity(2);
+ assertThat(ecommerceItem.getQuantity()).isEqualTo(2);
+ }
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/ExecutorServiceCloserTest.java b/core/src/test/java/org/matomo/java/tracking/ExecutorServiceCloserTest.java
new file mode 100644
index 00000000..e0d167a9
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/ExecutorServiceCloserTest.java
@@ -0,0 +1,46 @@
+package org.matomo.java.tracking;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.junit.jupiter.api.Test;
+
+class ExecutorServiceCloserTest {
+
+ @Test
+ void shutsDownExecutorService() {
+
+ ExecutorService executorService = Executors.newFixedThreadPool(2, new DaemonThreadFactory());
+
+ ExecutorServiceCloser.close(executorService);
+
+ assertThat(executorService.isTerminated()).isTrue();
+ assertThat(executorService.isShutdown()).isTrue();
+
+ }
+
+ @Test
+ void shutsDownExecutorServiceImmediately() throws Exception {
+
+ ExecutorService executorService = Executors.newSingleThreadExecutor();
+ executorService.submit(() -> {
+ try {
+ Thread.sleep(10000L);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ Thread thread = new Thread(() -> {
+ ExecutorServiceCloser.close(executorService);
+ });
+ thread.start();
+ Thread.sleep(1000L);
+ thread.interrupt();
+
+ assertThat(executorService.isShutdown()).isTrue();
+
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/matomo/java/tracking/InvalidUrlExceptionTest.java b/core/src/test/java/org/matomo/java/tracking/InvalidUrlExceptionTest.java
new file mode 100644
index 00000000..17605f04
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/InvalidUrlExceptionTest.java
@@ -0,0 +1,18 @@
+package org.matomo.java.tracking;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+class InvalidUrlExceptionTest {
+
+ @Test
+ void createsInvalidUrlException() {
+ InvalidUrlException invalidUrlException = new InvalidUrlException(new Throwable());
+
+ assertThat(invalidUrlException).isNotNull();
+ assertThat(invalidUrlException.getCause()).isNotNull();
+
+ }
+
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/MatomoExceptionTest.java b/core/src/test/java/org/matomo/java/tracking/MatomoExceptionTest.java
new file mode 100644
index 00000000..9c998a37
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/MatomoExceptionTest.java
@@ -0,0 +1,25 @@
+package org.matomo.java.tracking;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class MatomoExceptionTest {
+
+ @Test
+ void createsMatomoExceptionWithMessage() {
+ MatomoException matomoException = new MatomoException("message");
+
+ assertEquals("message", matomoException.getMessage());
+ }
+
+ @Test
+ void createsMatomoExceptionWithMessageAndCause() {
+ Throwable cause = new Throwable();
+ MatomoException matomoException = new MatomoException("message", cause);
+
+ assertEquals("message", matomoException.getMessage());
+ assertEquals(cause, matomoException.getCause());
+ }
+
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/MatomoLocaleTest.java b/core/src/test/java/org/matomo/java/tracking/MatomoLocaleTest.java
new file mode 100644
index 00000000..7822acd3
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/MatomoLocaleTest.java
@@ -0,0 +1,24 @@
+package org.matomo.java.tracking;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Locale;
+import org.junit.jupiter.api.Test;
+
+class MatomoLocaleTest {
+
+ @Test
+ void createsMatomoLocaleFromLocale() {
+ MatomoLocale locale = new MatomoLocale(Locale.US);
+ assertThat(locale.toString()).isEqualTo("us");
+ }
+
+ @Test
+ void failsIfLocaleIsNull() {
+ assertThatThrownBy(() -> new MatomoLocale(null))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("Locale must not be null");
+ }
+
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java b/core/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java
new file mode 100644
index 00000000..87dab6d5
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java
@@ -0,0 +1,88 @@
+package org.matomo.java.tracking;
+
+import static java.util.Collections.singletonMap;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+import org.matomo.java.tracking.parameters.CustomVariables;
+
+
+class MatomoRequestBuilderTest {
+
+ @Test
+ void buildsRequest() {
+ CustomVariable pageCustomVariable =
+ new CustomVariable("pageCustomVariableName", "pageCustomVariableValue");
+ CustomVariable visitCustomVariable =
+ new CustomVariable("visitCustomVariableName", "visitCustomVariableValue");
+
+ MatomoRequest matomoRequest = new MatomoRequestBuilder()
+ .siteId(42)
+ .actionName("ACTION_NAME")
+ .actionUrl("https://www.your-domain.tld/some/page?query=foo")
+ .referrerUrl("https://referrer.com")
+ .additionalParameters(singletonMap(
+ "trackingParameterName",
+ "trackingParameterValue"
+ ))
+ .pageCustomVariables(new CustomVariables().add(pageCustomVariable, 2))
+ .visitCustomVariables(new CustomVariables().add(visitCustomVariable, 3))
+ .customAction(true)
+ .build();
+
+ assertThat(matomoRequest.getSiteId()).isEqualTo(42);
+ assertThat(matomoRequest.getActionName()).isEqualTo("ACTION_NAME");
+ assertThat(matomoRequest.getApiVersion()).isEqualTo("1");
+ assertThat(matomoRequest.getActionUrl()).isEqualTo(
+ "https://www.your-domain.tld/some/page?query=foo");
+ assertThat(matomoRequest.getVisitorId().toString()).hasSize(16).isHexadecimal();
+ assertThat(matomoRequest.getRandomValue().toString()).hasSize(20).isHexadecimal();
+ assertThat(matomoRequest.getResponseAsImage()).isFalse();
+ assertThat(matomoRequest.getRequired()).isTrue();
+ assertThat(matomoRequest.getReferrerUrl()).isEqualTo("https://referrer.com");
+ assertThat(matomoRequest.getCustomTrackingParameter("trackingParameterName")).isEqualTo(
+ "trackingParameterValue");
+ assertThat(matomoRequest.getPageCustomVariables()).hasToString(
+ "{\"2\":[\"pageCustomVariableName\",\"pageCustomVariableValue\"]}");
+ assertThat(matomoRequest.getVisitCustomVariables()).hasToString(
+ "{\"3\":[\"visitCustomVariableName\",\"visitCustomVariableValue\"]}");
+ assertThat(matomoRequest.getCustomAction()).isTrue();
+
+ }
+
+ @Test
+ void setCustomTrackingParameters() {
+ MatomoRequest matomoRequest = new MatomoRequestBuilder()
+ .customTrackingParameters(singletonMap("foo", "bar"))
+ .siteId(42)
+ .actionName("ACTION_NAME")
+ .actionUrl("https://www.your-domain.tld/some/page?query=foo")
+ .referrerUrl("https://referrer.com")
+ .build();
+
+ assertThat(matomoRequest.getCustomTrackingParameter("foo")).isEqualTo("bar");
+ }
+
+ @Test
+ void setCustomTrackingParametersWithCollectopm() {
+ MatomoRequest matomoRequest = new MatomoRequestBuilder()
+ .customTrackingParameters(singletonMap("foo", "bar"))
+ .siteId(42)
+ .actionName("ACTION_NAME")
+ .actionUrl("https://www.your-domain.tld/some/page?query=foo")
+ .referrerUrl("https://referrer.com")
+ .build();
+
+ assertThat(matomoRequest.getCustomTrackingParameter("foo")).isEqualTo("bar");
+ }
+
+ @Test
+ void acceptsNullAsHeaderAcceptLanguage() {
+ MatomoRequest matomoRequest = new MatomoRequestBuilder()
+ .headerAcceptLanguage((String) null)
+ .build();
+
+ assertThat(matomoRequest.getHeaderAcceptLanguage()).isNull();
+ }
+
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/MatomoRequestTest.java b/core/src/test/java/org/matomo/java/tracking/MatomoRequestTest.java
new file mode 100644
index 00000000..b4ad0e04
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/MatomoRequestTest.java
@@ -0,0 +1,134 @@
+package org.matomo.java.tracking;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+
+import org.junit.jupiter.api.Test;
+
+class MatomoRequestTest {
+
+ private MatomoRequest request = new MatomoRequest();
+
+ @Test
+ void returnsEmptyListWhenCustomTrackingParametersDoesNotContainKey() {
+
+ request.setCustomTrackingParameter("foo", "bar");
+
+ assertThat(request.getCustomTrackingParameter("baz")).isNull();
+ assertThat(request.getAdditionalParameters()).isNotEmpty();
+ assertThat(request.getCustomTrackingParameter("foo")).isEqualTo("bar");
+ }
+
+ @Test
+ void getPageCustomVariableReturnsNullIfPageCustomVariablesIsNull() {
+ assertThat(request.getPageCustomVariable("foo")).isNull();
+ }
+
+ @Test
+ void getPageCustomVariableReturnsValueIfPageCustomVariablesIsNotNull() {
+ request.setPageCustomVariable("foo", "bar");
+ assertThat(request.getPageCustomVariable("foo")).isEqualTo("bar");
+ }
+
+ @Test
+ void setPageCustomVariableRequiresNonNullKey() {
+ assertThatThrownBy(() -> request.setPageCustomVariable(null, "bar")).isInstanceOf(
+ NullPointerException.class);
+ }
+
+ @Test
+ void setPageCustomVariableDoesNothingIfValueIsNull() {
+ request.setPageCustomVariable("foo", null);
+ assertThat(request.getPageCustomVariable("foo")).isNull();
+ }
+
+ @Test
+ void setPageCustomVariableRemovesValueIfValueIsNull() {
+ request.setPageCustomVariable("foo", "bar");
+ request.setPageCustomVariable("foo", null);
+ assertThat(request.getPageCustomVariable("foo")).isNull();
+ }
+
+ @Test
+ void setPageCustomVariableAddsCustomVariableIfValueIsNotNull() {
+ request.setPageCustomVariable("foo", "bar");
+ assertThat(request.getPageCustomVariable("foo")).isEqualTo("bar");
+ }
+
+ @Test
+ void setPageCustomVariableDoesNothingIfCustomVariableParameterIsNullAndIndexIsPositive() {
+ request.setPageCustomVariable(null, 1);
+ assertThat(request.getPageCustomVariable(1)).isNull();
+ }
+
+ @Test
+ void setPageCustomVariableInitializesPageCustomVariablesIfCustomVariableParameterIsNullAndIndexIsPositive() {
+ request.setPageCustomVariable(new CustomVariable("key", "value"), 1);
+ assertThat(request.getPageCustomVariables()).isNotNull();
+ }
+
+ @Test
+ void setUserCustomVariableDoesNothingIfValueIsNull() {
+ request.setUserCustomVariable("foo", null);
+ assertThat(request.getUserCustomVariable("foo")).isNull();
+ }
+
+ @Test
+ void setUserCustomVariableRemovesValueIfValueIsNull() {
+ request.setUserCustomVariable("foo", "bar");
+ request.setUserCustomVariable("foo", null);
+ assertThat(request.getUserCustomVariable("foo")).isNull();
+ }
+
+ @Test
+ void setVisitCustomVariableDoesNothingIfCustomVariableParameterIsNullAndIndexIsPositive() {
+ request.setVisitCustomVariable(null, 1);
+ assertThat(request.getVisitCustomVariable(1)).isNull();
+ }
+
+ @Test
+ void setVisitCustomVariableInitializesVisitCustomVariablesIfCustomVariableParameterIsNullAndIndexIsPositive() {
+ request.setVisitCustomVariable(new CustomVariable("key", "value"), 1);
+ assertThat(request.getVisitCustomVariables()).isNotNull();
+ }
+
+ @Test
+ void setsCustomParameter() {
+ request.setParameter("foo", 1);
+ assertThat(request.getCustomTrackingParameter("foo")).isEqualTo(1);
+ }
+
+ @Test
+ void failsToSetCustomParameterIfKeyIsNull() {
+ assertThatThrownBy(() -> request.setParameter(
+ null,
+ 1
+ )).isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void doesNothingWhenSettingCustomParameterIfValueIsNull() {
+ request.setParameter("foo", null);
+ assertThat(request.getAdditionalParameters()).isNull();
+ }
+
+ @Test
+ void removesCustomParameter() {
+ request.setParameter("foo", 1);
+ request.setParameter("foo", null);
+ assertThat(request.getAdditionalParameters()).isEmpty();
+ }
+
+ @Test
+ void setsDeviceResolutionString() {
+ request.setDeviceResolution("1920x1080");
+ assertThat(request.getDeviceResolution().toString()).isEqualTo("1920x1080");
+ }
+
+ @Test
+ void failsIfSetParameterParameterNameIsBlank() {
+ assertThatThrownBy(() -> request.setParameter(" ", "bar")).isInstanceOf(
+ IllegalArgumentException.class);
+ }
+
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/MatomoRequestsTest.java b/core/src/test/java/org/matomo/java/tracking/MatomoRequestsTest.java
new file mode 100644
index 00000000..3aa058d2
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/MatomoRequestsTest.java
@@ -0,0 +1,318 @@
+package org.matomo.java.tracking;
+
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNullPointerException;
+
+import org.junit.jupiter.api.Test;
+
+class MatomoRequestsTest {
+
+ @Test
+ void actionRequestBuilderContainsDownloadUrl() {
+ MatomoRequest.MatomoRequestBuilder builder =
+ MatomoRequests.action("https://example.com", ActionType.DOWNLOAD);
+ MatomoRequest request = builder.build();
+ assertThat(request.getDownloadUrl())
+ .isEqualTo("https://example.com");
+ }
+
+ @Test
+ void actionRequestBuilderContainsOutlinkUrl() {
+ MatomoRequest.MatomoRequestBuilder builder =
+ MatomoRequests.action("https://example.com", ActionType.LINK);
+ MatomoRequest request = builder.build();
+ assertThat(request.getOutlinkUrl())
+ .isEqualTo("https://example.com");
+ }
+
+ @Test
+ void contentImpressionRequestBuilderContainsContentInformation() {
+ MatomoRequest.MatomoRequestBuilder builder =
+ MatomoRequests.contentImpression("Product", "Smartphone", "https://example.com/product");
+ MatomoRequest request = builder.build();
+ assertThat(request)
+ .isNotNull()
+ .extracting(
+ MatomoRequest::getContentName,
+ MatomoRequest::getContentPiece,
+ MatomoRequest::getContentTarget
+ )
+ .containsExactly("Product", "Smartphone", "https://example.com/product");
+ }
+
+ @Test
+ void contentInteractionRequestBuilderContainsInteractionAndContentInformation() {
+ MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.contentInteraction(
+ "click",
+ "Product",
+ "Smartphone",
+ "https://example.com/product"
+ );
+ MatomoRequest request = builder.build();
+ assertThat(request)
+ .isNotNull()
+ .extracting(
+ MatomoRequest::getContentInteraction,
+ MatomoRequest::getContentName,
+ MatomoRequest::getContentPiece,
+ MatomoRequest::getContentTarget
+ )
+ .containsExactly("click", "Product", "Smartphone", "https://example.com/product");
+ }
+
+ @Test
+ void crashRequestBuilderContainsCrashInformation() {
+ MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.crash(
+ "Error",
+ "NullPointerException",
+ "payment failure",
+ "stackTrace",
+ "MainActivity.java",
+ 42,
+ 23
+ );
+ MatomoRequest request = builder.build();
+ assertThat(request)
+ .isNotNull()
+ .extracting(
+ MatomoRequest::getCrashMessage,
+ MatomoRequest::getCrashType,
+ MatomoRequest::getCrashCategory,
+ MatomoRequest::getCrashStackTrace,
+ MatomoRequest::getCrashLocation,
+ MatomoRequest::getCrashLine,
+ MatomoRequest::getCrashColumn
+ )
+ .containsExactly(
+ "Error",
+ "NullPointerException",
+ "payment failure",
+ "stackTrace",
+ "MainActivity.java",
+ 42,
+ 23
+ );
+ }
+
+ @Test
+ void crashWithThrowableRequestBuilderContainsCrashInformationFromThrowable() {
+ Throwable throwable = new NullPointerException("Test NullPointerException");
+ MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.crash(throwable, "payment failure");
+ MatomoRequest request = builder.build();
+ assertThat(request)
+ .isNotNull()
+ .extracting(MatomoRequest::getCrashMessage) // Additional assertions for other properties
+ .isEqualTo("Test NullPointerException");
+ }
+
+ @Test
+ void ecommerceCartUpdateRequestBuilderContainsEcommerceRevenue() {
+ MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.ecommerceCartUpdate(100.0);
+ MatomoRequest request = builder.build();
+ assertThat(request)
+ .isNotNull()
+ .extracting(MatomoRequest::getEcommerceRevenue)
+ .isEqualTo(100.0);
+ }
+
+ @Test
+ void ecommerceOrderRequestBuilderContainsEcommerceOrderInformation() {
+ MatomoRequest.MatomoRequestBuilder builder =
+ MatomoRequests.ecommerceOrder("123", 200.0, 180.0, 10.0, 5.0, 5.0);
+ MatomoRequest request = builder.build();
+ assertThat(request)
+ .isNotNull()
+ .extracting(
+ MatomoRequest::getEcommerceId,
+ MatomoRequest::getEcommerceRevenue,
+ MatomoRequest::getEcommerceSubtotal,
+ MatomoRequest::getEcommerceTax,
+ MatomoRequest::getEcommerceShippingCost,
+ MatomoRequest::getEcommerceDiscount
+ )
+ .containsExactly("123", 200.0, 180.0, 10.0, 5.0, 5.0);
+ }
+
+ @Test
+ void eventRequestBuilderContainsEventInformation() {
+ MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.event(
+ "Music",
+ "Play",
+ "Edvard Grieg - The Death of Ase",
+ 9.99
+ );
+ MatomoRequest request = builder.build();
+ assertThat(request)
+ .isNotNull()
+ .extracting(
+ MatomoRequest::getEventCategory,
+ MatomoRequest::getEventAction,
+ MatomoRequest::getEventName,
+ MatomoRequest::getEventValue
+ )
+ .containsExactly("Music", "Play", "Edvard Grieg - The Death of Ase", 9.99);
+ }
+
+ @Test
+ void goalRequestBuilderContainsGoalInformation() {
+ MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.goal(
+ 1,
+ 9.99
+ );
+ MatomoRequest request = builder.build();
+ assertThat(request)
+ .isNotNull()
+ .extracting(
+ MatomoRequest::getGoalId,
+ MatomoRequest::getEcommerceRevenue
+ )
+ .containsExactly(1, 9.99);
+ }
+
+ @Test
+ void pageViewRequestBuilderContainsPageViewInformation() {
+ MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.pageView("About");
+ MatomoRequest request = builder.build();
+ assertThat(request.getActionName())
+ .isEqualTo("About");
+ }
+
+ @Test
+ void searchRequestBuilderContainsSearchInformation() {
+ MatomoRequest.MatomoRequestBuilder builder =
+ MatomoRequests.siteSearch("Matomo", "Download", 42L);
+ MatomoRequest request = builder.build();
+ assertThat(request)
+ .isNotNull()
+ .extracting(
+ MatomoRequest::getSearchQuery,
+ MatomoRequest::getSearchCategory,
+ MatomoRequest::getSearchResultsCount
+ )
+ .containsExactly("Matomo", "Download", 42L);
+ }
+
+ @Test
+ void pingRequestBuilderContainsPingInformation() {
+ MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.ping();
+ MatomoRequest request = builder.build();
+ assertThat(request.getPing()).isTrue();
+ }
+
+ @Test
+ void nullParametersThrowNullPointerExceptionForInvalidInput() {
+ assertThatNullPointerException()
+ .isThrownBy(() -> MatomoRequests.action(null, ActionType.DOWNLOAD))
+ .withMessage("url is marked non-null but is null");
+ assertThatNullPointerException()
+ .isThrownBy(() -> MatomoRequests.contentImpression(null, null, null))
+ .withMessage("name is marked non-null but is null");
+ // Add similar checks for other methods
+ }
+
+ @Test
+ void actionNullUrlThrowsNullPointerException() {
+ assertThatNullPointerException()
+ .isThrownBy(() -> MatomoRequests.action(null, ActionType.DOWNLOAD))
+ .withMessage("url is marked non-null but is null");
+ }
+
+ @Test
+ void actionNullTypeThrowsNullPointerException() {
+ assertThatNullPointerException()
+ .isThrownBy(() -> MatomoRequests.action("https://example.com", null))
+ .withMessage("type is marked non-null but is null");
+ }
+
+ @Test
+ void contentImpressionNullNameThrowsNullPointerException() {
+ assertThatNullPointerException()
+ .isThrownBy(() -> MatomoRequests.contentImpression(
+ null,
+ "Smartphone",
+ "https://example.com/product"
+ ))
+ .withMessage("name is marked non-null but is null");
+ }
+
+ // Add similar null checks for other methods...
+
+ @Test
+ void crashNullMessageThrowsNullPointerException() {
+ assertThatNullPointerException()
+ .isThrownBy(() -> MatomoRequests.crash(
+ null,
+ "NullPointerException",
+ "payment failure",
+ "stackTrace",
+ "MainActivity.java",
+ 42,
+ 23
+ ))
+ .withMessage("message is marked non-null but is null");
+ }
+
+ @Test
+ void crashWithThrowableNullThrowableThrowsNullPointerException() {
+ assertThatNullPointerException()
+ .isThrownBy(() -> MatomoRequests.crash(null, "payment failure"))
+ .withMessage("throwable is marked non-null but is null");
+ }
+
+ @Test
+ void ecommerceCartUpdateNullRevenueThrowsNullPointerException() {
+ assertThatNullPointerException()
+ .isThrownBy(() -> MatomoRequests.ecommerceCartUpdate(null))
+ .withMessage("revenue is marked non-null but is null");
+ }
+
+ @Test
+ void ecommerceOrderNullIdThrowsNullPointerException() {
+ assertThatNullPointerException()
+ .isThrownBy(() -> MatomoRequests.ecommerceOrder(null, 200.0, 180.0, 10.0, 5.0, 5.0))
+ .withMessage("id is marked non-null but is null");
+ }
+
+ @Test
+ void eventNullCategoryThrowsNullPointerException() {
+ assertThatNullPointerException()
+ .isThrownBy(() -> MatomoRequests.event(
+ null,
+ "Play",
+ "Edvard Grieg - The Death of Ase",
+ 9.99
+ ))
+ .withMessage("category is marked non-null but is null");
+ }
+
+ @Test
+ void pageViewNullNameThrowsNullPointerException() {
+ assertThatNullPointerException()
+ .isThrownBy(() -> MatomoRequests.pageView(null))
+ .withMessage("name is marked non-null but is null");
+ }
+
+ @Test
+ void siteSearchNullQueryThrowsNullPointerException() {
+ assertThatNullPointerException()
+ .isThrownBy(() -> MatomoRequests.siteSearch(null, "Music", 42L))
+ .withMessage("query is marked non-null but is null");
+ }
+
+ @Test
+ void crashDoesNotIncludeStackTraceIfStackTraceOfThrowableIsEmpty() {
+ MatomoRequest.MatomoRequestBuilder builder =
+ MatomoRequests.crash(new TestThrowable(), "payment failure");
+ MatomoRequest request = builder.build();
+ assertThat(request.getCrashMessage()).isEqualTo("message");
+ assertThat(request.getCrashType()).isEqualTo("org.matomo.java.tracking.TestThrowable");
+ assertThat(request.getCrashCategory()).isEqualTo("payment failure");
+ assertThat(request.getCrashStackTrace()).isEqualTo(
+ "org.matomo.java.tracking.TestThrowable: message");
+ assertThat(request.getCrashLocation()).isNull();
+ assertThat(request.getCrashLine()).isNull();
+ assertThat(request.getCrashColumn()).isNull();
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java b/core/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java
new file mode 100644
index 00000000..c312c717
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java
@@ -0,0 +1,234 @@
+package org.matomo.java.tracking;
+
+import static java.util.Collections.singleton;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.net.URI;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.jupiter.api.Test;
+import org.matomo.java.tracking.parameters.RandomValue;
+import org.matomo.java.tracking.parameters.VisitorId;
+
+class MatomoTrackerIT {
+
+ private static final String HOST_URL = "http://localhost:8080/matomo.php";
+ public static final String QUERY =
+ "rec=1&idsite=1&action_name=test&apiv=1&_id=00000000343efaf5&send_image=0&rand=test-random";
+ private MatomoTracker matomoTracker;
+ private final TestSenderFactory senderFactory = new TestSenderFactory();
+ private final MatomoRequest request = MatomoRequest
+ .request()
+ .siteId(1)
+ .visitorId(VisitorId.fromString("test-visitor-id"))
+ .randomValue(RandomValue.fromString("test-random"))
+ .actionName("test")
+ .build();
+
+ @Test
+ void sendsRequest() {
+
+ matomoTracker = new MatomoTracker(HOST_URL);
+ matomoTracker.setSenderFactory(senderFactory);
+
+ matomoTracker.sendRequest(request);
+
+ TestSender testSender = senderFactory.getTestSender();
+ thenContainsRequest(testSender, QUERY);
+ assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL);
+
+ }
+
+ @Test
+ void validatesRequest() {
+
+ matomoTracker = new MatomoTracker(HOST_URL);
+ matomoTracker.setSenderFactory(senderFactory);
+ request.setSiteId(null);
+
+ assertThatThrownBy(() -> matomoTracker.sendRequest(request))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("No default site ID and no request site ID is given");
+
+ assertThat(senderFactory.getTestSender()).isNull();
+
+ }
+
+ @Test
+ void doesNotSendRequestIfNotEnabled() {
+
+ matomoTracker =
+ new MatomoTracker(TrackerConfiguration.builder().apiEndpoint(URI.create(HOST_URL)).enabled(false).build());
+ matomoTracker.setSenderFactory(senderFactory);
+
+ matomoTracker.sendRequest(request);
+
+ assertThat(senderFactory.getTestSender()).isNull();
+
+ }
+
+ @Test
+ void sendsRequestUsingProxy() {
+
+ matomoTracker = new MatomoTracker(HOST_URL, "localhost", 8081);
+ matomoTracker.setSenderFactory(senderFactory);
+
+ matomoTracker.sendRequest(request);
+
+ TestSender testSender = senderFactory.getTestSender();
+ thenContainsRequest(testSender, QUERY);
+ TrackerConfiguration trackerConfiguration = testSender.getTrackerConfiguration();
+ assertThat(trackerConfiguration.getProxyHost()).isEqualTo("localhost");
+ assertThat(trackerConfiguration.getProxyPort()).isEqualTo(8081);
+
+ }
+
+ @Test
+ void sendsRequestAsync() {
+
+ matomoTracker = new MatomoTracker(HOST_URL, 1000);
+ matomoTracker.setSenderFactory(senderFactory);
+
+ matomoTracker.sendRequestAsync(request);
+
+ TestSender testSender = senderFactory.getTestSender();
+ thenContainsRequest(testSender, QUERY);
+ assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL);
+
+ }
+
+ @Test
+ void sendsRequestAsyncWithCallback() {
+
+ matomoTracker = new MatomoTracker(HOST_URL, 1000);
+ matomoTracker.setSenderFactory(senderFactory);
+ AtomicBoolean callbackCalled = new AtomicBoolean();
+ matomoTracker.sendRequestAsync(request, request -> {
+ assertThat(request).isEqualTo(request);
+ callbackCalled.set(true);
+ return null;
+ });
+
+ TestSender testSender = senderFactory.getTestSender();
+ thenContainsRequest(testSender, QUERY);
+ assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL);
+ assertThat(callbackCalled).isTrue();
+
+ }
+
+ @Test
+ void doesNotSendRequestAsyncIfNotEnabled() {
+
+ matomoTracker =
+ new MatomoTracker(TrackerConfiguration.builder().apiEndpoint(URI.create(HOST_URL)).enabled(false).build());
+ matomoTracker.setSenderFactory(senderFactory);
+
+ matomoTracker.sendRequestAsync(request);
+
+ assertThat(senderFactory.getTestSender()).isNull();
+
+ }
+
+ @Test
+ void sendsBulkRequests() {
+
+ matomoTracker = new MatomoTracker(HOST_URL);
+ matomoTracker.setSenderFactory(senderFactory);
+
+ matomoTracker.sendBulkRequest(request);
+
+ TestSender testSender = senderFactory.getTestSender();
+ thenContainsRequest(testSender, QUERY);
+ assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL);
+
+ }
+
+ @Test
+ void doesNotSendBulkRequestsIfNotEnabled() {
+
+ matomoTracker =
+ new MatomoTracker(TrackerConfiguration.builder().apiEndpoint(URI.create(HOST_URL)).enabled(false).build());
+ matomoTracker.setSenderFactory(senderFactory);
+
+ matomoTracker.sendBulkRequest(request);
+
+ assertThat(senderFactory.getTestSender()).isNull();
+
+ }
+
+ @Test
+ void sendsBulkRequestsAsync() {
+
+ matomoTracker = new MatomoTracker(HOST_URL, 1000);
+ matomoTracker.setSenderFactory(senderFactory);
+
+ matomoTracker.sendBulkRequestAsync(request);
+
+ TestSender testSender = senderFactory.getTestSender();
+ thenContainsRequest(testSender, QUERY);
+ assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL);
+
+ }
+
+ @Test
+ void doesNotSendBulkRequestsAsyncIfNotEnabled() {
+
+ matomoTracker =
+ new MatomoTracker(TrackerConfiguration.builder().apiEndpoint(URI.create(HOST_URL)).enabled(false).build());
+ matomoTracker.setSenderFactory(senderFactory);
+
+ matomoTracker.sendBulkRequestAsync(request);
+
+ assertThat(senderFactory.getTestSender()).isNull();
+
+ }
+
+ @Test
+ void sendsBulkRequestAsyncWithCallback() {
+
+ matomoTracker = new MatomoTracker(HOST_URL, 1000);
+ matomoTracker.setSenderFactory(senderFactory);
+ AtomicBoolean callbackCalled = new AtomicBoolean();
+ matomoTracker.sendBulkRequestAsync(singleton(request), v -> callbackCalled.set(true));
+
+ TestSender testSender = senderFactory.getTestSender();
+ thenContainsRequest(testSender, QUERY);
+ assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL);
+ assertThat(callbackCalled).isTrue();
+
+ }
+
+ @Test
+ void sendsBulkRequestAsyncWithAuthToken() {
+
+ matomoTracker = new MatomoTracker(HOST_URL, 1000);
+ matomoTracker.setSenderFactory(senderFactory);
+ matomoTracker.sendBulkRequestAsync(singleton(request), "abc123def456abc123def456abc123de");
+
+ TestSender testSender = senderFactory.getTestSender();
+ thenContainsRequest(testSender, "token_auth=abc123def456abc123def456abc123de&rec=1&idsite=1&action_name=test&apiv=1&_id=00000000343efaf5&send_image=0&rand=test-random");
+ assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL);
+
+ }
+
+ @Test
+ void appliesGoalId() {
+
+ matomoTracker = new MatomoTracker(HOST_URL);
+ matomoTracker.setSenderFactory(senderFactory);
+ request.setEcommerceId("some-id");
+
+ matomoTracker.sendRequest(request);
+
+ TestSender testSender = senderFactory.getTestSender();
+ thenContainsRequest(testSender, "rec=1&idsite=1&action_name=test&apiv=1&_id=00000000343efaf5&idgoal=0&ec_id=some-id&send_image=0&rand=test-random");
+ assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL);
+
+ }
+
+ private void thenContainsRequest(TestSender testSender, String query) {
+ assertThat(testSender.getRequests()).containsExactly(request);
+ assertThat(testSender.getQueries()).containsExactly(query);
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/matomo/java/tracking/PiwikDateTest.java b/core/src/test/java/org/matomo/java/tracking/PiwikDateTest.java
new file mode 100644
index 00000000..40c8bd80
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/PiwikDateTest.java
@@ -0,0 +1,43 @@
+package org.matomo.java.tracking;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.TimeZone;
+import org.junit.jupiter.api.Test;
+import org.piwik.java.tracking.PiwikDate;
+
+
+class PiwikDateTest {
+
+ /**
+ * Test of constructor, of class PiwikDate.
+ */
+ @Test
+ void testConstructor0() {
+ PiwikDate date = new PiwikDate();
+ assertThat(date).isNotNull();
+ }
+
+ @Test
+ void testConstructor1() {
+ PiwikDate date = new PiwikDate(1433186085092L);
+ assertThat(date).isNotNull();
+ assertThat(date.getTime()).isEqualTo(1433186085092L);
+ }
+
+ @Test
+ void testConstructor2() {
+ PiwikDate date = new PiwikDate(1467437553000L);
+ assertThat(date.getTime()).isEqualTo(1467437553000L);
+ }
+
+ /**
+ * Test of setTimeZone method, of class PiwikDate.
+ */
+ @Test
+ void testSetTimeZone() {
+ PiwikDate date = new PiwikDate(1433186085092L);
+ date.setTimeZone(TimeZone.getTimeZone("America/New_York"));
+ assertThat(date.getTime()).isEqualTo(1433186085092L);
+ }
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java b/core/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java
new file mode 100644
index 00000000..46e1c563
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java
@@ -0,0 +1,29 @@
+package org.matomo.java.tracking;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Locale;
+import org.junit.jupiter.api.Test;
+import org.piwik.java.tracking.PiwikLocale;
+
+class PiwikLocaleTest {
+
+ private final PiwikLocale locale = new PiwikLocale(Locale.US);
+
+ /**
+ * Test of setLocale method, of class PiwikLocale.
+ */
+ @Test
+ void testLocale() {
+ locale.setLocale(Locale.GERMANY);
+ assertThat(locale.getLocale()).isEqualTo(Locale.GERMAN);
+ }
+
+ /**
+ * Test of toString method, of class PiwikLocale.
+ */
+ @Test
+ void testToString() {
+ assertThat(locale).hasToString("us");
+ }
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/PiwikRequestTest.java b/core/src/test/java/org/matomo/java/tracking/PiwikRequestTest.java
new file mode 100644
index 00000000..c88c7570
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/PiwikRequestTest.java
@@ -0,0 +1,940 @@
+package org.matomo.java.tracking;
+
+import static java.time.temporal.ChronoUnit.MINUTES;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.fail;
+import static org.assertj.core.api.Assertions.within;
+
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.util.Locale;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.matomo.java.tracking.parameters.AcceptLanguage;
+import org.matomo.java.tracking.parameters.DeviceResolution;
+import org.matomo.java.tracking.parameters.RandomValue;
+import org.matomo.java.tracking.parameters.VisitorId;
+import org.piwik.java.tracking.PiwikDate;
+import org.piwik.java.tracking.PiwikLocale;
+import org.piwik.java.tracking.PiwikRequest;
+
+class PiwikRequestTest {
+
+ private PiwikRequest request;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ request = new PiwikRequest(3, new URL("https://test.com"));
+ }
+
+ @Test
+ void testConstructor() throws Exception {
+ request = new PiwikRequest(3, new URL("https://test.com"));
+ assertThat(request.getSiteId()).isEqualTo(Integer.valueOf(3));
+ assertThat(request.getRequired()).isTrue();
+ assertThat(request.getActionUrl()).isEqualTo("https://test.com");
+ assertThat(request.getVisitorId()).isNotNull();
+ assertThat(request.getRandomValue()).isNotNull();
+ assertThat(request.getApiVersion()).isEqualTo("1");
+ assertThat(request.getResponseAsImage()).isFalse();
+ }
+
+ /**
+ * Test of getActionName method, of class PiwikRequest.
+ */
+ @Test
+ void testActionName() {
+ request.setActionName("action");
+ assertThat(request.getActionName()).isEqualTo("action");
+ request.setActionName(null);
+ assertThat(request.getActionName()).isNull();
+ }
+
+ /**
+ * Test of getActionUrl method, of class PiwikRequest.
+ */
+ @Test
+ void testActionUrl() {
+ request.setActionUrl(null);
+ assertThat(request.getActionUrl()).isNull();
+ request.setActionUrl("https://action.com");
+ assertThat(request.getActionUrl()).isEqualTo("https://action.com");
+ }
+
+ /**
+ * Test of getApiVersion method, of class PiwikRequest.
+ */
+ @Test
+ void testApiVersion() {
+ request.setApiVersion("2");
+ assertThat(request.getApiVersion()).isEqualTo("2");
+ }
+
+ @Test
+ void testAuthTokenTF() {
+ request.setAuthToken("12345678901234567890123456789012");
+ assertThat(request.getAuthToken()).isEqualTo("12345678901234567890123456789012");
+ }
+
+ @Test
+ void testAuthTokenF() {
+ request.setAuthToken("12345678901234567890123456789012");
+ request.setAuthToken(null);
+ assertThat(request.getAuthToken()).isNull();
+ }
+
+ /**
+ * Test of getCampaignKeyword method, of class PiwikRequest.
+ */
+ @Test
+ void testCampaignKeyword() {
+ request.setCampaignKeyword("keyword");
+ assertThat(request.getCampaignKeyword()).isEqualTo("keyword");
+ }
+
+ /**
+ * Test of getCampaignName method, of class PiwikRequest.
+ */
+ @Test
+ void testCampaignName() {
+ request.setCampaignName("name");
+ assertThat(request.getCampaignName()).isEqualTo("name");
+ }
+
+ /**
+ * Test of getCharacterSet method, of class PiwikRequest.
+ */
+ @Test
+ void testCharacterSet() {
+ Charset charset = Charset.defaultCharset();
+ request.setCharacterSet(charset);
+ assertThat(request.getCharacterSet()).isEqualTo(charset);
+ }
+
+ /**
+ * Test of getContentInteraction method, of class PiwikRequest.
+ */
+ @Test
+ void testContentInteraction() {
+ request.setContentInteraction("interaction");
+ assertThat(request.getContentInteraction()).isEqualTo("interaction");
+ }
+
+ /**
+ * Test of getContentName method, of class PiwikRequest.
+ */
+ @Test
+ void testContentName() {
+ request.setContentName("name");
+ assertThat(request.getContentName()).isEqualTo("name");
+ }
+
+ /**
+ * Test of getContentPiece method, of class PiwikRequest.
+ */
+ @Test
+ void testContentPiece() {
+ request.setContentPiece("piece");
+ assertThat(request.getContentPiece()).isEqualTo("piece");
+ }
+
+ /**
+ * Test of getContentTarget method, of class PiwikRequest.
+ */
+ @Test
+ void testContentTarget() {
+ request.setContentTarget("https://target.com");
+ assertThat(request.getContentTarget()).isEqualTo("https://target.com");
+ }
+
+ /**
+ * Test of getCurrentHour method, of class PiwikRequest.
+ */
+ @Test
+ void testCurrentHour() {
+ request.setCurrentHour(1);
+ assertThat(request.getCurrentHour()).isEqualTo(Integer.valueOf(1));
+ }
+
+ /**
+ * Test of getCurrentMinute method, of class PiwikRequest.
+ */
+ @Test
+ void testCurrentMinute() {
+ request.setCurrentMinute(2);
+ assertThat(request.getCurrentMinute()).isEqualTo(Integer.valueOf(2));
+ }
+
+ /**
+ * Test of getCurrentSecond method, of class PiwikRequest.
+ */
+ @Test
+ void testCurrentSecond() {
+ request.setCurrentSecond(3);
+ assertThat(request.getCurrentSecond()).isEqualTo(Integer.valueOf(3));
+ }
+
+ /**
+ * Test of getCustomTrackingParameter method, of class PiwikRequest.
+ */
+ @Test
+ void testGetCustomTrackingParameter_T() {
+ try {
+ request.getCustomTrackingParameter(null);
+ fail("Exception should have been thrown.");
+ } catch (NullPointerException e) {
+ assertThat(e.getLocalizedMessage()).isEqualTo("key is marked non-null but is null");
+ }
+ }
+
+ @Test
+ void testGetCustomTrackingParameter_FT() {
+ assertThat(request.getCustomTrackingParameter("key")).isNull();
+ }
+
+ @Test
+ void testSetCustomTrackingParameter_T() {
+ try {
+ request.setCustomTrackingParameter(null, null);
+ fail("Exception should have been thrown.");
+ } catch (NullPointerException e) {
+ assertThat(e.getLocalizedMessage()).isEqualTo("key is marked non-null but is null");
+ }
+ }
+
+ @Test
+ void testSetCustomTrackingParameter1() {
+ request.setCustomTrackingParameter("key", "value");
+ Object l = request.getCustomTrackingParameter("key");
+ assertThat(l).isEqualTo("value");
+ request.setCustomTrackingParameter("key", "value2");
+ }
+
+ @Test
+ void testSetCustomTrackingParameter2() {
+ request.setCustomTrackingParameter("key", "value2");
+ Object l = request.getCustomTrackingParameter("key");
+ assertThat(l).isEqualTo("value2");
+ request.setCustomTrackingParameter("key", null);
+ l = request.getCustomTrackingParameter("key");
+ assertThat(l).isNull();
+ }
+
+ @Test
+ void testSetCustomTrackingParameter3() {
+ request.setCustomTrackingParameter("key", null);
+ Object l = request.getCustomTrackingParameter("key");
+ assertThat(l).isNull();
+ }
+
+ @Test
+ void testAddCustomTrackingParameter_T() {
+ try {
+ request.addCustomTrackingParameter(null, null);
+ fail("Exception should have been thrown.");
+ } catch (NullPointerException e) {
+ assertThat(e.getLocalizedMessage()).isEqualTo("key is marked non-null but is null");
+ }
+ }
+
+ @Test
+ void testAddCustomTrackingParameter1() {
+ request.addCustomTrackingParameter("key", "value");
+ Object l = request.getCustomTrackingParameter("key");
+ assertThat(l).isEqualTo("value");
+ }
+
+ @Test
+ void testAddCustomTrackingParameter2() {
+ request.addCustomTrackingParameter("key", "value");
+ request.addCustomTrackingParameter("key", "value2");
+ Object l = request.getCustomTrackingParameter("key");
+ assertThat(l).isEqualTo("value2");
+ }
+
+ @Test
+ void testClearCustomTrackingParameter() {
+ request.setCustomTrackingParameter("key", "value");
+ request.clearCustomTrackingParameter();
+ Object l = request.getCustomTrackingParameter("key");
+ assertThat(l).isNull();
+ }
+
+ /**
+ * Test of getDeviceResolution method, of class PiwikRequest.
+ */
+ @Test
+ void testDeviceResolution() {
+ request.setDeviceResolution(DeviceResolution.fromString("100x200"));
+ assertThat(request.getDeviceResolution()).hasToString("100x200");
+ }
+
+ /**
+ * Test of getDownloadUrl method, of class PiwikRequest.
+ */
+ @Test
+ void testDownloadUrl() {
+
+ request.setDownloadUrl("https://download.com");
+ assertThat(request.getDownloadUrl()).isEqualTo("https://download.com");
+ }
+
+ /**
+ * Test of enableEcommerce method, of class PiwikRequest.
+ */
+ @Test
+ void testEnableEcommerce() {
+ request.enableEcommerce();
+ assertThat(request.getGoalId()).isEqualTo(Integer.valueOf(0));
+ }
+
+ /**
+ * Test of getEcommerceDiscount method, of class PiwikRequest.
+ */
+ @Test
+ void testEcommerceDiscountT() {
+ request.enableEcommerce();
+ request.setEcommerceId("1");
+ request.setEcommerceRevenue(2.0);
+ request.setEcommerceDiscount(1.0);
+ assertThat(request.getEcommerceDiscount()).isEqualTo(Double.valueOf(1.0));
+ }
+
+
+ @Test
+ void testEcommerceDiscountF() {
+ request.setEcommerceDiscount(null);
+ assertThat(request.getEcommerceDiscount()).isNull();
+ }
+
+ /**
+ * Test of getEcommerceId method, of class PiwikRequest.
+ */
+ @Test
+ void testEcommerceIdT() {
+ request.enableEcommerce();
+ request.setEcommerceId("1");
+ assertThat(request.getEcommerceId()).isEqualTo("1");
+ }
+
+ @Test
+ void testEcommerceIdF() {
+ request.setEcommerceId(null);
+ assertThat(request.getEcommerceId()).isNull();
+ }
+
+ @Test
+ void testEcommerceItemE2() {
+ try {
+ request.enableEcommerce();
+ request.setEcommerceId("1");
+ request.setEcommerceRevenue(2.0);
+ request.addEcommerceItem(null);
+ fail("Exception should have been thrown.");
+ } catch (NullPointerException e) {
+ assertThat(e.getLocalizedMessage()).isEqualTo("item is marked non-null but is null");
+ }
+ }
+
+ @Test
+ void testEcommerceItem() {
+ assertThat(request.getEcommerceItem(0)).isNull();
+ EcommerceItem item = new EcommerceItem("sku", "name", "category", 1.0, 2);
+ request.enableEcommerce();
+ request.setEcommerceId("1");
+ request.setEcommerceRevenue(2.0);
+ request.addEcommerceItem(item);
+ assertThat(request.getEcommerceItem(0)).isEqualTo(item);
+ request.clearEcommerceItems();
+ assertThat(request.getEcommerceItem(0)).isNull();
+ }
+
+ /**
+ * Test of getEcommerceLastOrderTimestamp method, of class PiwikRequest.
+ */
+ @Test
+ void testEcommerceLastOrderTimestampT() {
+ request.enableEcommerce();
+ request.setEcommerceId("1");
+ request.setEcommerceRevenue(2.0);
+ request.setEcommerceLastOrderTimestamp(Instant.ofEpochSecond(1000L));
+ assertThat(request.getEcommerceLastOrderTimestamp()).isEqualTo("1970-01-01T00:16:40Z");
+ }
+
+ @Test
+ void testEcommerceLastOrderTimestampF() {
+ request.setEcommerceLastOrderTimestamp(null);
+ assertThat(request.getEcommerceLastOrderTimestamp()).isNull();
+ }
+
+ /**
+ * Test of getEcommerceRevenue method, of class PiwikRequest.
+ */
+ @Test
+ void testEcommerceRevenueT() {
+ request.enableEcommerce();
+ request.setEcommerceId("1");
+ request.setEcommerceRevenue(20.0);
+ assertThat(request.getEcommerceRevenue()).isEqualTo(Double.valueOf(20.0));
+ }
+
+
+ @Test
+ void testEcommerceRevenueF() {
+ request.setEcommerceRevenue(null);
+ assertThat(request.getEcommerceRevenue()).isNull();
+ }
+
+ /**
+ * Test of getEcommerceShippingCost method, of class PiwikRequest.
+ */
+ @Test
+ void testEcommerceShippingCostT() {
+ request.enableEcommerce();
+ request.setEcommerceId("1");
+ request.setEcommerceRevenue(2.0);
+ request.setEcommerceShippingCost(20.0);
+ assertThat(request.getEcommerceShippingCost()).isEqualTo(Double.valueOf(20.0));
+ }
+
+ @Test
+ void testEcommerceShippingCostF() {
+ request.setEcommerceShippingCost(null);
+ assertThat(request.getEcommerceShippingCost()).isNull();
+ }
+
+ /**
+ * Test of getEcommerceSubtotal method, of class PiwikRequest.
+ */
+ @Test
+ void testEcommerceSubtotalT() {
+ request.enableEcommerce();
+ request.setEcommerceId("1");
+ request.setEcommerceRevenue(2.0);
+ request.setEcommerceSubtotal(20.0);
+ assertThat(request.getEcommerceSubtotal()).isEqualTo(Double.valueOf(20.0));
+ }
+
+ @Test
+ void testEcommerceSubtotalF() {
+ request.setEcommerceSubtotal(null);
+ assertThat(request.getEcommerceSubtotal()).isNull();
+ }
+
+ /**
+ * Test of getEcommerceTax method, of class PiwikRequest.
+ */
+ @Test
+ void testEcommerceTaxT() {
+ request.enableEcommerce();
+ request.setEcommerceId("1");
+ request.setEcommerceRevenue(2.0);
+ request.setEcommerceTax(20.0);
+ assertThat(request.getEcommerceTax()).isEqualTo(Double.valueOf(20.0));
+ }
+
+ @Test
+ void testEcommerceTaxF() {
+ request.setEcommerceTax(null);
+ assertThat(request.getEcommerceTax()).isNull();
+ }
+
+ /**
+ * Test of getEventAction method, of class PiwikRequest.
+ */
+ @Test
+ void testEventAction() {
+ request.setEventAction("action");
+ assertThat(request.getEventAction()).isEqualTo("action");
+ request.setEventAction(null);
+ assertThat(request.getEventAction()).isNull();
+ }
+
+ /**
+ * Test of getEventCategory method, of class PiwikRequest.
+ */
+ @Test
+ void testEventCategory() {
+ request.setEventCategory("category");
+ assertThat(request.getEventCategory()).isEqualTo("category");
+ }
+
+ /**
+ * Test of getEventName method, of class PiwikRequest.
+ */
+ @Test
+ void testEventName() {
+ request.setEventName("name");
+ assertThat(request.getEventName()).isEqualTo("name");
+ }
+
+ /**
+ * Test of getEventValue method, of class PiwikRequest.
+ */
+ @Test
+ void testEventValue() {
+ request.setEventValue(1.0);
+ assertThat(request.getEventValue()).isOne();
+ }
+
+ /**
+ * Test of getGoalId method, of class PiwikRequest.
+ */
+ @Test
+ void testGoalId() {
+ request.setGoalId(1);
+ assertThat(request.getGoalId()).isEqualTo(Integer.valueOf(1));
+ }
+
+ /**
+ * Test of getHeaderAcceptLanguage method, of class PiwikRequest.
+ */
+ @Test
+ void testHeaderAcceptLanguage() {
+ request.setHeaderAcceptLanguage(AcceptLanguage.fromHeader("en"));
+ assertThat(request.getHeaderAcceptLanguage()).hasToString("en");
+ }
+
+ /**
+ * Test of getHeaderUserAgent method, of class PiwikRequest.
+ */
+ @Test
+ void testHeaderUserAgent() {
+ request.setHeaderUserAgent("agent");
+ assertThat(request.getHeaderUserAgent()).isEqualTo("agent");
+ }
+
+ /**
+ * Test of getNewVisit method, of class PiwikRequest.
+ */
+ @Test
+ void testNewVisit() {
+ request.setNewVisit(true);
+ assertThat(request.getNewVisit()).isTrue();
+ request.setNewVisit(null);
+ assertThat(request.getNewVisit()).isNull();
+ }
+
+ /**
+ * Test of getOutlinkUrl method, of class PiwikRequest.
+ */
+ @Test
+ void testOutlinkUrl() {
+ request.setOutlinkUrl("https://outlink.com");
+ assertThat(request.getOutlinkUrl()).isEqualTo("https://outlink.com");
+ }
+
+ /**
+ * Test of getPageCustomVariable method, of class PiwikRequest.
+ */
+ @Test
+ void testPageCustomVariableStringStringE() {
+ assertThatThrownBy(() -> request.setPageCustomVariable(null, null));
+ }
+
+ @Test
+ void testPageCustomVariableStringStringE2() {
+ assertThatThrownBy(() -> request.setPageCustomVariable(null, "pageVal"));
+ }
+
+ @Test
+ void testPageCustomVariableCustomVariable() {
+ assertThat(request.getPageCustomVariable(1)).isNull();
+ CustomVariable cv = new CustomVariable("pageKey", "pageVal");
+ request.setPageCustomVariable(cv, 1);
+ assertThat(request.getPageCustomVariable(1)).isEqualTo(cv);
+ request.setPageCustomVariable(null, 1);
+ assertThat(request.getPageCustomVariable(1)).isNull();
+ request.setPageCustomVariable(cv, 2);
+ assertThat(request.getPageCustomVariable(2)).isEqualTo(cv);
+ }
+
+ /**
+ * Test of getPluginDirector method, of class PiwikRequest.
+ */
+ @Test
+ void testPluginDirector() {
+ request.setPluginDirector(true);
+ assertThat(request.getPluginDirector()).isTrue();
+ }
+
+ /**
+ * Test of getPluginFlash method, of class PiwikRequest.
+ */
+ @Test
+ void testPluginFlash() {
+ request.setPluginFlash(true);
+ assertThat(request.getPluginFlash()).isTrue();
+ }
+
+ /**
+ * Test of getPluginGears method, of class PiwikRequest.
+ */
+ @Test
+ void testPluginGears() {
+ request.setPluginGears(true);
+ assertThat(request.getPluginGears()).isTrue();
+ }
+
+ /**
+ * Test of getPluginJava method, of class PiwikRequest.
+ */
+ @Test
+ void testPluginJava() {
+ request.setPluginJava(true);
+ assertThat(request.getPluginJava()).isTrue();
+ }
+
+ /**
+ * Test of getPluginPDF method, of class PiwikRequest.
+ */
+ @Test
+ void testPluginPDF() {
+ request.setPluginPDF(true);
+ assertThat(request.getPluginPDF()).isTrue();
+ }
+
+ /**
+ * Test of getPluginQuicktime method, of class PiwikRequest.
+ */
+ @Test
+ void testPluginQuicktime() {
+ request.setPluginQuicktime(true);
+ assertThat(request.getPluginQuicktime()).isTrue();
+ }
+
+ /**
+ * Test of getPluginRealPlayer method, of class PiwikRequest.
+ */
+ @Test
+ void testPluginRealPlayer() {
+ request.setPluginRealPlayer(true);
+ assertThat(request.getPluginRealPlayer()).isTrue();
+ }
+
+ /**
+ * Test of getPluginSilverlight method, of class PiwikRequest.
+ */
+ @Test
+ void testPluginSilverlight() {
+ request.setPluginSilverlight(true);
+ assertThat(request.getPluginSilverlight()).isTrue();
+ }
+
+ /**
+ * Test of getPluginWindowsMedia method, of class PiwikRequest.
+ */
+ @Test
+ void testPluginWindowsMedia() {
+ request.setPluginWindowsMedia(true);
+ assertThat(request.getPluginWindowsMedia()).isTrue();
+ }
+
+ /**
+ * Test of getRandomValue method, of class PiwikRequest.
+ */
+ @Test
+ void testRandomValue() {
+ request.setRandomValue(RandomValue.fromString("value"));
+ assertThat(request.getRandomValue()).hasToString("value");
+ }
+
+ /**
+ * Test of setReferrerUrl method, of class PiwikRequest.
+ */
+ @Test
+ void testReferrerUrl() {
+ request.setReferrerUrl("https://referrer.com");
+ assertThat(request.getReferrerUrl()).isEqualTo("https://referrer.com");
+ }
+
+ /**
+ * Test of getRequestDatetime method, of class PiwikRequest.
+ */
+ @Test
+ void testRequestDatetimeTTT() {
+ request.setAuthToken("12345678901234567890123456789012");
+ PiwikDate date = new PiwikDate(1000L);
+ request.setRequestDatetime(date);
+ assertThat(request.getRequestDatetime().getTime()).isEqualTo(1000L);
+ }
+
+
+ @Test
+ void testRequestDatetimeTF() {
+ request.setRequestDatetime(new PiwikDate());
+ assertThat(request.getRequestDatetime().getZonedDateTime()).isCloseTo(
+ ZonedDateTime.now(),
+ within(2, MINUTES)
+ );
+ }
+
+ @Test
+ void testRequestDatetimeF() {
+ PiwikDate date = new PiwikDate();
+ request.setRequestDatetime(date);
+ request.setRequestDatetime(null);
+ assertThat(request.getRequestDatetime()).isNull();
+ }
+
+ /**
+ * Test of getRequired method, of class PiwikRequest.
+ */
+ @Test
+ void testRequired() {
+ request.setRequired(false);
+ assertThat(request.getRequired()).isFalse();
+ }
+
+ /**
+ * Test of getResponseAsImage method, of class PiwikRequest.
+ */
+ @Test
+ void testResponseAsImage() {
+ request.setResponseAsImage(true);
+ assertThat(request.getResponseAsImage()).isTrue();
+ }
+
+ @Test
+ void testSearchCategoryTF() {
+ request.setSearchQuery("query");
+ request.setSearchCategory("category");
+ assertThat(request.getSearchCategory()).isEqualTo("category");
+ }
+
+ @Test
+ void testSearchCategoryF() {
+ request.setSearchCategory(null);
+ assertThat(request.getSearchCategory()).isNull();
+ }
+
+ /**
+ * Test of getSearchQuery method, of class PiwikRequest.
+ */
+ @Test
+ void testSearchQuery() {
+ request.setSearchQuery("query");
+ assertThat(request.getSearchQuery()).isEqualTo("query");
+ }
+
+ @Test
+ void testSearchResultsCountTF() {
+ request.setSearchQuery("query");
+ request.setSearchResultsCount(100L);
+ assertThat(request.getSearchResultsCount()).isEqualTo(Long.valueOf(100L));
+ }
+
+ @Test
+ void testSearchResultsCountF() {
+ request.setSearchResultsCount(null);
+ assertThat(request.getSearchResultsCount()).isNull();
+ }
+
+ /**
+ * Test of getSiteId method, of class PiwikRequest.
+ */
+ @Test
+ void testSiteId() {
+ request.setSiteId(2);
+ assertThat(request.getSiteId()).isEqualTo(Integer.valueOf(2));
+ }
+
+ /**
+ * Test of setTrackBotRequest method, of class PiwikRequest.
+ */
+ @Test
+ void testTrackBotRequests() {
+ request.setTrackBotRequests(true);
+ assertThat(request.getTrackBotRequests()).isTrue();
+ }
+
+ /**
+ * Test of getUserCustomVariable method, of class PiwikRequest.
+ */
+ @Test
+ void testUserCustomVariableStringString() {
+ request.setUserCustomVariable("userKey", "userValue");
+ assertThat(request.getUserCustomVariable("userKey")).isEqualTo("userValue");
+ }
+
+
+ /**
+ * Test of getUserId method, of class PiwikRequest.
+ */
+ @Test
+ void testUserId() {
+ request.setUserId("id");
+ assertThat(request.getUserId()).isEqualTo("id");
+ }
+
+ /**
+ * Test of getVisitorCity method, of class PiwikRequest.
+ */
+ @Test
+ void testVisitorCityT() {
+ request.setAuthToken("12345678901234567890123456789012");
+ request.setVisitorCity("city");
+ assertThat(request.getVisitorCity()).isEqualTo("city");
+ }
+
+ @Test
+ void testVisitorCityF() {
+ request.setVisitorCity(null);
+ assertThat(request.getVisitorCity()).isNull();
+ }
+
+ /**
+ * Test of getVisitorCountry method, of class PiwikRequest.
+ */
+ @Test
+ void testVisitorCountryT() {
+ PiwikLocale country = new PiwikLocale(Locale.US);
+ request.setAuthToken("12345678901234567890123456789012");
+ request.setVisitorCountry(country);
+ assertThat(request.getVisitorCountry()).isEqualTo(country);
+ }
+
+ @Test
+ void testVisitorCountryF() {
+ request.setVisitorCountry(null);
+ assertThat(request.getVisitorCountry()).isNull();
+ }
+
+ @Test
+ void testVisitorCustomTF() {
+ request.setVisitorCustomId(VisitorId.fromHex("1234567890abcdef"));
+ assertThat(request.getVisitorCustomId()).hasToString("1234567890abcdef");
+ }
+
+ @Test
+ void testVisitorCustomIdF() {
+ request.setVisitorCustomId(VisitorId.fromHex("1234567890abcdef"));
+ request.setVisitorCustomId(null);
+ assertThat(request.getVisitorCustomId()).isNull();
+ }
+
+ /**
+ * Test of getVisitorFirstVisitTimestamp method, of class PiwikRequest.
+ */
+ @Test
+ void testVisitorFirstVisitTimestamp() {
+ request.setVisitorFirstVisitTimestamp(Instant.parse("2021-03-10T10:22:22.123Z"));
+ assertThat(request.getVisitorFirstVisitTimestamp()).isEqualTo("2021-03-10T10:22:22.123Z");
+ }
+
+ @Test
+ void testVisitorIdTFT() {
+ try {
+ request.setVisitorId(VisitorId.fromHex("1234567890abcdeg"));
+ fail("Exception should have been thrown.");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getLocalizedMessage()).isEqualTo("Input must be a valid hex string");
+ }
+ }
+
+ @Test
+ void testVisitorIdTFF() {
+ request.setVisitorId(VisitorId.fromHex("1234567890abcdef"));
+ assertThat(request.getVisitorId()).hasToString("1234567890abcdef");
+ }
+
+ @Test
+ void testVisitorIdF() {
+ request.setVisitorId(VisitorId.fromHex("1234567890abcdef"));
+ request.setVisitorId(null);
+ assertThat(request.getVisitorId()).isNull();
+ }
+
+ /**
+ * Test of getVisitorIp method, of class PiwikRequest.
+ */
+ @Test
+ void testVisitorIpT() {
+ request.setAuthToken("12345678901234567890123456789012");
+ request.setVisitorIp("ip");
+ assertThat(request.getVisitorIp()).isEqualTo("ip");
+ }
+
+ @Test
+ void testVisitorIpF() {
+ request.setVisitorIp(null);
+ assertThat(request.getVisitorIp()).isNull();
+ }
+
+ /**
+ * Test of getVisitorLatitude method, of class PiwikRequest.
+ */
+ @Test
+ void testVisitorLatitudeT() {
+ request.setAuthToken("12345678901234567890123456789012");
+ request.setVisitorLatitude(10.5);
+ assertThat(request.getVisitorLatitude()).isEqualTo(Double.valueOf(10.5));
+ }
+
+ @Test
+ void testVisitorLatitudeF() {
+ request.setVisitorLatitude(null);
+ assertThat(request.getVisitorLatitude()).isNull();
+ }
+
+ /**
+ * Test of getVisitorLongitude method, of class PiwikRequest.
+ */
+ @Test
+ void testVisitorLongitudeT() {
+ request.setAuthToken("12345678901234567890123456789012");
+ request.setVisitorLongitude(20.5);
+ assertThat(request.getVisitorLongitude()).isEqualTo(Double.valueOf(20.5));
+ }
+
+ @Test
+ void testVisitorLongitudeF() {
+ request.setVisitorLongitude(null);
+ assertThat(request.getVisitorLongitude()).isNull();
+ }
+
+ /**
+ * Test of getVisitorPreviousVisitTimestamp method, of class PiwikRequest.
+ */
+ @Test
+ void testVisitorPreviousVisitTimestamp() {
+ request.setVisitorPreviousVisitTimestamp(Instant.ofEpochSecond(1000L));
+ assertThat(request.getVisitorPreviousVisitTimestamp()).isEqualTo("1970-01-01T00:16:40Z");
+ }
+
+ /**
+ * Test of getVisitorRegion method, of class PiwikRequest.
+ */
+ @Test
+ void testVisitorRegionT() {
+ request.setAuthToken("12345678901234567890123456789012");
+ request.setVisitorRegion("region");
+ assertThat(request.getVisitorRegion()).isEqualTo("region");
+ }
+
+ @Test
+ void testVisitorRegionF() {
+ request.setVisitorRegion(null);
+ assertThat(request.getVisitorRegion()).isNull();
+ }
+
+ /**
+ * Test of getVisitorVisitCount method, of class PiwikRequest.
+ */
+ @Test
+ void testVisitorVisitCount() {
+ request.setVisitorVisitCount(100);
+ assertThat(request.getVisitorVisitCount()).isEqualTo(Integer.valueOf(100));
+ }
+
+ @Test
+ void failsIfActionUrlIsNull() {
+ assertThatThrownBy(() -> new PiwikRequest(3, null))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("Action URL must not be null");
+ }
+
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/ProxyAuthenticatorTest.java b/core/src/test/java/org/matomo/java/tracking/ProxyAuthenticatorTest.java
new file mode 100644
index 00000000..08893677
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/ProxyAuthenticatorTest.java
@@ -0,0 +1,61 @@
+package org.matomo.java.tracking;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.Authenticator;
+import java.net.Authenticator.RequestorType;
+import java.net.InetAddress;
+import java.net.PasswordAuthentication;
+import java.net.URL;
+import org.junit.jupiter.api.Test;
+
+class ProxyAuthenticatorTest {
+
+ private PasswordAuthentication passwordAuthentication;
+
+ @Test
+ void createsPasswordAuthentication() throws Exception {
+
+ ProxyAuthenticator proxyAuthenticator = new ProxyAuthenticator("user", "password");
+ Authenticator.setDefault(proxyAuthenticator);
+ givenPasswordAuthentication(RequestorType.PROXY);
+
+ assertThat(passwordAuthentication.getUserName()).isEqualTo("user");
+ assertThat(passwordAuthentication.getPassword()).contains(
+ 'p',
+ 'a',
+ 's',
+ 's',
+ 'w',
+ 'o',
+ 'r',
+ 'd'
+ );
+
+ }
+
+ private void givenPasswordAuthentication(RequestorType proxy) throws Exception {
+ passwordAuthentication = Authenticator.requestPasswordAuthentication("host",
+ InetAddress.getLocalHost(),
+ 8080,
+ "http",
+ "prompt",
+ "https",
+ new URL("https://www.daniel-heid.de"),
+ proxy
+ );
+ }
+
+ @Test
+ void returnsNullIfNoPasswordAuthentication() throws Exception {
+
+ ProxyAuthenticator proxyAuthenticator = new ProxyAuthenticator("user", "password");
+ Authenticator.setDefault(proxyAuthenticator);
+ givenPasswordAuthentication(RequestorType.SERVER);
+
+ assertThat(passwordAuthentication).isNull();
+
+ }
+
+
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/QueryCreatorTest.java b/core/src/test/java/org/matomo/java/tracking/QueryCreatorTest.java
new file mode 100644
index 00000000..a4e87e5b
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/QueryCreatorTest.java
@@ -0,0 +1,604 @@
+package org.matomo.java.tracking;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singleton;
+import static java.util.Collections.singletonMap;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Locale.LanguageRange;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.matomo.java.tracking.parameters.AcceptLanguage;
+import org.matomo.java.tracking.parameters.Country;
+import org.matomo.java.tracking.parameters.CustomVariable;
+import org.matomo.java.tracking.parameters.CustomVariables;
+import org.matomo.java.tracking.parameters.DeviceResolution;
+import org.matomo.java.tracking.parameters.EcommerceItem;
+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;
+
+class QueryCreatorTest {
+
+ private final MatomoRequest.MatomoRequestBuilder matomoRequestBuilder = MatomoRequest
+ .request()
+ .visitorId(VisitorId.fromHash(1234567890123456789L))
+ .randomValue(RandomValue.fromString("random-value"));
+
+ private String defaultAuthToken = "876de1876fb2cda2816c362a61bfc712";
+
+ private String query;
+
+ private MatomoRequest request;
+
+ @Test
+ void usesDefaultSiteId() {
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value");
+
+ }
+
+ private void whenCreatesQuery() {
+ request = matomoRequestBuilder.build();
+ TrackerConfiguration trackerConfiguration = TrackerConfiguration
+ .builder()
+ .apiEndpoint(URI.create("http://localhost"))
+ .defaultSiteId(42)
+ .defaultAuthToken(defaultAuthToken)
+ .build();
+ String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration);
+ query = new QueryCreator(trackerConfiguration).createQuery(request, authToken);
+ }
+
+ @Test
+ void overridesDefaultSiteId() {
+
+ matomoRequestBuilder.siteId(123);
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&idsite=123&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value");
+
+ }
+
+ @Test
+ void usesDefaultTokenAuth() {
+
+ defaultAuthToken = "f123bfc9a46de0bb5453afdab6f93200";
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=f123bfc9a46de0bb5453afdab6f93200&rec=1&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value");
+
+ }
+
+ @Test
+ void overridesDefaultTokenAuth() {
+
+ defaultAuthToken = "f123bfc9a46de0bb5453afdab6f93200";
+ matomoRequestBuilder.authToken("e456bfc9a46de0bb5453afdab6f93200");
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=e456bfc9a46de0bb5453afdab6f93200&rec=1&apiv=1&_id=112210f47de98115&token_auth=e456bfc9a46de0bb5453afdab6f93200&send_image=0&rand=random-value");
+
+ }
+
+ @Test
+ void validatesTokenAuth() {
+
+ matomoRequestBuilder.authToken("invalid-token-auth");
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Auth token must be exactly 32 characters long");
+
+ }
+
+ @Test
+ void convertsTrueBooleanTo1() {
+
+ matomoRequestBuilder.pluginFlash(true);
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&fla=1&send_image=0&rand=random-value");
+
+ }
+
+ @Test
+ void convertsFalseBooleanTo0() {
+
+ matomoRequestBuilder.pluginJava(false);
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&java=0&send_image=0&rand=random-value");
+
+ }
+
+ @Test
+ void encodesUrl() {
+
+ matomoRequestBuilder.actionUrl("https://www.daniel-heid.de/some/page?foo=bar");
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&url=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fpage%3Ffoo%3Dbar&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value");
+
+ }
+
+ @Test
+ void encodesReferrerUrl() {
+
+ matomoRequestBuilder.referrerUrl("https://www.daniel-heid.de/some/referrer?foo2=bar2");
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&urlref=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Freferrer%3Ffoo2%3Dbar2&send_image=0&rand=random-value");
+
+ }
+
+ @Test
+ void encodesLink() {
+
+ matomoRequestBuilder.outlinkUrl("https://www.daniel-heid.de/some/external/link#");
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&link=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fexternal%2Flink%23&send_image=0&rand=random-value");
+
+ }
+
+ @Test
+ void encodesDownloadUrl() {
+
+ matomoRequestBuilder.downloadUrl("https://www.daniel-heid.de/some/download.pdf");
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&download=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fdownload.pdf&send_image=0&rand=random-value");
+
+ }
+
+ @Test
+ void tracksMinimalRequest() {
+
+ matomoRequestBuilder
+ .actionName("Help / Feedback")
+ .actionUrl("https://www.daniel-heid.de/portfolio")
+ .visitorId(VisitorId.fromHash(3434343434343434343L))
+ .referrerUrl("https://www.daniel-heid.de/referrer")
+ .visitCustomVariables(new CustomVariables()
+ .add(new CustomVariable("customVariable1Key", "customVariable1Value"), 5)
+ .add(new CustomVariable("customVariable2Key", "customVariable2Value"), 6))
+ .visitorVisitCount(2)
+ .visitorPreviousVisitTimestamp(Instant.parse("2022-08-09T18:34:12Z"))
+ .deviceResolution(DeviceResolution.builder().width(1024).height(768).build())
+ .headerAcceptLanguage(AcceptLanguage
+ .builder()
+ .languageRange(new LanguageRange("de"))
+ .languageRange(new LanguageRange("de-DE", 0.9))
+ .languageRange(new LanguageRange("en", 0.8))
+ .build())
+ .pageViewId(UniqueId.fromValue(999999999999999999L))
+ .goalId(0)
+ .ecommerceRevenue(12.34)
+ .ecommerceItems(EcommerceItems
+ .builder()
+ .item(EcommerceItem.builder().sku("SKU").build())
+ .item(EcommerceItem
+ .builder()
+ .sku("SKU")
+ .name("NAME")
+ .category("CATEGORY")
+ .price(123.4)
+ .build())
+ .build())
+ .authToken("fdf6e8461ea9de33176b222519627f78")
+ .visitorCountry(Country.fromLanguageRanges("en-GB;q=0.7,de,de-DE;q=0.9,en;q=0.8,en-US;q=0.6"));
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=fdf6e8461ea9de33176b222519627f78&rec=1&action_name=Help+%2F+Feedback&url=https%3A%2F%2Fwww.daniel-heid.de%2Fportfolio&apiv=1&_id=2fa93d2858bc4867&urlref=https%3A%2F%2Fwww.daniel-heid.de%2Freferrer&_cvar=%7B%225%22%3A%5B%22customVariable1Key%22%2C%22customVariable1Value%22%5D%2C%226%22%3A%5B%22customVariable2Key%22%2C%22customVariable2Value%22%5D%7D&_idvc=2&_viewts=1660070052&res=1024x768&lang=de%2Cde-de%3Bq%3D0.9%2Cen%3Bq%3D0.8&pv_id=lbBbxG&idgoal=0&revenue=12.34&ec_items=%5B%5B%22SKU%22%2C%22%22%2C%22%22%2C0.0%2C0%5D%2C%5B%22SKU%22%2C%22NAME%22%2C%22CATEGORY%22%2C123.4%2C0%5D%5D&token_auth=fdf6e8461ea9de33176b222519627f78&country=de&send_image=0&rand=random-value");
+
+ }
+
+ @Test
+ void testGetQueryString() {
+ matomoRequestBuilder
+ .siteId(3)
+ .actionUrl("http://test.com")
+ .randomValue(RandomValue.fromString("random"))
+ .visitorId(VisitorId.fromHex("1234567890123456"));
+ defaultAuthToken = null;
+ whenCreatesQuery();
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random");
+ matomoRequestBuilder.pageCustomVariables(new CustomVariables().add(new CustomVariable(
+ "key",
+ "val"
+ ), 7));
+ whenCreatesQuery();
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random");
+ matomoRequestBuilder.additionalParameters(singletonMap("key", singleton("test")));
+ whenCreatesQuery();
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random&key=%5Btest%5D");
+ matomoRequestBuilder.additionalParameters(singletonMap("key", asList("test", "test2")));
+ whenCreatesQuery();
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random&key=%5Btest%2C+test2%5D");
+ Map customTrackingParameters = new HashMap<>();
+ customTrackingParameters.put("key", "test2");
+ customTrackingParameters.put("key2", "test3");
+ matomoRequestBuilder.additionalParameters(customTrackingParameters);
+ whenCreatesQuery();
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random&key2=test3&key=test2");
+ customTrackingParameters.put("key", "test4");
+ whenCreatesQuery();
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random&key2=test3&key=test4");
+ matomoRequestBuilder.randomValue(null);
+ matomoRequestBuilder.siteId(null);
+ matomoRequestBuilder.required(null);
+ matomoRequestBuilder.apiVersion(null);
+ matomoRequestBuilder.responseAsImage(null);
+ matomoRequestBuilder.visitorId(null);
+ matomoRequestBuilder.actionUrl(null);
+ whenCreatesQuery();
+ assertThat(query).isEqualTo(
+ "idsite=42&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&key2=test3&key=test4");
+ }
+
+ @Test
+ void testGetQueryString2() {
+ matomoRequestBuilder
+ .actionUrl("http://test.com")
+ .randomValue(RandomValue.fromString("random"))
+ .visitorId(VisitorId.fromHex("1234567890123456"))
+ .siteId(3);
+ defaultAuthToken = null;
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random");
+ }
+
+ @Test
+ void testGetUrlEncodedQueryString() {
+ defaultAuthToken = null;
+ matomoRequestBuilder
+ .actionUrl("http://test.com")
+ .randomValue(RandomValue.fromString("random"))
+ .visitorId(VisitorId.fromHex("1234567890123456"))
+ .siteId(3);
+ whenCreatesQuery();
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random");
+ Map customTrackingParameters = new HashMap<>();
+ customTrackingParameters.put("ke/y", "te:st");
+ matomoRequestBuilder.additionalParameters(customTrackingParameters);
+ whenCreatesQuery();
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random&ke%2Fy=te%3Ast");
+ customTrackingParameters.put("ke/y", "te:st2");
+ whenCreatesQuery();
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random&ke%2Fy=te%3Ast2");
+ customTrackingParameters.put("ke/y2", "te:st3");
+ whenCreatesQuery();
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random&ke%2Fy=te%3Ast2&ke%2Fy2=te%3Ast3");
+ customTrackingParameters.put("ke/y", "te:st3");
+ whenCreatesQuery();
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random&ke%2Fy=te%3Ast3&ke%2Fy2=te%3Ast3");
+ matomoRequestBuilder
+ .randomValue(null)
+ .siteId(null)
+ .required(null)
+ .apiVersion(null)
+ .responseAsImage(null)
+ .visitorId(null)
+ .actionUrl(null);
+ whenCreatesQuery();
+ assertThat(query).isEqualTo("idsite=42&ke%2Fy=te%3Ast3&ke%2Fy2=te%3Ast3");
+
+ }
+
+ @Test
+ void testGetUrlEncodedQueryString2() {
+ matomoRequestBuilder
+ .actionUrl("http://test.com")
+ .randomValue(RandomValue.fromString("random"))
+ .visitorId(VisitorId.fromHex("1234567890123456"));
+ defaultAuthToken = null;
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&rec=1&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random");
+
+ }
+
+ @Test
+ void testVisitCustomVariableCustomVariable() {
+ matomoRequestBuilder
+ .randomValue(RandomValue.fromString("random"))
+ .visitorId(VisitorId.fromHex("1234567890123456"))
+ .siteId(3);
+ org.matomo.java.tracking.CustomVariable cv =
+ new org.matomo.java.tracking.CustomVariable("visitKey", "visitVal");
+ matomoRequestBuilder.visitCustomVariables(new CustomVariables().add(cv, 8));
+ defaultAuthToken = null;
+
+ whenCreatesQuery();
+
+ assertThat(request.getVisitCustomVariable(1)).isNull();
+ assertThat(query).isEqualTo(
+ "rec=1&idsite=3&apiv=1&_id=1234567890123456&_cvar=%7B%228%22%3A%5B%22visitKey%22%2C%22visitVal%22%5D%7D&send_image=0&rand=random");
+ }
+
+ @Test
+ void doesNotAppendEmptyString() {
+
+ matomoRequestBuilder.eventAction("");
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&e_a=&send_image=0&rand=random-value");
+
+ }
+
+ @Test
+ void testAuthTokenTT() {
+
+ matomoRequestBuilder.authToken("1234");
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Auth token must be exactly 32 characters long");
+ }
+
+ @Test
+ void createsQueryWithDimensions() {
+ Map dimensions = new LinkedHashMap<>();
+ dimensions.put(1L, "firstDimension");
+ dimensions.put(3L, "thirdDimension");
+ matomoRequestBuilder.dimensions(dimensions);
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value&dimension1=firstDimension&dimension3=thirdDimension");
+ }
+
+ @Test
+ void appendsCharsetParameters() {
+ matomoRequestBuilder.characterSet(StandardCharsets.ISO_8859_1);
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo(
+ "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&cs=ISO-8859-1&send_image=0&rand=random-value");
+ }
+
+ @Test
+ void failsIfIdSiteIsNegative() {
+ matomoRequestBuilder.siteId(-1);
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Could not append parameter")
+ .hasRootCauseInstanceOf(MatomoException.class)
+ .hasRootCauseMessage("Invalid value for idsite. Must be greater or equal than 1");
+ }
+
+ @Test
+ void failsIfIdSiteIsZero() {
+ matomoRequestBuilder.siteId(0);
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Could not append parameter")
+ .hasRootCauseInstanceOf(MatomoException.class)
+ .hasRootCauseMessage("Invalid value for idsite. Must be greater or equal than 1");
+ }
+
+ @Test
+ void failsIfCurrentHourIsNegative() {
+ matomoRequestBuilder.currentHour(-1);
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Could not append parameter")
+ .hasRootCauseInstanceOf(MatomoException.class)
+ .hasRootCauseMessage("Invalid value for h. Must be greater or equal than 0");
+ }
+
+ @Test
+ void failsIfCurrentHourIsGreaterThan23() {
+ matomoRequestBuilder.currentHour(24);
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Could not append parameter")
+ .hasRootCauseInstanceOf(MatomoException.class)
+ .hasRootCauseMessage("Invalid value for h. Must be less or equal than 23");
+ }
+
+ @Test
+ void failsIfCurrentMinuteIsNegative() {
+ matomoRequestBuilder.currentMinute(-1);
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Could not append parameter")
+ .hasRootCauseInstanceOf(MatomoException.class)
+ .hasRootCauseMessage("Invalid value for m. Must be greater or equal than 0");
+ }
+
+ @Test
+ void failsIfCurrentMinuteIsGreaterThan59() {
+ matomoRequestBuilder.currentMinute(60);
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Could not append parameter")
+ .hasRootCauseInstanceOf(MatomoException.class)
+ .hasRootCauseMessage("Invalid value for m. Must be less or equal than 59");
+ }
+
+ @Test
+ void failsIfCurrentSecondIsNegative() {
+ matomoRequestBuilder.currentSecond(-1);
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Could not append parameter")
+ .hasRootCauseInstanceOf(MatomoException.class)
+ .hasRootCauseMessage("Invalid value for s. Must be greater or equal than 0");
+ }
+
+ @Test
+ void failsIfCurrentSecondIsGreaterThan59() {
+ matomoRequestBuilder.currentSecond(60);
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Could not append parameter")
+ .hasRootCauseInstanceOf(MatomoException.class)
+ .hasRootCauseMessage("Invalid value for s. Must be less or equal than 59");
+ }
+
+ @Test
+ void failsIfLatitudeIsLessThanMinus90() {
+ matomoRequestBuilder.visitorLatitude(-90.1);
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Could not append parameter")
+ .hasRootCauseInstanceOf(MatomoException.class)
+ .hasRootCauseMessage("Invalid value for lat. Must be greater or equal than -90");
+ }
+
+ @Test
+ void failsIfLatitudeIsGreaterThan90() {
+ matomoRequestBuilder.visitorLatitude(90.1);
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Could not append parameter")
+ .hasRootCauseInstanceOf(MatomoException.class)
+ .hasRootCauseMessage("Invalid value for lat. Must be less or equal than 90");
+ }
+
+ @Test
+ void failsIfLongitudeIsLessThanMinus180() {
+ matomoRequestBuilder.visitorLongitude(-180.1);
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Could not append parameter")
+ .hasRootCauseMessage("Invalid value for long. Must be greater or equal than -180");
+ }
+
+ @Test
+ void failsIfLongitudeIsGreaterThan180() {
+ matomoRequestBuilder.visitorLongitude(180.1);
+
+ assertThatThrownBy(this::whenCreatesQuery)
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Could not append parameter")
+ .hasRootCauseMessage("Invalid value for long. Must be less or equal than 180");
+ }
+
+ @Test
+ void tracksEvent() {
+ matomoRequestBuilder.eventName("Event Name")
+ .eventValue(23.456)
+ .eventAction("Event Action")
+ .eventCategory("Event Category");
+
+ whenCreatesQuery();
+
+ assertThat(query).isEqualTo("idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&e_c=Event+Category&e_a=Event+Action&e_n=Event+Name&e_v=23.456&send_image=0&rand=random-value");
+ }
+
+ @Test
+ void allowsZeroForEventValue() {
+ matomoRequestBuilder.eventName("Event Name")
+ .eventValue(0.0)
+ .eventAction("Event Action")
+ .eventCategory("Event Category");
+
+ whenCreatesQuery();
+
+ assertThat(query)
+ .isEqualTo("idsite=42&" +
+ "token_auth=876de1876fb2cda2816c362a61bfc712&" +
+ "rec=1&" +
+ "apiv=1&" +
+ "_id=112210f47de98115&" +
+ "e_c=Event+Category&" +
+ "e_a=Event+Action&" +
+ "e_n=Event+Name&" +
+ "e_v=0.0&" +
+ "send_image=0&" +
+ "rand=random-value"
+ );
+ }
+
+ @Test
+ void allowsZeroForEcommerceValues() {
+ matomoRequestBuilder
+ .ecommerceRevenue(0.0)
+ .ecommerceSubtotal(0.0)
+ .ecommerceTax(0.0)
+ .ecommerceShippingCost(0.0)
+ .ecommerceDiscount(0.0);
+
+ whenCreatesQuery();
+
+ assertThat(query)
+ .isEqualTo("idsite=42&" +
+ "token_auth=876de1876fb2cda2816c362a61bfc712&" +
+ "rec=1&" +
+ "apiv=1&" +
+ "_id=112210f47de98115&" +
+ "revenue=0.0&" +
+ "ec_st=0.0&" +
+ "ec_tx=0.0&" +
+ "ec_sh=0.0&" +
+ "ec_dt=0.0&" +
+ "send_image=0&" +
+ "rand=random-value"
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/matomo/java/tracking/RequestValidatorTest.java b/core/src/test/java/org/matomo/java/tracking/RequestValidatorTest.java
new file mode 100644
index 00000000..6d1ab348
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/RequestValidatorTest.java
@@ -0,0 +1,97 @@
+package org.matomo.java.tracking;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Locale;
+import org.junit.jupiter.api.Test;
+import org.piwik.java.tracking.PiwikDate;
+import org.piwik.java.tracking.PiwikLocale;
+
+class RequestValidatorTest {
+
+ private final MatomoRequest request = new MatomoRequest();
+
+
+ @Test
+ void testSearchResultsCount() {
+
+ request.setSearchResultsCount(100L);
+
+ assertThatThrownBy(() -> RequestValidator.validate(request, null))
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Search query must be set if search results count is set");
+
+ }
+
+ @Test
+ void testVisitorLongitude() {
+ request.setVisitorLongitude(20.5);
+
+ assertThatThrownBy(() -> RequestValidator.validate(request, null))
+ .isInstanceOf(MatomoException.class)
+ .hasMessage(
+ "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set");
+ }
+
+ @Test
+ void testVisitorLatitude() {
+ request.setVisitorLatitude(10.5);
+
+ assertThatThrownBy(() -> RequestValidator.validate(request, null))
+ .isInstanceOf(MatomoException.class)
+ .hasMessage(
+ "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set");
+ }
+
+ @Test
+ void testVisitorCity() {
+ request.setVisitorCity("city");
+
+ assertThatThrownBy(() -> RequestValidator.validate(request, null))
+ .isInstanceOf(MatomoException.class)
+ .hasMessage(
+ "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set");
+ }
+
+ @Test
+ void testVisitorRegion() {
+ request.setVisitorRegion("region");
+
+ assertThatThrownBy(() -> RequestValidator.validate(request, null))
+ .isInstanceOf(MatomoException.class)
+ .hasMessage(
+ "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set");
+ }
+
+ @Test
+ void testVisitorCountryTE() {
+ PiwikLocale country = new PiwikLocale(Locale.US);
+ request.setVisitorCountry(country);
+
+
+ assertThatThrownBy(() -> RequestValidator.validate(request, null))
+ .isInstanceOf(MatomoException.class)
+ .hasMessage(
+ "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set");
+ }
+
+ @Test
+ void testRequestDatetime() {
+
+ PiwikDate date = new PiwikDate(1000L);
+ request.setRequestDatetime(date);
+
+ assertThatThrownBy(() -> RequestValidator.validate(request, null))
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("Auth token must be present if request timestamp is more than four hours ago");
+
+ }
+
+ @Test
+ void failsIfAuthTokenIsNot32CharactersLong() {
+ assertThatThrownBy(() -> RequestValidator.validate(request, "123456789012345678901234567890"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Auth token must be exactly 32 characters long");
+ }
+
+}
diff --git a/core/src/test/java/org/matomo/java/tracking/ServiceLoaderSenderFactoryTest.java b/core/src/test/java/org/matomo/java/tracking/ServiceLoaderSenderFactoryTest.java
new file mode 100644
index 00000000..0a5d2253
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/ServiceLoaderSenderFactoryTest.java
@@ -0,0 +1,24 @@
+package org.matomo.java.tracking;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.net.URI;
+import org.junit.jupiter.api.Test;
+
+class ServiceLoaderSenderFactoryTest {
+
+ @Test
+ void failsIfNoImplementationFound() {
+ ServiceLoaderSenderFactory serviceLoaderSenderFactory = new ServiceLoaderSenderFactory();
+
+ TrackerConfiguration trackerConfiguration =
+ TrackerConfiguration.builder().apiEndpoint(URI.create("http://localhost/matomo.php")).build();
+
+ assertThatThrownBy(() -> serviceLoaderSenderFactory.createSender(trackerConfiguration,
+ new QueryCreator(trackerConfiguration)
+ ))
+ .isInstanceOf(MatomoException.class)
+ .hasMessage("No SenderProvider found");
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/matomo/java/tracking/TestSender.java b/core/src/test/java/org/matomo/java/tracking/TestSender.java
new file mode 100644
index 00000000..f000bc8d
--- /dev/null
+++ b/core/src/test/java/org/matomo/java/tracking/TestSender.java
@@ -0,0 +1,76 @@
+package org.matomo.java.tracking;
+
+import static java.util.Collections.singleton;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * A {@link Sender} implementation that does not send anything but stores the requests and queries.
+ *
+ *
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, for 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..e9f1acd2
--- /dev/null
+++ b/spring/src/main/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizer.java
@@ -0,0 +1,50 @@
+/*
+ * 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.matomo.java.tracking.TrackerConfiguration;
+import org.springframework.boot.context.properties.PropertyMapper;
+import org.springframework.core.Ordered;
+import org.springframework.lang.NonNull;
+
+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(@NonNull TrackerConfiguration.TrackerConfigurationBuilder builder) {
+ PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
+ 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);
+ }
+
+
+}
\ No newline at end of file
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..e147a64f
--- /dev/null
+++ b/spring/src/main/java/org/matomo/java/tracking/spring/TrackerConfigurationBuilderCustomizer.java
@@ -0,0 +1,34 @@
+/*
+ * 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.matomo.java.tracking.TrackerConfiguration;
+import org.springframework.lang.NonNull;
+
+/**
+ * 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(@NonNull TrackerConfiguration.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..784c9d72
--- /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;
\ No newline at end of file
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..c008dbc8
--- /dev/null
+++ b/spring/src/test/java/org/matomo/java/tracking/spring/MatomoTrackerAutoConfigurationIT.java
@@ -0,0 +1,70 @@
+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);
+ }
+
+ }
+
+}
\ No newline at end of file
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..b62eafec
--- /dev/null
+++ b/spring/src/test/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizerIT.java
@@ -0,0 +1,55 @@
+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);
+ }
+
+}
\ No newline at end of file
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/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/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