diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..322b027f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,1380 @@
+[*]
+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
+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
new file mode 100644
index 00000000..cd85bf2d
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+version: 2
+updates:
+ - package-ecosystem: maven
+ directory: /
+ schedule:
+ 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
new file mode 100644
index 00000000..42d2b4f0
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +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 bdf8136c..60bd73bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,16 +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
\ No newline at end of file
+.idea
+.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 b62ae238..db941103 100644
--- a/README.md
+++ b/README.md
@@ -1,108 +1,796 @@
-Matomo Java Tracker
-================
+# Official Matomo Java Tracker
[](https://maven-badges.herokuapp.com/maven-central/org.piwik.java.tracking/matomo-java-tracker)
-## Code Status
-[](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
-### Create a Request
-Each PiwikRequest represents an action the user has taken that you want tracked by your Piwik server. Create a PiwikRequest through
-```java
-PiwikRequest request = new PiwikRequest(siteId, actionUrl);
+
+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. For Java 8:
+
+```xml
+
+
+ org.piwik.java.tracking
+ matomo-java-tracker
+ 3.4.0
+
```
-The following parameters are also enabled by default:
+For Java 11:
-```java
-required = true;
-visitorId = random 16 character hex string;
-randomValue = random 20 character hex string;
-apiVersion = 1;
-responseAsImage = false;
+```xml
+
+
+ org.piwik.java.tracking
+ matomo-java-tracker-java11
+ 3.4.0
+
```
-Overwrite these properties as desired.
+or Gradle (Java 8):
-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,
-```java
-URL actionUrl = new URL("http://example.org/landing.html?pk_campaign=Email-Nov2011&pk_kwd=LearnMore");
+```groovy
+dependencies {
+ implementation("org.piwik.java.tracking:matomo-java-tracker:3.4.0")
+}
+```
+
+or Gradle (Java 11):
+
+```groovy
+dependencies {
+ implementation("org.piwik.java.tracking:matomo-java-tracker-java11:3.4.0")
+}
+```
+
+or Gradle with Kotlin DSL (Java 8)
+
+```kotlin
+implementation("org.piwik.java.tracking:matomo-java-tracker:3.4.0")
+```
+
+or Gradle with Kotlin DSL (Java 11)
+
+```kotlin
+implementation("org.piwik.java.tracking:matomo-java-tracker-java11:3.4.0")
+```
+
+### Spring Boot Module
+
+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:
+
+```xml
+
+
+ org.piwik.java.tracking
+ matomo-java-tracker-spring-boot-starter
+ 3.4.0
+
+```
+
+or Gradle:
+
+```groovy
+dependencies {
+ implementation("org.piwik.java.tracking:matomo-java-tracker-spring-boot-starter:3.4.0")
+}
+```
+
+or Gradle with Kotlin DSL
+
+```kotlin
+implementation("org.piwik.java.tracking:matomo-java-tracker-spring-boot-starter:3.4.0")
+```
+
+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
+```
+
+Or if you use YAML:
+
+```yaml
+matomo:
+ tracker:
+ api-endpoint: https://your-matomo-domain.tld/matomo.php
+```
+
+You can automatically add the `MatomoTrackerFilter` to your Spring Boot application if you add the following property:
+
+```properties
+matomo.tracker.filter.enabled=true
+```
+
+Or if you use YAML:
+
+```yaml
+matomo:
+ tracker:
+ filter:
+ enabled: true
```
-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 PiwikRequest.java for the mappings of the parameters to their corresponding Java getters/setters.
+The filter uses `ServletMatomoRequest` to create a `MatomoRequest` from a `HttpServletRequest` on every filter call.
+
+### Sending a Tracking Request
+
+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);
+ }
+ }
-Some parameters are dependent on the state of other parameters:
-EcommerceEnabled must be called before the following parameters are set: EcommerceId and EcommerceRevenue.
+}
-EcommerceId and EcommerceRevenue must be set before the following parameters are set: EcommerceDiscount, EcommerceItem, EcommerceLastOrderTimestamp, EcommerceShippingCost, EcommerceSubtotal, and EcommerceTax.
+```
+
+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.
-AuthToken must be set before the following parameters are set: VisitorCity, VisitorCountry, VisitorIp, VisitorLatitude, VisitorLongitude, and VisitorRegion.
+If you want to perform an operation after a successful asynchronous call to Matomo, you can use the completable future
+result like this:
-### Sending Requests
-Create a PiwikTracker through
```java
-PiwikTracker tracker = new PiwikTracker(hostUrl);
+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);
+ }
+
+ }
+
+}
+
+
```
-where hostUrl is the url endpoint of the Piwik server. Usually in the format http://your-piwik-domain.tld/piwik.php.
-To send a single request, call
+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
-HttpResponse response = tracker.sendRequest(request);
+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);
+ }
+
+ }
+
+}
```
-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
+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
-HttpResponse response = tracker.sendBulkRequest(requests);
+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);
+ }
+
+ }
+
+}
```
-If some of the parameters that you've specified in the bulk request requre AuthToken to be set, this can also be set in the bulk request through
+
+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
-HttpResponse response = tracker.sendBulkRequest(requests, authToken);
+import jakarta.servlet.http.HttpServletRequest;
+import org.matomo.java.tracking.MatomoRequest;
+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;
+
+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);
+ // ...
+ }
+
+}
```
-## Install
+
+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
+
```shell
-mvn package
+mvn install
```
-The built jars and javadoc can be found in target
-Test this project using
+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
+ 3.4.1-SNAPSHOT
+
+```
+
+## 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 test
+docker-compose up -d
+```
+
+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"
```
-This project also supports [Pitest](http://pitest.org/) mutation testing. This report can be generated by calling
+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 org.pitest:pitest-maven:mutationCoverage
+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
```
-and will produce an html report at target/pit-reports/YYYYMMDDHHMI
-Clean this project using
+Use the following snippet to do this:
+
```shell
-mvn clean
+docker-compose exec matomo sh -c 'echo -e "\n\n[Tracker]\ndebug = 1\n" >> /var/www/html/config/config.ini.php'
```
+To test the servlet integration, run `MatomoServletTester` in your favorite IDE. It starts an embedded Jetty server
+that serves a simple servlet. The servlet sends a request to the local Matomo instance if you call the URL
+http://localhost:8090/track.html. Maybe you need to disable support for the Do Not Track preference in Matomo to get the
+request tracked: Go to _Administration > Privacy > Do Not Track_ and disable the checkbox _Respect Do Not Track.
+We also recommend to install the Custom Variables plugin from Marketplace to the test custom variables feature and
+setup some dimensions.
+
+## Versioning
+
+We use [SemVer](https://semver.org/) for versioning. For the versions available, see
+the [tags on this repository](https://github.com/matomo-org/matomo-java-tracker/tags).
+
## Contribute
-Have a fantastic feature idea? Spot a bug? We would absolutely love for you to contribute to this project! Please feel free to:
+
+Have a fantastic feature idea? Spot a bug? We would absolutely love for you to contribute to this project! Please feel
+free to:
* Fork this project
-* Create a feature branch from the dev branch
+* Create a feature branch from the _master_ branch
* Write awesome code that does awesome things
* 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!
+* 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 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).
## 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/core/src/main/java/org/matomo/java/tracking/MatomoDate.java b/core/src/main/java/org/matomo/java/tracking/MatomoDate.java
new file mode 100644
index 00000000..331f4923
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/MatomoDate.java
@@ -0,0 +1,69 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.matomo.java.tracking;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+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 ZonedDateTime zonedDateTime;
+
+ /**
+ * Allocates a Date object and initializes it so that it represents the time
+ * at which it was allocated, measured to the nearest millisecond.
+ */
+ @Deprecated
+ public MatomoDate() {
+ zonedDateTime = ZonedDateTime.now(ZoneOffset.UTC);
+ }
+
+ /**
+ * Allocates a Date object and initializes it to represent the specified number
+ * of milliseconds since the standard base time known as "the epoch", namely
+ * January 1, 1970, 00:00:00 GMT.
+ *
+ * @param epochMilli the milliseconds since January 1, 1970, 00:00:00 GMT.
+ */
+ @Deprecated
+ public MatomoDate(long epochMilli) {
+ zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), ZoneOffset.UTC);
+ }
+
+ /**
+ * Sets the time zone of the String that will be returned by {@link #toString()}.
+ * Defaults to UTC.
+ *
+ * @param zone the TimeZone to set
+ */
+ public void setTimeZone(ZoneId zone) {
+ zonedDateTime = zonedDateTime.withZoneSameInstant(zone);
+ }
+
+ /**
+ * Converts this datetime to the number of milliseconds from the epoch
+ * of 1970-01-01T00:00:00Z.
+ *
+ * @return the number of milliseconds since the epoch of 1970-01-01T00:00:00Z
+ * @throws ArithmeticException if numeric overflow occurs
+ */
+ public long getTime() {
+ return zonedDateTime.toInstant().toEpochMilli();
+ }
+}
diff --git a/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/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java
new file mode 100644
index 00000000..7f5ba051
--- /dev/null
+++ b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.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 lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Represents an item in an ecommerce order.
+ */
+@Builder
+@AllArgsConstructor
+@Getter
+@Setter
+public class EcommerceItem {
+
+ private String sku;
+
+ @Builder.Default
+ private String name = "";
+
+ @Builder.Default
+ private String category = "";
+
+ @Builder.Default
+ private Double price = 0.0;
+
+ @Builder.Default
+ private Integer quantity = 0;
+
+ public String toString() {
+ return 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/core/src/main/java/org/piwik/java/tracking/PiwikRequest.java b/core/src/main/java/org/piwik/java/tracking/PiwikRequest.java
new file mode 100644
index 00000000..e4d64fe4
--- /dev/null
+++ b/core/src/main/java/org/piwik/java/tracking/PiwikRequest.java
@@ -0,0 +1,36 @@
+/*
+ * Matomo Java Tracker
+ *
+ * @link https://github.com/matomo/matomo-java-tracker
+ * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause
+ */
+
+package org.piwik.java.tracking;
+
+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.
+ */
+@Deprecated
+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, 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/piwik/java/tracking/CustomVariable.java b/src/main/java/org/piwik/java/tracking/CustomVariable.java
deleted file mode 100644
index 9300ef62..00000000
--- a/src/main/java/org/piwik/java/tracking/CustomVariable.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-/**
- * A user defined custom variable.
- * @author brettcsorba
- */
-public final class CustomVariable{
- private final String key;
- private final String value;
-
- /**
- * Create a new CustomVariable
- * @param key the key of this CustomVariable
- * @param value the value of this CustomVariable
- */
- public CustomVariable(String key, String value){
- if (key == null){
- throw new NullPointerException("Key cannot be null.");
- }
- if (value == null){
- throw new NullPointerException("Value cannot be null.");
- }
- this.key = key;
- this.value = value;
- }
-
- /**
- * Get the key of this CustomVariable
- * @return the key of this CustomVariable
- */
- public String getKey() {
- return key;
- }
-
- /**
- * Get the value of this CustomVariable
- * @return the value of this CustomVariable
- */
- public String getValue() {
- return value;
- }
-}
diff --git a/src/main/java/org/piwik/java/tracking/CustomVariableList.java b/src/main/java/org/piwik/java/tracking/CustomVariableList.java
deleted file mode 100644
index 2392030c..00000000
--- a/src/main/java/org/piwik/java/tracking/CustomVariableList.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Map.Entry;
-import javax.json.Json;
-import javax.json.JsonArrayBuilder;
-import javax.json.JsonObjectBuilder;
-
-/**
- *
- * @author brettcsorba
- */
-class CustomVariableList{
- private final Map map = new HashMap<>();
-
- void add(CustomVariable cv){
- boolean found = false;
- for (Entry entry : map.entrySet()){
- if (entry.getValue().getKey().equals(cv.getKey())){
- map.put(entry.getKey(), cv);
- found = true;
- }
- }
- if (!found){
- int i = 1;
- while (map.containsKey(i)) {
- ++i;
- }
-
- map.put(i, cv);
- }
- }
-
- void add(CustomVariable cv, int index){
- if (index <= 0){
- throw new IllegalArgumentException("Index must be greater than 0.");
- }
- map.put(index, cv);
- }
-
- CustomVariable get(int index){
- if (index <= 0){
- throw new IllegalArgumentException("Index must be greater than 0.");
- }
- return map.get(index);
- }
-
- String get(String key){
- Iterator> i = map.entrySet().iterator();
- while (i.hasNext()){
- CustomVariable value = i.next().getValue();
- if (value.getKey().equals(key)){
- return value.getValue();
- }
- }
- return null;
- }
-
- void remove(int index){
- map.remove(index);
- }
-
- void remove(String key){
- Iterator> i = map.entrySet().iterator();
- while (i.hasNext()){
- Entry entry = i.next();
- if (entry.getValue().getKey().equals(key)){
- i.remove();
- }
- }
- }
-
- boolean isEmpty(){
- return map.isEmpty();
- }
-
- @Override
- public String toString(){
- JsonObjectBuilder ob = Json.createObjectBuilder();
-
- for (Entry entry : map.entrySet()){
- JsonArrayBuilder ab = Json.createArrayBuilder();
- ab.add(entry.getValue().getKey());
- ab.add(entry.getValue().getValue());
- ob.add(entry.getKey().toString(), ab);
- }
-
- return ob.build().toString();
- }
-}
diff --git a/src/main/java/org/piwik/java/tracking/EcommerceItem.java b/src/main/java/org/piwik/java/tracking/EcommerceItem.java
deleted file mode 100644
index b8225311..00000000
--- a/src/main/java/org/piwik/java/tracking/EcommerceItem.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import javax.json.Json;
-import javax.json.JsonArray;
-import javax.json.JsonArrayBuilder;
-import javax.json.JsonValue;
-
-/**
- * Represents an item in an ecommerce order.
- *
- * @author brettcsorba
- */
-public class EcommerceItem implements JsonValue{
- private String sku;
- private String name;
- private String category;
- private Double price;
- private Integer quantity;
-
- /**
- * Construct an EcommerceItem from its sku, name, category, price, and
- * quantity of the order.
- * @param sku the item's sku
- * @param name the item's name
- * @param category the item's category
- * @param price the item's price
- * @param quantity the quantity of this item in this order
- */
- public EcommerceItem(String sku, String name, String category, Double price, Integer quantity){
- this.sku = sku;
- this.name = name;
- this.category = category;
- this.price = price;
- this.quantity = quantity;
- }
-
- /**
- * Get an item's sku.
- * @return the item's sku
- */
- public String getSku(){
- return sku;
- }
-
- /**
- * Set an item's sku.
- * @param sku the sku to set
- */
- public void setSku(String sku){
- this.sku = sku;
- }
-
- /**
- * Get an item's name.
- * @return the item's name
- */
- public String getName(){
- return name;
- }
-
- /**
- * Set an item's name.
- * @param name the name to set
- */
- public void setName(String name){
- this.name = name;
- }
-
- /**
- * Get an item's category.
- * @return an item's category
- */
- public String getCategory(){
- return category;
- }
-
- /**
- * Set an item's category.
- * @param category the category to set
- */
- public void setCategory(String category){
- this.category = category;
- }
-
- /**
- * Get an item's price.
- * @return an item's price
- */
- public Double getPrice(){
- return price;
- }
-
- /**
- * Set an item's price.
- * @param price the price to set
- */
- public void setPrice(Double price){
- this.price = price;
- }
-
- /**
- * Get the quantity of this item in this order.
- * @return the quantity of this item in the order
- */
- public Integer getQuantity(){
- return quantity;
- }
-
- /**
- * Set the quantity of this item in this order
- * @param quantity the quantity of this item to set
- */
- public void setQuantity(Integer quantity){
- this.quantity = quantity;
- }
-
- /**
- * Get the JSON value type of EcommerceItem.
- * @return ValueType.ARRAY
- */
- @Override
- public ValueType getValueType(){
- return ValueType.ARRAY;
- }
-
- /**
- * Returns the value of this EcommerceItem as a JSON Array string.
- * @return this as a JSON array string
- */
- @Override
- public String toString(){
- return toJsonArray().toString();
- }
- /**
- * Returns the value of this EcommerceItem as a JsonArray.
- * @return this as a JsonArray
- */
- JsonArray toJsonArray(){
- JsonArrayBuilder ab = Json.createArrayBuilder();
- ab.add(sku);
- ab.add(name);
- ab.add(category);
- ab.add(price);
- ab.add(quantity);
-
- return ab.build();
- }
-}
diff --git a/src/main/java/org/piwik/java/tracking/PiwikDate.java b/src/main/java/org/piwik/java/tracking/PiwikDate.java
deleted file mode 100644
index bb10053e..00000000
--- a/src/main/java/org/piwik/java/tracking/PiwikDate.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.TimeZone;
-
-/**
- * A datetime object that will return the datetime in the format {@code yyyy-MM-dd hh:mm:ss}.
- *
- * @author brettcsorba
- */
-public class PiwikDate extends Date{
- SimpleDateFormat format;
-
- /**
- * Allocates a Date object and initializes it so that it represents the time
- * at which it was allocated, measured to the nearest millisecond.
- */
- public PiwikDate(){
- super();
- format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- format.setTimeZone(TimeZone.getTimeZone("UTC"));
- }
-
- /**
- * Allocates a Date object and initializes it to represent the specified number
- * of milliseconds since the standard base time known as "the epoch", namely
- * January 1, 1970, 00:00:00 GMT.
- * @param date the milliseconds since January 1, 1970, 00:00:00 GMT.
- */
- public PiwikDate(long date){
- super(date);
- format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- format.setTimeZone(TimeZone.getTimeZone("UTC"));
- }
-
- /**
- * Sets the time zone of the String that will be returned by {@link #toString()}.
- * Defaults to UTC.
- * @param zone the TimeZone to set
- */
- public void setTimeZone(TimeZone zone){
- format.setTimeZone(zone);
- }
-
- /**
- * Converts this PiwikDate object to a String of the form:
- *
- * {@code yyyy-MM-dd hh:mm:ss}.
- * @return a string representation of this PiwikDate
- */
- @Override
- public String toString(){
- return format.format(this);
- }
-}
diff --git a/src/main/java/org/piwik/java/tracking/PiwikJsonArray.java b/src/main/java/org/piwik/java/tracking/PiwikJsonArray.java
deleted file mode 100644
index 01177c43..00000000
--- a/src/main/java/org/piwik/java/tracking/PiwikJsonArray.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import java.util.ArrayList;
-import java.util.List;
-import javax.json.Json;
-import javax.json.JsonArrayBuilder;
-import javax.json.JsonValue;
-
-/**
- * Object representing a JSON array required by some Piwik query parameters.
- *
- * @author brettcsorba
- */
-public class PiwikJsonArray{
- List list = new ArrayList<>();
-
- /**
- * Get the value stored at the specified index.
- * @param index the index of the value to return
- * @return the value stored at the specified index
- */
- public JsonValue get(int index){
- return list.get(index);
- }
-
- /**
- * Add a value to the end of this array.
- * @param value value to add to the end of the array
- */
- public void add(JsonValue value){
- list.add(value);
- }
-
- /**
- * Set the value at the specified index to the specified value.
- * @param index the index of the value to set
- * @param value the value to set at the specified index
- */
- public void set(int index, JsonValue value){
- list.set(index, value);
- }
-
- /**
- * Returns a JSON encoded array string representing this object.
- * @return returns the current array as a JSON encode string
- */
- @Override
- public String toString(){
- JsonArrayBuilder ab = Json.createArrayBuilder();
-
- for (int x = 0; x < list.size(); ++x){
- JsonValue value = list.get(x);
- if (value instanceof EcommerceItem){
- ab.add(((EcommerceItem)value).toJsonArray());
- }
- else{
- ab.add(value);
- }
- }
-
- return ab.build().toString();
- }
-}
diff --git a/src/main/java/org/piwik/java/tracking/PiwikJsonObject.java b/src/main/java/org/piwik/java/tracking/PiwikJsonObject.java
deleted file mode 100644
index 505a3dde..00000000
--- a/src/main/java/org/piwik/java/tracking/PiwikJsonObject.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-import javax.json.Json;
-import javax.json.JsonArrayBuilder;
-import javax.json.JsonObjectBuilder;
-
-/**
- * Object representing the custom variable array required by some
- * Piwik query parameters. An array is represented by an object that has
- * the index of the entry as the key (1 indexed) and a 2 entry array representing
- * a custom key and custom value as the value.
- *
- * @author brettcsorba
- */
-@Deprecated
-public class PiwikJsonObject{
- Map map = new LinkedHashMap<>();
-
- /**
- * Gets the custom value stored at this custom key.
- * @param key key used to lookup value
- * @return value stored at specified key, null if not present
- */
- public String get(String key){
- return map.get(key);
- }
-
- /**
- * Returns true if this object contains no custom key-value pairs.
- * @return true if this object contains no custom key-value pairs
- */
- public boolean isEmpty(){
- return size() == 0;
- }
-
- /**
- * Puts a custom value at this custom key.
- * @param key key to store value at
- * @param value value to store at specified key
- * @return previous value stored at key if present, null otherwise
- */
- public String put(String key, String value){
- return map.put(key, value);
- }
-
- /**
- * Removes the custom value stored at this custom key.
- * @param key key used to lookup value to remove
- * @return the value that was removed, null if no value was there to remove
- */
- public String remove(String key){
- return map.remove(key);
- }
-
- /**
- * Returns the number of custom key-value pairs.
- * @return the number of custom key-value pairs
- */
- public int size(){
- return map.size();
- }
-
- /**
- * Produces the JSON string representing this object.
- *
- * For example, if the following values were put into the object
- *
- * {@code ("key1", "value1") } and {@code ("key2", "value2") }
- *
- * {@code {"1": ["key1", "value1"], "2": ["key2": "value2"]} }
- *
- * would be produced. The produced JSON will be ordered according to the
- * order the values were put in. Removing an object will cause the values
- * to backfill accordingly.
- *
- * For example, if the following values were put into the object
- *
- * {@code ("key1", "value1")}, {@code ("key2", "value2")}, and {@code ("key3", "value3")}
- *
- * and {@code ("key2", "value2") } was then removed, then
- *
- * {@code {"1": ["key1", "value1"], "2": ["key3": "value3"]} }
- *
- * would be produced.
- *
- * @return the JSON string representation of this object
- */
- @Override
- public String toString(){
- JsonObjectBuilder ob = Json.createObjectBuilder();
-
- int x = 1;
- for (Entry entry : map.entrySet()){
- JsonArrayBuilder ab = Json.createArrayBuilder();
- ab.add(entry.getKey());
- ab.add(entry.getValue());
- ob.add(Integer.toString(x++), ab);
- }
-
- return ob.build().toString();
- }
-}
diff --git a/src/main/java/org/piwik/java/tracking/PiwikLocale.java b/src/main/java/org/piwik/java/tracking/PiwikLocale.java
deleted file mode 100644
index 45f14338..00000000
--- a/src/main/java/org/piwik/java/tracking/PiwikLocale.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import java.util.Locale;
-
-/**
- * Object representing a locale required by some Piwik query parameters.
- *
- * @author brettcsorba
- */
-public class PiwikLocale{
- private Locale locale;
-
- /**
- * Create this PiwikLocale from a Locale.
- * @param locale the locale to create this object from
- */
- public PiwikLocale(Locale locale){
- this.locale = locale;
- }
-
- /**
- * Gets the locale.
- * @return the locale
- */
- public Locale getLocale(){
- return locale;
- }
-
- /**
- * Sets the locale.
- * @param locale the locale to set
- */
- public void setLocale(Locale locale){
- this.locale = locale;
- }
-
- /**
- * Returns the locale's lowercase country code.
- * @return the locale's lowercase country code
- */
- @Override
- public String toString(){
- return locale.getCountry().toLowerCase();
- }
-}
diff --git a/src/main/java/org/piwik/java/tracking/PiwikRequest.java b/src/main/java/org/piwik/java/tracking/PiwikRequest.java
deleted file mode 100644
index bf27a70e..00000000
--- a/src/main/java/org/piwik/java/tracking/PiwikRequest.java
+++ /dev/null
@@ -1,1958 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Random;
-import javax.json.JsonValue;
-import javax.xml.bind.DatatypeConverter;
-import javax.xml.bind.TypeConstraintException;
-
-/**
- * A class that implements the
- * Piwik Tracking HTTP API. These requests can be sent using {@link PiwikTracker}.
- * @author brettcsorba
- */
-public class PiwikRequest{
- 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 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 final Map parameters = new HashMap<>();
- private final Map customTrackingParameters = new HashMap<>();
-
- /**
- * 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 PiwikRequest(Integer siteId, URL 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);
- }
-
- /**
- * Get the title of the action being tracked
- * @return the title of the action being tracked
- */
-
- public String getActionName(){
- return (String)getParameter(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
- */
- public Long getActionTime(){
- return (Long)getParameter(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
- * @throws TypeConstraintException if the stored Action URL is a string
- */
- public URL getActionUrl(){
- return returnAsUrl(ACTION_URL, "Action URL", "getActionUrlAsString");
- }
-
- /**
- * Get the full URL for the current action.
- * @return the full URL
- * @throws TypeConstraintException if the stored Action URL is a URL
- */
- public String getActionUrlAsString(){
- return returnAsString(ACTION_URL, "Action URL", "getActionUrl");
- }
-
- /**
- * 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(URL 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
- */
- public void setActionUrlWithString(String actionUrl){
- setParameter(ACTION_URL, actionUrl);
- }
-
- /**
- * Get the api version
- * @return the api version
- */
- public String getApiVersion(){
- return (String)getParameter(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
- */
- public String getAuthToken(){
- return (String)getParameter(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
- */
- public String getCampaignKeyword(){
- return (String)getParameter(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
- */
- public String getCampaignName(){
- return (String)getParameter(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
- */
- public Charset getCharacterSet(){
- return (Charset)getParameter(CHARACTER_SET);
- }
-
- /**
- * The charset of the page being tracked. Specify the charset if the data
- * you send to Piwik 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
- */
- public String getContentInteraction(){
- return (String)getParameter(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
- */
- public String getContentName(){
- return (String)getParameter(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.
- */
- public String getContentPiece(){
- return (String)getParameter(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
- * @throws TypeConstraintException if the stored Content Target is a string
- */
- public URL getContentTarget(){
- return returnAsUrl(CONTENT_TARGET, "Content Target", "getContentTargetAsString");
- }
-
- /**
- * Get the content target
- * @return the target
- * @throws TypeConstraintException if the stored Content Target is a URL
- */
- public String getContentTargetAsString(){
- return returnAsString(CONTENT_TARGET, "Content Target", "getContentTarget");
- }
-
- /**
- * 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(URL 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
- */
- public void setContentTargetWithString(String contentTarget){
- setParameter(CONTENT_TARGET, contentTarget);
- }
-
- /**
- * Get the current hour.
- * @return the current hour
- */
- public Integer getCurrentHour(){
- return (Integer)getParameter(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
- */
- public Integer getCurrentMinute(){
- return (Integer)getParameter(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
- */
- public Integer getCurrentSecond(){
- return (Integer)getParameter(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);
- }
-
- /**
- * 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 getCustomTrackingParameter(String key){
- if (key == null){
- throw new NullPointerException("Key cannot be null.");
- }
- List l = customTrackingParameters.get(key);
- if (l == null){
- return new ArrayList(0);
- }
- return new ArrayList(l);
- }
-
- /**
- * Set a custom tracking parameter whose toString() value will be sent to
- * the Piwik server. These parameters are stored separately from named Piwik
- * parameters, meaning it is not possible to overwrite or clear named Piwik
- * parameters with this method. A custom parameter that has the same name
- * as a named Piwik 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
- */
- public void setCustomTrackingParameter(String key, Object value){
- if (key == null){
- throw new NullPointerException("Key cannot be null.");
- }
- if (value == null){
- customTrackingParameters.remove(key);
- }
- else{
- List l = new ArrayList();
- l.add(value);
- customTrackingParameters.put(key, l);
- }
- }
-
- /**
- * Add a custom tracking parameter to the specified key. This allows users
- * to have multiple parameters with the same name and different values,
- * commonly used during situations where list parameters are needed
- * @param key the parameter's key. Cannot be null
- * @param value the parameter's value. Cannot be null
- */
- public void addCustomTrackingParameter(String key, Object value){
- if (key == null){
- throw new NullPointerException("Key cannot be null.");
- }
- if (value == null){
- throw new NullPointerException("Cannot add a null custom tracking parameter.");
- }
- else{
- List l = customTrackingParameters.get(key);
- if (l == null){
- l = new ArrayList();
- customTrackingParameters.put(key, l);
- }
- l.add(value);
- }
- }
-
- /**
- * Removes all custom tracking parameters
- */
- public void clearCustomTrackingParameter(){
- customTrackingParameters.clear();
- }
-
- /**
- * Get the resolution of the device
- * @return the resolution
- */
- public String getDeviceResolution(){
- return (String)getParameter(DEVICE_RESOLUTION);
- }
-
- /**
- * Set the resolution of the device the visitor is using, eg 1280x1024.
- * @param deviceResolution the resolution to set. A null value will remove this parameter
- */
- public void setDeviceResolution(String deviceResolution){
- setParameter(DEVICE_RESOLUTION, deviceResolution);
- }
-
- /**
- * Get the url of a file the user had downloaded
- * @return the url
- * @throws TypeConstraintException if the stored Download URL is a String
- */
- public URL getDownloadUrl(){
- return returnAsUrl(DOWNLOAD_URL, "Download URL", "getDownloadUrlAsString");
- }
-
- /**
- * Get the url of a file the user had downloaded
- * @return the url
- * @throws TypeConstraintException if the stored Download URL is a URL
- */
- public String getDownloadUrlAsString(){
- return returnAsString(DOWNLOAD_URL, "Download URL", "getDownloadUrl");
- }
-
- /**
- * Set the url of a file the user has downloaded. Used for tracking downloads.
- * We recommend to also set the url parameter to this same value.
- * @param downloadUrl the url to set. A null value will remove this parameter
- */
- public void setDownloadUrl(URL downloadUrl){
- setParameter(DOWNLOAD_URL, downloadUrl);
- }
-
- /**
- * Set the url of a file the user has downloaded. Used for tracking downloads.
- * We recommend to also set the url parameter to this same value.
- * @param downloadUrl the url to set. A null value will remove this parameter
- */
- public void setDownloadUrlWithString(String downloadUrl){
- setParameter(DOWNLOAD_URL, downloadUrl);
- }
-
- /**
- * Sets idgoal=0 in the request to track an ecommerce interaction:
- * cart update or an ecommerce order.
- */
- public void enableEcommerce(){
- setGoalId(0);
- }
-
- /**
- * Verifies that Ecommerce has been enabled for the request. Will throw an
- * {@link IllegalStateException} if not.
- */
- public void verifyEcommerceEnabled(){
- if (getGoalId() == null || getGoalId() != 0){
- throw new IllegalStateException("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.");
- }
- }
-
- /**
- * Verifies that Ecommerce has been enabled and that Ecommerce Id and
- * Ecommerce Revenue have been set for the request. Will throw an
- * {@link IllegalStateException} if not.
- *
- */
- public void verifyEcommerceState(){
- verifyEcommerceEnabled();
- if (getEcommerceId() == null){
- throw new IllegalStateException("EcommerceId must be set before this value can be set.");
- }
- if (getEcommerceRevenue() == null){
- throw new IllegalStateException("EcommerceRevenue must be set before this value can be set.");
- }
- }
-
- /**
- * Get the discount offered.
- * @return the discount
- */
- public Double getEcommerceDiscount(){
- return (Double)getParameter(ECOMMERCE_DISCOUNT);
- }
-
- /**
- * Set the discount offered. Ecommerce must be enabled, and EcommerceId and
- * EcommerceRevenue must first be set.
- * @param discount the discount to set. A null value will remove this parameter
- */
- public void setEcommerceDiscount(Double discount){
- if (discount != null){
- verifyEcommerceState();
- }
- setParameter(ECOMMERCE_DISCOUNT, discount);
- }
-
- /**
- * Get the id of this order.
- * @return the id
- */
- public String getEcommerceId(){
- return (String)getParameter(ECOMMERCE_ID);
- }
-
- /**
- * Set the unique string identifier for the ecommerce order (required when
- * tracking an ecommerce order). Ecommerce must be enabled.
- * @param id the id to set. A null value will remove this parameter
- */
- public void setEcommerceId(String id){
- if (id != null){
- verifyEcommerceEnabled();
- }
- setParameter(ECOMMERCE_ID, id);
- }
-
- /**
- * 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
- */
- public EcommerceItem getEcommerceItem(int index){
- return (EcommerceItem)getFromJsonArray(ECOMMERCE_ITEMS, 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
- */
- public void addEcommerceItem(EcommerceItem item){
- if (item != null){
- verifyEcommerceState();
- }
- addToJsonArray(ECOMMERCE_ITEMS, item);
- }
-
- /**
- * Clears all {@link EcommerceItem} from this order.
- */
- public void clearEcommerceItems(){
- removeJsonArray(ECOMMERCE_ITEMS);
- }
-
- /**
- * Get the timestamp of the customer's last ecommerce order
- * @return the timestamp
- */
- public Long getEcommerceLastOrderTimestamp(){
- return (Long)getParameter(ECOMMERCE_LAST_ORDER_TIMESTAMP);
- }
-
- /**
- * Set the UNUX timestamp of this customer's last ecommerce order. This value
- * is used to process the "Days since last order" report. Ecommerce must be
- * enabled, and EcommerceId and EcommerceRevenue must first be set.
- * @param timestamp the timestamp to set. A null value will remove this parameter
- */
- public void setEcommerceLastOrderTimestamp(Long timestamp){
- if (timestamp != null){
- verifyEcommerceState();
- }
- setParameter(ECOMMERCE_LAST_ORDER_TIMESTAMP, timestamp);
- }
-
- /**
- * Get the grand total of the ecommerce order.
- * @return the grand total
- */
- public Double getEcommerceRevenue(){
- return (Double)getParameter(ECOMMERCE_REVENUE);
- }
-
- /**
- * Set the grand total of the ecommerce order (required when tracking an
- * ecommerce order). Ecommerce must be enabled.
- * @param revenue the grand total to set. A null value will remove this parameter
- */
- public void setEcommerceRevenue(Double revenue){
- if (revenue != null){
- verifyEcommerceEnabled();
- }
- setParameter(ECOMMERCE_REVENUE, revenue);
- }
-
- /**
- * Get the shipping cost of the ecommerce order.
- * @return the shipping cost
- */
- public Double getEcommerceShippingCost(){
- return (Double)getParameter(ECOMMERCE_SHIPPING_COST);
- }
-
- /**
- * Set the shipping cost of the ecommerce order. Ecommerce must be enabled,
- * and EcommerceId and EcommerceRevenue must first be set.
- * @param shippingCost the shipping cost to set. A null value will remove this parameter
- */
- public void setEcommerceShippingCost(Double shippingCost){
- if (shippingCost != null){
- verifyEcommerceState();
- }
- setParameter(ECOMMERCE_SHIPPING_COST, shippingCost);
- }
-
- /**
- * Get the subtotal of the ecommerce order; excludes shipping.
- * @return the subtotal
- */
- public Double getEcommerceSubtotal(){
- return (Double)getParameter(ECOMMERCE_SUBTOTAL);
- }
-
- /**
- * Set the subtotal of the ecommerce order; excludes shipping. Ecommerce
- * must be enabled and EcommerceId and EcommerceRevenue must first be set.
- * @param subtotal the subtotal to set. A null value will remove this parameter
- */
- public void setEcommerceSubtotal(Double subtotal){
- if (subtotal != null){
- verifyEcommerceState();
- }
- setParameter(ECOMMERCE_SUBTOTAL, subtotal);
- }
-
- /**
- * Get the tax amount of the ecommerce order.
- * @return the tax amount
- */
- public Double getEcommerceTax(){
- return (Double)getParameter(ECOMMERCE_TAX);
- }
-
- /**
- * Set the tax amount of the ecommerce order. Ecommerce must be enabled, and
- * EcommerceId and EcommerceRevenue must first be set.
- * @param tax the tax amount to set. A null value will remove this parameter
- */
- public void setEcommerceTax(Double tax){
- if (tax != null){
- verifyEcommerceState();
- }
- setParameter(ECOMMERCE_TAX, tax);
- }
-
- /**
- * Get the event action.
- * @return the event action
- */
- public String getEventAction(){
- return getNonEmptyStringParameter(EVENT_ACTION);
- }
-
- /**
- * Set the event action. Must not be empty. (eg. Play, Pause, Duration,
- * Add Playlist, Downloaded, Clicked...).
- * @param eventAction the event action to set. A null value will remove this parameter
- */
- public void setEventAction(String eventAction){
- setNonEmptyStringParameter(EVENT_ACTION, eventAction);
- }
-
- /**
- * Get the event category.
- * @return the event category
- */
- public String getEventCategory(){
- return getNonEmptyStringParameter(EVENT_CATEGORY);
- }
-
- /**
- * Set the event category. Must not be empty. (eg. Videos, Music, Games...).
- * @param eventCategory the event category to set. A null value will remove this parameter
- */
- public void setEventCategory(String eventCategory){
- setNonEmptyStringParameter(EVENT_CATEGORY, eventCategory);
- }
-
- /**
- * Get the event name.
- * @return the event name
- */
- public String getEventName(){
- return (String)getParameter(EVENT_NAME);
- }
-
- /**
- * Set the event name. (eg. a Movie name, or Song name, or File name...).
- * @param eventName the event name to set. A null value will remove this parameter
- */
- public void setEventName(String eventName){
- setParameter(EVENT_NAME, eventName);
- }
-
- /**
- * Get the event value.
- * @return the event value
- */
- public Number getEventValue(){
- return (Number)getParameter(EVENT_VALUE);
- }
-
- /**
- * Set the event value. Must be a float or integer value (numeric), not a string.
- * @param eventValue the event value to set. A null value will remove this parameter
- */
- public void setEventValue(Number eventValue){
- setParameter(EVENT_VALUE, eventValue);
- }
-
- /**
- * Get the goal id
- * @return the goal id
- */
- public Integer getGoalId(){
- return (Integer)getParameter(GOAL_ID);
- }
-
- /**
- * Set the goal id. If specified, the tracking request will trigger a
- * conversion for the goal of the website being tracked with this id.
- * @param goalId the goal id to set. A null value will remove this parameter
- */
- public void setGoalId(Integer goalId){
- setParameter(GOAL_ID, goalId);
- }
-
- /**
- * Get the goal revenue.
- * @return the goal revenue
- */
- public Double getGoalRevenue(){
- return (Double)getParameter(GOAL_REVENUE);
- }
-
- /**
- * Set a monetary value that was generated as revenue by this goal conversion.
- * Only used if idgoal is specified in the request.
- * @param goalRevenue the goal revenue to set. A null value will remove this parameter
- */
- public void setGoalRevenue(Double goalRevenue){
- if (goalRevenue != null && getGoalId() == null){
- throw new IllegalStateException("GoalId must be set before GoalRevenue can be set.");
- }
- setParameter(GOAL_REVENUE, goalRevenue);
- }
-
- /**
- * Get the Accept-Language HTTP header
- * @return the Accept-Language HTTP header
- */
- public String getHeaderAcceptLanguage(){
- return (String)getParameter(HEADER_ACCEPT_LANGUAGE);
- }
-
- /**
- * Set 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.
- * @param acceptLangage the Accept-Language HTTP header to set. A null value will remove this parameter
- */
- public void setHeaderAcceptLanguage(String acceptLangage){
- setParameter(HEADER_ACCEPT_LANGUAGE, acceptLangage);
- }
-
- /**
- * Get the User-Agent HTTP header
- * @return the User-Agent HTTP header
- */
- public String getHeaderUserAgent(){
- return (String)getParameter(HEADER_USER_AGENT);
- }
-
- /**
- * Set an override value for the User-Agent HTTP header field.
- * The user agent is used to detect the operating system and browser used.
- * @param userAgent the User-Agent HTTP header tos et
- */
- public void setHeaderUserAgent(String userAgent){
- setParameter(HEADER_USER_AGENT, userAgent);
- }
-
- /**
- * Get if this request will force a new visit.
- * @return true if this request will force a new visit
- */
- public Boolean getNewVisit(){
- return getBooleanParameter(NEW_VISIT);
- }
-
- /**
- * If set to true, will force a new visit to be created for this action.
- * @param newVisit if this request will force a new visit
- */
- public void setNewVisit(Boolean newVisit){
- setBooleanParameter(NEW_VISIT, newVisit);
- }
-
- /**
- * Get the outlink url
- * @return the outlink url
- * @throws TypeConstraintException if the stored Outlink URL is a String
- */
- public URL getOutlinkUrl(){
- return returnAsUrl(OUTLINK_URL, "Outlink URL", "getOutlinkUrlAsString");
- }
-
- /**
- * Get the outlink url
- * @return the outlink url
- * @throws TypeConstraintException if the stored Outlink URL is a URL
- */
- public String getOutlinkUrlAsString(){
- return returnAsString(OUTLINK_URL, "Outlink URL", "getOutlinkUrl");
- }
-
- /**
- * Set an external URL the user has opened. Used for tracking outlink clicks.
- * We recommend to also set the url parameter to this same value.
- * @param outlinkUrl the outlink url to set. A null value will remove this parameter
- */
- public void setOutlinkUrl(URL outlinkUrl){
- setParameter(OUTLINK_URL, outlinkUrl);
- }
-
- /**
- * Set an external URL the user has opened. Used for tracking outlink clicks.
- * We recommend to also set the url parameter to this same value.
- * @param outlinkUrl the outlink url to set. A null value will remove this parameter
- */
- public void setOutlinkUrlWithString(String outlinkUrl){
- setParameter(OUTLINK_URL, outlinkUrl);
- }
-
- /**
- * 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 #getPageCustomVariable(int)} method instead.
- */
- @Deprecated
- public String getPageCustomVariable(String key){
- return getCustomVariable(PAGE_CUSTOM_VARIABLE, 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
- */
- public CustomVariable getPageCustomVariable(int index){
- return getCustomVariable(PAGE_CUSTOM_VARIABLE, 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 the {@link #setPageCustomVariable(CustomVariable, int)} method instead.
- */
- @Deprecated
- public void setPageCustomVariable(String key, String value){
- if (value == null){
- removeCustomVariable(PAGE_CUSTOM_VARIABLE, key);
- } else {
- setCustomVariable(PAGE_CUSTOM_VARIABLE, new CustomVariable(key, value), null);
- }
- }
-
- /**
- * 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
- */
- public void setPageCustomVariable(CustomVariable customVariable, int index){
- setCustomVariable(PAGE_CUSTOM_VARIABLE, customVariable, index);
- }
-
- /**
- * Check if the visitor has the Director plugin.
- * @return true if visitor has the Director plugin
- */
- public Boolean getPluginDirector(){
- return getBooleanParameter(PLUGIN_DIRECTOR);
- }
-
- /**
- * Set if the visitor has the Director plugin.
- * @param director true if the visitor has the Director plugin
- */
- public void setPluginDirector(Boolean director){
- setBooleanParameter(PLUGIN_DIRECTOR, director);
- }
-
- /**
- * Check if the visitor has the Flash plugin.
- * @return true if the visitor has the Flash plugin
- */
- public Boolean getPluginFlash(){
- return getBooleanParameter(PLUGIN_FLASH);
- }
-
- /**
- * Set if the visitor has the Flash plugin.
- * @param flash true if the visitor has the Flash plugin
- */
- public void setPluginFlash(Boolean flash){
- setBooleanParameter(PLUGIN_FLASH, flash);
- }
-
- /**
- * Check if the visitor has the Gears plugin.
- * @return true if the visitor has the Gears plugin
- */
- public Boolean getPluginGears(){
- return getBooleanParameter(PLUGIN_GEARS);
- }
-
- /**
- * Set if the visitor has the Gears plugin.
- * @param gears true if the visitor has the Gears plugin
- */
- public void setPluginGears(Boolean gears){
- setBooleanParameter(PLUGIN_GEARS, gears);
- }
-
- /**
- * Check if the visitor has the Java plugin.
- * @return true if the visitor has the Java plugin
- */
- public Boolean getPluginJava(){
- return getBooleanParameter(PLUGIN_JAVA);
- }
-
- /**
- * Set if the visitor has the Java plugin.
- * @param java true if the visitor has the Java plugin
- */
- public void setPluginJava(Boolean java){
- setBooleanParameter(PLUGIN_JAVA, java);
- }
-
- /**
- * Check if the visitor has the PDF plugin.
- * @return true if the visitor has the PDF plugin
- */
- public Boolean getPluginPDF(){
- return getBooleanParameter(PLUGIN_PDF);
- }
-
- /**
- * Set if the visitor has the PDF plugin.
- * @param pdf true if the visitor has the PDF plugin
- */
- public void setPluginPDF(Boolean pdf){
- setBooleanParameter(PLUGIN_PDF, pdf);
- }
-
- /**
- * Check if the visitor has the Quicktime plugin.
- * @return true if the visitor has the Quicktime plugin
- */
- public Boolean getPluginQuicktime(){
- return getBooleanParameter(PLUGIN_QUICKTIME);
- }
-
- /**
- * Set if the visitor has the Quicktime plugin.
- * @param quicktime true if the visitor has the Quicktime plugin
- */
- public void setPluginQuicktime(Boolean quicktime){
- setBooleanParameter(PLUGIN_QUICKTIME, quicktime);
- }
-
- /**
- * Check if the visitor has the RealPlayer plugin.
- * @return true if the visitor has the RealPlayer plugin
- */
- public Boolean getPluginRealPlayer(){
- return getBooleanParameter(PLUGIN_REAL_PLAYER);
- }
-
- /**
- * Set if the visitor has the RealPlayer plugin.
- * @param realPlayer true if the visitor has the RealPlayer plugin
- */
- public void setPluginRealPlayer(Boolean realPlayer){
- setBooleanParameter(PLUGIN_REAL_PLAYER, realPlayer);
- }
-
- /**
- * Check if the visitor has the Silverlight plugin.
- * @return true if the visitor has the Silverlight plugin
- */
- public Boolean getPluginSilverlight(){
- return getBooleanParameter(PLUGIN_SILVERLIGHT);
- }
-
- /**
- * Set if the visitor has the Silverlight plugin.
- * @param silverlight true if the visitor has the Silverlight plugin
- */
- public void setPluginSilverlight(Boolean silverlight){
- setBooleanParameter(PLUGIN_SILVERLIGHT, silverlight);
- }
-
- /**
- * Check if the visitor has the Windows Media plugin.
- * @return true if the visitor has the Windows Media plugin
- */
- public Boolean getPluginWindowsMedia(){
- return getBooleanParameter(PLUGIN_WINDOWS_MEDIA);
- }
-
- /**
- * Set if the visitor has the Windows Media plugin.
- * @param windowsMedia true if the visitor has the Windows Media plugin
- */
- public void setPluginWindowsMedia(Boolean windowsMedia){
- setBooleanParameter(PLUGIN_WINDOWS_MEDIA, windowsMedia);
- }
-
- /**
- * Get the random value for this request
- * @return the random value
- */
- public String getRandomValue(){
- return (String)getParameter(RANDOM_VALUE);
- }
-
- /**
- * Set a random value that is generated before each request. Using it helps
- * avoid the tracking request being cached by the browser or a proxy.
- * @param randomValue the random value to set. A null value will remove this parameter
- */
- public void setRandomValue(String randomValue){
- setParameter(RANDOM_VALUE, randomValue);
- }
-
- /**
- * Get the referrer url
- * @return the referrer url
- * @throws TypeConstraintException if the stored Referrer URL is a String
- */
- public URL getReferrerUrl(){
- return returnAsUrl(REFERRER_URL, "Referrer URL", "getReferrerUrlAsString");
- }
-
- /**
- * Get the referrer url
- * @return the referrer url
- * @throws TypeConstraintException if the stored Referrer URL is a URL
- */
- public String getReferrerUrlAsString(){
- return returnAsString(REFERRER_URL, "Referrer URL", "getReferrerUrl");
- }
-
- /**
- * Set 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).
- * @param referrerUrl the referrer url to set. A null value will remove this parameter
- */
- public void setReferrerUrl(URL referrerUrl){
- setParameter(REFERRER_URL, referrerUrl);
- }
-
- /**
- * Set 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).
- * @param referrerUrl the referrer url to set. A null value will remove this parameter
- */
- public void setReferrerUrlWithString(String referrerUrl){
- setParameter(REFERRER_URL, referrerUrl);
- }
-
- /**
- * Get the datetime of the request
- * @return the datetime of the request
- */
- public PiwikDate getRequestDatetime(){
- return (PiwikDate)getParameter(REQUEST_DATETIME);
- }
-
- /**
- * 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 Piwik 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 datetime the datetime of the request to set. A null value will remove this parameter
- */
- public void setRequestDatetime(PiwikDate datetime){
- if (datetime != null && new Date().getTime()-datetime.getTime() > REQUEST_DATETIME_AUTH_LIMIT && getAuthToken() == null){
- throw new IllegalStateException("Because you are trying to set RequestDatetime for a time greater than 4 hours ago, AuthToken must be set first.");
- }
- setParameter(REQUEST_DATETIME, datetime);
- }
-
- /**
- * Get if this request will be tracked.
- * @return true if request will be tracked
- */
- public Boolean getRequired(){
- return getBooleanParameter(REQUIRED);
- }
-
- /**
- * Set if this request will be tracked by the Piwik server.
- * @param required true if request will be tracked
- */
- public void setRequired(Boolean required){
- setBooleanParameter(REQUIRED, required);
- }
-
- /**
- * Get if the response will be an image.
- * @return true if the response will be an an image
- */
- public Boolean getResponseAsImage(){
- return getBooleanParameter(RESPONSE_AS_IMAGE);
- }
-
- /**
- * Set if the response will be an image. If set to false, Piwik will respond
- * with a 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
- * (eg Chrome Apps). Available since Piwik 2.10.0.
-
- * @param responseAsImage true if the response will be an image
- */
- public void setResponseAsImage(Boolean responseAsImage){
- setBooleanParameter(RESPONSE_AS_IMAGE, responseAsImage);
- }
-
- /**
- * Get the search category
- * @return the search category
- */
- public String getSearchCategory(){
- return (String)getParameter(SEARCH_CATEGORY);
- }
-
- /**
- * Specify a search category with this parameter. SearchQuery must first be
- * set.
- * @param searchCategory the search category to set. A null value will remove this parameter
- */
- public void setSearchCategory(String searchCategory){
- if (searchCategory != null && getSearchQuery() == null){
- throw new IllegalStateException("SearchQuery must be set before SearchCategory can be set.");
- }
- setParameter(SEARCH_CATEGORY, searchCategory);
- }
-
- /**
- * Get the search query.
- * @return the search query
- */
- public String getSearchQuery(){
- return (String)getParameter(SEARCH_QUERY);
- }
-
- /**
- * Set the search query. When specified, the request will not be tracked as
- * a normal pageview but will instead be tracked as a Site Search request.
- * @param searchQuery the search query to set. A null value will remove this parameter
- */
- public void setSearchQuery(String searchQuery){
- setParameter(SEARCH_QUERY, searchQuery);
- }
-
- /**
- * Get the search results count.
- * @return the search results count
- */
- public Long getSearchResultsCount(){
- return (Long)getParameter(SEARCH_RESULTS_COUNT);
- }
-
- /**
- * We recommend to set the
- * search count to the number of search results displayed on the results page.
- * When keywords are tracked with {@code Search Results Count=0} they will appear in
- * the "No Result Search Keyword" report. SearchQuery must first be set.
- * @param searchResultsCount the search results count to set. A null value will remove this parameter
- */
- public void setSearchResultsCount(Long searchResultsCount){
- if (searchResultsCount != null && getSearchQuery() == null){
- throw new IllegalStateException("SearchQuery must be set before SearchResultsCount can be set.");
- }
- setParameter(SEARCH_RESULTS_COUNT, searchResultsCount);
- }
-
- /**
- * Get the id of the website we're tracking.
- * @return the id of the website
- */
- public Integer getSiteId(){
- return (Integer)getParameter(SITE_ID);
- }
-
- /**
- * Set the ID of the website we're tracking a visit/action for.
- * @param siteId the id of the website to set. A null value will remove this parameter
- */
- public void setSiteId(Integer siteId){
- setParameter(SITE_ID, siteId);
- }
-
- /**
- * Set if bot requests should be tracked
- * @return true if bot requests should be tracked
- */
- public Boolean getTrackBotRequests(){
- return getBooleanParameter(TRACK_BOT_REQUESTS);
- }
-
- /**
- * By default Piwik does not track bots. If you use the Tracking Java API,
- * you may be interested in tracking bot requests. To enable Bot Tracking in
- * Piwik, set Track Bot Requests to true.
- * @param trackBotRequests true if bot requests should be tracked
- */
- public void setTrackBotRequests(Boolean trackBotRequests){
- setBooleanParameter(TRACK_BOT_REQUESTS, trackBotRequests);
- }
-
- /**
- * 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 #getVisitCustomVariable(int)} method instead.
- */
- @Deprecated
- public String getUserCustomVariable(String key){
- return getCustomVariable(VISIT_CUSTOM_VARIABLE, 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
- */
- public CustomVariable getVisitCustomVariable(int index){
- return getCustomVariable(VISIT_CUSTOM_VARIABLE, 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 the {@link #setVisitCustomVariable(CustomVariable, int)} method instead.
- */
- @Deprecated
- public void setUserCustomVariable(String key, String value){
- if (value == null){
- removeCustomVariable(VISIT_CUSTOM_VARIABLE, key);
- } else {
- setCustomVariable(VISIT_CUSTOM_VARIABLE, new CustomVariable(key, value), null);
- }
- }
- /**
- * 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.
- */
- public void setVisitCustomVariable(CustomVariable customVariable, int index){
- setCustomVariable(VISIT_CUSTOM_VARIABLE, customVariable, index);
- }
-
- /**
- * Get the user id for this request.
- * @return the user id
- */
- public String getUserId(){
- return (String)getParameter(USER_ID);
- }
-
- /**
- * Set 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). To access this value, users must be logged-in in your
- * system so you can fetch this user id from your system, and pass it to Piwik.
- * The user id appears in the visitor log, the Visitor profile, and you can
- * Segment
- * reports for one or several user ids. 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.
- * @param userId the user id to set. A null value will remove this parameter
- */
- public void setUserId(String userId){
- setNonEmptyStringParameter(USER_ID, userId);
- }
-
- /**
- * Get the visitor's city.
- * @return the visitor's city
- */
- public String getVisitorCity(){
- return (String)getParameter(VISITOR_CITY);
- }
-
- /**
- * Set an override value for the city. The name of the city the visitor is
- * located in, eg, Tokyo. AuthToken must first be set.
- * @param city the visitor's city to set. A null value will remove this parameter
- */
- public void setVisitorCity(String city){
- if (city != null){
- verifyAuthTokenSet();
- }
- setParameter(VISITOR_CITY, city);
- }
-
- /**
- * Get the visitor's country.
- * @return the visitor's country
- */
- public PiwikLocale getVisitorCountry(){
- return (PiwikLocale)getParameter(VISITOR_COUNTRY);
- }
-
- /**
- * Set an override value for the country. AuthToken must first be set.
- * @param country the visitor's country to set. A null value will remove this parameter
- */
- public void setVisitorCountry(PiwikLocale country){
- if (country != null){
- verifyAuthTokenSet();
- }
- setParameter(VISITOR_COUNTRY, country);
- }
-
- /**
- * Get the visitor's custom id.
- * @return the visitor's custom id
- */
- public String getVisitorCustomId(){
- return (String)getParameter(VISITOR_CUSTOM_ID);
- }
-
- /**
- * Set a custom visitor ID for this request. You must set this value to exactly
- * a {@value #ID_LENGTH} character hexadecimal string (containing only characters 01234567890abcdefABCDEF).
- * We recommended to set the UserId rather than the VisitorCustomId.
- * @param visitorCustomId the visitor's custom id to set. A null value will remove this parameter
- */
- public void setVisitorCustomId(String visitorCustomId){
- if (visitorCustomId != null){
- if (visitorCustomId.length() != ID_LENGTH){
- throw new IllegalArgumentException(visitorCustomId+" is not "+ID_LENGTH+" characters long.");
- }
- // Verify visitorID is a 16 character hexadecimal string
- else if (!visitorCustomId.matches("[0-9A-Fa-f]+")){
- throw new IllegalArgumentException(visitorCustomId+" is not a hexadecimal string.");
- }
- }
- setParameter(VISITOR_CUSTOM_ID, visitorCustomId);
- }
-
- /**
- * Get the timestamp of the visitor's first visit.
- * @return the timestamp of the visitor's first visit
- */
- public Long getVisitorFirstVisitTimestamp(){
- return (Long)getParameter(VISITOR_FIRST_VISIT_TIMESTAMP);
- }
-
- /**
- * Set 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. This parameter is used to populate the
- * Goals > Days to Conversion report.
- * @param timestamp the timestamp of the visitor's first visit to set. A null value will remove this parameter
- */
- public void setVisitorFirstVisitTimestamp(Long timestamp){
- setParameter(VISITOR_FIRST_VISIT_TIMESTAMP, timestamp);
- }
-
- /**
- * Get the visitor's id.
- * @return the visitor's id
- */
- public String getVisitorId(){
- return (String)getParameter(VISITOR_ID);
- }
-
- /**
- * Set the unique visitor ID, must be a {@value #ID_LENGTH} 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 Piwik will still
- * track visits, but the unique visitors metric might be less accurate.
- * @param visitorId the visitor id to set. A null value will remove this parameter
- */
- public void setVisitorId(String visitorId){
- if (visitorId != null){
- if (visitorId.length() != ID_LENGTH){
- throw new IllegalArgumentException(visitorId+" is not "+ID_LENGTH+" characters long.");
- }
- // Verify visitorID is a 16 character hexadecimal string
- else if (!visitorId.matches("[0-9A-Fa-f]+")){
- throw new IllegalArgumentException(visitorId+" is not a hexadecimal string.");
- }
- }
- setParameter(VISITOR_ID, visitorId);
- }
-
- /**
- * Get the visitor's ip.
- * @return the visitor's ip
- */
- public String getVisitorIp(){
- return (String)getParameter(VISITOR_IP);
- }
-
- /**
- * Set the override value for the visitor IP (both IPv4 and IPv6 notations
- * supported). AuthToken must first be set.
- * @param visitorIp the visitor's ip to set. A null value will remove this parameter
- */
- public void setVisitorIp(String visitorIp){
- if (visitorIp != null){
- verifyAuthTokenSet();
- }
- setParameter(VISITOR_IP, visitorIp);
- }
-
- /**
- * Get the visitor's latitude.
- * @return the visitor's latitude
- */
- public Double getVisitorLatitude(){
- return (Double)getParameter(VISITOR_LATITUDE);
- }
-
- /**
- * Set an override value for the visitor's latitude, eg 22.456. AuthToken
- * must first be set.
- * @param latitude the visitor's latitude to set. A null value will remove this parameter
- */
- public void setVisitorLatitude(Double latitude){
- if (latitude != null){
- verifyAuthTokenSet();
- }
- setParameter(VISITOR_LATITUDE, latitude);
- }
-
- /**
- * Get the visitor's longitude.
- * @return the visitor's longitude
- */
- public Double getVisitorLongitude(){
- return (Double)getParameter(VISITOR_LONGITUDE);
- }
-
- /**
- * Set an override value for the visitor's longitude, eg 22.456. AuthToken
- * must first be set.
- * @param longitude the visitor's longitude to set. A null value will remove this parameter
- */
- public void setVisitorLongitude(Double longitude){
- if (longitude != null){
- verifyAuthTokenSet();
- }
- setParameter(VISITOR_LONGITUDE, longitude);
- }
-
- /**
- * Get the timestamp of the visitor's previous visit.
- * @return the timestamp of the visitor's previous visit
- */
- public Long getVisitorPreviousVisitTimestamp(){
- return (Long)getParameter(VISITOR_PREVIOUS_VISIT_TIMESTAMP);
- }
-
- /**
- * Set 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.
- * @param timestamp the timestamp of the visitor's previous visit to set. A null value will remove this parameter
- */
- public void setVisitorPreviousVisitTimestamp(Long timestamp){
- setParameter(VISITOR_PREVIOUS_VISIT_TIMESTAMP, timestamp);
- }
-
- /**
- * Get the visitor's region.
- * @return the visitor's region
- */
- public String getVisitorRegion(){
- return (String)getParameter(VISITOR_REGION);
- }
-
- /**
- * Set an override value for the region. Should be set to the two letter
- * region code as defined by
- * MaxMind's GeoIP databases.
- * See here
- * for a list of them for every country (the region codes are located in the
- * second column, to the left of the region name and to the right of the country
- * code).
- * @param region the visitor's region to set. A null value will remove this parameter
- */
- public void setVisitorRegion(String region){
- if (region != null){
- verifyAuthTokenSet();
- }
- setParameter(VISITOR_REGION, region);
- }
-
- /**
- * Get the count of visits for this visitor.
- * @return the count of visits for this visitor
- */
- public Integer getVisitorVisitCount(){
- return (Integer)getParameter(VISITOR_VISIT_COUNT);
- }
-
- /**
- * Set 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. This value is used to populate the report
- * Visitors > Engagement > Visits by visit number.
- * @param visitorVisitCount the count of visits for this visitor to set. A null value will remove this parameter
- */
- public void setVisitorVisitCount(Integer visitorVisitCount){
- setParameter(VISITOR_VISIT_COUNT, visitorVisitCount);
- }
-
- /**
- * Get the query string represented by this object.
- * @return the query string represented by this object
- */
-
- public String getQueryString(){
- StringBuilder sb = new StringBuilder();
- for (Entry parameter : parameters.entrySet()){
- if (sb.length() > 0){
- sb.append("&");
- }
- sb.append(parameter.getKey());
- sb.append("=");
- sb.append(parameter.getValue().toString());
- }
- for (Entry customTrackingParameter : customTrackingParameters.entrySet()){
- for (Object o : customTrackingParameter.getValue()){
- if (sb.length() > 0){
- sb.append("&");
- }
- sb.append(customTrackingParameter.getKey());
- sb.append("=");
- sb.append(o.toString());
- }
- }
-
- return sb.toString();
- }
-
- /**
- * Get the url encoded query string represented by this object.
- * @return the url encoded query string represented by this object
- */
- public String getUrlEncodedQueryString(){
- StringBuilder sb = new StringBuilder();
- for (Entry parameter : parameters.entrySet()){
- if (sb.length() > 0){
- sb.append("&");
- }
- try {
- StringBuilder sb2 = new StringBuilder();
- sb2.append(parameter.getKey());
- sb2.append("=");
- sb2.append(URLEncoder.encode(parameter.getValue().toString(), "UTF-8"));
- sb.append(sb2);
- }
- catch (UnsupportedEncodingException e) {
- System.err.println(e.getMessage());
- }
- }
- for (Entry customTrackingParameter : customTrackingParameters.entrySet()){
- for (Object o : customTrackingParameter.getValue()){
- if (sb.length() > 0){
- sb.append("&");
- }
- try {
- StringBuilder sb2 = new StringBuilder();
- sb2.append(URLEncoder.encode(customTrackingParameter.getKey(), "UTF-8"));
- sb2.append("=");
- sb2.append(URLEncoder.encode(o.toString(), "UTF-8"));
- sb.append(sb2);
- }
- catch (UnsupportedEncodingException e) {
- System.err.println(e.getMessage());
- }
- }
- }
-
- return sb.toString();
- }
-
- /**
- * Get a random hexadecimal string of a specified length.
- * @param length length of the string to produce
- * @return a random string consisting only of hexadecimal characters
- */
- public static String getRandomHexString(int length){
- byte[] bytes = new byte[length/2];
- new Random().nextBytes(bytes);
- return DatatypeConverter.printHexBinary(bytes);
- }
-
- /**
- * Get a stored parameter.
- * @param key the parameter's key
- * @return the stored parameter's value
- */
- private Object getParameter(String key){
- return parameters.get(key);
- }
-
- /**
- * Set a stored parameter.
- * @param key the parameter's key
- * @param value the parameter's value. Removes the parameter if null
- */
- private void setParameter(String key, Object value){
- if (value == null){
- parameters.remove(key);
- }
- else{
- parameters.put(key, value);
- }
- }
-
- /**
- * Get a stored parameter that is a non-empty string.
- * @param key the parameter's key
- * @return the stored parameter's value
- */
- private String getNonEmptyStringParameter(String key){
- return (String)parameters.get(key);
- }
-
- /**
- * Set a stored parameter and verify it is a non-empty string.
- * @param key the parameter's key
- * @param value the parameter's value. Cannot be the empty. Removes the parameter if null
- * string
- */
- private void setNonEmptyStringParameter(String key, String value){
- if (value == null){
- parameters.remove(key);
- }
- else if (value.length() == 0){
- throw new IllegalArgumentException("Value cannot be empty.");
- }
- else{
- parameters.put(key, value);
- }
- }
-
- /**
- * Get a stored parameter that is a boolean.
- * @param key the parameter's key
- * @return the stored parameter's value
- */
- private Boolean getBooleanParameter(String key){
- Integer i = (Integer)parameters.get(key);
- if (i == null){
- return null;
- }
- return i.equals(1);
- }
-
- /**
- * Set a stored parameter that is a boolean. This value will be stored as "1"
- * for true and "0" for false.
- * @param key the parameter's key
- * @param value the parameter's value. Removes the parameter if null
- */
- private void setBooleanParameter(String key, Boolean value){
- if (value == null){
- parameters.remove(key);
- }
- else if (value){
- parameters.put(key, 1);
- }
- else{
- parameters.put(key, 0);
- }
- }
-
- /**
- * Get a value that is stored in a json object at the specified parameter.
- * @param parameter the parameter to retrieve the json object from
- * @param key the key of the value. Cannot be null
- * @return the value
- */
- private CustomVariable getCustomVariable(String parameter, int index){
- CustomVariableList cvl = (CustomVariableList)parameters.get(parameter);
- if (cvl == null){
- return null;
- }
-
- return cvl.get(index);
- }
-
- private String getCustomVariable(String parameter, String key){
- if (key == null){
- throw new NullPointerException("Key cannot be null.");
- }
-
- CustomVariableList cvl = (CustomVariableList)parameters.get(parameter);
- if (cvl == null){
- return null;
- }
-
- return cvl.get(key);
- }
-
- /**
- * Store a value in a json object at the specified parameter.
- * @param parameter the parameter to store the json object at
- * @param key the key of the value. Cannot be null
- * @param value the value. Removes the parameter if null
- */
- private void setCustomVariable(String parameter, CustomVariable customVariable, Integer index){
- CustomVariableList cvl = (CustomVariableList)parameters.get(parameter);
- if (cvl == null){
- cvl = new CustomVariableList();
- parameters.put(parameter, cvl);
- }
-
- if (customVariable == null){
- cvl.remove(index);
- if (cvl.isEmpty()){
- parameters.remove(parameter);
- }
- }
- else if (index == null){
- cvl.add(customVariable);
- }
- else {
- cvl.add(customVariable, index);
- }
- }
-
- private void removeCustomVariable(String parameter, String key){
- if (key == null){
- throw new NullPointerException("Key cannot be null.");
- }
- CustomVariableList cvl = (CustomVariableList)parameters.get(parameter);
- if (cvl != null){
- cvl.remove(key);
- if (cvl.isEmpty()){
- parameters.remove(parameter);
- }
- }
- }
-
- /**
- * Get the value at the specified index from the json array at the specified
- * parameter.
- * @param key the key of the json array to access
- * @param index the index of the value in the json array
- * @return the value at the index in the json array
- */
- private JsonValue getFromJsonArray(String key, int index){
- PiwikJsonArray a = (PiwikJsonArray)parameters.get(key);
- if (a == null){
- return null;
- }
-
- return a.get(index);
- }
- /**
- * Add a value to the json array at the specified parameter
- * @param key the key of the json array to add to
- * @param value the value to add. Cannot be null
- */
- private void addToJsonArray(String key, JsonValue value){
- if (value == null){
- throw new NullPointerException("Value cannot be null.");
- }
-
- PiwikJsonArray a = (PiwikJsonArray)parameters.get(key);
- if (a == null){
- a = new PiwikJsonArray();
- parameters.put(key, a);
- }
- a.add(value);
- }
-
- /**
- * Removes the json array at the specified parameter
- * @param key the key of the json array to remove
- */
- private void removeJsonArray(String key){
- parameters.remove(key);
- }
-
- private URL returnAsUrl(String parameter, String name, String altMethod){
- Object obj = getParameter(parameter);
- if (obj == null){
- return null;
- }
- if (obj instanceof URL){
- return (URL)obj;
- }
- throw new TypeConstraintException("The stored " + name +
- " is a String, not a URL. Use \""+
- altMethod + "\" instead.");
- }
-
- private String returnAsString(String parameter, String name, String altMethod){
- Object obj = getParameter(parameter);
- if (obj == null){
- return null;
- }
- if (obj instanceof String){
- return (String)obj;
- }
- throw new TypeConstraintException("The stored " + name +
- " is a URL, not a String. Use \""+
- altMethod + "\" instead.");
- }
-}
diff --git a/src/main/java/org/piwik/java/tracking/PiwikTracker.java b/src/main/java/org/piwik/java/tracking/PiwikTracker.java
deleted file mode 100644
index 47d330a4..00000000
--- a/src/main/java/org/piwik/java/tracking/PiwikTracker.java
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import org.apache.http.HttpHost;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.utils.URIBuilder;
-import org.apache.http.entity.ContentType;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.HttpClientBuilder;
-import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
-import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
-import org.apache.http.impl.conn.DefaultProxyRoutePlanner;
-
-import javax.json.Json;
-import javax.json.JsonArrayBuilder;
-import javax.json.JsonObjectBuilder;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.concurrent.Future;
-
-/**
- * A class that sends {@link PiwikRequest}s to a specified Piwik server.
- * @author brettcsorba
- */
-public class PiwikTracker{
- private static final String AUTH_TOKEN = "token_auth";
- private static final String REQUESTS = "requests";
- private static final int DEFAULT_TIMEOUT = 5000;
- private final URIBuilder uriBuilder;
- private final int timeout;
- private String proxyHost = null;
- private int proxyPort = 0;
-
- /**
- * Creates a tracker that will send {@link PiwikRequest}s to the specified
- * Tracking HTTP API endpoint.
- * @param hostUrl url endpoint to send requests to. Usually in the format
- * http://your-piwik-domain.tld/piwik.php.
- */
- public PiwikTracker(String hostUrl){
- this(hostUrl, DEFAULT_TIMEOUT);
- }
-
- /**
- * Creates a tracker that will send {@link PiwikRequest}s to the specified
- * Tracking HTTP API endpoint.
- * @param hostUrl url endpoint to send requests to. Usually in the format
- * http://your-piwik-domain.tld/piwik.php.
- * @param timeout the timeout of the sent request in milliseconds
- */
- public PiwikTracker(String hostUrl, int timeout){
- uriBuilder = new URIBuilder(URI.create(hostUrl));
- this.timeout = timeout;
- }
-
- /**
- * Creates a tracker that will send {@link PiwikRequest}s to the specified
- * Tracking HTTP API endpoint via the provided proxy
- *
- * @param hostUrl url endpoint to send requests to. Usually in the format
- * http://your-piwik-domain.tld/piwik.php.
- * @param proxyHost url endpoint for the proxy
- * @param proxyPort proxy server port number
- */
- public PiwikTracker(String hostUrl, String proxyHost, int proxyPort){
- this(hostUrl, proxyHost, proxyPort, DEFAULT_TIMEOUT);
- }
-
- public PiwikTracker(String hostUrl, String proxyHost, int proxyPort, int timeout){
- uriBuilder = new URIBuilder(URI.create(hostUrl));
- this.proxyHost = proxyHost;
- this.proxyPort = proxyPort;
- this.timeout = timeout;
- }
-
- /**
- * Send a request.
- * @param request request to send
- * @return the response from this request
- * @throws IOException thrown if there was a problem with this connection
- * @deprecated use sendRequestAsync instead
- */
- @Deprecated
- public HttpResponse sendRequest(PiwikRequest request) throws IOException{
- HttpClient client = getHttpClient();
- uriBuilder.setCustomQuery(request.getQueryString());
- HttpGet get = null;
-
- try {
- get = new HttpGet(uriBuilder.build());
- return client.execute(get);
- } catch (URISyntaxException e) {
- throw new IOException(e);
- }
- }
-
- /**
- * Send a request.
- * @param request request to send
- * @return future with response from this request
- * @throws IOException thrown if there was a problem with this connection
- */
- public Future sendRequestAsync(PiwikRequest request) throws IOException{
- CloseableHttpAsyncClient client = getHttpAsyncClient();
- client.start();
- uriBuilder.setCustomQuery(request.getQueryString());
- HttpGet get = null;
-
- try {
- get = new HttpGet(uriBuilder.build());
- return client.execute(get,null);
- } catch (URISyntaxException e) {
- throw new IOException(e);
- }
- }
-
- /**
- * Send multiple requests in a single HTTP call. More efficient than sending
- * several individual requests.
- * @param requests the requests to send
- * @return the response from these requests
- * @throws IOException thrown if there was a problem with this connection
- * @deprecated use sendBulkRequestAsync instead
- */
- @Deprecated
- public HttpResponse sendBulkRequest(Iterable requests) throws IOException{
- return sendBulkRequest(requests, null);
- }
-
- /**
- * Send multiple requests in a single HTTP call. More efficient than sending
- * several individual requests.
- * @param requests the requests to send
- * @return future with response from these requests
- * @throws IOException thrown if there was a problem with this connection
- */
- public Future sendBulkRequestAsync(Iterable requests) throws IOException{
- return sendBulkRequestAsync(requests, 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
- * @return the response from these requests
- * @throws IOException thrown if there was a problem with this connection
- * @deprecated use sendBulkRequestAsync instead
- */
- @Deprecated
- public HttpResponse sendBulkRequest(Iterable requests, String authToken) throws IOException{
- if (authToken != null && authToken.length() != PiwikRequest.AUTH_TOKEN_LENGTH){
- throw new IllegalArgumentException(authToken+" is not "+PiwikRequest.AUTH_TOKEN_LENGTH+" characters long.");
- }
-
- JsonObjectBuilder ob = Json.createObjectBuilder();
- JsonArrayBuilder ab = Json.createArrayBuilder();
-
- for (PiwikRequest request : requests){
- ab.add("?"+request.getQueryString());
- }
-
- ob.add(REQUESTS, ab);
-
- if (authToken != null){
- ob.add(AUTH_TOKEN, authToken);
- }
-
- HttpClient client = getHttpClient();
- HttpPost post = null;
-
- try {
- post = new HttpPost(uriBuilder.build());
- post.setEntity(new StringEntity(ob.build().toString(),
- ContentType.APPLICATION_JSON));
- return client.execute(post);
- } catch (URISyntaxException e) {
- throw new IOException(e);
- }
- }
-
- /**
- * 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
- * @return the response from these requests
- * @throws IOException thrown if there was a problem with this connection
- */
- public Future sendBulkRequestAsync(Iterable requests, String authToken) throws IOException{
- if (authToken != null && authToken.length() != PiwikRequest.AUTH_TOKEN_LENGTH){
- throw new IllegalArgumentException(authToken+" is not "+PiwikRequest.AUTH_TOKEN_LENGTH+" characters long.");
- }
-
- JsonObjectBuilder ob = Json.createObjectBuilder();
- JsonArrayBuilder ab = Json.createArrayBuilder();
-
- for (PiwikRequest request : requests){
- ab.add("?"+request.getQueryString());
- }
-
- ob.add(REQUESTS, ab);
-
- if (authToken != null){
- ob.add(AUTH_TOKEN, authToken);
- }
-
- CloseableHttpAsyncClient client = getHttpAsyncClient();
- client.start();
- HttpPost post = null;
-
- try {
- post = new HttpPost(uriBuilder.build());
- post.setEntity(new StringEntity(ob.build().toString(),
- ContentType.APPLICATION_JSON));
- return client.execute(post,null);
- } catch (URISyntaxException e) {
- throw new IOException(e);
- }
- }
-
- /**
- * Get a HTTP client. With proxy if a proxy is provided in the constructor.
- * @return a HTTP client
- */
- protected HttpClient getHttpClient(){
-
- HttpClientBuilder builder = HttpClientBuilder.create();
-
- if(proxyHost != null && proxyPort != 0) {
- HttpHost proxy = new HttpHost(proxyHost, proxyPort);
- DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);
- builder.setRoutePlanner(routePlanner);
- }
-
- RequestConfig.Builder config = RequestConfig.custom()
- .setConnectTimeout(timeout)
- .setConnectionRequestTimeout(timeout)
- .setSocketTimeout(timeout);
-
- builder.setDefaultRequestConfig(config.build());
-
- return builder.build();
- }
-
- /**
- * Get an async HTTP client. With proxy if a proxy is provided in the constructor.
- * @return an async HTTP client
- */
- protected CloseableHttpAsyncClient getHttpAsyncClient(){
-
- HttpAsyncClientBuilder builder = HttpAsyncClientBuilder.create();
-
- if(proxyHost != null && proxyPort != 0) {
- HttpHost proxy = new HttpHost(proxyHost, proxyPort);
- DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);
- builder.setRoutePlanner(routePlanner);
- }
-
- RequestConfig.Builder config = RequestConfig.custom()
- .setConnectTimeout(timeout)
- .setConnectionRequestTimeout(timeout)
- .setSocketTimeout(timeout);
-
- builder.setDefaultRequestConfig(config.build());
-
- return builder.build();
- }
-}
diff --git a/src/test/java/org/piwik/java/tracking/CustomVariableListTest.java b/src/test/java/org/piwik/java/tracking/CustomVariableListTest.java
deleted file mode 100644
index 861400f2..00000000
--- a/src/test/java/org/piwik/java/tracking/CustomVariableListTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * To change this license header, choose License Headers in Project Properties.
- * To change this template file, choose Tools | Templates
- * and open the template in the editor.
- */
-package org.piwik.java.tracking;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- *
- * @author Katie
- */
-public class CustomVariableListTest {
- private CustomVariableList cvl;
-
- @Before
- public void setUp() {
- cvl = new CustomVariableList();
- }
-
- @Test
- public void testAdd_CustomVariable() {
- CustomVariable a = new CustomVariable("a", "b");
- CustomVariable b = new CustomVariable("c", "d");
- CustomVariable c = new CustomVariable("a", "e");
- CustomVariable d = new CustomVariable("a", "f");
-
- assertTrue(cvl.isEmpty());
- cvl.add(a);
- assertFalse(cvl.isEmpty());
- assertEquals("b", cvl.get("a"));
- assertEquals(a, cvl.get(1));
- assertEquals("{\"1\":[\"a\",\"b\"]}", cvl.toString());
-
- cvl.add(b);
- assertEquals("d", cvl.get("c"));
- assertEquals(b, cvl.get(2));
- assertEquals("{\"1\":[\"a\",\"b\"],\"2\":[\"c\",\"d\"]}", cvl.toString());
-
- cvl.add(c, 5);
- assertEquals("b", cvl.get("a"));
- assertEquals(c, cvl.get(5));
- assertNull(cvl.get(3));
- assertEquals("{\"1\":[\"a\",\"b\"],\"2\":[\"c\",\"d\"],\"5\":[\"a\",\"e\"]}", cvl.toString());
-
- cvl.add(d);
- assertEquals("f", cvl.get("a"));
- assertEquals(d, cvl.get(1));
- assertEquals(d, cvl.get(5));
- assertEquals("{\"1\":[\"a\",\"f\"],\"2\":[\"c\",\"d\"],\"5\":[\"a\",\"f\"]}", cvl.toString());
-
- cvl.remove("a");
- assertNull(cvl.get("a"));
- assertNull(cvl.get(1));
- assertNull(cvl.get(5));
- assertEquals("{\"2\":[\"c\",\"d\"]}", cvl.toString());
-
- cvl.remove(2);
- assertNull(cvl.get("c"));
- assertNull(cvl.get(2));
- assertTrue(cvl.isEmpty());
- assertEquals("{}", cvl.toString());
- }
-
- @Test
- public void testAddCustomVariableIndexLessThan1(){
- try{
- cvl.add(new CustomVariable("a", "b"), 0);
- fail("Exception should have been throw.");
- }
- catch(IllegalArgumentException e){
- assertEquals("Index must be greater than 0.", e.getLocalizedMessage());
- }
- }
-
- @Test
- public void testGetCustomVariableIntegerLessThan1(){
- try{
- cvl.get(0);
- fail("Exception should have been throw.");
- }
- catch(IllegalArgumentException e){
- assertEquals("Index must be greater than 0.", e.getLocalizedMessage());
- }
- }
-}
diff --git a/src/test/java/org/piwik/java/tracking/CustomVariableTest.java b/src/test/java/org/piwik/java/tracking/CustomVariableTest.java
deleted file mode 100644
index 28b4e475..00000000
--- a/src/test/java/org/piwik/java/tracking/CustomVariableTest.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * To change this license header, choose License Headers in Project Properties.
- * To change this template file, choose Tools | Templates
- * and open the template in the editor.
- */
-package org.piwik.java.tracking;
-
-import org.junit.Before;
-import org.junit.Test;
-import static org.junit.Assert.*;
-
-/**
- *
- * @author Katie
- */
-public class CustomVariableTest {
- private CustomVariable customVariable;
-
- @Before
- public void setUp() {
- customVariable = new CustomVariable("key", "value");
- }
-
- @Test
- public void testConstructorNullKey(){
- try{
- new CustomVariable(null, null);
- fail("Exception should have been throw.");
- }
- catch(NullPointerException e){
- assertEquals("Key cannot be null.", e.getLocalizedMessage());
- }
- }
- @Test
- public void testConstructorNullValue(){
- try{
- new CustomVariable("key", null);
- fail("Exception should have been throw.");
- }
- catch(NullPointerException e){
- assertEquals("Value cannot be null.", e.getLocalizedMessage());
- }
- }
-
- @Test
- public void testGetKey() {
- assertEquals("key", customVariable.getKey());
- }
-
- @Test
- public void testGetValue() {
- assertEquals("value", customVariable.getValue());
- }
-}
diff --git a/src/test/java/org/piwik/java/tracking/EcommerceItemTest.java b/src/test/java/org/piwik/java/tracking/EcommerceItemTest.java
deleted file mode 100644
index 4359920b..00000000
--- a/src/test/java/org/piwik/java/tracking/EcommerceItemTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import javax.json.JsonValue.ValueType;
-import org.junit.After;
-import org.junit.AfterClass;
-import static org.junit.Assert.assertEquals;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-/**
- *
- * @author brettcsorba
- */
-public class EcommerceItemTest{
- EcommerceItem ecommerceItem;
-
- public EcommerceItemTest(){
- }
-
- @BeforeClass
- public static void setUpClass(){
- }
-
- @AfterClass
- public static void tearDownClass(){
- }
-
- @Before
- public void setUp(){
- ecommerceItem = new EcommerceItem(null, null, null, null, null);
- }
-
- @After
- public void tearDown(){
- }
-
- /**
- * Test of constructor, of class EcommerceItem.
- */
- @Test
- public void testConstructor(){
- EcommerceItem ecommerceItem = new EcommerceItem("sku", "name", "category", 1.0, 1);
-
- assertEquals("sku", ecommerceItem.getSku());
- assertEquals("name", ecommerceItem.getName());
- assertEquals("category", ecommerceItem.getCategory());
- assertEquals(new Double(1.0), ecommerceItem.getPrice());
- assertEquals(new Integer(1), ecommerceItem.getQuantity());
- }
-
- /**
- * Test of getSku method, of class EcommerceItem.
- */
- @Test
- public void testGetSku(){
- ecommerceItem.setSku("sku");
-
- assertEquals("sku", ecommerceItem.getSku());
- }
-
- /**
- * Test of getName method, of class EcommerceItem.
- */
- @Test
- public void testGetName(){
- ecommerceItem.setName("name");
-
- assertEquals("name", ecommerceItem.getName());
- }
-
- /**
- * Test of getCategory method, of class EcommerceItem.
- */
- @Test
- public void testGetCategory(){
- ecommerceItem.setCategory("category");
-
- assertEquals("category", ecommerceItem.getCategory());
- }
-
- /**
- * Test of getPrice method, of class EcommerceItem.
- */
- @Test
- public void testGetPrice(){
- ecommerceItem.setPrice(1.0);
-
- assertEquals(new Double(1.0), ecommerceItem.getPrice());
- }
-
- /**
- * Test of getQuantity method, of class EcommerceItem.
- */
- @Test
- public void testGetQuantity(){
- ecommerceItem.setQuantity(1);
-
- assertEquals(new Integer(1), ecommerceItem.getQuantity());
- }
-
- /**
- * Test of getValueType method, of class EcommerceItem.
- */
- @Test
- public void testGetValueType(){
- assertEquals(ValueType.ARRAY, ecommerceItem.getValueType());
- }
-
- /**
- * Test of toString method, of class EcommerceItem.
- */
- @Test
- public void testToString(){
- ecommerceItem = new EcommerceItem("sku", "name", "category", 1.0, 1);
-
- assertEquals("[\"sku\",\"name\",\"category\",1.0,1]", ecommerceItem.toString());
- }
-
-}
diff --git a/src/test/java/org/piwik/java/tracking/PiwikDateTest.java b/src/test/java/org/piwik/java/tracking/PiwikDateTest.java
deleted file mode 100644
index 704f937e..00000000
--- a/src/test/java/org/piwik/java/tracking/PiwikDateTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import java.util.TimeZone;
-import org.junit.After;
-import org.junit.AfterClass;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-/**
- *
- * @author brettcsorba
- */
-public class PiwikDateTest{
- /**
- * Test of constructor, of class PiwikDate.
- */
- @Test
- public void testConstructor0(){
- PiwikDate date = new PiwikDate();
-
- assertNotNull(date);
- }
- @Test
- public void testConstructor1(){
- PiwikDate date = new PiwikDate(1433186085092L);
-
- assertNotNull(date);
-
- assertEquals("2015-06-01 19:14:45", date.toString());
-
- date = new PiwikDate(1467437553000L);
-
- assertEquals("2016-07-02 05:32:33", date.toString());
- }
-
- /**
- * Test of setTimeZone method, of class PiwikDate.
- */
- @Test
- public void testSetTimeZone(){
- PiwikDate date = new PiwikDate(1433186085092L);
-
- date.setTimeZone(TimeZone.getTimeZone("America/New_York"));
-
- assertEquals("2015-06-01 15:14:45", date.toString());
- }
-
-}
diff --git a/src/test/java/org/piwik/java/tracking/PiwikJsonArrayTest.java b/src/test/java/org/piwik/java/tracking/PiwikJsonArrayTest.java
deleted file mode 100644
index a3d9187a..00000000
--- a/src/test/java/org/piwik/java/tracking/PiwikJsonArrayTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import javax.json.JsonValue;
-import org.junit.After;
-import org.junit.AfterClass;
-import static org.junit.Assert.assertEquals;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import static org.mockito.Mockito.mock;
-
-/**
- *
- * @author brettcsorba
- */
-public class PiwikJsonArrayTest{
- PiwikJsonArray array;
-
- public PiwikJsonArrayTest(){
- }
-
- @BeforeClass
- public static void setUpClass(){
- }
-
- @AfterClass
- public static void tearDownClass(){
- }
-
- @Before
- public void setUp(){
- array = new PiwikJsonArray();
- }
-
- @After
- public void tearDown(){
- }
-
- /**
- * Test of get method, of class PiwikJsonArray.
- */
- @Test
- public void testAddGetSet(){
- JsonValue value = mock(JsonValue.class);
- JsonValue value2 = mock(JsonValue.class);
-
- array.add(value);
- assertEquals(value, array.get(0));
-
- array.set(0, value2);
- assertEquals(value2, array.get(0));
- }
-
- /**
- * Test of toString method, of class PiwikJsonArray.
- */
- @Test
- public void testToString(){
- array.add(JsonValue.TRUE);
- array.add(new EcommerceItem("a", "b", "c", 1.0, 2));
- array.add(JsonValue.FALSE);
-
- assertEquals("[true,[\"a\",\"b\",\"c\",1.0,2],false]", array.toString());
- }
-
-}
diff --git a/src/test/java/org/piwik/java/tracking/PiwikJsonObjectTest.java b/src/test/java/org/piwik/java/tracking/PiwikJsonObjectTest.java
deleted file mode 100644
index 0a840fce..00000000
--- a/src/test/java/org/piwik/java/tracking/PiwikJsonObjectTest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import org.junit.After;
-import org.junit.AfterClass;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-/**
- *
- * @author brettcsorba
- */
-public class PiwikJsonObjectTest{
- PiwikJsonObject obj;
-
- public PiwikJsonObjectTest(){
- }
-
- @BeforeClass
- public static void setUpClass(){
- }
-
- @AfterClass
- public static void tearDownClass(){
- }
-
- @Before
- public void setUp(){
- obj = new PiwikJsonObject();
- }
-
- @After
- public void tearDown(){
- }
-
- /**
- * Test of get method, of class PiwikJsonObject.
- */
- @Test
- public void testMethods(){
- assertTrue(obj.isEmpty());
- assertEquals(0, obj.size());
- assertNull(obj.put("key", "value"));
- assertFalse(obj.isEmpty());
- assertEquals(1, obj.size());
- assertEquals("value", obj.get("key"));
- assertEquals("value", obj.remove("key"));
- assertNull(obj.remove("key"));
- assertTrue(obj.isEmpty());
- assertEquals(0, obj.size());
- }
-
- /**
- * Test of toString method, of class PiwikJsonObject.
- */
- @Test
- public void testToString(){
- obj.put("key", "value");
- obj.put("key2", "value2");
- obj.put("key3", "value3");
- obj.remove("key2");
-
- assertEquals("{\"1\":[\"key\",\"value\"],\"2\":[\"key3\",\"value3\"]}", obj.toString());
- }
-
-}
diff --git a/src/test/java/org/piwik/java/tracking/PiwikLocaleTest.java b/src/test/java/org/piwik/java/tracking/PiwikLocaleTest.java
deleted file mode 100644
index e2d720da..00000000
--- a/src/test/java/org/piwik/java/tracking/PiwikLocaleTest.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import java.util.Locale;
-import org.junit.After;
-import org.junit.AfterClass;
-import static org.junit.Assert.assertEquals;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-/**
- *
- * @author brettcsorba
- */
-public class PiwikLocaleTest{
- PiwikLocale locale;
-
- public PiwikLocaleTest(){
- }
-
- @BeforeClass
- public static void setUpClass(){
- }
-
- @AfterClass
- public static void tearDownClass(){
- }
-
- @Before
- public void setUp(){
- locale = new PiwikLocale(Locale.US);
- }
-
- @After
- public void tearDown(){
- }
-
- /**
- * Test of getLocale method, of class PiwikLocale.
- */
- @Test
- public void testConstructor(){
- assertEquals(Locale.US, locale.getLocale());
- }
-
- /**
- * Test of setLocale method, of class PiwikLocale.
- */
- @Test
- public void testLocale(){
- locale.setLocale(Locale.GERMANY);
- assertEquals(Locale.GERMANY, locale.getLocale());
- }
-
- /**
- * Test of toString method, of class PiwikLocale.
- */
- @Test
- public void testToString(){
- assertEquals("us", locale.toString());
- }
-
-}
diff --git a/src/test/java/org/piwik/java/tracking/PiwikRequestTest.java b/src/test/java/org/piwik/java/tracking/PiwikRequestTest.java
deleted file mode 100644
index 162a5210..00000000
--- a/src/test/java/org/piwik/java/tracking/PiwikRequestTest.java
+++ /dev/null
@@ -1,1571 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import java.net.URL;
-import java.nio.charset.Charset;
-import java.util.List;
-import java.util.Locale;
-import javax.xml.bind.TypeConstraintException;
-import org.junit.After;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- *
- * @author brettcsorba
- */
-public class PiwikRequestTest{
- private PiwikRequest request;
-
- @Before
- public void setUp() throws Exception{
- request = new PiwikRequest(3, new URL("http://test.com"));
- }
-
- @After
- public void tearDown(){
- }
-
- @Test
- public void testConstructor() throws Exception{
- request = new PiwikRequest(3, new URL("http://test.com"));
- assertEquals(new Integer(3), request.getSiteId());
- assertTrue(request.getRequired());
- assertEquals(new URL("http://test.com"), request.getActionUrl());
- assertNotNull(request.getVisitorId());
- assertNotNull(request.getRandomValue());
- assertEquals("1", request.getApiVersion());
- assertFalse(request.getResponseAsImage());
- }
-
- /**
- * Test of getActionName method, of class PiwikRequest.
- */
- @Test
- public void testActionName(){
- request.setActionName("action");
- assertEquals("action", request.getActionName());
- request.setActionName(null);
- assertNull(request.getActionName());
- }
-
- /**
- * Test of getActionTime method, of class PiwikRequest.
- */
- @Test
- public void testActionTime(){
- request.setActionTime(1000L);
- assertEquals(new Long(1000L), request.getActionTime());
- }
-
- /**
- * Test of getActionUrl method, of class PiwikRequest.
- */
- @Test
- public void testActionUrl() throws Exception{
- request.setActionUrl(null);
- assertNull(request.getActionUrl());
- assertNull(request.getActionUrlAsString());
-
- URL url = new URL("http://action.com");
- request.setActionUrl(url);
- assertEquals(url, request.getActionUrl());
- try{
- request.getActionUrlAsString();
- fail("Exception should have been thrown.");
- }
- catch(TypeConstraintException e){
- assertEquals("The stored Action URL is a URL, not a String. Use \"getActionUrl\" instead.", e.getLocalizedMessage());
- }
-
- request.setActionUrlWithString(null);
- assertNull(request.getActionUrl());
- assertNull(request.getActionUrlAsString());
-
- request.setActionUrlWithString("actionUrl");
- assertEquals("actionUrl", request.getActionUrlAsString());
- try{
- request.getActionUrl();
- fail("Exception should have been thrown.");
- }
- catch(TypeConstraintException e){
- assertEquals("The stored Action URL is a String, not a URL. Use \"getActionUrlAsString\" instead.", e.getLocalizedMessage());
- }
- }
-
- /**
- * Test of getApiVersion method, of class PiwikRequest.
- */
- @Test
- public void testApiVersion(){
- request.setApiVersion("2");
- assertEquals("2", request.getApiVersion());
- }
-
- /**
- * Test of getAuthToken method, of class PiwikRequest.
- */
- @Test
- public void testAuthTokenTT(){
- try{
- request.setAuthToken("1234");
- fail("Exception should have been thrown.");
- }
- catch(IllegalArgumentException e){
- assertEquals("1234 is not 32 characters long.", e.getLocalizedMessage());
- }
- }
- @Test
- public void testAuthTokenTF(){
- request.setAuthToken("12345678901234567890123456789012");
- assertEquals("12345678901234567890123456789012", request.getAuthToken());
- }
- @Test
- public void testAuthTokenF(){
- request.setAuthToken("12345678901234567890123456789012");
- request.setAuthToken(null);
- assertNull(request.getAuthToken());
- }
-
- /**
- * Test of verifyAuthTokenSet method, of class PiwikRequest.
- */
- @Test
- public void testVerifyAuthTokenSet(){
- try{
- request.verifyAuthTokenSet();
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("AuthToken must be set before this value can be set.", e.getLocalizedMessage());
- }
- }
-
- /**
- * Test of getCampaignKeyword method, of class PiwikRequest.
- */
- @Test
- public void testCampaignKeyword(){
- request.setCampaignKeyword("keyword");
- assertEquals("keyword", request.getCampaignKeyword());
- }
-
- /**
- * Test of getCampaignName method, of class PiwikRequest.
- */
- @Test
- public void testCampaignName(){
- request.setCampaignName("name");
- assertEquals("name", request.getCampaignName());
- }
-
- /**
- * Test of getCharacterSet method, of class PiwikRequest.
- */
- @Test
- public void testCharacterSet(){
- Charset charset = Charset.defaultCharset();
- request.setCharacterSet(charset);
- assertEquals(charset, request.getCharacterSet());
- }
-
- /**
- * Test of getContentInteraction method, of class PiwikRequest.
- */
- @Test
- public void testContentInteraction(){
- request.setContentInteraction("interaction");
- assertEquals("interaction", request.getContentInteraction());
- }
-
- /**
- * Test of getContentName method, of class PiwikRequest.
- */
- @Test
- public void testContentName(){
- request.setContentName("name");
- assertEquals("name", request.getContentName());
- }
-
- /**
- * Test of getContentPiece method, of class PiwikRequest.
- */
- @Test
- public void testContentPiece(){
- request.setContentPiece("piece");
- assertEquals("piece", request.getContentPiece());
- }
-
- /**
- * Test of getContentTarget method, of class PiwikRequest.
- */
- @Test
- public void testContentTarget() throws Exception{
- URL url = new URL("http://target.com");
- request.setContentTarget(url);
- assertEquals(url, request.getContentTarget());
-
- try{
- request.getContentTargetAsString();
- fail("Exception should have been thrown.");
- }
- catch(TypeConstraintException e){
- assertEquals("The stored Content Target is a URL, not a String. Use \"getContentTarget\" instead.", e.getLocalizedMessage());
- }
-
- request.setContentTargetWithString("contentTarget");
- assertEquals("contentTarget", request.getContentTargetAsString());
-
- try{
- request.getContentTarget();
- fail("Exception should have been thrown.");
- }
- catch(TypeConstraintException e){
- assertEquals("The stored Content Target is a String, not a URL. Use \"getContentTargetAsString\" instead.", e.getLocalizedMessage());
- }
- }
-
- /**
- * Test of getCurrentHour method, of class PiwikRequest.
- */
- @Test
- public void testCurrentHour(){
- request.setCurrentHour(1);
- assertEquals(new Integer(1), request.getCurrentHour());
- }
-
- /**
- * Test of getCurrentMinute method, of class PiwikRequest.
- */
- @Test
- public void testCurrentMinute(){
- request.setCurrentMinute(2);
- assertEquals(new Integer(2), request.getCurrentMinute());
- }
-
- /**
- * Test of getCurrentSecond method, of class PiwikRequest.
- */
- @Test
- public void testCurrentSecond(){
- request.setCurrentSecond(3);
- assertEquals(new Integer(3), request.getCurrentSecond());
- }
-
- /**
- * Test of getCustomTrackingParameter method, of class PiwikRequest.
- */
- @Test
- public void testGetCustomTrackingParameter_T(){
- try{
- request.getCustomTrackingParameter(null);
- fail("Exception should have been thrown.");
- }
- catch(NullPointerException e){
- assertEquals("Key cannot be null.", e.getLocalizedMessage());
- }
- }
- @Test
- public void testGetCustomTrackingParameter_FT(){
- assertTrue(request.getCustomTrackingParameter("key").isEmpty());
- }
- @Test
- public void testSetCustomTrackingParameter_T(){
- try{
- request.setCustomTrackingParameter(null, null);
- fail("Exception should have been thrown.");
- }
- catch(NullPointerException e){
- assertEquals("Key cannot be null.", e.getLocalizedMessage());
- }
- }
- @Test
- public void testSetCustomTrackingParameter_F(){
- request.setCustomTrackingParameter("key", "value");
- List l = request.getCustomTrackingParameter("key");
- assertEquals(1, l.size());
- assertEquals("value", l.get(0));
-
- request.setCustomTrackingParameter("key", "value2");
- l = request.getCustomTrackingParameter("key");
- assertEquals(1, l.size());
- assertEquals("value2", l.get(0));
-
- request.setCustomTrackingParameter("key", null);
- l = request.getCustomTrackingParameter("key");
- assertTrue(l.isEmpty());
- }
- @Test
- public void testAddCustomTrackingParameter_T(){
- try{
- request.addCustomTrackingParameter(null, null);
- fail("Exception should have been thrown.");
- }
- catch(NullPointerException e){
- assertEquals("Key cannot be null.", e.getLocalizedMessage());
- }
- }
- @Test
- public void testAddCustomTrackingParameter_FT(){
- try{
- request.addCustomTrackingParameter("key", null);
- fail("Exception should have been thrown.");
- }
- catch(NullPointerException e){
- assertEquals("Cannot add a null custom tracking parameter.", e.getLocalizedMessage());
- }
- }
- @Test
- public void testAddCustomTrackingParameter_FF(){
- request.addCustomTrackingParameter("key", "value");
- List l = request.getCustomTrackingParameter("key");
- assertEquals(1, l.size());
- assertEquals("value", l.get(0));
-
- request.addCustomTrackingParameter("key", "value2");
- l = request.getCustomTrackingParameter("key");
- assertEquals(2, l.size());
- assertTrue(l.contains("value"));
- assertTrue(l.contains("value2"));
- }
- @Test
- public void testClearCustomTrackingParameter(){
- request.setCustomTrackingParameter("key", "value");
- request.clearCustomTrackingParameter();
- List l = request.getCustomTrackingParameter("key");
- assertTrue(l.isEmpty());
- }
-
- /**
- * Test of getDeviceResolution method, of class PiwikRequest.
- */
- @Test
- public void testDeviceResolution(){
- request.setDeviceResolution("1x2");
- assertEquals("1x2", request.getDeviceResolution());
- }
-
- /**
- * Test of getDownloadUrl method, of class PiwikRequest.
- */
- @Test
- public void testDownloadUrl() throws Exception{
- URL url = new URL("http://download.com");
- request.setDownloadUrl(url);
- assertEquals(url, request.getDownloadUrl());
-
- try{
- request.getDownloadUrlAsString();
- fail("Exception should have been thrown.");
- }
- catch(TypeConstraintException e){
- assertEquals("The stored Download URL is a URL, not a String. Use \"getDownloadUrl\" instead.", e.getLocalizedMessage());
- }
-
- request.setDownloadUrlWithString("downloadUrl");
- assertEquals("downloadUrl", request.getDownloadUrlAsString());
-
- try{
- request.getDownloadUrl();
- fail("Exception should have been thrown.");
- }
- catch(TypeConstraintException e){
- assertEquals("The stored Download URL is a String, not a URL. Use \"getDownloadUrlAsString\" instead.", e.getLocalizedMessage());
- }
- }
-
- /**
- * Test of enableEcommerce method, of class PiwikRequest.
- */
- @Test
- public void testEnableEcommerce(){
- request.enableEcommerce();
- assertEquals(new Integer(0), request.getGoalId());
- }
-
- /**
- * Test of verifyEcommerceEnabled method, of class PiwikRequest.
- */
- @Test
- public void testVerifyEcommerceEnabledT(){
- try{
- request.verifyEcommerceEnabled();
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVerifyEcommerceEnabledFT(){
- try{
- request.setGoalId(1);
- request.verifyEcommerceEnabled();
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVerifyEcommerceEnabledFF(){
- request.enableEcommerce();
- request.verifyEcommerceEnabled();
- }
-
- /**
- * Test of verifyEcommerceState method, of class PiwikRequest.
- */
- @Test
- public void testVerifyEcommerceStateE(){
- try{
- request.verifyEcommerceState();
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVerifyEcommerceStateT(){
- try{
- request.enableEcommerce();
- request.verifyEcommerceState();
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("EcommerceId must be set before this value can be set.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVerifyEcommerceStateFT(){
- try{
- request.enableEcommerce();
- request.setEcommerceId("1");
- request.verifyEcommerceState();
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("EcommerceRevenue must be set before this value can be set.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVerifyEcommerceStateFF(){
- request.enableEcommerce();
- request.setEcommerceId("1");
- request.setEcommerceRevenue(2.0);
- request.verifyEcommerceState();
- }
-
- /**
- * Test of getEcommerceDiscount method, of class PiwikRequest.
- */
- @Test
- public void testEcommerceDiscountT(){
- request.enableEcommerce();
- request.setEcommerceId("1");
- request.setEcommerceRevenue(2.0);
- request.setEcommerceDiscount(1.0);
-
- assertEquals(new Double(1.0), request.getEcommerceDiscount());
- }
- @Test
- public void testEcommerceDiscountTE(){
- try{
- request.setEcommerceDiscount(1.0);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testEcommerceDiscountF(){
- request.setEcommerceDiscount(null);
-
- assertNull(request.getEcommerceDiscount());
- }
-
- /**
- * Test of getEcommerceId method, of class PiwikRequest.
- */
- @Test
- public void testEcommerceIdT(){
- request.enableEcommerce();
- request.setEcommerceId("1");
-
- assertEquals("1", request.getEcommerceId());
- }
- @Test
- public void testEcommerceIdTE(){
- try{
- request.setEcommerceId("1");
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testEcommerceIdF(){
- request.setEcommerceId(null);
-
- assertNull(request.getEcommerceId());
- }
-
- /**
- * Test of getEcommerceItem method, of class PiwikRequest.
- */
- @Test
- public void testEcommerceItemE(){
- try{
- EcommerceItem item = new EcommerceItem("sku", "name", "category", 1.0, 2);
- request.addEcommerceItem(item);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testEcommerceItemE2(){
- try{
- request.enableEcommerce();
- request.setEcommerceId("1");
- request.setEcommerceRevenue(2.0);
- request.addEcommerceItem(null);
- fail("Exception should have been thrown.");
- }
- catch(NullPointerException e){
- assertEquals("Value cannot be null.", e.getLocalizedMessage());
- }
- }
- @Test
- public void testEcommerceItem(){
- assertNull(request.getEcommerceItem(0));
-
- EcommerceItem item = new EcommerceItem("sku", "name", "category", 1.0, 2);
- request.enableEcommerce();
- request.setEcommerceId("1");
- request.setEcommerceRevenue(2.0);
- request.addEcommerceItem(item);
-
- assertEquals(item, request.getEcommerceItem(0));
-
- request.clearEcommerceItems();
- assertNull(request.getEcommerceItem(0));
- }
-
- /**
- * Test of getEcommerceLastOrderTimestamp method, of class PiwikRequest.
- */
- @Test
- public void testEcommerceLastOrderTimestampT(){
- request.enableEcommerce();
- request.setEcommerceId("1");
- request.setEcommerceRevenue(2.0);
- request.setEcommerceLastOrderTimestamp(1000L);
-
- assertEquals(new Long(1000L), request.getEcommerceLastOrderTimestamp());
- }
- @Test
- public void testEcommerceLastOrderTimestampTE(){
- try{
- request.setEcommerceLastOrderTimestamp(1000L);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testEcommerceLastOrderTimestampF(){
- request.setEcommerceLastOrderTimestamp(null);
-
- assertNull(request.getEcommerceLastOrderTimestamp());
- }
-
- /**
- * Test of getEcommerceRevenue method, of class PiwikRequest.
- */
- @Test
- public void testEcommerceRevenueT(){
- request.enableEcommerce();
- request.setEcommerceId("1");
- request.setEcommerceRevenue(20.0);
-
- assertEquals(new Double(20.0), request.getEcommerceRevenue());
- }
- @Test
- public void testEcommerceRevenueTE(){
- try{
- request.setEcommerceRevenue(20.0);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testEcommerceRevenueF(){
- request.setEcommerceRevenue(null);
-
- assertNull(request.getEcommerceRevenue());
- }
-
- /**
- * Test of getEcommerceShippingCost method, of class PiwikRequest.
- */
- @Test
- public void testEcommerceShippingCostT(){
- request.enableEcommerce();
- request.setEcommerceId("1");
- request.setEcommerceRevenue(2.0);
- request.setEcommerceShippingCost(20.0);
-
- assertEquals(new Double(20.0), request.getEcommerceShippingCost());
- }
- @Test
- public void testEcommerceShippingCostTE(){
- try{
- request.setEcommerceShippingCost(20.0);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testEcommerceShippingCostF(){
- request.setEcommerceShippingCost(null);
-
- assertNull(request.getEcommerceShippingCost());
- }
-
- /**
- * Test of getEcommerceSubtotal method, of class PiwikRequest.
- */
- @Test
- public void testEcommerceSubtotalT(){
- request.enableEcommerce();
- request.setEcommerceId("1");
- request.setEcommerceRevenue(2.0);
- request.setEcommerceSubtotal(20.0);
-
- assertEquals(new Double(20.0), request.getEcommerceSubtotal());
- }
- @Test
- public void testEcommerceSubtotalTE(){
- try{
- request.setEcommerceSubtotal(20.0);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testEcommerceSubtotalF(){
- request.setEcommerceSubtotal(null);
-
- assertNull(request.getEcommerceSubtotal());
- }
-
- /**
- * Test of getEcommerceTax method, of class PiwikRequest.
- */
- @Test
- public void testEcommerceTaxT(){
- request.enableEcommerce();
- request.setEcommerceId("1");
- request.setEcommerceRevenue(2.0);
- request.setEcommerceTax(20.0);
-
- assertEquals(new Double(20.0), request.getEcommerceTax());
- }
- @Test
- public void testEcommerceTaxTE(){
- try{
- request.setEcommerceTax(20.0);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testEcommerceTaxF(){
- request.setEcommerceTax(null);
-
- assertNull(request.getEcommerceTax());
- }
-
- /**
- * Test of getEventAction method, of class PiwikRequest.
- */
- @Test
- public void testEventAction(){
- request.setEventAction("action");
- assertEquals("action", request.getEventAction());
- request.setEventAction(null);
- assertNull(request.getEventAction());
- }
- @Test
- public void testEventActionException(){
- try{
- request.setEventAction("");
- fail("Exception should have been thrown");
- }
- catch(IllegalArgumentException e){
- assertEquals("Value cannot be empty.", e.getLocalizedMessage());
- }
- }
-
- /**
- * Test of getEventCategory method, of class PiwikRequest.
- */
- @Test
- public void testEventCategory(){
- request.setEventCategory("category");
- assertEquals("category", request.getEventCategory());
- }
-
- /**
- * Test of getEventName method, of class PiwikRequest.
- */
- @Test
- public void testEventName(){
- request.setEventName("name");
- assertEquals("name", request.getEventName());
- }
-
-
- /**
- * Test of getEventValue method, of class PiwikRequest.
- */
- @Test
- public void testEventValue(){
- request.setEventValue(1);
- assertEquals(1, request.getEventValue());
- }
-
- /**
- * Test of getGoalId method, of class PiwikRequest.
- */
- @Test
- public void testGoalId(){
- request.setGoalId(1);
- assertEquals(new Integer(1), request.getGoalId());
- }
-
- /**
- * Test of getGoalRevenue method, of class PiwikRequest.
- */
- @Test
- public void testGoalRevenueTT(){
- try{
- request.setGoalRevenue(20.0);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("GoalId must be set before GoalRevenue can be set.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testGoalRevenueTF(){
- request.setGoalId(1);
- request.setGoalRevenue(20.0);
-
- assertEquals(new Double(20.0), request.getGoalRevenue());
- }
- @Test
- public void testGoalRevenueF(){
- request.setGoalRevenue(null);
-
- assertNull(request.getGoalRevenue());
- }
-
- /**
- * Test of getHeaderAcceptLanguage method, of class PiwikRequest.
- */
- @Test
- public void testHeaderAcceptLanguage(){
- request.setHeaderAcceptLanguage("language");
- assertEquals("language", request.getHeaderAcceptLanguage());
- }
-
- /**
- * Test of getHeaderUserAgent method, of class PiwikRequest.
- */
- @Test
- public void testHeaderUserAgent(){
- request.setHeaderUserAgent("agent");
- assertEquals("agent", request.getHeaderUserAgent());
- }
-
- /**
- * Test of getNewVisit method, of class PiwikRequest.
- */
- @Test
- public void testNewVisit(){
- request.setNewVisit(true);
- assertEquals(true, request.getNewVisit());
- request.setNewVisit(null);
- assertNull(request.getNewVisit());
- }
-
- /**
- * Test of getOutlinkUrl method, of class PiwikRequest.
- */
- @Test
- public void testOutlinkUrl() throws Exception{
- URL url = new URL("http://outlink.com");
- request.setOutlinkUrl(url);
- assertEquals(url, request.getOutlinkUrl());
-
- try{
- request.getOutlinkUrlAsString();
- fail("Exception should have been thrown.");
- }
- catch(TypeConstraintException e){
- assertEquals("The stored Outlink URL is a URL, not a String. Use \"getOutlinkUrl\" instead.", e.getLocalizedMessage());
- }
-
- request.setOutlinkUrlWithString("outlinkUrl");
- assertEquals("outlinkUrl", request.getOutlinkUrlAsString());
-
- try{
- request.getOutlinkUrl();
- fail("Exception should have been thrown.");
- }
- catch(TypeConstraintException e){
- assertEquals("The stored Outlink URL is a String, not a URL. Use \"getOutlinkUrlAsString\" instead.", e.getLocalizedMessage());
- }
- }
-
- /**
- * Test of getPageCustomVariable method, of class PiwikRequest.
- */
- @Test
- public void testPageCustomVariableStringStringE(){
- try{
- request.setPageCustomVariable(null, null);
- fail("Exception should have been thrown");
- }
- catch(NullPointerException e){
- assertEquals("Key cannot be null.", e.getLocalizedMessage());
- }
- }
- @Test
- public void testPageCustomVariableStringStringE2(){
- try{
- request.setPageCustomVariable(null, "pageVal");
- fail("Exception should have been thrown");
- }
- catch(NullPointerException e){
- assertEquals("Key cannot be null.", e.getLocalizedMessage());
- }
- }
- @Test
- public void testPageCustomVariableStringStringE3(){
- try{
- request.getPageCustomVariable(null);
- fail("Exception should have been thrown");
- }
- catch(NullPointerException e){
- assertEquals("Key cannot be null.", e.getLocalizedMessage());
- }
- }
- @Test
- public void testPageCustomVariableStringString(){
- assertNull(request.getPageCustomVariable("pageKey"));
- request.setPageCustomVariable("pageKey", "pageVal");
- assertEquals("pageVal", request.getPageCustomVariable("pageKey"));
- request.setPageCustomVariable("pageKey", null);
- assertNull(request.getPageCustomVariable("pageKey"));
- request.setPageCustomVariable("pageKey", "pageVal");
- assertEquals("pageVal", request.getPageCustomVariable("pageKey"));
- }
- @Test
- public void testPageCustomVariableCustomVariable(){
- assertNull(request.getPageCustomVariable(1));
- CustomVariable cv = new CustomVariable("pageKey", "pageVal");
- request.setPageCustomVariable(cv, 1);
- assertEquals(cv, request.getPageCustomVariable(1));
- request.setPageCustomVariable(null, 1);
- assertNull(request.getPageCustomVariable(1));
- request.setPageCustomVariable(cv, 2);
- assertEquals(cv, request.getPageCustomVariable(2));
- }
-
- /**
- * Test of getPluginDirector method, of class PiwikRequest.
- */
- @Test
- public void testPluginDirector(){
- request.setPluginDirector(true);
- assertEquals(true, request.getPluginDirector());
- }
-
- /**
- * Test of getPluginFlash method, of class PiwikRequest.
- */
- @Test
- public void testPluginFlash(){
- request.setPluginFlash(true);
- assertEquals(true, request.getPluginFlash());
- }
-
- /**
- * Test of getPluginGears method, of class PiwikRequest.
- */
- @Test
- public void testPluginGears(){
- request.setPluginGears(true);
- assertEquals(true, request.getPluginGears());
- }
-
- /**
- * Test of getPluginJava method, of class PiwikRequest.
- */
- @Test
- public void testPluginJava(){
- request.setPluginJava(true);
- assertEquals(true, request.getPluginJava());
- }
-
- /**
- * Test of getPluginPDF method, of class PiwikRequest.
- */
- @Test
- public void testPluginPDF(){
- request.setPluginPDF(true);
- assertEquals(true, request.getPluginPDF());
- }
-
- /**
- * Test of getPluginQuicktime method, of class PiwikRequest.
- */
- @Test
- public void testPluginQuicktime(){
- request.setPluginQuicktime(true);
- assertEquals(true, request.getPluginQuicktime());
- }
-
- /**
- * Test of getPluginRealPlayer method, of class PiwikRequest.
- */
- @Test
- public void testPluginRealPlayer(){
- request.setPluginRealPlayer(true);
- assertEquals(true, request.getPluginRealPlayer());
- }
-
- /**
- * Test of getPluginSilverlight method, of class PiwikRequest.
- */
- @Test
- public void testPluginSilverlight(){
- request.setPluginSilverlight(true);
- assertEquals(true, request.getPluginSilverlight());
- }
-
- /**
- * Test of getPluginWindowsMedia method, of class PiwikRequest.
- */
- @Test
- public void testPluginWindowsMedia(){
- request.setPluginWindowsMedia(true);
- assertEquals(true, request.getPluginWindowsMedia());
- }
-
- /**
- * Test of getRandomValue method, of class PiwikRequest.
- */
- @Test
- public void testRandomValue(){
- request.setRandomValue("value");
- assertEquals("value", request.getRandomValue());
- }
-
- /**
- * Test of setReferrerUrl method, of class PiwikRequest.
- */
- @Test
- public void testReferrerUrl() throws Exception{
- URL url = new URL("http://referrer.com");
- request.setReferrerUrl(url);
- assertEquals(url, request.getReferrerUrl());
-
- try{
- request.getReferrerUrlAsString();
- fail("Exception should have been thrown.");
- }
- catch(TypeConstraintException e){
- assertEquals("The stored Referrer URL is a URL, not a String. Use \"getReferrerUrl\" instead.", e.getLocalizedMessage());
- }
-
- request.setReferrerUrlWithString("referrerUrl");
- assertEquals("referrerUrl", request.getReferrerUrlAsString());
-
- try{
- request.getReferrerUrl();
- fail("Exception should have been thrown.");
- }
- catch(TypeConstraintException e){
- assertEquals("The stored Referrer URL is a String, not a URL. Use \"getReferrerUrlAsString\" instead.", e.getLocalizedMessage());
- }
- }
-
- /**
- * Test of getRequestDatetime method, of class PiwikRequest.
- */
- @Test
- public void testRequestDatetimeTTT(){
- request.setAuthToken("12345678901234567890123456789012");
- PiwikDate date = new PiwikDate(1000L);
- request.setRequestDatetime(date);
-
- assertEquals(date, request.getRequestDatetime());
- }
- @Test
- public void testRequestDatetimeTTF(){
- try{
- PiwikDate date = new PiwikDate(1000L);
- request.setRequestDatetime(date);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("Because you are trying to set RequestDatetime for a time greater than 4 hours ago, AuthToken must be set first.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testRequestDatetimeTF(){
- PiwikDate date = new PiwikDate();
- request.setRequestDatetime(date);
- assertEquals(date, request.getRequestDatetime());
- }
- @Test
- public void testRequestDatetimeF(){
- PiwikDate date = new PiwikDate();
- request.setRequestDatetime(date);
- request.setRequestDatetime(null);
- assertNull(request.getRequestDatetime());
- }
-
- /**
- * Test of getRequired method, of class PiwikRequest.
- */
- @Test
- public void testRequired(){
- request.setRequired(false);
- assertEquals(false, request.getRequired());
- }
-
- /**
- * Test of getResponseAsImage method, of class PiwikRequest.
- */
- @Test
- public void testResponseAsImage(){
- request.setResponseAsImage(true);
- assertEquals(true, request.getResponseAsImage());
- }
-
- /**
- * Test of getSearchCategory method, of class PiwikRequest.
- */
- @Test
- public void testSearchCategoryTT(){
- try{
- request.setSearchCategory("category");
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("SearchQuery must be set before SearchCategory can be set.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testSearchCategoryTF(){
- request.setSearchQuery("query");
- request.setSearchCategory("category");
- assertEquals("category", request.getSearchCategory());
- }
- @Test
- public void testSearchCategoryF(){
- request.setSearchCategory(null);
- assertNull(request.getSearchCategory());
- }
-
- /**
- * Test of getSearchQuery method, of class PiwikRequest.
- */
- @Test
- public void testSearchQuery(){
- request.setSearchQuery("query");
- assertEquals("query", request.getSearchQuery());
- }
-
- /**
- * Test of getSearchResultsCount method, of class PiwikRequest.
- */
- @Test
- public void testSearchResultsCountTT(){
- try{
- request.setSearchResultsCount(100L);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("SearchQuery must be set before SearchResultsCount can be set.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testSearchResultsCountTF(){
- request.setSearchQuery("query");
- request.setSearchResultsCount(100L);
- assertEquals(new Long(100L), request.getSearchResultsCount());
- }
- @Test
- public void testSearchResultsCountF(){
- request.setSearchResultsCount(null);
- assertNull(request.getSearchResultsCount());
- }
-
- /**
- * Test of getSiteId method, of class PiwikRequest.
- */
- @Test
- public void testSiteId(){
- request.setSiteId(2);
- assertEquals(new Integer(2), request.getSiteId());
- }
-
- /**
- * Test of setTrackBotRequest method, of class PiwikRequest.
- */
- @Test
- public void testTrackBotRequests(){
- request.setTrackBotRequests(true);
- assertEquals(true, request.getTrackBotRequests());
- }
-
-
- /**
- * Test of getUserrCustomVariable method, of class PiwikRequest.
- */
- @Test
- public void testUserCustomVariableStringString(){
- request.setUserCustomVariable("userKey", "userValue");
- assertEquals("userValue", request.getUserCustomVariable("userKey"));
- }
- @Test
- public void testVisitCustomVariableCustomVariable(){
- request.setRandomValue("random");
- request.setVisitorId("1234567890123456");
-
- assertNull(request.getVisitCustomVariable(1));
- CustomVariable cv = new CustomVariable("visitKey", "visitVal");
- request.setVisitCustomVariable(cv, 1);
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_cvar={\"1\":[\"visitKey\",\"visitVal\"]}&_id=1234567890123456&url=http://test.com", request.getQueryString());
-
- request.setUserCustomVariable("key", "val");
- assertEquals(cv, request.getVisitCustomVariable(1));
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_cvar={\"1\":[\"visitKey\",\"visitVal\"],\"2\":[\"key\",\"val\"]}&_id=1234567890123456&url=http://test.com", request.getQueryString());
-
- request.setVisitCustomVariable(null, 1);
- assertNull(request.getVisitCustomVariable(1));
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_cvar={\"2\":[\"key\",\"val\"]}&_id=1234567890123456&url=http://test.com", request.getQueryString());
-
- request.setVisitCustomVariable(cv, 2);
- assertEquals(cv, request.getVisitCustomVariable(2));
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_cvar={\"2\":[\"visitKey\",\"visitVal\"]}&_id=1234567890123456&url=http://test.com", request.getQueryString());
-
- request.setUserCustomVariable("visitKey", null);
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http://test.com", request.getQueryString());
- }
-
- /**
- * Test of getUserId method, of class PiwikRequest.
- */
- @Test
- public void testUserId(){
- request.setUserId("id");
- assertEquals("id", request.getUserId());
- }
-
- /**
- * Test of getVisitorCity method, of class PiwikRequest.
- */
- @Test
- public void testVisitorCityT(){
- request.setAuthToken("12345678901234567890123456789012");
- request.setVisitorCity("city");
- assertEquals("city", request.getVisitorCity());
- }
- @Test
- public void testVisitorCityTE(){
- try{
- request.setVisitorCity("city");
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("AuthToken must be set before this value can be set.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVisitorCityF(){
- request.setVisitorCity(null);
- assertNull(request.getVisitorCity());
- }
-
- /**
- * Test of getVisitorCountry method, of class PiwikRequest.
- */
- @Test
- public void testVisitorCountryT(){
- PiwikLocale country = new PiwikLocale(Locale.US);
- request.setAuthToken("12345678901234567890123456789012");
- request.setVisitorCountry(country);
-
- assertEquals(country, request.getVisitorCountry());
- }
- @Test
- public void testVisitorCountryTE(){
- try{
- PiwikLocale country = new PiwikLocale(Locale.US);
- request.setVisitorCountry(country);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("AuthToken must be set before this value can be set.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVisitorCountryF(){
- request.setVisitorCountry(null);
-
- assertNull(request.getVisitorCountry());
- }
-
- /**
- * Test of getVisitorCustomId method, of class PiwikRequest.
- */
- @Test
- public void testVisitorCustomTT(){
- try{
- request.setVisitorCustomId("1");
- fail("Exception should have been thrown.");
- }
- catch(IllegalArgumentException e){
- assertEquals("1 is not 16 characters long.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVisitorCustomTFT(){
- try{
- request.setVisitorCustomId("1234567890abcdeg");
- fail("Exception should have been thrown.");
- }
- catch(IllegalArgumentException e){
- assertEquals("1234567890abcdeg is not a hexadecimal string.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVisitorCustomIdTFF(){
- request.setVisitorCustomId("1234567890abcdef");
- assertEquals("1234567890abcdef", request.getVisitorCustomId());
- }
- @Test
- public void testVisitorCustomIdF(){
- request.setVisitorCustomId("1234567890abcdef");
- request.setVisitorCustomId(null);
- assertNull(request.getVisitorCustomId());
- }
-
- /**
- * Test of getVisitorFirstVisitTimestamp method, of class PiwikRequest.
- */
- @Test
- public void testVisitorFirstVisitTimestamp(){
- request.setVisitorFirstVisitTimestamp(1000L);
- assertEquals(new Long(1000L), request.getVisitorFirstVisitTimestamp());
- }
-
- /**
- * Test of getVisitorId method, of class PiwikRequest.
- */
- @Test
- public void testVisitorIdTT(){
- try{
- request.setVisitorId("1");
- fail("Exception should have been thrown.");
- }
- catch(IllegalArgumentException e){
- assertEquals("1 is not 16 characters long.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVisitorIdTFT(){
- try{
- request.setVisitorId("1234567890abcdeg");
- fail("Exception should have been thrown.");
- }
- catch(IllegalArgumentException e){
- assertEquals("1234567890abcdeg is not a hexadecimal string.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVisitorIdTFF(){
- request.setVisitorId("1234567890abcdef");
- assertEquals("1234567890abcdef", request.getVisitorId());
- }
- @Test
- public void testVisitorIdF(){
- request.setVisitorId("1234567890abcdef");
- request.setVisitorId(null);
- assertNull(request.getVisitorId());
- }
-
- /**
- * Test of getVisitorIp method, of class PiwikRequest.
- */
- @Test
- public void testVisitorIpT(){
- request.setAuthToken("12345678901234567890123456789012");
- request.setVisitorIp("ip");
- assertEquals("ip", request.getVisitorIp());
- }
- @Test
- public void testVisitorIpTE(){
- try{
- request.setVisitorIp("ip");
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("AuthToken must be set before this value can be set.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVisitorIpF(){
- request.setVisitorIp(null);
- assertNull(request.getVisitorIp());
- }
-
- /**
- * Test of getVisitorLatitude method, of class PiwikRequest.
- */
- @Test
- public void testVisitorLatitudeT(){
- request.setAuthToken("12345678901234567890123456789012");
- request.setVisitorLatitude(10.5);
- assertEquals(new Double(10.5), request.getVisitorLatitude());
- }
- @Test
- public void testVisitorLatitudeTE(){
- try{
- request.setVisitorLatitude(10.5);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("AuthToken must be set before this value can be set.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVisitorLatitudeF(){
- request.setVisitorLatitude(null);
- assertNull(request.getVisitorLatitude());
- }
-
- /**
- * Test of getVisitorLongitude method, of class PiwikRequest.
- */
- @Test
- public void testVisitorLongitudeT(){
- request.setAuthToken("12345678901234567890123456789012");
- request.setVisitorLongitude(20.5);
- assertEquals(new Double(20.5), request.getVisitorLongitude());
- }
- @Test
- public void testVisitorLongitudeTE(){
- try{
- request.setVisitorLongitude(20.5);
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("AuthToken must be set before this value can be set.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVisitorLongitudeF(){
- request.setVisitorLongitude(null);
- assertNull(request.getVisitorLongitude());
- }
-
- /**
- * Test of getVisitorPreviousVisitTimestamp method, of class PiwikRequest.
- */
- @Test
- public void testVisitorPreviousVisitTimestamp(){
- request.setVisitorPreviousVisitTimestamp(1000L);
- assertEquals(new Long(1000L), request.getVisitorPreviousVisitTimestamp());
- }
-
- /**
- * Test of getVisitorRegion method, of class PiwikRequest.
- */
- @Test
- public void testVisitorRegionT(){
- request.setAuthToken("12345678901234567890123456789012");
- request.setVisitorRegion("region");
-
- assertEquals("region", request.getVisitorRegion());
- }
- @Test
- public void testGetVisitorRegionTE(){
- try{
- request.setVisitorRegion("region");
- fail("Exception should have been thrown.");
- }
- catch(IllegalStateException e){
- assertEquals("AuthToken must be set before this value can be set.",
- e.getLocalizedMessage());
- }
- }
- @Test
- public void testVisitorRegionF(){
- request.setVisitorRegion(null);
-
- assertNull(request.getVisitorRegion());
- }
-
- /**
- * Test of getVisitorVisitCount method, of class PiwikRequest.
- */
- @Test
- public void testVisitorVisitCount(){
- request.setVisitorVisitCount(100);
- assertEquals(new Integer(100), request.getVisitorVisitCount());
- }
-
- /**
- * Test of getQueryString method, of class PiwikRequest.
- */
- @Test
- public void testGetQueryString(){
- request.setRandomValue("random");
- request.setVisitorId("1234567890123456");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http://test.com", request.getQueryString());
- request.setPageCustomVariable("key", "val");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&cvar={\"1\":[\"key\",\"val\"]}&_id=1234567890123456&url=http://test.com",
- request.getQueryString());
- request.setPageCustomVariable("key", null);
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http://test.com", request.getQueryString());
- request.addCustomTrackingParameter("key", "test");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http://test.com&key=test", request.getQueryString());
- request.addCustomTrackingParameter("key", "test2");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http://test.com&key=test&key=test2", request.getQueryString());
- request.setCustomTrackingParameter("key2", "test3");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http://test.com&key2=test3&key=test&key=test2", request.getQueryString());
- request.setCustomTrackingParameter("key", "test4");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http://test.com&key2=test3&key=test4", request.getQueryString());
- request.setRandomValue(null);
- request.setSiteId(null);
- request.setRequired(null);
- request.setApiVersion(null);
- request.setResponseAsImage(null);
- request.setVisitorId(null);
- request.setActionUrl(null);
- assertEquals("key2=test3&key=test4", request.getQueryString());
- request.clearCustomTrackingParameter();
- assertEquals("", request.getQueryString());
- }
- @Test
- public void testGetQueryString2(){
- request.setActionUrlWithString("http://test.com");
- request.setRandomValue("random");
- request.setVisitorId("1234567890123456");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http://test.com", request.getQueryString());
- }
-
- /**
- * Test of getUrlEncodedQueryString method, of class PiwikRequest.
- */
- @Test
- public void testGetUrlEncodedQueryString(){
- request.setRandomValue("random");
- request.setVisitorId("1234567890123456");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString());
- request.addCustomTrackingParameter("ke/y", "te:st");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http%3A%2F%2Ftest.com&ke%2Fy=te%3Ast", request.getUrlEncodedQueryString());
- request.addCustomTrackingParameter("ke/y", "te:st2");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http%3A%2F%2Ftest.com&ke%2Fy=te%3Ast&ke%2Fy=te%3Ast2", request.getUrlEncodedQueryString());
- request.setCustomTrackingParameter("ke/y2", "te:st3");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http%3A%2F%2Ftest.com&ke%2Fy=te%3Ast&ke%2Fy=te%3Ast2&ke%2Fy2=te%3Ast3", request.getUrlEncodedQueryString());
- request.setCustomTrackingParameter("ke/y", "te:st4");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http%3A%2F%2Ftest.com&ke%2Fy=te%3Ast4&ke%2Fy2=te%3Ast3", request.getUrlEncodedQueryString());
- request.setRandomValue(null);
- request.setSiteId(null);
- request.setRequired(null);
- request.setApiVersion(null);
- request.setResponseAsImage(null);
- request.setVisitorId(null);
- request.setActionUrl(null);
- assertEquals("ke%2Fy=te%3Ast4&ke%2Fy2=te%3Ast3", request.getUrlEncodedQueryString());
- request.clearCustomTrackingParameter();
- assertEquals("", request.getUrlEncodedQueryString());
- }
- @Test
- public void testGetUrlEncodedQueryString2(){
- request.setActionUrlWithString("http://test.com");
- request.setRandomValue("random");
- request.setVisitorId("1234567890123456");
- assertEquals("rand=random&idsite=3&rec=1&apiv=1&send_image=0&_id=1234567890123456&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString());
- }
-
- /**
- * Test of getRandomHexString method, of class PiwikRequest.
- */
- @Test
- public void testGetRandomHexString(){
- String s = PiwikRequest.getRandomHexString(10);
-
- assertEquals(10, s.length());
- Long.parseLong(s, 16);
- }
-}
diff --git a/src/test/java/org/piwik/java/tracking/PiwikTrackerTest.java b/src/test/java/org/piwik/java/tracking/PiwikTrackerTest.java
deleted file mode 100644
index e4589287..00000000
--- a/src/test/java/org/piwik/java/tracking/PiwikTrackerTest.java
+++ /dev/null
@@ -1,369 +0,0 @@
-/*
- * Piwik Java Tracker
- *
- * @link https://github.com/piwik/piwik-java-tracker
- * @license https://github.com/piwik/piwik-java-tracker/blob/master/LICENSE BSD-3 Clause
- */
-package org.piwik.java.tracking;
-
-import com.sun.net.httpserver.HttpExchange;
-import com.sun.net.httpserver.HttpHandler;
-import com.sun.net.httpserver.HttpServer;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
-import org.apache.http.util.EntityUtils;
-import org.junit.*;
-import org.mockito.ArgumentMatcher;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.InetSocketAddress;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.Future;
-
-import static org.junit.Assert.*;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Matchers.argThat;
-import static org.mockito.Mockito.*;
-
-/**
- * @author brettcsorba
- */
-public class PiwikTrackerTest {
- // https://stackoverflow.com/a/3732328
- static class Handler implements HttpHandler {
- @Override
- public void handle(HttpExchange exchange) throws IOException {
- String response = "OK";
- exchange.sendResponseHeaders(200, response.length());
- OutputStream os = exchange.getResponseBody();
- os.write(response.getBytes());
- os.close();
- }
- }
-
- PiwikTracker piwikTracker;
- PiwikTracker localTracker;
- HttpServer server;
-
- public PiwikTrackerTest() {
- }
-
- @BeforeClass
- public static void setUpClass() {
- }
-
- @AfterClass
- public static void tearDownClass() {
- }
-
- @Before
- public void setUp() {
- // test with mocks
- piwikTracker = spy(new PiwikTracker("http://test.com"));
-
- // test with local server
- localTracker = new PiwikTracker("http://localhost:8001/test");
- try {
- server = HttpServer.create(new InetSocketAddress(8001), 0);
- server.createContext("/test", new Handler());
- server.setExecutor(null); // creates a default executor
- server.start();
- } catch (IOException ex) {
- }
- }
-
- @After
- public void tearDown() {
- server.stop(0);
- }
-
- /**
- * Test of addParameter method, of class PiwikTracker.
- */
- @Test
- public void testAddParameter() {
- }
-
- /**
- * Test of sendRequest method, of class PiwikTracker.
- */
- @Test
- public void testSendRequest() throws Exception {
- PiwikRequest request = mock(PiwikRequest.class);
- HttpClient client = mock(HttpClient.class);
- HttpResponse response = mock(HttpResponse.class);
-
- doReturn(client).when(piwikTracker).getHttpClient();
- doReturn("query").when(request).getQueryString();
- doReturn(response).when(client)
- .execute(argThat(new CorrectGetRequest("http://test.com?query")));
-
- assertEquals(response, piwikTracker.sendRequest(request));
- }
-
- /**
- * Test of sendRequestAsync method, of class PiwikTracker.
- */
- @Test
- public void testSendRequestAsync() throws Exception {
- PiwikRequest request = mock(PiwikRequest.class);
- CloseableHttpAsyncClient client = mock(CloseableHttpAsyncClient.class);
- HttpResponse response = mock(HttpResponse.class);
- Future future = mock(Future.class);
-
- doReturn(client).when(piwikTracker).getHttpAsyncClient();
- doReturn("query").when(request).getQueryString();
- doReturn(response).when(future).get();
- doReturn(true).when(future).isDone();
- doReturn(future).when(client)
- .execute(argThat(new CorrectGetRequest("http://test.com?query")), any());
-
- assertEquals(response, piwikTracker.sendRequestAsync(request).get());
- }
-
- /**
- * Test sync API with local server
- */
- @Test
- public void testWithLocalServer() throws Exception {
- // one
- PiwikRequest request = new PiwikRequest(3, new URL("http://test.com"));
- HttpResponse response = localTracker.sendRequest(request);
- String msg = EntityUtils.toString(response.getEntity());
- assertEquals("OK", msg);
-
- // bulk
- List requests = Collections.singletonList(request);
- HttpResponse responseBulk = localTracker.sendBulkRequest(requests);
- String msgBulk = EntityUtils.toString(responseBulk.getEntity());
- assertEquals("OK", msgBulk);
- }
-
- /**
- * Test async API with local server
- */
- @Test
- public void testWithLocalServerAsync() throws Exception {
- // one
- PiwikRequest request = new PiwikRequest(3, new URL("http://test.com"));
- HttpResponse response = localTracker.sendRequestAsync(request).get();
- String msg = EntityUtils.toString(response.getEntity());
- assertEquals("OK", msg);
-
- // bulk
- List requests = Collections.singletonList(request);
- HttpResponse responseBulk = localTracker.sendBulkRequestAsync(requests).get();
- String msgBulk = EntityUtils.toString(responseBulk.getEntity());
- assertEquals("OK", msgBulk);
- }
-
- static class CorrectGetRequest implements ArgumentMatcher {
- String url;
-
- public CorrectGetRequest(String url) {
- this.url = url;
- }
-
- @Override
- public boolean matches(HttpGet get) {
- return url.equals(get.getURI().toString());
- }
- }
-
- /**
- * Test of sendBulkRequest method, of class PiwikTracker.
- */
- @Test
- public void testSendBulkRequest_Iterable() throws Exception {
- List requests = new ArrayList<>();
- HttpResponse response = mock(HttpResponse.class);
-
- doReturn(response).when(piwikTracker).sendBulkRequest(requests, null);
-
- assertEquals(response, piwikTracker.sendBulkRequest(requests));
- }
-
- /**
- * Test of sendBulkRequest method, of class PiwikTracker.
- */
- @Test
- public void testSendBulkRequest_Iterable_StringTT() throws Exception {
- try {
- List requests = new ArrayList<>();
- HttpClient client = mock(HttpClient.class);
- PiwikRequest request = mock(PiwikRequest.class);
-
- doReturn("query").when(request).getQueryString();
- requests.add(request);
- doReturn(client).when(piwikTracker).getHttpClient();
-
- piwikTracker.sendBulkRequest(requests, "1");
- fail("Exception should have been thrown.");
- } catch (IllegalArgumentException e) {
- assertEquals("1 is not 32 characters long.", e.getLocalizedMessage());
- }
- }
-
- @Test
- public void testSendBulkRequest_Iterable_StringFF() throws Exception {
- List requests = new ArrayList<>();
- HttpClient client = mock(HttpClient.class);
- PiwikRequest request = mock(PiwikRequest.class);
- HttpResponse response = mock(HttpResponse.class);
-
- doReturn("query").when(request).getQueryString();
- requests.add(request);
- doReturn(client).when(piwikTracker).getHttpClient();
- doReturn(response).when(client).execute(argThat(new CorrectPostRequest("{\"requests\":[\"?query\"]}")));
-
- assertEquals(response, piwikTracker.sendBulkRequest(requests, null));
- }
-
- @Test
- public void testSendBulkRequest_Iterable_StringFT() throws Exception {
- List requests = new ArrayList<>();
- HttpClient client = mock(HttpClient.class);
- PiwikRequest request = mock(PiwikRequest.class);
- HttpResponse response = mock(HttpResponse.class);
-
- doReturn("query").when(request).getQueryString();
- requests.add(request);
- doReturn(client).when(piwikTracker).getHttpClient();
- doReturn(response).when(client)
- .execute(argThat(new CorrectPostRequest("{\"requests\":[\"?query\"],\"token_auth\":\"12345678901234567890123456789012\"}")));
-
- assertEquals(response, piwikTracker.sendBulkRequest(requests, "12345678901234567890123456789012"));
- }
-
- /**
- * Test of sendBulkRequestAsync method, of class PiwikTracker.
- */
- @Test
- public void testSendBulkRequestAsync_Iterable() throws Exception {
- List requests = new ArrayList<>();
- HttpResponse response = mock(HttpResponse.class);
- Future future = mock(Future.class);
- doReturn(response).when(future).get();
- doReturn(true).when(future).isDone();
-
- doReturn(future).when(piwikTracker).sendBulkRequestAsync(requests, null);
-
- assertEquals(response, piwikTracker.sendBulkRequestAsync(requests).get());
- }
-
- /**
- * Test of sendBulkRequestAsync method, of class PiwikTracker.
- */
- @Test
- public void testSendBulkRequestAsync_Iterable_StringTT() throws Exception {
- try {
- List requests = new ArrayList<>();
- CloseableHttpAsyncClient client = mock(CloseableHttpAsyncClient.class);
- PiwikRequest request = mock(PiwikRequest.class);
-
- doReturn("query").when(request).getQueryString();
- requests.add(request);
- doReturn(client).when(piwikTracker).getHttpAsyncClient();
-
- piwikTracker.sendBulkRequestAsync(requests, "1");
- fail("Exception should have been thrown.");
- } catch (IllegalArgumentException e) {
- assertEquals("1 is not 32 characters long.", e.getLocalizedMessage());
- }
- }
-
- @Test
- public void testSendBulkRequestAsync_Iterable_StringFF() throws Exception {
- List requests = new ArrayList<>();
- CloseableHttpAsyncClient client = mock(CloseableHttpAsyncClient.class);
- PiwikRequest request = mock(PiwikRequest.class);
- HttpResponse response = mock(HttpResponse.class);
- Future future = mock(Future.class);
- doReturn(response).when(future).get();
- doReturn(true).when(future).isDone();
-
- doReturn("query").when(request).getQueryString();
- requests.add(request);
- doReturn(client).when(piwikTracker).getHttpAsyncClient();
- doReturn(future).when(client)
- .execute(argThat(new CorrectPostRequest("{\"requests\":[\"?query\"]}")), any());
-
- assertEquals(response, piwikTracker.sendBulkRequestAsync(requests, null).get());
- }
-
- @Test
- public void testSendBulkRequestAsync_Iterable_StringFT() throws Exception {
- List requests = new ArrayList<>();
- CloseableHttpAsyncClient client = mock(CloseableHttpAsyncClient.class);
- PiwikRequest request = mock(PiwikRequest.class);
- HttpResponse response = mock(HttpResponse.class);
- Future future = mock(Future.class);
- doReturn(response).when(future).get();
- doReturn(true).when(future).isDone();
-
- doReturn("query").when(request).getQueryString();
- requests.add(request);
- doReturn(client).when(piwikTracker).getHttpAsyncClient();
- doReturn(future).when(client)
- .execute(argThat(new CorrectPostRequest("{\"requests\":[\"?query\"],\"token_auth\":\"12345678901234567890123456789012\"}")), any());
-
- assertEquals(response, piwikTracker.sendBulkRequestAsync(requests, "12345678901234567890123456789012").get());
- }
-
- static class CorrectPostRequest implements ArgumentMatcher {
- String body;
-
- public CorrectPostRequest(String body) {
- this.body = body;
- }
-
- @Override
- public boolean matches(HttpPost post) {
- try {
- InputStream bais = post.getEntity().getContent();
- byte[] bytes = new byte[bais.available()];
- bais.read(bytes);
- String str = new String(bytes);
- return body.equals(str);
- } catch (IOException e) {
- fail("Exception should not have been throw.");
- }
- return false;
- }
- }
-
- /**
- * Test of getHttpClient method, of class PiwikTracker.
- */
- @Test
- public void testGetHttpClient() {
- assertNotNull(piwikTracker.getHttpClient());
- }
-
- /**
- * Test of getHttpAsyncClient method, of class PiwikTracker.
- */
- @Test
- public void testGetHttpAsyncClient() {
- assertNotNull(piwikTracker.getHttpAsyncClient());
- }
-
- /**
- * Test of getHttpClient method, of class PiwikTracker, with proxy.
- */
- @Test
- public void testGetHttpClientWithProxy() {
- piwikTracker = new PiwikTracker("http://test.com", "http://proxy", 8080);
- HttpClient httpClient = piwikTracker.getHttpClient();
-
- assertNotNull(httpClient);
- }
-}
diff --git a/test/pom.xml b/test/pom.xml
new file mode 100644
index 00000000..7a2eb183
--- /dev/null
+++ b/test/pom.xml
@@ -0,0 +1,53 @@
+
+ 4.0.0
+
+
+ org.piwik.java.tracking
+ matomo-java-tracker-parent
+ 3.4.1-SNAPSHOT
+ ../pom.xml
+
+
+ matomo-java-tracker-test
+
+ Matomo Java Tracker Test
+ Test application for Matomo Java Tracker
+
+
+ 11
+ 11
+
+
+
+
+ org.piwik.java.tracking
+ matomo-java-tracker-java11
+ ${project.version}
+
+
+ org.piwik.java.tracking
+ matomo-java-tracker-servlet-jakarta
+ ${project.version}
+
+
+ com.github.javafaker
+ javafaker
+ 1.0.2
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+ org.slf4j
+ slf4j-simple
+
+
+ org.eclipse.jetty.ee10
+ jetty-ee10-servlet
+ 12.0.16
+
+
+
+
diff --git a/test/src/main/java/org/matomo/java/tracking/test/BulkExample.java b/test/src/main/java/org/matomo/java/tracking/test/BulkExample.java
new file mode 100644
index 00000000..54f35f92
--- /dev/null
+++ b/test/src/main/java/org/matomo/java/tracking/test/BulkExample.java
@@ -0,0 +1,46 @@
+package org.matomo.java.tracking.test;
+
+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);
+ }
+
+ }
+
+}
diff --git a/test/src/main/java/org/matomo/java/tracking/test/ConsumerExample.java b/test/src/main/java/org/matomo/java/tracking/test/ConsumerExample.java
new file mode 100644
index 00000000..6c37626a
--- /dev/null
+++ b/test/src/main/java/org/matomo/java/tracking/test/ConsumerExample.java
@@ -0,0 +1,48 @@
+package org.matomo.java.tracking.test;
+
+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);
+ }
+
+ }
+
+}
diff --git a/test/src/main/java/org/matomo/java/tracking/test/EcommerceExample.java b/test/src/main/java/org/matomo/java/tracking/test/EcommerceExample.java
new file mode 100644
index 00000000..3f16258a
--- /dev/null
+++ b/test/src/main/java/org/matomo/java/tracking/test/EcommerceExample.java
@@ -0,0 +1,62 @@
+package org.matomo.java.tracking.test;
+
+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);
+ }
+
+ }
+
+}
diff --git a/test/src/main/java/org/matomo/java/tracking/test/MatomoServletTester.java b/test/src/main/java/org/matomo/java/tracking/test/MatomoServletTester.java
new file mode 100644
index 00000000..0b4d9174
--- /dev/null
+++ b/test/src/main/java/org/matomo/java/tracking/test/MatomoServletTester.java
@@ -0,0 +1,42 @@
+package org.matomo.java.tracking.test;
+
+import java.net.URI;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.jetty.ee10.servlet.DefaultServlet;
+import org.eclipse.jetty.ee10.servlet.FilterHolder;
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee10.servlet.ServletHolder;
+import org.eclipse.jetty.server.Server;
+import org.matomo.java.tracking.MatomoTracker;
+import org.matomo.java.tracking.TrackerConfiguration;
+import org.matomo.java.tracking.servlet.MatomoTrackerFilter;
+
+@Slf4j
+class MatomoServletTester {
+ public static void main(String[] args) throws Exception {
+
+ ServletHolder servletHolder = new ServletHolder("default", new DefaultServlet());
+ servletHolder.setInitParameter(
+ "resourceBase",
+ MatomoServletTester.class.getClassLoader().getResource("web").toExternalForm()
+ );
+
+ ServletContextHandler context = new ServletContextHandler();
+ context.setContextPath("/");
+ context.addServlet(servletHolder, "/");
+ context.addFilter(new FilterHolder(new MatomoTrackerFilter(new MatomoTracker(
+ TrackerConfiguration
+ .builder()
+ .apiEndpoint(URI.create("http://localhost:8080/matomo.php"))
+ .defaultSiteId(1)
+ .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71")
+ .logFailedTracking(true)
+ .build()))), "/*", null);
+
+ Server server = new Server(8090);
+ server.setHandler(context);
+ server.start();
+ server.join();
+
+ }
+}
\ No newline at end of file
diff --git a/test/src/main/java/org/matomo/java/tracking/test/MatomoTrackerTester.java b/test/src/main/java/org/matomo/java/tracking/test/MatomoTrackerTester.java
new file mode 100644
index 00000000..8532c370
--- /dev/null
+++ b/test/src/main/java/org/matomo/java/tracking/test/MatomoTrackerTester.java
@@ -0,0 +1,195 @@
+package org.matomo.java.tracking.test;
+
+import com.github.javafaker.Country;
+import com.github.javafaker.Faker;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import lombok.extern.slf4j.Slf4j;
+import org.matomo.java.tracking.MatomoRequest;
+import org.matomo.java.tracking.MatomoTracker;
+import org.matomo.java.tracking.TrackerConfiguration;
+import org.matomo.java.tracking.parameters.AcceptLanguage;
+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.UniqueId;
+import org.matomo.java.tracking.parameters.VisitorId;
+
+@Slf4j
+class MatomoTrackerTester implements AutoCloseable {
+
+ private final MatomoTracker tracker;
+
+ private final Faker faker = new Faker();
+ private final List vistors = new ArrayList<>(5);
+
+ MatomoTrackerTester(TrackerConfiguration configuration) {
+ tracker = new MatomoTracker(configuration);
+ for (int i = 0; i < 5; i++) {
+ vistors.add(VisitorId.random());
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+
+ TrackerConfiguration configuration = TrackerConfiguration
+ .builder()
+ .apiEndpoint(URI.create("http://localhost:8080/matomo.php"))
+ .defaultSiteId(1)
+ .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71")
+ .logFailedTracking(true)
+ .build();
+
+ try (MatomoTrackerTester matomoTrackerTester = new MatomoTrackerTester(configuration)) {
+ matomoTrackerTester.sendRequestAsync();
+ matomoTrackerTester.sendBulkRequestsAsync();
+ matomoTrackerTester.sendRequest();
+ matomoTrackerTester.sendBulkRequests();
+ }
+
+ }
+
+ private void sendRequest() {
+ MatomoRequest request = randomRequest();
+ tracker.sendRequest(request);
+ log.info("Successfully sent single request to Matomo server: {}", request);
+ }
+
+ private void sendBulkRequests() {
+ List requests = randomRequests();
+ tracker.sendBulkRequest(requests);
+ log.info("Successfully sent bulk requests to Matomo server: {}", requests);
+ }
+
+ private void sendRequestAsync() {
+ MatomoRequest request = randomRequest();
+ CompletableFuture> future = tracker.sendRequestAsync(request);
+ future.thenAccept(v -> log.info("Successfully sent async single request to Matomo server: {}", request));
+ }
+
+ private void sendBulkRequestsAsync() {
+ List requests = randomRequests();
+ tracker
+ .sendBulkRequestAsync(requests)
+ .thenAccept(v -> log.info("Successfully sent async bulk requests to Matomo server: {}", requests));
+ }
+
+ private List randomRequests() {
+ return IntStream
+ .range(0, 5)
+ .mapToObj(i -> randomRequest())
+ .collect(Collectors.toCollection(() -> new ArrayList<>(10)));
+ }
+
+ private MatomoRequest randomRequest() {
+ Country country = faker.country();
+ return MatomoRequest
+ .request()
+ .actionName(faker.funnyName().name())
+ .actionUrl("https://" + faker.internet().url())
+ .visitorId(vistors.get(faker.random().nextInt(vistors.size())))
+ .referrerUrl("https://" + faker.internet().url())
+ .visitCustomVariables(new CustomVariables()
+ .add(new CustomVariable("color", faker.color().hex()))
+ .add(new CustomVariable("beer", faker.beer().name())))
+ .visitorVisitCount(faker.random().nextInt(10))
+ .visitorPreviousVisitTimestamp(Instant.now().minusSeconds(faker.random().nextInt(10000)))
+ .visitorFirstVisitTimestamp(Instant.now().minusSeconds(faker.random().nextInt(10000)))
+ .campaignName(faker.dragonBall().character())
+ .campaignKeyword(faker.buffy().celebrities())
+ .deviceResolution(DeviceResolution
+ .builder()
+ .width(faker.random().nextInt(1920))
+ .height(faker.random().nextInt(1280))
+ .build())
+ .currentHour(faker.random().nextInt(24))
+ .currentMinute(faker.random().nextInt(60))
+ .currentSecond(faker.random().nextInt(60))
+ .pluginJava(true)
+ .pluginFlash(true)
+ .pluginDirector(true)
+ .pluginQuicktime(true)
+ .pluginPDF(true)
+ .pluginWindowsMedia(true)
+ .pluginGears(true)
+ .pluginSilverlight(true)
+ .supportsCookies(true)
+ .headerUserAgent(faker.internet().userAgentAny())
+ .headerAcceptLanguage(AcceptLanguage.fromHeader("de"))
+ .userId(faker.random().hex())
+ .visitorCustomId(VisitorId.random())
+ .newVisit(true)
+ .pageCustomVariables(new CustomVariables()
+ .add(new CustomVariable("job", faker.job().position()))
+ .add(new CustomVariable("team", faker.team().name())))
+ .outlinkUrl("https://" + faker.internet().url())
+ .downloadUrl("https://" + faker.internet().url())
+ .searchQuery(faker.cat().name())
+ .searchCategory(faker.hipster().word())
+ .searchResultsCount(Long.valueOf(faker.random().nextInt(20)))
+ .pageViewId(UniqueId.random())
+ .goalId(0)
+ .ecommerceRevenue(faker.random().nextInt(50) + faker.random().nextDouble())
+ .ecommerceId(faker.random().hex())
+ .ecommerceItems(EcommerceItems
+ .builder()
+ .item(EcommerceItem
+ .builder()
+ .sku(faker.random().hex())
+ .name(faker.commerce().productName())
+ .quantity(faker.random().nextInt(10))
+ .price(faker.random().nextInt(100) + faker.random().nextDouble())
+ .build())
+ .item(EcommerceItem
+ .builder()
+ .sku(faker.random().hex())
+ .name(faker.commerce().productName())
+ .quantity(faker.random().nextInt(10))
+ .price(faker.random().nextInt(100) + faker.random().nextDouble())
+ .build())
+ .build())
+ .ecommerceSubtotal(faker.random().nextInt(1000) + faker.random().nextDouble())
+ .ecommerceTax(faker.random().nextInt(100) + faker.random().nextDouble())
+ .ecommerceDiscount(faker.random().nextInt(100) + faker.random().nextDouble())
+ .ecommerceLastOrderTimestamp(Instant.now())
+ .visitorIp(faker.internet().ipV4Address())
+ .requestTimestamp(Instant.now())
+ .visitorCountry(org.matomo.java.tracking.parameters.Country.fromCode(faker.address().countryCode()))
+ .visitorCity(faker.address().cityName())
+ .visitorLatitude(faker.random().nextDouble() * 180 - 90)
+ .visitorLongitude(faker.random().nextDouble() * 360 - 180)
+ .trackBotRequests(true)
+ .characterSet(StandardCharsets.UTF_8)
+ .customAction(true)
+ .networkTime(Long.valueOf(faker.random().nextInt(100)))
+ .serverTime(Long.valueOf(faker.random().nextInt(100)))
+ .transferTime(Long.valueOf(faker.random().nextInt(100)))
+ .domProcessingTime(Long.valueOf(faker.random().nextInt(100)))
+ .domCompletionTime(Long.valueOf(faker.random().nextInt(100)))
+ .onloadTime(Long.valueOf(faker.random().nextInt(100)))
+ .eventCategory(country.name())
+ .eventAction(country.capital())
+ .eventName(country.currencyCode())
+ .contentName(faker.aviation().aircraft())
+ .contentPiece(faker.ancient().god())
+ .contentTarget("https://" + faker.internet().url())
+ .contentInteraction(faker.app().name())
+ .dimensions(Map.of(1L, faker.artist().name(), 2L, faker.dog().name()))
+ .debug(true)
+ .build();
+ }
+
+ @Override
+ public void close() throws Exception {
+ tracker.close();
+ }
+}
diff --git a/test/src/main/java/org/matomo/java/tracking/test/SendExample.java b/test/src/main/java/org/matomo/java/tracking/test/SendExample.java
new file mode 100644
index 00000000..d43bcfc3
--- /dev/null
+++ b/test/src/main/java/org/matomo/java/tracking/test/SendExample.java
@@ -0,0 +1,40 @@
+package org.matomo.java.tracking.test;
+
+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);
+ }
+ }
+
+}
diff --git a/test/src/main/java/org/matomo/java/tracking/test/ServletMatomoRequestExample.java b/test/src/main/java/org/matomo/java/tracking/test/ServletMatomoRequestExample.java
new file mode 100644
index 00000000..3a2be7a1
--- /dev/null
+++ b/test/src/main/java/org/matomo/java/tracking/test/ServletMatomoRequestExample.java
@@ -0,0 +1,43 @@
+package org.matomo.java.tracking.test;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.matomo.java.tracking.MatomoRequest;
+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;
+
+/**
+ * This is an example of how to use the ServletMatomoRequest class.
+ */
+public class ServletMatomoRequestExample {
+
+ private final MatomoTracker tracker;
+
+ public ServletMatomoRequestExample(MatomoTracker tracker) {
+ this.tracker = tracker;
+ }
+
+ /**
+ * Example for sending a request from a servlet request.
+ *
+ * @param request the servlet request
+ */
+ 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);
+ // ...
+ }
+
+}
diff --git a/test/src/main/resources/simplelogger.properties b/test/src/main/resources/simplelogger.properties
new file mode 100644
index 00000000..835f39d5
--- /dev/null
+++ b/test/src/main/resources/simplelogger.properties
@@ -0,0 +1,2 @@
+org.slf4j.simpleLogger.defaultLogLevel=debug
+org.slf4j.simpleLogger.log.org.eclipse.jetty=info
\ No newline at end of file
diff --git a/test/src/main/resources/web/track.html b/test/src/main/resources/web/track.html
new file mode 100644
index 00000000..508b59ac
--- /dev/null
+++ b/test/src/main/resources/web/track.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Matomo Java Tracker Servlet Test
+
+
+Thank you! Your request was sent to Matomo at http://localhost:8080.
+
+
\ No newline at end of file