diff --git a/.editorconfig b/.editorconfig index c6c8b362..322b027f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,1380 @@ -root = true - [*] +charset = utf-8 +end_of_line = lf +indent_size = 4 indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_block_comment_add_space = false +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.feature] indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true +ij_gherkin_keep_indents_on_empty_lines = false + +[*.java] +indent_size = 2 +max_line_length = 100 +ij_continuation_indent_size = 4 +ij_wrap_on_typing = true +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = true +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = true +ij_java_align_multiline_deconstruction_list_components = false +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = false +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = false +ij_java_align_multiline_resources = false +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = true +ij_java_align_types_in_multi_catch = true +ij_java_annotation_parameter_wrap = split_into_lines +ij_java_array_initializer_new_line_after_left_brace = true +ij_java_array_initializer_right_brace_on_new_line = true +ij_java_array_initializer_wrap = on_every_item +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = normal +ij_java_assignment_wrap = normal +ij_java_binary_operation_sign_on_next_line = true +ij_java_binary_operation_wrap = normal +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 1 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_add_space = false +ij_java_block_comment_at_first_column = true +ij_java_builder_methods = +ij_java_call_parameters_new_line_after_left_paren = true +ij_java_call_parameters_right_paren_on_new_line = true +ij_java_call_parameters_wrap = on_every_item +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_class_names_in_javadoc = 1 +ij_java_deconstruction_list_wrap = on_every_item +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = false +ij_java_do_while_brace_force = always +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = true +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = false +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_prefix = +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_prefix = +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_prefix = +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_li_suffix = +ij_java_entity_pk_class = java.lang.String +ij_java_entity_ri_prefix = +ij_java_entity_ri_suffix = +ij_java_entity_vo_prefix = +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = split_into_lines +ij_java_extends_keyword_wrap = normal +ij_java_extends_list_wrap = on_every_item +ij_java_field_annotation_wrap = split_into_lines +ij_java_field_name_prefix = +ij_java_field_name_suffix = +ij_java_filter_class_prefix = +ij_java_filter_class_suffix = +ij_java_filter_dd_prefix = +ij_java_filter_dd_suffix = +ij_java_finally_on_new_line = false +ij_java_for_brace_force = always +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = on_every_item +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = always +ij_java_imports_layout = $*,|,* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = true +ij_java_keep_control_statement_in_one_line = false +ij_java_keep_first_column_comment = false +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_add_space_on_reformat = false +ij_java_line_comment_at_first_column = true +ij_java_listener_class_prefix = +ij_java_listener_class_suffix = +ij_java_local_variable_name_prefix = +ij_java_local_variable_name_suffix = +ij_java_message_dd_prefix = +ij_java_message_dd_suffix = EJB +ij_java_message_eb_prefix = +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = on_every_item +ij_java_method_parameters_new_line_after_left_paren = true +ij_java_method_parameters_right_paren_on_new_line = true +ij_java_method_parameters_wrap = on_every_item +ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = on_every_item +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_new_line_after_lparen_in_annotation = true +ij_java_new_line_after_lparen_in_deconstruction_pattern = true +ij_java_new_line_after_lparen_in_record_header = true +ij_java_packages_to_use_import_on_demand = +ij_java_parameter_annotation_wrap = normal +ij_java_parameter_name_prefix = +ij_java_parameter_name_suffix = +ij_java_parentheses_expression_new_line_after_left_paren = true +ij_java_parentheses_expression_right_paren_on_new_line = true +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = on_every_item +ij_java_repeat_annotations = +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = true +ij_java_resource_list_right_paren_on_new_line = true +ij_java_resource_list_wrap = on_every_item +ij_java_rparen_on_new_line_in_annotation = true +ij_java_rparen_on_new_line_in_deconstruction_pattern = true +ij_java_rparen_on_new_line_in_record_header = true +ij_java_servlet_class_prefix = +ij_java_servlet_class_suffix = +ij_java_servlet_dd_prefix = +ij_java_servlet_dd_suffix = +ij_java_session_dd_prefix = +ij_java_session_dd_suffix = EJB +ij_java_session_eb_prefix = +ij_java_session_eb_suffix = Bean +ij_java_session_hi_prefix = +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_li_suffix = +ij_java_session_ri_prefix = +ij_java_session_ri_suffix = +ij_java_session_si_prefix = +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = true +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_deconstruction_list = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_annotation_eq = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_deconstruction_list = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_static_field_name_prefix = +ij_java_static_field_name_suffix = +ij_java_subclass_name_prefix = +ij_java_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = normal +ij_java_test_name_prefix = +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = normal +ij_java_throws_list_wrap = on_every_item +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = split_into_lines +ij_java_visibility = public +ij_java_while_brace_force = always +ij_java_while_on_new_line = false +ij_java_wrap_comments = true +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false + +[*.less] +indent_size = 2 +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_block_comment_add_space = false +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_line_comment_add_space = false +ij_less_line_comment_at_first_column = false +ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.proto] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_protobuf_keep_blank_lines_in_code = 2 +ij_protobuf_keep_indents_on_empty_lines = false +ij_protobuf_keep_line_breaks = true +ij_protobuf_space_after_comma = true +ij_protobuf_space_before_comma = false +ij_protobuf_spaces_around_assignment_operators = true +ij_protobuf_spaces_within_braces = false +ij_protobuf_spaces_within_brackets = false + +[*.vue] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = true +ij_vue_interpolation_new_line_before_end_delimiter = true +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdd,*.wsdl,*.xjb,*.xml,*.xsd,*.xsl,*.xslt,*.xul,phpunit.xml.dist}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_add_space = false +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal + +[{*.ats,*.cts,*.mts,*.ts}] +ij_continuation_indent_size = 4 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_enum_constants_wrap = on_every_item +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_object_types_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_property_prefix = +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.js}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_property_prefix = +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.ctp,*.hphp,*.inc,*.module,*.php,*.php4,*.php5,*.phtml}] +ij_continuation_indent_size = 4 +ij_php_align_assignments = false +ij_php_align_class_constants = false +ij_php_align_enum_cases = false +ij_php_align_group_field_declarations = false +ij_php_align_inline_comments = false +ij_php_align_key_value_pairs = false +ij_php_align_match_arm_bodies = false +ij_php_align_multiline_array_initializer_expression = false +ij_php_align_multiline_binary_operation = false +ij_php_align_multiline_chained_methods = false +ij_php_align_multiline_extends_list = false +ij_php_align_multiline_for = true +ij_php_align_multiline_parameters = true +ij_php_align_multiline_parameters_in_calls = false +ij_php_align_multiline_ternary_operation = false +ij_php_align_named_arguments = false +ij_php_align_phpdoc_comments = false +ij_php_align_phpdoc_param_names = false +ij_php_anonymous_brace_style = end_of_line +ij_php_api_weight = 28 +ij_php_array_initializer_new_line_after_left_brace = false +ij_php_array_initializer_right_brace_on_new_line = false +ij_php_array_initializer_wrap = off +ij_php_assignment_wrap = off +ij_php_attributes_wrap = off +ij_php_author_weight = 28 +ij_php_binary_operation_sign_on_next_line = false +ij_php_binary_operation_wrap = off +ij_php_blank_lines_after_class_header = 0 +ij_php_blank_lines_after_function = 1 +ij_php_blank_lines_after_imports = 1 +ij_php_blank_lines_after_opening_tag = 0 +ij_php_blank_lines_after_package = 0 +ij_php_blank_lines_around_class = 1 +ij_php_blank_lines_around_constants = 0 +ij_php_blank_lines_around_enum_cases = 0 +ij_php_blank_lines_around_field = 0 +ij_php_blank_lines_around_method = 1 +ij_php_blank_lines_before_class_end = 0 +ij_php_blank_lines_before_imports = 1 +ij_php_blank_lines_before_method_body = 0 +ij_php_blank_lines_before_package = 1 +ij_php_blank_lines_before_return_statement = 0 +ij_php_blank_lines_between_imports = 0 +ij_php_block_brace_style = end_of_line +ij_php_call_parameters_new_line_after_left_paren = false +ij_php_call_parameters_right_paren_on_new_line = false +ij_php_call_parameters_wrap = off +ij_php_catch_on_new_line = false +ij_php_category_weight = 28 +ij_php_class_brace_style = next_line +ij_php_comma_after_last_argument = false +ij_php_comma_after_last_array_element = false +ij_php_comma_after_last_closure_use_var = false +ij_php_comma_after_last_match_arm = false +ij_php_comma_after_last_parameter = false +ij_php_concat_spaces = true +ij_php_copyright_weight = 28 +ij_php_deprecated_weight = 28 +ij_php_do_while_brace_force = never +ij_php_else_if_style = as_is +ij_php_else_on_new_line = false +ij_php_example_weight = 28 +ij_php_extends_keyword_wrap = off +ij_php_extends_list_wrap = off +ij_php_fields_default_visibility = private +ij_php_filesource_weight = 28 +ij_php_finally_on_new_line = false +ij_php_for_brace_force = never +ij_php_for_statement_new_line_after_left_paren = false +ij_php_for_statement_right_paren_on_new_line = false +ij_php_for_statement_wrap = off +ij_php_force_empty_methods_in_one_line = false +ij_php_force_short_declaration_array_style = false +ij_php_getters_setters_naming_style = camel_case +ij_php_getters_setters_order_style = getters_first +ij_php_global_weight = 28 +ij_php_group_use_wrap = on_every_item +ij_php_if_brace_force = never +ij_php_if_lparen_on_next_line = false +ij_php_if_rparen_on_next_line = false +ij_php_ignore_weight = 28 +ij_php_import_sorting = alphabetic +ij_php_indent_break_from_case = true +ij_php_indent_case_from_switch = true +ij_php_indent_code_in_php_tags = false +ij_php_internal_weight = 28 +ij_php_keep_blank_lines_after_lbrace = 2 +ij_php_keep_blank_lines_before_right_brace = 2 +ij_php_keep_blank_lines_in_code = 2 +ij_php_keep_blank_lines_in_declarations = 2 +ij_php_keep_control_statement_in_one_line = true +ij_php_keep_first_column_comment = true +ij_php_keep_indents_on_empty_lines = false +ij_php_keep_line_breaks = true +ij_php_keep_rparen_and_lbrace_on_one_line = false +ij_php_keep_simple_classes_in_one_line = false +ij_php_keep_simple_methods_in_one_line = false +ij_php_lambda_brace_style = end_of_line +ij_php_license_weight = 28 +ij_php_line_comment_add_space = false +ij_php_line_comment_at_first_column = true +ij_php_link_weight = 28 +ij_php_lower_case_boolean_const = false +ij_php_lower_case_keywords = true +ij_php_lower_case_null_const = false +ij_php_method_brace_style = next_line +ij_php_method_call_chain_wrap = off +ij_php_method_parameters_new_line_after_left_paren = false +ij_php_method_parameters_right_paren_on_new_line = false +ij_php_method_parameters_wrap = off +ij_php_method_weight = 28 +ij_php_modifier_list_wrap = false +ij_php_multiline_chained_calls_semicolon_on_new_line = false +ij_php_namespace_brace_style = 1 +ij_php_new_line_after_php_opening_tag = false +ij_php_null_type_position = in_the_end +ij_php_package_weight = 28 +ij_php_param_weight = 0 +ij_php_parameters_attributes_wrap = off +ij_php_parentheses_expression_new_line_after_left_paren = false +ij_php_parentheses_expression_right_paren_on_new_line = false +ij_php_phpdoc_blank_line_before_tags = false +ij_php_phpdoc_blank_lines_around_parameters = false +ij_php_phpdoc_keep_blank_lines = true +ij_php_phpdoc_param_spaces_between_name_and_description = 1 +ij_php_phpdoc_param_spaces_between_tag_and_type = 1 +ij_php_phpdoc_param_spaces_between_type_and_name = 1 +ij_php_phpdoc_use_fqcn = false +ij_php_phpdoc_wrap_long_lines = false +ij_php_place_assignment_sign_on_next_line = false +ij_php_place_parens_for_constructor = 0 +ij_php_property_read_weight = 28 +ij_php_property_weight = 28 +ij_php_property_write_weight = 28 +ij_php_return_type_on_new_line = false +ij_php_return_weight = 1 +ij_php_see_weight = 28 +ij_php_since_weight = 28 +ij_php_sort_phpdoc_elements = true +ij_php_space_after_colon = true +ij_php_space_after_colon_in_enum_backed_type = true +ij_php_space_after_colon_in_named_argument = true +ij_php_space_after_colon_in_return_type = true +ij_php_space_after_comma = true +ij_php_space_after_for_semicolon = true +ij_php_space_after_quest = true +ij_php_space_after_type_cast = false +ij_php_space_after_unary_not = false +ij_php_space_before_array_initializer_left_brace = false +ij_php_space_before_catch_keyword = true +ij_php_space_before_catch_left_brace = true +ij_php_space_before_catch_parentheses = true +ij_php_space_before_class_left_brace = true +ij_php_space_before_closure_left_parenthesis = true +ij_php_space_before_colon = true +ij_php_space_before_colon_in_enum_backed_type = false +ij_php_space_before_colon_in_named_argument = false +ij_php_space_before_colon_in_return_type = false +ij_php_space_before_comma = false +ij_php_space_before_do_left_brace = true +ij_php_space_before_else_keyword = true +ij_php_space_before_else_left_brace = true +ij_php_space_before_finally_keyword = true +ij_php_space_before_finally_left_brace = true +ij_php_space_before_for_left_brace = true +ij_php_space_before_for_parentheses = true +ij_php_space_before_for_semicolon = false +ij_php_space_before_if_left_brace = true +ij_php_space_before_if_parentheses = true +ij_php_space_before_method_call_parentheses = false +ij_php_space_before_method_left_brace = true +ij_php_space_before_method_parentheses = false +ij_php_space_before_quest = true +ij_php_space_before_short_closure_left_parenthesis = false +ij_php_space_before_switch_left_brace = true +ij_php_space_before_switch_parentheses = true +ij_php_space_before_try_left_brace = true +ij_php_space_before_unary_not = false +ij_php_space_before_while_keyword = true +ij_php_space_before_while_left_brace = true +ij_php_space_before_while_parentheses = true +ij_php_space_between_ternary_quest_and_colon = false +ij_php_spaces_around_additive_operators = true +ij_php_spaces_around_arrow = false +ij_php_spaces_around_assignment_in_declare = false +ij_php_spaces_around_assignment_operators = true +ij_php_spaces_around_bitwise_operators = true +ij_php_spaces_around_equality_operators = true +ij_php_spaces_around_logical_operators = true +ij_php_spaces_around_multiplicative_operators = true +ij_php_spaces_around_null_coalesce_operator = true +ij_php_spaces_around_pipe_in_union_type = false +ij_php_spaces_around_relational_operators = true +ij_php_spaces_around_shift_operators = true +ij_php_spaces_around_unary_operator = false +ij_php_spaces_around_var_within_brackets = false +ij_php_spaces_within_array_initializer_braces = false +ij_php_spaces_within_brackets = false +ij_php_spaces_within_catch_parentheses = false +ij_php_spaces_within_for_parentheses = false +ij_php_spaces_within_if_parentheses = false +ij_php_spaces_within_method_call_parentheses = false +ij_php_spaces_within_method_parentheses = false +ij_php_spaces_within_parentheses = false +ij_php_spaces_within_short_echo_tags = true +ij_php_spaces_within_switch_parentheses = false +ij_php_spaces_within_while_parentheses = false +ij_php_special_else_if_treatment = false +ij_php_subpackage_weight = 28 +ij_php_ternary_operation_signs_on_next_line = false +ij_php_ternary_operation_wrap = off +ij_php_throws_weight = 2 +ij_php_todo_weight = 28 +ij_php_treat_multiline_arrays_and_lambdas_multiline = false +ij_php_unknown_tag_weight = 28 +ij_php_upper_case_boolean_const = false +ij_php_upper_case_null_const = false +ij_php_uses_weight = 28 +ij_php_var_weight = 28 +ij_php_variable_naming_style = mixed +ij_php_version_weight = 28 +ij_php_while_brace_force = never +ij_php_while_on_new_line = false + +[{*.gant,*.groovy,*.gy}] +ij_groovy_align_group_field_declarations = false +ij_groovy_align_multiline_array_initializer_expression = false +ij_groovy_align_multiline_assignment = false +ij_groovy_align_multiline_binary_operation = false +ij_groovy_align_multiline_chained_methods = false +ij_groovy_align_multiline_extends_list = false +ij_groovy_align_multiline_for = true +ij_groovy_align_multiline_list_or_map = true +ij_groovy_align_multiline_method_parentheses = false +ij_groovy_align_multiline_parameters = true +ij_groovy_align_multiline_parameters_in_calls = false +ij_groovy_align_multiline_resources = true +ij_groovy_align_multiline_ternary_operation = false +ij_groovy_align_multiline_throws_list = false +ij_groovy_align_named_args_in_map = true +ij_groovy_align_throws_keyword = false +ij_groovy_array_initializer_new_line_after_left_brace = false +ij_groovy_array_initializer_right_brace_on_new_line = false +ij_groovy_array_initializer_wrap = off +ij_groovy_assert_statement_wrap = off +ij_groovy_assignment_wrap = off +ij_groovy_binary_operation_wrap = off +ij_groovy_blank_lines_after_class_header = 0 +ij_groovy_blank_lines_after_imports = 1 +ij_groovy_blank_lines_after_package = 1 +ij_groovy_blank_lines_around_class = 1 +ij_groovy_blank_lines_around_field = 0 +ij_groovy_blank_lines_around_field_in_interface = 0 +ij_groovy_blank_lines_around_method = 1 +ij_groovy_blank_lines_around_method_in_interface = 1 +ij_groovy_blank_lines_before_imports = 1 +ij_groovy_blank_lines_before_method_body = 0 +ij_groovy_blank_lines_before_package = 0 +ij_groovy_block_brace_style = end_of_line +ij_groovy_block_comment_add_space = false +ij_groovy_block_comment_at_first_column = true +ij_groovy_call_parameters_new_line_after_left_paren = false +ij_groovy_call_parameters_right_paren_on_new_line = false +ij_groovy_call_parameters_wrap = off +ij_groovy_catch_on_new_line = false +ij_groovy_class_annotation_wrap = split_into_lines +ij_groovy_class_brace_style = end_of_line +ij_groovy_class_count_to_use_import_on_demand = 5 +ij_groovy_do_while_brace_force = never +ij_groovy_else_on_new_line = false +ij_groovy_enable_groovydoc_formatting = true +ij_groovy_enum_constants_wrap = off +ij_groovy_extends_keyword_wrap = off +ij_groovy_extends_list_wrap = off +ij_groovy_field_annotation_wrap = split_into_lines +ij_groovy_finally_on_new_line = false +ij_groovy_for_brace_force = never +ij_groovy_for_statement_new_line_after_left_paren = false +ij_groovy_for_statement_right_paren_on_new_line = false +ij_groovy_for_statement_wrap = off +ij_groovy_ginq_general_clause_wrap_policy = 2 +ij_groovy_ginq_having_wrap_policy = 1 +ij_groovy_ginq_indent_having_clause = true +ij_groovy_ginq_indent_on_clause = true +ij_groovy_ginq_on_wrap_policy = 1 +ij_groovy_ginq_space_after_keyword = true +ij_groovy_if_brace_force = never +ij_groovy_import_annotation_wrap = 2 +ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* +ij_groovy_indent_case_from_switch = true +ij_groovy_indent_label_blocks = true +ij_groovy_insert_inner_class_imports = false +ij_groovy_keep_blank_lines_before_right_brace = 2 +ij_groovy_keep_blank_lines_in_code = 2 +ij_groovy_keep_blank_lines_in_declarations = 2 +ij_groovy_keep_control_statement_in_one_line = true +ij_groovy_keep_first_column_comment = true +ij_groovy_keep_indents_on_empty_lines = false +ij_groovy_keep_line_breaks = true +ij_groovy_keep_multiple_expressions_in_one_line = false +ij_groovy_keep_simple_blocks_in_one_line = false +ij_groovy_keep_simple_classes_in_one_line = true +ij_groovy_keep_simple_lambdas_in_one_line = true +ij_groovy_keep_simple_methods_in_one_line = true +ij_groovy_label_indent_absolute = false +ij_groovy_label_indent_size = 0 +ij_groovy_lambda_brace_style = end_of_line +ij_groovy_layout_static_imports_separately = true +ij_groovy_line_comment_add_space = false +ij_groovy_line_comment_add_space_on_reformat = false +ij_groovy_line_comment_at_first_column = true +ij_groovy_method_annotation_wrap = split_into_lines +ij_groovy_method_brace_style = end_of_line +ij_groovy_method_call_chain_wrap = off +ij_groovy_method_parameters_new_line_after_left_paren = false +ij_groovy_method_parameters_right_paren_on_new_line = false +ij_groovy_method_parameters_wrap = off +ij_groovy_modifier_list_wrap = false +ij_groovy_names_count_to_use_import_on_demand = 3 +ij_groovy_packages_to_use_import_on_demand = java.awt.*,javax.swing.* +ij_groovy_parameter_annotation_wrap = off +ij_groovy_parentheses_expression_new_line_after_left_paren = false +ij_groovy_parentheses_expression_right_paren_on_new_line = false +ij_groovy_prefer_parameters_wrap = false +ij_groovy_resource_list_new_line_after_left_paren = false +ij_groovy_resource_list_right_paren_on_new_line = false +ij_groovy_resource_list_wrap = off +ij_groovy_space_after_assert_separator = true +ij_groovy_space_after_colon = true +ij_groovy_space_after_comma = true +ij_groovy_space_after_comma_in_type_arguments = true +ij_groovy_space_after_for_semicolon = true +ij_groovy_space_after_quest = true +ij_groovy_space_after_type_cast = true +ij_groovy_space_before_annotation_parameter_list = false +ij_groovy_space_before_array_initializer_left_brace = false +ij_groovy_space_before_assert_separator = false +ij_groovy_space_before_catch_keyword = true +ij_groovy_space_before_catch_left_brace = true +ij_groovy_space_before_catch_parentheses = true +ij_groovy_space_before_class_left_brace = true +ij_groovy_space_before_closure_left_brace = true +ij_groovy_space_before_colon = true +ij_groovy_space_before_comma = false +ij_groovy_space_before_do_left_brace = true +ij_groovy_space_before_else_keyword = true +ij_groovy_space_before_else_left_brace = true +ij_groovy_space_before_finally_keyword = true +ij_groovy_space_before_finally_left_brace = true +ij_groovy_space_before_for_left_brace = true +ij_groovy_space_before_for_parentheses = true +ij_groovy_space_before_for_semicolon = false +ij_groovy_space_before_if_left_brace = true +ij_groovy_space_before_if_parentheses = true +ij_groovy_space_before_method_call_parentheses = false +ij_groovy_space_before_method_left_brace = true +ij_groovy_space_before_method_parentheses = false +ij_groovy_space_before_quest = true +ij_groovy_space_before_record_parentheses = false +ij_groovy_space_before_switch_left_brace = true +ij_groovy_space_before_switch_parentheses = true +ij_groovy_space_before_synchronized_left_brace = true +ij_groovy_space_before_synchronized_parentheses = true +ij_groovy_space_before_try_left_brace = true +ij_groovy_space_before_try_parentheses = true +ij_groovy_space_before_while_keyword = true +ij_groovy_space_before_while_left_brace = true +ij_groovy_space_before_while_parentheses = true +ij_groovy_space_in_named_argument = true +ij_groovy_space_in_named_argument_before_colon = false +ij_groovy_space_within_empty_array_initializer_braces = false +ij_groovy_space_within_empty_method_call_parentheses = false +ij_groovy_spaces_around_additive_operators = true +ij_groovy_spaces_around_assignment_operators = true +ij_groovy_spaces_around_bitwise_operators = true +ij_groovy_spaces_around_equality_operators = true +ij_groovy_spaces_around_lambda_arrow = true +ij_groovy_spaces_around_logical_operators = true +ij_groovy_spaces_around_multiplicative_operators = true +ij_groovy_spaces_around_regex_operators = true +ij_groovy_spaces_around_relational_operators = true +ij_groovy_spaces_around_shift_operators = true +ij_groovy_spaces_within_annotation_parentheses = false +ij_groovy_spaces_within_array_initializer_braces = false +ij_groovy_spaces_within_braces = true +ij_groovy_spaces_within_brackets = false +ij_groovy_spaces_within_cast_parentheses = false +ij_groovy_spaces_within_catch_parentheses = false +ij_groovy_spaces_within_for_parentheses = false +ij_groovy_spaces_within_gstring_injection_braces = false +ij_groovy_spaces_within_if_parentheses = false +ij_groovy_spaces_within_list_or_map = false +ij_groovy_spaces_within_method_call_parentheses = false +ij_groovy_spaces_within_method_parentheses = false +ij_groovy_spaces_within_parentheses = false +ij_groovy_spaces_within_switch_parentheses = false +ij_groovy_spaces_within_synchronized_parentheses = false +ij_groovy_spaces_within_try_parentheses = false +ij_groovy_spaces_within_tuple_expression = false +ij_groovy_spaces_within_while_parentheses = false +ij_groovy_special_else_if_treatment = true +ij_groovy_ternary_operation_wrap = off +ij_groovy_throws_keyword_wrap = off +ij_groovy_throws_list_wrap = off +ij_groovy_use_flying_geese_braces = false +ij_groovy_use_fq_class_names = false +ij_groovy_use_fq_class_names_in_javadoc = true +ij_groovy_use_relative_indents = false +ij_groovy_use_single_class_imports = true +ij_groovy_variable_annotation_wrap = off +ij_groovy_while_brace_force = never +ij_groovy_while_on_new_line = false +ij_groovy_wrap_chain_calls_after_dot = false +ij_groovy_wrap_long_lines = false + +[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,composer.lock,jest.config}] +indent_size = 2 +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_add_space = false +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.http,*.rest}] +indent_size = 0 +ij_continuation_indent_size = 4 +ij_http-request_call_parameters_wrap = normal +ij_http-request_method_parameters_wrap = split_into_lines +ij_http-request_space_before_comma = true +ij_http-request_spaces_around_assignment_operators = true + +[{*.kt,*.kts}] +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = off +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = false +ij_kotlin_call_parameters_right_paren_on_new_line = false +ij_kotlin_call_parameters_wrap = off +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_continuation_indent_for_chained_calls = true +ij_kotlin_continuation_indent_for_expression_bodies = true +ij_kotlin_continuation_indent_in_argument_lists = true +ij_kotlin_continuation_indent_in_elvis = true +ij_kotlin_continuation_indent_in_if_conditions = true +ij_kotlin_continuation_indent_in_parameter_lists = true +ij_kotlin_continuation_indent_in_supertype_lists = true +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = off +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = false +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = off +ij_kotlin_method_parameters_new_line_after_left_paren = false +ij_kotlin_method_parameters_right_paren_on_new_line = false +ij_kotlin_method_parameters_wrap = off +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.** +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 0 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.pb,*.textproto}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_prototext_keep_blank_lines_in_code = 2 +ij_prototext_keep_indents_on_empty_lines = false +ij_prototext_keep_line_breaks = true +ij_prototext_space_after_colon = true +ij_prototext_space_after_comma = true +ij_prototext_space_before_colon = false +ij_prototext_space_before_comma = false +ij_prototext_spaces_within_braces = true +ij_prototext_spaces_within_brackets = false + +[{*.properties,spring.handlers,spring.schemas}] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..adbb253e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,20 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: dheid + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Code snippets** +Code to reproduce the behavior + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..d5a45c1a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[REQUEST]" +labels: enhancement +assignees: dheid + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 04ceb6e8..cd85bf2d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,14 @@ version: 2 updates: - - package-ecosystem: "maven" - directory: "/" + - package-ecosystem: maven + directory: / schedule: - interval: "weekly" + interval: weekly + assignees: + - dheid + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + assignees: + - dheid diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8b137891..42d2b4f0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1 +1,11 @@ + +- [ ] Ensure that the pull request title represents the desired changelog entry +- [ ] Please describe what you did +- [ ] Link to relevant issues in GitHub +- [ ] Link to relevant pull requests, esp. upstream and downstream changes +- [ ] Ensure you have provided tests - that demonstrates feature works or fixes the issue + + diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..6830782a --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,81 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' + +categories: + - title: 💥 Breaking changes + labels: + - breaking + - title: 🚨 Removed + labels: + - removed + - title: 🎉 Major features and improvements + labels: + - major-enhancement + - major-rfe + - title: 🐛 Major bug fixes + labels: + - major-bug + - title: ⚠️ Deprecated + labels: + - deprecated + - title: 🚀 New features and improvements + labels: + - enhancement + - feature + - rfe + - title: 🐛 Bug fixes + labels: + - bug + - fix + - bugfix + - regression + - regression-fix + - title: 🌐 Localization and translation + labels: + - localization + - title: 👷 Changes for developers + labels: + - developer + - title: 📝 Documentation updates + labels: + - documentation + - title: 👻 Maintenance + labels: + - chore + - internal + - maintenance + - title: 🚦 Tests + labels: + - test + - tests + - title: ✍ Other changes + - title: 📦 Dependency updates + labels: + - dependencies + collapse-after: 15 + +exclude-labels: + - reverted + - no-changelog + - skip-changelog + - invalid + +template: | + ## Changes + + $CHANGES + +autolabeler: + - label: 'documentation' + files: + - '*.md' + branch: + - '/docs{0,1}\/.+/' + - label: 'bug' + branch: + - '/fix\/.+/' + title: + - '/fix/i' + - label: 'enhancement' + branch: + - '/feature\/.+/' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..1711ae4b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: Java CI with Maven + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + pull-requests: write + checks: write + contents: read + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - run: mvn -B verify + - uses: madrapps/jacoco-report@v1.7.2 + with: + paths: ${{ github.workspace }}/target/site/jacoco/jacoco.xml + token: ${{ secrets.GITHUB_TOKEN }} + min-coverage-overall: 80 + min-coverage-changed-files: 80 + - uses: scacap/action-surefire-report@v1.9.1 \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..c00ed329 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,36 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '27 11 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + - uses: github/codeql-action/autobuild@v4 + - uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 00000000..ace69696 --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,32 @@ +name: Deploy GitHub Pages +on: + push: + branches: ["main"] + workflow_dispatch: +permissions: + contents: read + pages: write + id-token: write +concurrency: + group: "pages" + cancel-in-progress: false +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/configure-pages@v5 + - uses: actions/jekyll-build-pages@v1 + with: + source: ./ + destination: ./_site + - uses: actions/upload-pages-artifact@v4 + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..3ab20fb0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,36 @@ +name: Release with Maven +on: + workflow_dispatch: +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + server-id: ossrh + server-username: OSSRH_USERNAME + server-password: OSSRH_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PASSPHRASE + - run: | + git config user.email "matomo-java-tracker@daniel-heid.de" + git config user.name "Matomo Java Tracker" + - id: version + run: | + VERSION=$( mvn -B help:evaluate -Dexpression=project.version -q -DforceStdout ) + echo "::set-output name=version::${VERSION%-SNAPSHOT}" + - run: mvn -B release:prepare release:perform + env: + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + - uses: release-drafter/release-drafter@v7 + with: + version: ${{ steps.version.outputs.version }} + publish: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 74c79135..60bd73bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,13 @@ -# General System Files # *~ *\.DS_Store *#*# *.swp info - -# Package Files # **/target/ - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* - -# IDEA files *.iml .idea - -# Eclipse files .classpath .project +test/logs/* +!test/logs/.gitkeep \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 13a50d11..00000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -install: mvn install -DskipTests=true -Dgpg.skip=true -jdk: - - openjdk8 -language: java -after_success: - - mvn clean test jacoco:report coveralls:report - \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..494c2390 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +mail@daniel-heid.de. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..7dfdf721 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,93 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Pull Request Process + +1. Ensure any install or build dependencies are removed before the end of the layer when doing a + build. +2. Update the README.md with details of changes to the interface, this includes new environment + variables, exposed ports, useful file locations and container parameters. +3. Increase the version numbers in any examples files and the README.md to the new version that this + Pull Request would represent. The versioning scheme we use is [SemVer](https://semver.org/). +4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you + do not have permission to do that, you may request the second reviewer to merge it for you. + +## Code of Conduct + +### Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +### Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at mail@daniel-heid.de. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [https://contributor-covenant.org/version/1/4][version] + +[homepage]: https://contributor-covenant.org + +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/README.md b/README.md index 7b325aa1..db941103 100644 --- a/README.md +++ b/README.md @@ -1,283 +1,677 @@ -# Matomo Java Tracker +# Official Matomo Java Tracker [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.piwik.java.tracking/matomo-java-tracker/badge.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/org.piwik.java.tracking/matomo-java-tracker) -[![Build Status](https://travis-ci.org/matomo-org/matomo-java-tracker.svg?branch=master)](https://travis-ci.org/matomo-org/matomo-java-tracker) +[![Build Status](https://github.com/matomo-org/matomo-java-tracker/actions/workflows/build.yml/badge.svg)](https://github.com/matomo-org/matomo-java-tracker/actions/workflows/build.yml) [![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/matomo-org/matomo-java-tracker.svg)](https://isitmaintained.com/project/matomo-org/matomo-java-tracker "Average time to resolve an issue") [![Percentage of issues still open](https://isitmaintained.com/badge/open/matomo-org/matomo-java-tracker.svg)](https://isitmaintained.com/project/matomo-org/matomo-java-tracker "Percentage of issues still open") -Official Java implementation of the [Matomo Tracking HTTP API](https://developer.matomo.org/api-reference/tracking-api). +The Matomo Java Tracker functions as the official Java implementation for +the [Matomo Tracking HTTP API](https://developer.matomo.org/api-reference/tracking-api). This versatile tracker empowers +you to monitor visits, goals, and ecommerce transactions and items. Specifically designed for integration into +server-side applications, it seamlessly integrates with Java-based web applications or web services. + +Key features: + +* Comprehensive tracking capabilities: Monitor page views, goals, ecommerce transactions, and items. +* Customization options: Support for custom dimensions and variables. +* Extensive tracking parameters: Capture data on campaigns, events, downloads, outlinks, site searches, devices, and + visitors. +* Java compatibility: Supports Java 8 and higher, with a dedicated artifact (matomo-java-tracker-java11) for Java 11. +* SSL certificate flexibility: Option to skip SSL certificate validation (caution: not recommended for production). +* Minimal runtime dependencies: Relies solely on SLF4J. +* Asynchronous request support: Permits non-blocking requests. +* Compatibility with Matomo versions 4 and 5. +* Versatile request handling: Send both single and multiple requests. +* Robust documentation: Thoroughly documented with Javadoc for easy reference. +* Data accuracy assurance: Ensures correct values are transmitted to the Matomo Tracking API. +* Logging capabilities: Include debug and error logging for effective troubleshooting. +* Seamless integration: Easily integrates into frameworks such as Spring by creating the MatomoTracker Spring bean for + use in other beans. + +Please prefer the Java 11 version as the Java 8 will become obsolete in the future. + +You can find our [Developer Guide here](https://developer.matomo.org/api-reference/tracking-java) + +Further information on Matomo and Matomo HTTP tracking: + +* [Matomo PHP Tracker](https://github.com/matomo-org/matomo-php-tracker) +* [Matomo Tracking HTTP API](https://developer.matomo.org/api-reference/tracking-api) +* [Introducing the Matomo Java Tracker](https://matomo.org/blog/2015/11/introducing-piwik-java-tracker/) +* [Tracking API User Guide](https://matomo.org/guide/apis/tracking-api/) +* [Matomo Developer](https://developer.matomo.org/) +* [The Matomo project](https://matomo.org/) + +Projects that use Matomo Java Tracker: + +* [Box-c - supports the UNC Libraries' Digital Collections Repository](https://github.com/UNC-Libraries/box-c) +* [DSpace - provide durable access to digital resources](https://github.com/thanvanlong/dspace) +* [Identifiers.org satellite Web SPA](https://github.com/identifiers-org/cloud-satellite-web-spa) +* [Cloud native Resolver Web Service for identifiers.org](https://github.com/identifiers-org/cloud-ws-resolver) +* [Resource Catalogue](https://github.com/madgeek-arc/resource-catalogue) +* [INCEpTION - A semantic annotation platform offering intelligent assistance and knowledge management](https://github.com/inception-project/inception) +* [QualiChain Analytics Intelligent Profiling](https://github.com/JoaoCabrita95/IP) +* [Digitale Ehrenamtskarte](https://github.com/digitalfabrik/entitlementcard) +* [skidfuscator-java-obfuscator](https://github.com/skidfuscatordev/skidfuscator-java-obfuscator) +* [DnA](https://github.com/mercedes-benz/DnA) +* And many closed source projects that we are not aware of :smile: + +## Table of Contents + +* [What Is New?](#what-is-new) +* [Javadoc](#javadoc) +* [Need help?](#need-help) +* [Using this API](#using-this-api) +* [Migration from Version 2 to 3](#migration-from-version-2-to-3) +* [Building and Testing](#building-and-testing) +* [Versioning](#versioning) +* [Contribute](#contribute) +* [License](#license) + +## What Is New? + +### Version 3.4.x + +We fixed a synchronization issue in the Java 8 sender (https://github.com/matomo-org/matomo-java-tracker/issues/168). +To consume the exact amount of space needed for the queries to send to Matomo, we need the collection size of the incoming +requests. So we changed `Iterable` to `Collection` in some `MatomoTracker`. This could affect users, that use parameters +of type `Iterable` in the tracker. Please use `Collection` instead. + +### Version 3.3.x + +Do you still use Matomo Java Tracker 2.x? We created version 3, that is compatible with Matomo 4 and 5 and contains +fewer +dependencies. Release notes can be found here: https://github.com/matomo-org/matomo-java-tracker/releases + +Here are the most important changes: + +* Matomo Java Tracker 3.4.0 is compatible with Matomo 4 and 5 +* less dependencies +* new dimension parameter +* special types allow to provide valid parameters now +* a new implementation for Java 11 uses the HttpClient available since Java 11 + +See also the [Developer Guide here](https://developer.matomo.org/api-reference/tracking-java) ## Javadoc -The Javadoc for this project is hosted as a Github page for this repo. The latest Javadoc can be -found [here](https://matomo-org.github.io/matomo-java-tracker/javadoc/HEAD/index.html). Javadoc for the latest and all -releases can be found [here](https://matomo-org.github.io/matomo-java-tracker/javadoc/index.html). +The Javadoc for all versions can be found +[at javadoc.io](https://javadoc.io/doc/org.piwik.java.tracking/matomo-java-tracker-core/latest/index.html). Thanks to +[javadoc.io](https://javadoc.io) for hosting it. + +## Need help? + +* Check the [Developer Guide](https://developer.matomo.org/api-reference/tracking-java) +* Open an issue in the [Issue Tracker](https://github.com/matomo-org/matomo-java-tracker/issues) +* Use [our GitHub discussions](https://github.com/matomo-org/matomo-java-tracker/discussions) +* Ask your question on [Stackoverflow with the tag `matomo`](https://stackoverflow.com/questions/tagged/matomo) +* Create a thread in the [Matomo Forum](https://forum.matomo.org/) +* Contact [Matomo Support](https://matomo.org/support/) ## Using this API +See the following sections for information on how to use this API. For more information, see the Javadoc. We also +recommend to read the [Tracking API User Guide](https://matomo.org/guide/apis/tracking-api/). +The [Matomo Tracking HTTP API](https://developer.matomo.org/api-reference/tracking-api) is well +documented and contains many examples. + +This project contains the following Maven artifacts: + +1. **matomo-java-tracker-core**: This artifact is the core module of the Matomo Java Tracker project. It provides the + main functionality of the Matomo Java Tracker, which is a Java implementation for the Matomo Tracking HTTP API. This + module is designed to be used as a base for other modules in the project that provide additional functionality or + integrations. +2. **matomo-java-tracker**: This is a specific implementation of the core module designed for Java 8. It provides the + main functionality of the Matomo Java Tracker and is built upon the core. This artifact is + specifically designed for applications running on Java 8. +3. **matomo-java-tracker-java11**: This artifact is a Java 11 implementation of the Matomo Java Tracker. It uses the + HttpClient available since Java 11. It is recommended to use this version if you are using Java 11 or higher. +4. **matomo-java-tracker-spring-boot-starter**: This artifact is a Spring Boot Starter for the Matomo Java Tracker. It + provides auto-configuration for the Matomo Java Tracker in a Spring Boot application. By including this artifact in + your project, you can take advantage of Spring Boot's auto-configuration features to automatically set up and + configure the Matomo Java Tracker. +5. **matomo-java-tracker-servlet-jakarta**: This artifact is specifically designed for applications using the Jakarta + Servlet API (part of Jakarta EE). +6. **matomo-java-tracker-servlet-javax**: This artifact is specifically designed for applications using the older Java + Servlet API (part of Java EE). +7. **matomo-java-tracker-test**: This artifact contains tools for manual testing against a local Matomo instance created + with Docker. It contains a tester class that sends randomized requests to a local Matomo instance and a servlet that + can be used to test the servlet integration. + +Each of these artifacts serves a different purpose and can be used depending on the specific needs of your project and +the Java version you are using. + ### Add library to your build -Add a dependency on Matomo Java Tracker using Maven: +Add a dependency on Matomo Java Tracker using Maven. For Java 8: ```xml - org.piwik.java.tracking - matomo-java-tracker - 2.0 + org.piwik.java.tracking + matomo-java-tracker + 3.4.0 ``` -or Gradle: +For Java 11: + +```xml + + + org.piwik.java.tracking + matomo-java-tracker-java11 + 3.4.0 + +``` + +or Gradle (Java 8): ```groovy dependencies { - implementation("org.piwik.java.tracking:matomo-java-tracker:2.0") + implementation("org.piwik.java.tracking:matomo-java-tracker:3.4.0") } ``` -### Create a Request - -Each MatomoRequest represents an action the user has taken that you want tracked by your Matomo server. Create a -MatomoRequest through - -```java - -import org.matomo.java.tracking.MatomoRequest; - -public class YourImplementation { - - public void yourMethod() { - MatomoRequest request = MatomoRequest.builder() - .siteId(42) - .actionUrl("https://www.mydomain.com/signup") - .actionName("Signup") - .build(); - } +or Gradle (Java 11): +```groovy +dependencies { + implementation("org.piwik.java.tracking:matomo-java-tracker-java11:3.4.0") } - ``` -Per default every request has the following default parameters: - -| Parameter Name | Default Value | -|------------------|--------------------------------| -| required | true | -| visitorId | random 16 character hex string | -| randomValue | random 20 character hex string | -| apiVersion | 1 | -| responseAsImage | false | +or Gradle with Kotlin DSL (Java 8) -Overwrite these properties as desired. +```kotlin +implementation("org.piwik.java.tracking:matomo-java-tracker:3.4.0") +``` -Note that if you want to be able to track campaigns using *Referrers > Campaigns*, you must add the correct -URL parameters to your actionUrl. For example, +or Gradle with Kotlin DSL (Java 11) -```java +```kotlin +implementation("org.piwik.java.tracking:matomo-java-tracker-java11:3.4.0") +``` -package example; +### Spring Boot Module -import org.matomo.java.tracking.MatomoRequest; +If you use Spring Boot 3, you can use the Spring Boot Starter artifact. It will create a MatomoTracker bean for you +and allows you to configure the tracker via application properties. Add the following dependency to your build: -public class YourImplementation { +```xml - public void yourMethod() { + + org.piwik.java.tracking + matomo-java-tracker-spring-boot-starter + 3.4.0 + +``` - MatomoRequest request = MatomoRequest.builder() - .siteId(42) - .actionUrl("http://example.org/landing.html?pk_campaign=Email-Nov2011&pk_kwd=LearnMore") // include the query parameters to the url - .actionName("LearnMore") - .build(); - } +or Gradle: +```groovy +dependencies { + implementation("org.piwik.java.tracking:matomo-java-tracker-spring-boot-starter:3.4.0") } ``` -See [Tracking Campaigns](https://matomo.org/docs/tracking-campaigns/) for more information. +or Gradle with Kotlin DSL -All HTTP query parameters denoted on -the [Matomo Tracking HTTP API](https://developer.matomo.org/api-reference/tracking-api) can be set using the appropriate -getters and setters. See _MatomoRequest.java_ for the mappings of the parameters to their corresponding -Java getters/setters. +```kotlin +implementation("org.piwik.java.tracking:matomo-java-tracker-spring-boot-starter:3.4.0") +``` -Some parameters are dependent on the state of other parameters: -_EcommerceEnabled_ must be called before the following parameters are set: *EcommerceId* and * -EcommerceRevenue*. +The following properties are supported: + +| Property Name | Description | +|----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| matomo.tracker.api-endpoint (required) | The URL to the Matomo Tracking API endpoint. Must be set. | +| matomo.tracker.default-site-id | If you provide a default site id, it will be taken if the action does not contain a site id. | +| matomo.tracker.default-token-auth | If you provide a default token auth, it will be taken if the action does not contain a token auth. | +| matomo.tracker.enabled | The tracker is enabled per default. You can disable it per configuration with this flag. | +| matomo.tracker.log-failed-tracking | Will send errors to the log if the Matomo Tracking API responds with an erroneous HTTP code | +| matomo.tracker.connect-timeout | allows you to change the default connection timeout of 10 seconds. 0 is interpreted as infinite, null uses the system default | +| matomo.tracker.socket-timeout | allows you to change the default socket timeout of 10 seconds. 0 is interpreted as infinite, null uses the system default | +| matomo.tracker.user-agent | used by the request made to the endpoint is `MatomoJavaClient` per default. You can change it by using this builder method. | +| matomo.tracker.proxy-host | The hostname or IP address of an optional HTTP proxy. `proxyPort` must be configured as well | +| matomo.tracker.proxy-port | The port of an HTTP proxy. `proxyHost` must be configured as well. | +| matomo.tracker.proxy-username | If the HTTP proxy requires a username for basic authentication, it can be configured with this method. Proxy host, port and password must also be set. | +| matomo.tracker.proxy-password | The corresponding password for the basic auth proxy user. The proxy host, port and username must be set as well. | +| matomo.tracker.disable-ssl-cert-validation | If set to true, the SSL certificate of the Matomo server will not be validated. This should only be used for testing purposes. Default: false | +| matomo.tracker.disable-ssl-host-verification | If set to true, the SSL host of the Matomo server will not be validated. This should only be used for testing purposes. Default: false | +| matomo.tracker.thread-pool-size | The number of threads that will be used to asynchronously send requests. Default: 2 | +| matomo.tracker.filter.enabled | Enables a servlet filter that tracks every request within the application | + +To ensure the `MatomoTracker` bean is created by the auto configuration, you have to add the following property to +your `application.properties` file: + +```properties +matomo.tracker.api-endpoint=https://your-matomo-domain.tld/matomo.php +``` -_EcommerceId_ and _EcommerceRevenue_ must be set before the following parameters are -set: *EcommerceDiscount*, *EcommerceItem*, *EcommerceLastOrderTimestamp*, * -EcommerceShippingCost*, *EcommerceSubtotal*, and *EcommerceTax*. +Or if you use YAML: -_AuthToken_ must be set before the following parameters are set: *VisitorCity*, * -VisitorCountry*, *VisitorIp*, *VisitorLatitude*, *VisitorLongitude*, and *VisitorRegion* -. +```yaml +matomo: + tracker: + api-endpoint: https://your-matomo-domain.tld/matomo.php +``` -### Sending Requests +You can automatically add the `MatomoTrackerFilter` to your Spring Boot application if you add the following property: -Create a MatomoTracker using the constructor +```properties +matomo.tracker.filter.enabled=true +``` -```java -package example; +Or if you use YAML: -import org.matomo.java.tracking.MatomoTracker; +```yaml +matomo: + tracker: + filter: + enabled: true +``` -public class YourImplementation { +The filter uses `ServletMatomoRequest` to create a `MatomoRequest` from a `HttpServletRequest` on every filter call. - public void yourMethod() { +### Sending a Tracking Request - MatomoTracker tracker = new MatomoTracker("https://your-matomo-domain.tld/matomo.php"); +To let the Matomo Java Tracker send a request to the Matomo instance, you need the following minimal code: - } +```java +import java.net.URI; +import org.matomo.java.tracking.MatomoRequests; +import org.matomo.java.tracking.MatomoTracker; +import org.matomo.java.tracking.TrackerConfiguration; +import org.matomo.java.tracking.parameters.VisitorId; + +/** + * Example for sending a request. + */ +public class SendExample { + + /** + * Example for sending a request. + * + * @param args ignored + */ + public static void main(String[] args) { + + TrackerConfiguration configuration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create("https://www.yourdomain.com/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") + .logFailedTracking(true) + .build(); + + try (MatomoTracker tracker = new MatomoTracker(configuration)) { + tracker.sendRequestAsync(MatomoRequests + .event("Training", "Workout completed", "Bench press", 60.0) + .visitorId(VisitorId.fromString("customer@mail.com")) + .build() + ); + } catch (Exception e) { + throw new RuntimeException("Could not close tracker", e); + } + } } + ``` -using the Matomo Endpoint URL as the first parameter. +This will send a request to the Matomo instance at https://www.yourdomain.com/matomo.php and track a page view for the +visitor customer@mail.com with the action name "Checkout" and action URL "https://www.yourdomain.com/checkout" for +the site with id 1. The request will be sent asynchronously, that means the method will return immediately and your +application will not wait for the response of the Matomo server. In the configuration we set the default site id to 1 +and configure the default auth token. With `logFailedTracking` we enable logging of failed tracking requests. -To send a single request, call +If you want to perform an operation after a successful asynchronous call to Matomo, you can use the completable future +result like this: ```java -package example; - -import org.apache.http.HttpResponse; +import java.net.URI; import org.matomo.java.tracking.MatomoRequest; +import org.matomo.java.tracking.MatomoRequests; import org.matomo.java.tracking.MatomoTracker; +import org.matomo.java.tracking.TrackerConfiguration; +import org.matomo.java.tracking.parameters.VisitorId; + +/** + * Example for sending a request and performing an action when the request was sent successfully. + */ +public class ConsumerExample { + + /** + * Example for sending a request and performing an action when the request was sent successfully. + * + * @param args ignored + */ + public static void main(String[] args) { + + TrackerConfiguration configuration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create("https://www.yourdomain.com/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") + .logFailedTracking(true) + .build(); + + try (MatomoTracker tracker = new MatomoTracker(configuration)) { + MatomoRequest request = MatomoRequests + .event("Training", "Workout completed", "Bench press", 60.0) + .visitorId(VisitorId.fromString("customer@mail.com")) + .build(); + + tracker.sendRequestAsync(request) + .thenAccept(req -> System.out.printf("Sent request %s%n", req)) + .exceptionally(throwable -> { + System.err.printf("Failed to send request: %s%n", throwable.getMessage()); + return null; + }); + } catch (Exception e) { + throw new RuntimeException("Could not close tracker", e); + } -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; - -public class YourImplementation { - - public void yourMethod() { - - MatomoRequest request = MatomoRequest.builder().siteId(42).actionUrl("https://www.mydomain.com/some/page").actionName("Signup").build(); - - MatomoTracker tracker = new MatomoTracker("https://your-matomo-domain.tld/matomo.php"); - try { - Future response = tracker.sendRequestAsync(request); - // usually not needed: - HttpResponse httpResponse = response.get(); - int statusCode = httpResponse.getStatusLine().getStatusCode(); - if (statusCode > 399) { - // problem - } - } catch (IOException e) { - throw new UncheckedIOException("Could not send request to Matomo", e); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException("Error while getting response", e); } - } - } + + ``` If you have multiple requests to wish to track, it may be more efficient to send them in a single HTTP call. To do this, send a bulk request. Place your requests in an _Iterable_ data structure and call ```java -package example; - -import org.apache.http.HttpResponse; -import org.matomo.java.tracking.MatomoRequest; -import org.matomo.java.tracking.MatomoRequestBuilder; +import java.net.URI; +import org.matomo.java.tracking.MatomoRequests; import org.matomo.java.tracking.MatomoTracker; +import org.matomo.java.tracking.TrackerConfiguration; +import org.matomo.java.tracking.parameters.VisitorId; + +/** + * Example for sending multiple requests in one bulk request. + */ +public class BulkExample { + + /** + * Example for sending multiple requests in one bulk request. + * + * @param args ignored + */ + public static void main(String[] args) { + + TrackerConfiguration configuration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create("https://www.yourdomain.com/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") + .logFailedTracking(true) + .build(); + + try (MatomoTracker tracker = new MatomoTracker(configuration)) { + VisitorId visitorId = VisitorId.fromString("customer@mail.com"); + tracker.sendBulkRequestAsync( + MatomoRequests.siteSearch("Running shoes", "Running", 120L) + .visitorId(visitorId).build(), + MatomoRequests.pageView("VelocityStride ProX Running Shoes") + .visitorId(visitorId).build(), + MatomoRequests.ecommerceOrder("QXZ-789LMP", 100.0, 124.0, 19.0, 10.0, 5.0) + .visitorId(visitorId) + .build() + ); + } catch (Exception e) { + throw new RuntimeException("Could not close tracker", e); + } -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; - -public class YourImplementation { - - public void yourMethod() { - - Collection requests = new ArrayList<>(); - MatomoRequestBuilder builder = MatomoRequest.builder().siteId(42); - requests.add(builder.actionUrl("https://www.mydomain.com/some/page").actionName("Some Page").build()); - requests.add(builder.actionUrl("https://www.mydomain.com/another/page").actionName("Another Page").build()); - - MatomoTracker tracker = new MatomoTracker("https://your-matomo-domain.tld/matomo.php"); - try { - Future response = tracker.sendBulkRequestAsync(requests); - // usually not needed: - HttpResponse httpResponse = response.get(); - int statusCode = httpResponse.getStatusLine().getStatusCode(); - if (statusCode > 399) { - // problem - } - } catch (IOException e) { - throw new UncheckedIOException("Could not send request to Matomo", e); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException("Error while getting response", e); } - } - } - ``` -If some of the parameters that you've specified in the bulk request require AuthToken to be set, this can also be set in -the bulk request through +This will send two requests in a single HTTP call. The requests will be sent asynchronously. + +Per default every request has the following default parameters: + +| Parameter Name | Default Value | +|-----------------|--------------------------------| +| required | true | +| visitorId | random 16 character hex string | +| randomValue | random 20 character hex string | +| apiVersion | 1 | +| responseAsImage | false | + +Overwrite these properties as desired. We strongly recommend your to determine the visitor id for every user using +a unique identifier, e.g. an email address. If you do not provide a visitor id, a random visitor id will be generated. + +Ecommerce requests contain ecommerce items, that can be fluently build: ```java -package example; +import java.net.URI; +import org.matomo.java.tracking.MatomoRequests; +import org.matomo.java.tracking.MatomoTracker; +import org.matomo.java.tracking.TrackerConfiguration; +import org.matomo.java.tracking.parameters.EcommerceItem; +import org.matomo.java.tracking.parameters.EcommerceItems; +import org.matomo.java.tracking.parameters.VisitorId; + +/** + * Example for sending an ecommerce request. + */ +public class EcommerceExample { + + /** + * Example for sending an ecommerce request. + * + * @param args ignored + */ + public static void main(String[] args) { + + TrackerConfiguration configuration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create("https://www.yourdomain.com/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") + .logFailedTracking(true) + .build(); + + try (MatomoTracker tracker = new MatomoTracker(configuration)) { + tracker.sendBulkRequestAsync(MatomoRequests + .ecommerceCartUpdate(50.0) + .ecommerceItems(EcommerceItems + .builder() + .item(EcommerceItem + .builder() + .sku("XYZ12345") + .name("Matomo - The big book about web analytics") + .category("Education & Teaching") + .price(23.1) + .quantity(2) + .build()) + .item(EcommerceItem + .builder() + .sku("B0C2WV3MRJ") + .name("Matomo for data visualization") + .category("Education & Teaching") + .price(15.0) + .quantity(1) + .build()) + .build()) + .visitorId(VisitorId.fromString("customer@mail.com")) + .build() + ); + } catch (Exception e) { + throw new RuntimeException("Could not close tracker", e); + } + + } -import org.apache.http.HttpResponse; -import org.matomo.java.tracking.MatomoLocale; +} +``` + +Note that if you want to be able to track campaigns using *Referrers > Campaigns*, you must add the correct +URL parameters to your actionUrl. See [Tracking Campaigns](https://matomo.org/docs/tracking-campaigns/) for more +information. All HTTP query parameters +denoted on the [Matomo Tracking HTTP API](https://developer.matomo.org/api-reference/tracking-api) can be set using the +appropriate getters and setters. See +[MatomoRequest](core/src/main/java/org/matomo/java/tracking/MatomoRequest.java) for the mappings of the parameters to +their corresponding attributes. + +Requests are validated prior to sending. If a request is invalid, a `MatomoException` will be thrown. + +In a Servlet environment, it might be easier to use the `ServletMatomoRequest` class to create a `MatomoRequest` from a +`HttpServletRequest`: + +```java +import jakarta.servlet.http.HttpServletRequest; import org.matomo.java.tracking.MatomoRequest; -import org.matomo.java.tracking.MatomoRequestBuilder; +import org.matomo.java.tracking.MatomoRequests; import org.matomo.java.tracking.MatomoTracker; +import org.matomo.java.tracking.parameters.VisitorId; +import org.matomo.java.tracking.servlet.JakartaHttpServletWrapper; +import org.matomo.java.tracking.servlet.ServletMatomoRequest; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Locale; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; - -public class YourImplementation { - - public void yourMethod() { - - Collection requests = new ArrayList<>(); - MatomoRequestBuilder builder = MatomoRequest.builder().siteId(42); - requests.add(builder.actionUrl("https://www.mydomain.com/some/page").actionName("Some Page").build()); - requests.add(builder.actionUrl("https://www.mydomain.com/another/page").actionName("Another Page").visitorCountry(new MatomoLocale(Locale.GERMANY)).build()); - - MatomoTracker tracker = new MatomoTracker("https://your-matomo-domain.tld/matomo.php"); - try { - Future response = tracker.sendBulkRequestAsync(requests, "33dc3f2536d3025974cccb4b4d2d98f4"); // second parameter is authentication token need for country override - // usually not needed: - HttpResponse httpResponse = response.get(); - int statusCode = httpResponse.getStatusLine().getStatusCode(); - if (statusCode > 399) { - // problem - } - } catch (IOException e) { - throw new UncheckedIOException("Could not send request to Matomo", e); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException("Error while getting response", e); - } +public class ServletMatomoRequestExample { - } + private final MatomoTracker tracker; -} + public ServletMatomoRequestExample(MatomoTracker tracker) { + this.tracker = tracker; + } + public void someControllerMethod(HttpServletRequest request) { + MatomoRequest matomoRequest = ServletMatomoRequest + .addServletRequestHeaders( + MatomoRequests.contentImpression( + "Latest Product Announced", + "Main Blog Text", + "https://www.yourdomain.com/blog/2018/10/01/new-product-launches" + ), + JakartaHttpServletWrapper.fromHttpServletRequest(request) + ).visitorId(VisitorId.fromString("customer@mail.com")) + // ... + .build(); + tracker.sendRequestAsync(matomoRequest); + // ... + } +} ``` -## Building - -You need a GPG signing key on your machine. Please follow these -instructions: https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key +The `ServletMatomoRequest` automatically sets the action URL, applies browser request headers, corresponding Matomo +cookies and the visitor IP address. It sets the visitor ID, Matomo session ID, custom variables and heatmap +if Matomo cookies are present. Since there was a renaming from Java EE (javax) to Jakarta EE (jakarta), we provide a +wrapper class `JakartaHttpServletWrapper` for Jakarta and `JavaxHttpServletWrapper` for javax. + +### Tracking Configuration + +The `MatomoTracker` can be configured using the `TrackerConfiguration` object. The following configuration options are +available: + +* `.apiEndpoint(...)` An `URI` object that points to the Matomo Tracking API endpoint of your Matomo installation. Must + be set. +* `.defaultSiteId(...)` If you provide a default site id, it will be taken if the action does not contain a site id. +* `.defaultTokenAuth(...)` If you provide a default token auth, it will be taken if the action does not contain a token + auth. +* `.enabled(...)` The tracker is enabled per default. You can disable it per configuration with this flag. +* `.logFailedTracking(...)` Will send errors to the log if the Matomo Tracking API responds with an erroneous HTTP code +* `.connectTimeout(...)` allows you to change the default connection timeout of 10 seconds. 0 is + interpreted as infinite, null uses the system default +* `.socketTimeout(...)` allows you to change the default socket timeout of 10 seconds. 0 is + interpreted as infinite, null uses the system default +* `.userAgent(...)` used by the request made to the endpoint is `MatomoJavaClient` per default. You can change it by + using this builder method. +* `.proxyHost(...)` The hostname or IP address of an optional HTTP proxy. `proxyPort` must be + configured as well +* `.proxyPort(...)` The port of an HTTP proxy. `proxyHost` must be configured as well. +* `.proxyUsername(...)` If the HTTP proxy requires a username for basic authentication, it can be + configured with this method. Proxy host, port and password must also be set. +* `.proxyPassword(...)` The corresponding password for the basic auth proxy user. The proxy host, + port and username must be set as well. +* `.disableSslCertValidation(...)` If set to true, the SSL certificate of the Matomo server will not be validated. This + should only be used for testing purposes. Default: false +* `.disableSslHostVerification(...)` If set to true, the SSL host of the Matomo server will not be validated. This + should only be used for testing purposes. Default: false +* `.threadPoolSize(...)` The number of threads that will be used to asynchronously send requests. Default: 2 + +## Migration from Version 2 to 3 + +We improved this library by adding the dimension parameter and removing outdated parameters in Matomo version 5, +removing some dependencies (that even contained vulnerabilities) and increasing maintainability. Sadly this includes the +following breaking changes: + +### Removals + +* The parameter `actionTime` (`gt_ms`) is no longer supported by Matomo 5 and was removed. +* Many methods marked as deprecated in version 2 were removed. Please see the + former [Javadoc](https://javadoc.io/doc/org.piwik.java.tracking/matomo-java-tracker/2.1/index.html) of version 2 to + get the + deprecated methods. +* We removed the vulnerable dependency to the Apache HTTP client. Callbacks are no longer of + type `FutureCallback`, but + `Consumer` instead. +* The `send...` methods of `MatomoTracker` no longer return a value (usually Matomo always returns an HTTP 204 response + without a body). If the request fails, an exception will be thrown. +* Since there are several ways on how to set the auth token, `verifyAuthTokenSet` was removed. Just check yourself, + whether your auth token is null. However, the tracker checks, whether an auth token is either set by parameter, by + request or per configuration. +* Due to a major refactoring on how the queries are created, we no longer use a large map instead of concrete attributes + to collect the Matomo parameters. Therefore `getParameters()` of class `MatomoRequest` no longer exists. Please use + getters and setters instead. +* The methods `verifyEcommerceEnabled()` and `verifyEcommerceState()` were removed from `MatomoRequest`. The request + will be validated prior to sending and not during construction. +* `getRandomHexString` was removed. Use `RandomValue.random()` or `VisitorId.random()` instead. + +### Type Changes and Renaming + +* `requestDatetime`, `visitorPreviousVisitTimestamp`, `visitorFirstVisitTimestamp`, `ecommerceLastOrderTimestamp` are + of type `Instant`. You can use `Instant.ofEpochSecond()` to create + them from epoch seconds. +* `requestDatetime` was renamed to `requestTimestamp` due to setter collision and downwards compatibility +* `goalRevenue` is the same parameter as `ecommerceRevenue` and was removed to prevent duplication. + Use `ecommerceRevenue` instead. +* `setEventValue` requires a double parameter +* `setEcommerceLastOrderTimestamp` requires an `Instant` parameter +* `headerAcceptLanguage` is of type `AcceptLanguage`. You can build it easily + using `AcceptLanguage.fromHeader("de")` +* `visitorCountry` is of type `Country`. You can build it easily using `AcceptLanguage.fromCode("fr")` +* `deviceResolution` is of type `DeviceResolution`. You can build it easily + using `DeviceResolution.builder.width(...).height(...).build()`. To easy the migration, we added a constructor + method `DeviceResolution.fromString()` that accepts inputs of kind _width_x_height_, e.g. `100x200` +* `pageViewId` is of type `UniqueId`. You can build it easily using `UniqueId.random()` +* `randomValue` is of type `RandomValue`. You can build it easily using `RandomValue.random()`. However, if you + really + want to insert a custom string here, use `RandomValue.fromString()` construction method. +* URL was removed due to performance and complicated exception handling and problems with parsing of complex + URLs. `actionUrl`, `referrerUrl`, `outlinkUrl`, `contentTarget` and `downloadUrl` are strings. +* `getCustomTrackingParameter()` of `MatomoRequest` returns an unmodifiable list. +* Instead of `IllegalStateException` the tracker throws `MatomoException` +* In former versions the goal id had always to be zero or null. You can define higher numbers than zero. +* For more type changes see the sections below. + +### Visitor ID + +* `visitorId` and `visitorCustomId` are of type `VisitorId`. You can build them easily + using `VisitorId.fromHash(...)`. +* You can use `VisitorId.fromHex()` to create a `VisitorId` from a string that contains only hexadecimal characters. +* Or simply use `VisitorId.fromUUID()` to create a `VisitorId` from a `UUID` object. +* VisitorId.fromHex() supports less than 16 hexadecimal characters. If the string is shorter than 16 characters, + the remaining characters will be filled with zeros. + +### Custom Variables + +* According to Matomo, custom variables should no longer be used. Please use dimensions instead. Dimension support has + been introduced. +* `CustomVariable` is in package `org.matomo.java.tracking.parameters`. +* `customTrackingParameters` in `MatomoRequestBuilder` requires a `Map>` instead + of `Map` +* `pageCustomVariables` and `visitCustomVariables` are of type `CustomVariables` instead of collections. Create them + with `new CustomVariables().add(customVariable)` +* `setPageCustomVariable` and `getPageCustomVariable` no longer accept a string as an index. Please use integers + instead. +* Custom variables will be sent URL encoded + +## Building and testing This project can be tested and built by calling @@ -285,32 +679,81 @@ This project can be tested and built by calling mvn install ``` -The built jars and javadoc can be found in `target`. By using the install Maven goal, the snapshot -version can be used using your local Maven repository for testing purposes, e.g. +The built jars and javadoc can be found in `target`. By using +the Maven goal `install, a snapshot +version can be used in your local Maven repository for testing purposes, e.g. ```xml - - org.piwik.java.tracking - matomo-java-tracker - 2.1-SNAPSHOT + org.piwik.java.tracking + matomo-java-tracker + 3.4.1-SNAPSHOT ``` -This project also supports [Pitest](http://pitest.org/) mutation testing. This report can be generated by calling +## Testing on a local Matomo instance + +To start a local Matomo instance for testing, you can use the docker-compose file in the root directory of this project. +Start the docker containers with ```shell -mvn org.pitest:pitest-maven:mutationCoverage +docker-compose up -d ``` -and will produce an HTML report at `target/pit-reports/YYYYMMDDHHMI` +You need to adapt your config.ini.php file and change +the following line: + +```ini +[General] +trusted_hosts[] = "localhost:8080" +``` + +to + +```ini +[General] +trusted_hosts[] = "localhost:8080" +``` -Clean this project using +After that you can access Matomo at http://localhost:8080. You have to set up Matomo first. The database credentials are +`matomo` and `matomo`. The database name is `matomo`. The (internal) database host address is `database`. The database +port is `3306`. Set the URL to http://localhost and enable ecommerce. + +The following snippets helps you to do this quickly: ```shell -mvn clean +docker-compose exec matomo sed -i 's/localhost/localhost:8080/g' /var/www/html/config/config.ini.php +``` + +After the installation you can run `MatomoTrackerTester` in the module `test` to test the tracker. It will send +multiple randomized +requests to the local Matomo instance. + +To enable debug logging, you append the following line to the `config.ini.php` file: + +```ini +[Tracker] +debug = 1 ``` +Use the following snippet to do this: + +```shell +docker-compose exec matomo sh -c 'echo -e "\n\n[Tracker]\ndebug = 1\n" >> /var/www/html/config/config.ini.php' +``` + +To test the servlet integration, run `MatomoServletTester` in your favorite IDE. It starts an embedded Jetty server +that serves a simple servlet. The servlet sends a request to the local Matomo instance if you call the URL +http://localhost:8090/track.html. Maybe you need to disable support for the Do Not Track preference in Matomo to get the +request tracked: Go to _Administration > Privacy > Do Not Track_ and disable the checkbox _Respect Do Not Track. +We also recommend to install the Custom Variables plugin from Marketplace to the test custom variables feature and +setup some dimensions. + +## Versioning + +We use [SemVer](https://semver.org/) for versioning. For the versions available, see +the [tags on this repository](https://github.com/matomo-org/matomo-java-tracker/tags). + ## Contribute Have a fantastic feature idea? Spot a bug? We would absolutely love for you to contribute to this project! Please feel @@ -322,11 +765,26 @@ free to: * Write awesome test to test your awesome code * Verify that everything is working as it should by running _mvn test_. If everything passes, you may want to make sure that your tests are covering everything you think they are! - Run `mvn org.pitest:pitest-maven:mutationCoverage` to find out! + Run `mvn verify` to find out! * Commit this code to your repository * Submit a pull request from your branch to our dev branch and let us know why you made the changes you did * We'll take a look at your request and work to get it integrated with the repo! +Please read [the contribution document](CONTRIBUTING.md) for details on our code of conduct, and the +process for submitting pull requests to us. + +We use Checkstyle and JaCoCo to ensure code quality. Please run `mvn verify` before submitting a pull request. Please +provide tests for your changes. We use JUnit 5 for testing. Coverage should be at least 80%. + +## Other Java Matomo Tracker Implementations + +* [The original Piwik Java Tracker Implementation](https://github.com/summitsystemsinc/piwik-java-tracking) +* [Matomo SDK for Android](https://github.com/matomo-org/matomo-sdk-android) +* [Piwik SDK Android]( https://github.com/lkogler/piwik-sdk-android) +* [piwik-tracking](https://github.com/ralscha/piwik-tracking) +* [Matomo Tracking API Java Client](https://github.com/dheid/matomo-tracker) -> Most of the code was integrated in the + official Matomo Java Tracker + ## License This software is released under the BSD 3-Clause license. See [LICENSE](LICENSE). @@ -334,3 +792,5 @@ This software is released under the BSD 3-Clause license. See [LICENSE](LICENSE) ## Copyright Copyright (c) 2015 General Electric Company. All rights reserved. + + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..2ebd49e5 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +# Security Policy + +## Supported Versions + +The following versions of this library are +currently being supported with security updates. + +| Version | Supported | +|---------|------------------------| +| >3 | :white_check_mark: yes | +| <=2 | ✖️ no | + +## Reporting a Vulnerability + +If you found a security vulerability please don't hesitate to send me a message, +open a new [discussion](https://github.com/matomo-org/matomo-java-tracker/discussions) or +open a new [issue](https://github.com/matomo-org/matomo-java-tracker/issues). diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 00000000..8a41bc88 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 00000000..a716cbbb --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,46 @@ + + 4.0.0 + + + org.piwik.java.tracking + matomo-java-tracker-parent + 3.4.1-SNAPSHOT + ../pom.xml + + + matomo-java-tracker-core + jar + + Matomo Java Tracker Core + + + + com.github.spotbugs + spotbugs-annotations + + + org.slf4j + slf4j-api + + + org.projectlombok + lombok + provided + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + org.slf4j + slf4j-simple + test + + + diff --git a/core/src/main/java/lombok.config b/core/src/main/java/lombok.config new file mode 100644 index 00000000..7a21e880 --- /dev/null +++ b/core/src/main/java/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/core/src/main/java/org/matomo/java/tracking/ActionType.java b/core/src/main/java/org/matomo/java/tracking/ActionType.java new file mode 100644 index 00000000..5033a776 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/ActionType.java @@ -0,0 +1,33 @@ +package org.matomo.java.tracking; + +import java.util.function.BiConsumer; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +/** + * The type of action performed (download or outlink). + */ +@RequiredArgsConstructor +public enum ActionType { + DOWNLOAD(MatomoRequest.MatomoRequestBuilder::downloadUrl), + LINK(MatomoRequest.MatomoRequestBuilder::outlinkUrl); + + @NonNull + private final BiConsumer consumer; + + /** + * Applies the action URL to the given builder. + * + * @param builder The builder to apply the action URL to. + * @param actionUrl The action URL to apply. + * + * @return The builder with the action URL applied. + */ + public MatomoRequest.MatomoRequestBuilder applyUrl( + @NonNull MatomoRequest.MatomoRequestBuilder builder, + @NonNull String actionUrl + ) { + consumer.accept(builder, actionUrl); + return builder; + } +} diff --git a/core/src/main/java/org/matomo/java/tracking/AuthToken.java b/core/src/main/java/org/matomo/java/tracking/AuthToken.java new file mode 100644 index 00000000..1c775234 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/AuthToken.java @@ -0,0 +1,49 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import edu.umd.cs.findbugs.annotations.Nullable; + +final class AuthToken { + + private AuthToken() { + // utility + } + + @Nullable + static String determineAuthToken( + @Nullable + String overrideAuthToken, + @Nullable + Iterable requests, + @Nullable + TrackerConfiguration trackerConfiguration + ) { + if (isNotBlank(overrideAuthToken)) { + return overrideAuthToken; + } + if (requests != null) { + for (MatomoRequest request : requests) { + if (request != null && isNotBlank(request.getAuthToken())) { + return request.getAuthToken(); + } + } + } + if (trackerConfiguration != null && isNotBlank(trackerConfiguration.getDefaultAuthToken())) { + return trackerConfiguration.getDefaultAuthToken(); + } + return null; + } + + private static boolean isNotBlank( + @Nullable + String str + ) { + return str != null && !str.isEmpty() && !str.trim().isEmpty(); + } +} diff --git a/core/src/main/java/org/matomo/java/tracking/BulkRequest.java b/core/src/main/java/org/matomo/java/tracking/BulkRequest.java new file mode 100644 index 00000000..10483dc5 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/BulkRequest.java @@ -0,0 +1,43 @@ +package org.matomo.java.tracking; + +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Iterator; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +@Builder +@Value +class BulkRequest { + + @NonNull + Collection queries; + + @Nullable + String authToken; + + byte[] toBytes( + + ) { + if (queries.isEmpty()) { + throw new IllegalArgumentException("Queries must not be empty"); + } + StringBuilder payload = new StringBuilder("{\"requests\":["); + Iterator iterator = queries.iterator(); + while (iterator.hasNext()) { + String query = iterator.next(); + payload.append("\"?").append(query).append('"'); + if (iterator.hasNext()) { + payload.append(','); + } + } + payload.append(']'); + if (authToken != null) { + payload.append(",\"token_auth\":\"").append(authToken).append('"'); + } + return payload.append('}').toString().getBytes(StandardCharsets.UTF_8); + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/CustomVariable.java b/core/src/main/java/org/matomo/java/tracking/CustomVariable.java new file mode 100644 index 00000000..04cbbee5 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/CustomVariable.java @@ -0,0 +1,30 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import lombok.NonNull; + +/** + * A user defined custom variable. + * + * @author brettcsorba + * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead. + */ +@Deprecated +public class CustomVariable extends org.matomo.java.tracking.parameters.CustomVariable { + + /** + * Instantiates a new custom variable. + * + * @param key the key of the custom variable (required) + * @param value the value of the custom variable (required) + */ + public CustomVariable(@NonNull String key, @NonNull String value) { + super(key, value); + } +} diff --git a/core/src/main/java/org/matomo/java/tracking/DaemonThreadFactory.java b/core/src/main/java/org/matomo/java/tracking/DaemonThreadFactory.java new file mode 100644 index 00000000..9dc93b45 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/DaemonThreadFactory.java @@ -0,0 +1,17 @@ +package org.matomo.java.tracking; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +class DaemonThreadFactory implements ThreadFactory { + + private final AtomicInteger count = new AtomicInteger(); + + @Override + public Thread newThread(@NonNull Runnable r) { + Thread thread = new Thread(null, r, String.format("MatomoJavaTracker-%d", count.getAndIncrement())); + thread.setDaemon(true); + return thread; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/matomo/java/tracking/EcommerceItem.java b/core/src/main/java/org/matomo/java/tracking/EcommerceItem.java new file mode 100644 index 00000000..64ac6037 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/EcommerceItem.java @@ -0,0 +1,35 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + + +/** + * A user defined custom variable. + * + * @author brettcsorba + * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead. + */ +@Deprecated +public class EcommerceItem extends org.matomo.java.tracking.parameters.EcommerceItem { + + + /** + * Instantiates a new ecommerce item. + * + * @param sku the sku (Stock Keeping Unit) of the item + * @param name the name of the item + * @param category the category of the item + * @param price the price of the item + * @param quantity the quantity of the item + */ + public EcommerceItem( + String sku, String name, String category, Double price, Integer quantity + ) { + super(sku, name, category, price, quantity); + } +} diff --git a/core/src/main/java/org/matomo/java/tracking/ExecutorServiceCloser.java b/core/src/main/java/org/matomo/java/tracking/ExecutorServiceCloser.java new file mode 100644 index 00000000..1ef11c97 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/ExecutorServiceCloser.java @@ -0,0 +1,42 @@ +package org.matomo.java.tracking; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import lombok.NonNull; + +/** + * Helps to close an executor service. + */ +public class ExecutorServiceCloser { + + /** + * Closes the given executor service. + * + *

This will check whether the executor service is already terminated, and if not, it + * initiates a shutdown and waits a minute. If the minute expires, the executor service + * is shutdown immediately. + * + * @param executorService The executor service to close + */ + public static void close(@NonNull ExecutorService executorService) { + boolean terminated = executorService.isTerminated(); + if (!terminated) { + executorService.shutdown(); + boolean interrupted = false; + while (!terminated) { + try { + terminated = executorService.awaitTermination(1L, TimeUnit.MINUTES); + } catch (InterruptedException e) { + if (!interrupted) { + executorService.shutdownNow(); + interrupted = true; + } + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/InvalidUrlException.java b/core/src/main/java/org/matomo/java/tracking/InvalidUrlException.java new file mode 100644 index 00000000..0b243ab0 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/InvalidUrlException.java @@ -0,0 +1,18 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +/** + * Thrown when an invalid URL is passed to the tracker. + */ +public class InvalidUrlException extends RuntimeException { + + InvalidUrlException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/matomo/java/tracking/MatomoDate.java b/core/src/main/java/org/matomo/java/tracking/MatomoDate.java similarity index 78% rename from src/main/java/org/matomo/java/tracking/MatomoDate.java rename to core/src/main/java/org/matomo/java/tracking/MatomoDate.java index 25706998..331f4923 100644 --- a/src/main/java/org/matomo/java/tracking/MatomoDate.java +++ b/core/src/main/java/org/matomo/java/tracking/MatomoDate.java @@ -4,22 +4,24 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ + package org.matomo.java.tracking; import java.time.Instant; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Locale; +import lombok.Getter; /** * A datetime object that will return the datetime in the format {@code yyyy-MM-dd hh:mm:ss}. * * @author brettcsorba + * @deprecated Please use {@link Instant} */ +@Deprecated +@Getter public class MatomoDate { - private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); private ZonedDateTime zonedDateTime; @@ -27,6 +29,7 @@ public class MatomoDate { * Allocates a Date object and initializes it so that it represents the time * at which it was allocated, measured to the nearest millisecond. */ + @Deprecated public MatomoDate() { zonedDateTime = ZonedDateTime.now(ZoneOffset.UTC); } @@ -38,6 +41,7 @@ public MatomoDate() { * * @param epochMilli the milliseconds since January 1, 1970, 00:00:00 GMT. */ + @Deprecated public MatomoDate(long epochMilli) { zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), ZoneOffset.UTC); } @@ -52,18 +56,6 @@ public void setTimeZone(ZoneId zone) { zonedDateTime = zonedDateTime.withZoneSameInstant(zone); } - /** - * Converts this MatomoDate object to a String of the form:
- *
- * {@code yyyy-MM-dd hh:mm:ss}. - * - * @return a string representation of this MatomoDate - */ - @Override - public String toString() { - return DATE_TIME_FORMATTER.format(zonedDateTime); - } - /** * Converts this datetime to the number of milliseconds from the epoch * of 1970-01-01T00:00:00Z. diff --git a/core/src/main/java/org/matomo/java/tracking/MatomoException.java b/core/src/main/java/org/matomo/java/tracking/MatomoException.java new file mode 100644 index 00000000..1f94a08f --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/MatomoException.java @@ -0,0 +1,25 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +/** + * Thrown when an error occurs while communicating with the Matomo server or when the request is invalid. + */ +public class MatomoException extends RuntimeException { + + private static final long serialVersionUID = 4592083764365938934L; + + MatomoException(String message) { + super(message); + } + + MatomoException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/MatomoLocale.java b/core/src/main/java/org/matomo/java/tracking/MatomoLocale.java new file mode 100644 index 00000000..d0dad67d --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/MatomoLocale.java @@ -0,0 +1,43 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import static java.util.Objects.requireNonNull; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Locale; +import lombok.Getter; +import lombok.Setter; +import org.matomo.java.tracking.parameters.Country; + +/** + * Object representing a locale required by some Matomo query parameters. + * + * @author brettcsorba + * @deprecated Use {@link Country} instead + */ +@Setter +@Getter +@Deprecated +public class MatomoLocale extends Country { + + /** + * Constructs a new MatomoLocale. + * + * @param locale The locale to get the country code from + * @deprecated Please use {@link Country} + */ + @Deprecated + public MatomoLocale( + @NonNull + Locale locale + ) { + super(requireNonNull(locale, "Locale must not be null")); + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/MatomoRequest.java b/core/src/main/java/org/matomo/java/tracking/MatomoRequest.java new file mode 100644 index 00000000..42ef0c29 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/MatomoRequest.java @@ -0,0 +1,1171 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.charset.Charset; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Tolerate; +import org.matomo.java.tracking.parameters.AcceptLanguage; +import org.matomo.java.tracking.parameters.Country; +import org.matomo.java.tracking.parameters.CustomVariable; +import org.matomo.java.tracking.parameters.CustomVariables; +import org.matomo.java.tracking.parameters.DeviceResolution; +import org.matomo.java.tracking.parameters.EcommerceItem; +import org.matomo.java.tracking.parameters.EcommerceItems; +import org.matomo.java.tracking.parameters.RandomValue; +import org.matomo.java.tracking.parameters.UniqueId; +import org.matomo.java.tracking.parameters.VisitorId; + +/** + * A class that implements the + * Matomo Tracking HTTP API. These requests can be sent using {@link MatomoTracker}. + * + * @author brettcsorba + */ +@Builder(builderMethodName = "request") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class MatomoRequest { + + /** + * The ID of the website we're tracking a visit/action for. Only needed, if no default site id is + * configured. + */ + @TrackingParameter(name = "rec") + @Default + private Boolean required = true; + + /** + * The ID of the website we're tracking a visit/action for. Only needed, if no default site id is + * configured. + */ + @TrackingParameter( + name = "idsite", + min = 1 + ) + private Integer siteId; + + /** + * The title of the action being tracked. For page tracks this is used as page title. If enabled + * in your installation you may use the category tree structure in this field. For example, "game + * / register new user" would then create a group "game" and add the item "register new user" in + * it. + */ + @TrackingParameter(name = "action_name") + private String actionName; + + /** + * The full URL for the current action. + */ + @TrackingParameter(name = "url") + private String actionUrl; + + /** + * Defines the API version to use (default: 1). + */ + @TrackingParameter(name = "apiv") + @Default + private String apiVersion = "1"; + + /** + * The unique visitor ID. See {@link VisitorId}. Default is {@link VisitorId#random()} + * + *

Since version 3.0.0 this parameter is of type {@link VisitorId} and not a String anymore. + * Use {@link VisitorId#fromHex(String)} to create a VisitorId from a hex string, + * {@link VisitorId#fromUUID(UUID)} to create it from a UUID or {@link VisitorId#fromHash(long)} + * to create it from a long value. + */ + @TrackingParameter(name = "_id") + @Default + private VisitorId visitorId = VisitorId.random(); + + /** + * Tracks if the visitor is a returning visitor. + * + *

This is done by storing a visitor ID in a 1st party cookie. + */ + @TrackingParameter(name = "_idn") + private Boolean newVisitor; + + /** + * The full HTTP Referrer URL. This value is used to determine how someone got to your website + * (ie, through a website, search engine or campaign) + */ + @TrackingParameter(name = "urlref") + private String referrerUrl; + + /** + * Custom variables are custom name-value pairs that you can assign to your visitors (or page + * views). + */ + @TrackingParameter(name = "_cvar") + private CustomVariables visitCustomVariables; + + /** + * The current count of visits for this visitor. To set this value correctly, it would be required + * to store the value for each visitor in your application (using sessions or persisting in a + * database). Then you would manually increment the counts by one on each new visit or "session", + * depending on how you choose to define a visit. + */ + @TrackingParameter(name = "_idvc", min = 0) + private Integer visitorVisitCount; + + /** + * The UNIX timestamp of this visitor's previous visit. This parameter is used to populate the + * report Visitors > Engagement > Visits by days since last visit. + */ + @TrackingParameter(name = "_viewts") + private Instant visitorPreviousVisitTimestamp; + + /** + * The UNIX timestamp of this visitor's first visit. This could be set to the date where the user + * first started using your software/app, or when he/she created an account. + */ + @TrackingParameter(name = "_idts") + private Instant visitorFirstVisitTimestamp; + + /** + * The campaign name. This parameter will only be used for the first pageview of a visit. + */ + @TrackingParameter(name = "_rcn") + private String campaignName; + + /** + * The campaign keyword (see + * Tracking Campaigns). Used to + * populate the Referrers > Campaigns report (clicking on a campaign loads all + * keywords for this campaign). This parameter will only be used for the first pageview of a + * visit. + */ + @TrackingParameter(name = "_rck") + private String campaignKeyword; + + /** + * The resolution of the device the visitor is using. + */ + @TrackingParameter(name = "res") + private DeviceResolution deviceResolution; + + /** + * The current hour (local time). + */ + @TrackingParameter( + name = "h", + min = 0, + max = 23 + ) + private Integer currentHour; + + /** + * The current minute (local time). + */ + @TrackingParameter( + name = "m", + min = 0, + max = 59 + ) + private Integer currentMinute; + + /** + * The current second (local time). + */ + @TrackingParameter( + name = "s", + min = 0, + max = 59 + ) + private Integer currentSecond; + + /** + * Does the visitor use the Adobe Flash Plugin. + */ + @TrackingParameter(name = "fla") + private Boolean pluginFlash; + + /** + * Does the visitor use the Java plugin. + */ + @TrackingParameter(name = "java") + private Boolean pluginJava; + + /** + * Does the visitor use Director plugin. + */ + @TrackingParameter(name = "dir") + private Boolean pluginDirector; + + /** + * Does the visitor use Quicktime plugin. + */ + @TrackingParameter(name = "qt") + private Boolean pluginQuicktime; + + /** + * Does the visitor use Realplayer plugin. + */ + @TrackingParameter(name = "realp") + private Boolean pluginRealPlayer; + + /** + * Does the visitor use a PDF plugin. + */ + @TrackingParameter(name = "pdf") + private Boolean pluginPDF; + + /** + * Does the visitor use a Windows Media plugin. + */ + @TrackingParameter(name = "wma") + private Boolean pluginWindowsMedia; + + /** + * Does the visitor use a Gears plugin. + */ + @TrackingParameter(name = "gears") + private Boolean pluginGears; + + /** + * Does the visitor use a Silverlight plugin. + */ + @TrackingParameter(name = "ag") + private Boolean pluginSilverlight; + + /** + * Does the visitor's client is known to support cookies. + */ + @TrackingParameter(name = "cookie") + private Boolean supportsCookies; + + /** + * An override value for the User-Agent HTTP header field. + */ + @TrackingParameter(name = "ua") + private String headerUserAgent; + + /** + * An override value for the Accept-Language HTTP header field. This value is used to detect the + * visitor's country if GeoIP is not enabled. + */ + @TrackingParameter(name = "lang") + private AcceptLanguage headerAcceptLanguage; + + /** + * Defines the User ID for this request. User ID is any non-empty unique string identifying the + * user (such as an email address or a username). When specified, the User ID will be "enforced". + * This means that if there is no recent visit with this User ID, a new one will be created. If a + * visit is found in the last 30 minutes with your specified User ID, then the new action will be + * recorded to this existing visit. + */ + @TrackingParameter(name = "uid") + private String userId; + + /** + * defines the visitor ID for this request. + */ + @TrackingParameter(name = "cid") + private VisitorId visitorCustomId; + + /** + * will force a new visit to be created for this action. + */ + @TrackingParameter(name = "new_visit") + private Boolean newVisit; + + /** + * Custom variables are custom name-value pairs that you can assign to your visitors (or page + * views). + */ + @TrackingParameter(name = "cvar") + private CustomVariables pageCustomVariables; + + /** + * An external URL the user has opened. Used for tracking outlink clicks. We recommend to also set + * the url parameter to this same value. + */ + @TrackingParameter(name = "link") + private String outlinkUrl; + + /** + * URL of a file the user has downloaded. Used for tracking downloads. We recommend to also set + * the url parameter to this same value. + */ + @TrackingParameter(name = "download") + private String downloadUrl; + + /** + * The Site Search keyword. When specified, the request will not be tracked as a normal pageview + * but will instead be tracked as a Site Search request + */ + @TrackingParameter(name = "search") + private String searchQuery; + + /** + * When search is specified, you can optionally specify a search category with this parameter. + */ + @TrackingParameter(name = "search_cat") + private String searchCategory; + + /** + * When search is specified, we also recommend setting the search_count to the number of search + * results displayed on the results page. When keywords are tracked with &search_count=0 they will + * appear in the "No Result Search Keyword" report. + */ + @TrackingParameter( + name = "search_count", + min = 0 + ) + private Long searchResultsCount; + + /** + * Accepts a six character unique ID that identifies which actions were performed on a specific + * page view. When a page was viewed, all following tracking requests (such as events) during that + * page view should use the same pageview ID. Once another page was viewed a new unique ID should + * be generated. Use [0-9a-Z] as possible characters for the unique ID. + */ + @TrackingParameter(name = "pv_id") + private UniqueId pageViewId; + + /** + * If specified, the tracking request will trigger a conversion for the goal of the website being + * tracked with this ID. The value 0 tracks an ecommerce interaction. + */ + @TrackingParameter(name = "idgoal", min = 0) + private Integer goalId; + + /** + * The grand total for the ecommerce order (required when tracking an ecommerce order). + */ + @TrackingParameter(name = "revenue", min = 0) + private Double ecommerceRevenue; + + /** + * The charset of the page being tracked. Specify the charset if the data you send to Matomo is + * encoded in a different character set than the default utf-8 + */ + @TrackingParameter(name = "cs") + private Charset characterSet; + + /** + * can be optionally sent along any tracking request that isn't a page view. For example, it can + * be sent together with an event tracking request. The advantage being that should you ever + * disable the event plugin, then the event tracking requests will be ignored vs if the parameter + * is not set, a page view would be tracked even though it isn't a page view. + */ + @TrackingParameter(name = "ca") + private Boolean customAction; + + /** + * How long it took to connect to server. + */ + @TrackingParameter(name = "pf_net", min = 0) + private Long networkTime; + + /** + * How long it took the server to generate page. + */ + @TrackingParameter(name = "pf_srv", min = 0) + private Long serverTime; + + /** + * How long it takes the browser to download the response from the server. + */ + @TrackingParameter(name = "pf_tfr", min = 0) + private Long transferTime; + + /** + * How long the browser spends loading the webpage after the response was fully received until the + * user can start interacting with it. + */ + @TrackingParameter(name = "pf_dm1", min = 0) + private Long domProcessingTime; + + /** + * How long it takes for the browser to load media and execute any Javascript code listening for + * the DOMContentLoaded event. + */ + @TrackingParameter(name = "pf_dm2", min = 0) + private Long domCompletionTime; + + /** + * How long it takes the browser to execute Javascript code waiting for the window.load event. + */ + @TrackingParameter(name = "pf_onl", min = 0) + private Long onloadTime; + + /** + * eg. Videos, Music, Games... + */ + @TrackingParameter(name = "e_c") + private String eventCategory; + + /** + * An event action like Play, Pause, Duration, Add Playlist, Downloaded, Clicked... + */ + @TrackingParameter(name = "e_a") + private String eventAction; + + /** + * The event name for example a Movie name, or Song name, or File name... + */ + @TrackingParameter(name = "e_n") + private String eventName; + + /** + * Some numeric value that represents the event value. + */ + @TrackingParameter(name = "e_v", min = 0) + private Double eventValue; + + /** + * The name of the content. For instance 'Ad Foo Bar' + */ + @TrackingParameter(name = "c_n") + private String contentName; + + /** + * The actual content piece. For instance the path to an image, video, audio, any text + */ + @TrackingParameter(name = "c_p") + private String contentPiece; + + /** + * The target of the content. For instance the URL of a landing page + */ + @TrackingParameter(name = "c_t") + private String contentTarget; + + /** + * The name of the interaction with the content. For instance a 'click' + */ + @TrackingParameter(name = "c_i") + private String contentInteraction; + + /** + * The unique string identifier for the ecommerce order (required when tracking an ecommerce + * order). + */ + @TrackingParameter(name = "ec_id") + private String ecommerceId; + + /** + * Items in the Ecommerce order. + */ + @TrackingParameter(name = "ec_items") + private EcommerceItems ecommerceItems; + + /** + * The subtotal of the order; excludes shipping. + */ + @TrackingParameter(name = "ec_st", min = 0) + private Double ecommerceSubtotal; + + /** + * Tax amount of the order. + */ + @TrackingParameter(name = "ec_tx", min = 0) + private Double ecommerceTax; + + /** + * Shipping cost of the order. + */ + @TrackingParameter(name = "ec_sh", min = 0) + private Double ecommerceShippingCost; + + /** + * Discount offered. + */ + @TrackingParameter(name = "ec_dt", min = 0) + private Double ecommerceDiscount; + + /** + * The UNIX timestamp of this customer's last ecommerce order. This value is used to process the + * "Days since last order" report. + */ + @TrackingParameter(name = "_ects") + private Instant ecommerceLastOrderTimestamp; + + /** + * 32 character authorization key used to authenticate the API request. We recommend to create a + * user specifically for accessing the Tracking API, and give the user only write permission on + * the website(s). + */ + @TrackingParameter( + name = "token_auth", + regex = "[a-z0-9]{32}" + ) + private String authToken; + + + /** + * Override value for the visitor IP (both IPv4 and IPv6 notations supported). + */ + @TrackingParameter(name = "cip") + private String visitorIp; + + /** + * Override for the datetime of the request (normally the current time is used). This can be used + * to record visits and page views in the past. + */ + @TrackingParameter(name = "cdt") + private Instant requestTimestamp; + + /** + * An override value for the country. Must be a two-letter ISO 3166 Alpha-2 country code. + */ + @TrackingParameter( + name = "country", + maxLength = 2 + ) + private Country visitorCountry; + + /** + * An override value for the region. Should be set to a ISO 3166-2 region code, which are used by + * MaxMind's and DB-IP's GeoIP2 databases. See here for a list of them for every country. + */ + @TrackingParameter( + name = "region", + maxLength = 2 + ) + private String visitorRegion; + + /** + * An override value for the city. The name of the city the visitor is located in, eg, Tokyo. + */ + @TrackingParameter(name = "city") + private String visitorCity; + + /** + * An override value for the visitor's latitude, eg 22.456. + */ + @TrackingParameter(name = "lat", min = -90, max = 90) + private Double visitorLatitude; + + /** + * An override value for the visitor's longitude, eg 22.456. + */ + @TrackingParameter(name = "long", min = -180, max = 180) + private Double visitorLongitude; + + /** + * When set to false, the queued tracking handler won't be used and instead the tracking request + * will be executed directly. This can be useful when you need to debug a tracking problem or want + * to test that the tracking works in general. + */ + @TrackingParameter(name = "queuedtracking") + private Boolean queuedTracking; + + /** + * If set to 0 (send_image=0) Matomo will respond with an HTTP 204 response code instead of a GIF + * image. This improves performance and can fix errors if images are not allowed to be obtained + * directly (like Chrome Apps). Available since Matomo 2.10.0 + * + *

Default is {@code false} + */ + @TrackingParameter(name = "send_image") + @Default + private Boolean responseAsImage = false; + + /** + * If set to true, the request will be a Heartbeat request which will not track any new activity + * (such as a new visit, new action or new goal). The heartbeat request will only update the + * visit's total time to provide accurate "Visit duration" metric when this parameter is set. It + * won't record any other data. This means by sending an additional tracking request when the user + * leaves your site or app with &ping=1, you fix the issue where the time spent of the last page + * visited is reported as 0 seconds. + */ + @TrackingParameter(name = "ping") + private Boolean ping; + + /** + * By default, Matomo does not track bots. If you use the Tracking HTTP API directly, you may be + * interested in tracking bot requests. + */ + @TrackingParameter(name = "bots") + private Boolean trackBotRequests; + + + /** + * Meant to hold a random value that is generated before each request. Using it helps avoid the + * tracking request being cached by the browser or a proxy. + */ + @TrackingParameter(name = "rand") + @Default + private RandomValue randomValue = RandomValue.random(); + + /** + * Meant to hold a random value that is generated before each request. Using it helps avoid the + * tracking request being cached by the browser or a proxy. + */ + @TrackingParameter(name = "debug") + private Boolean debug; + + /** + * Contains an error message describing the error that occurred during the last tracking request. + * + *

Custom action must be enabled for this. + * + *

Required for crash analytics + */ + @TrackingParameter(name = "cra") + private String crashMessage; + + /** + * The type of exception that occurred during the last tracking request. + * + *

Custom action must be enabled for this. + * + *

Typically a fully qualified class name of the exception, e.g. + * {@code java.lang.NullPointerException}. + * + *

Optional for crash analytics + */ + @TrackingParameter(name = "cra_tp") + private String crashType; + + /** + * Category of a crash to group crashes by. + * + *

Custom action must be enabled for this. + * + *

Optional for crash analytics + */ + @TrackingParameter(name = "cra_ct") + private String crashCategory; + + /** + * A stack trace of the exception that occurred during the last tracking request. + * + *

Custom action must be enabled for this. + * + *

Optional for crash analytics + */ + @TrackingParameter(name = "cra_st") + private String crashStackTrace; + + /** + * The originating source of the crash. + * + *

Could be a source file URI or something similar + * + *

Custom action must be enabled for this. + * + *

Optional for crash analytics + */ + @TrackingParameter(name = "cra_ru") + private String crashLocation; + + /** + * The line number of the crash source, where the crash occurred. + * + *

Custom action must be enabled for this. + * + *

Optional for crash analytics + */ + @TrackingParameter(name = "cra_rl", min = 0) + private Integer crashLine; + + /** + * The column within the line where the crash occurred. + * + *

Optional for crash analytics + */ + @TrackingParameter(name = "cra_rc", min = 0) + private Integer crashColumn; + + /** + * The Matomo session ID sent as a cookie {@code MATOMO_SESSID}. + * + *

If not null a cookie with the name {@code MATOMO_SESSID} will be sent with the value of + * this parameter. + */ + private String sessionId; + + /** + * Custom Dimension values for specific Custom Dimension IDs. + * + *

Custom Dimensions plugin must be + * installed. See the + * Custom Dimensions guide. Requires + * Matomo at least 2.15.1 + */ + private Map dimensions; + + /** + * Allows you to specify additional HTTP request parameters that will be sent to Matomo. + * + *

For example, you can use this to set the Accept-Language header, or to set the + * Content-Type. + */ + private Map additionalParameters; + + /** + * You can set additional HTTP headers for the request sent to Matomo. + * + *

For example, you can use this to set the Accept-Language header, or to set the + * Content-Type. + */ + private Map headers; + + /** + * Appends additional cookies to the request. + * + *

This allows you to add Matomo specific cookies, like {@code _pk_id} or {@code _pk_sess} + * coming from Matomo responses to the request. + */ + private Map cookies; + + /** + * Create a new request from the id of the site being tracked and the full url for the current + * action. This constructor also sets: + *

+   * {@code
+   * Required = true
+   * Visior Id = random 16 character hex string
+   * Random Value = random 20 character hex string
+   * API version = 1
+   * Response as Image = false
+   * }
+   * 
+ * Overwrite these values yourself as desired. + * + * @param siteId the id of the website we're tracking a visit/action for + * @param actionUrl the full URL for the current action + * + * @deprecated Please use {@link MatomoRequest#request()} + */ + @Deprecated + public MatomoRequest(int siteId, String actionUrl) { + this.siteId = siteId; + this.actionUrl = actionUrl; + required = true; + visitorId = VisitorId.random(); + randomValue = RandomValue.random(); + apiVersion = "1"; + responseAsImage = false; + } + + /** + * Gets the list of objects currently stored at the specified custom tracking parameter. An empty + * list will be returned if there are no objects set at that key. + * + * @param key the key of the parameter whose list of objects to get. Cannot be null + * + * @return the parameter at the specified key, null if nothing at this key + */ + @Nullable + public Object getCustomTrackingParameter(@NonNull String key) { + if (additionalParameters == null || additionalParameters.isEmpty()) { + return null; + } + return additionalParameters.get(key); + } + + /** + * Set a custom tracking parameter whose toString() value will be sent to the Matomo server. These + * parameters are stored separately from named Matomo parameters, meaning it is not possible to + * overwrite or clear named Matomo parameters with this method. A custom parameter that has the + * same name as a named Matomo parameter will be sent in addition to that named parameter. + * + * @param key the parameter's key. Cannot be null + * @param value the parameter's value. Removes the parameter if null + * + * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder#additionalParameters(Map)} instead. + */ + @Deprecated + public void setCustomTrackingParameter( + @NonNull String key, @Nullable Object value + ) { + + if (value == null) { + if (additionalParameters != null) { + additionalParameters.remove(key); + } + } else { + if (additionalParameters == null) { + additionalParameters = new LinkedHashMap<>(); + } + additionalParameters.put(key, value); + } + } + + /** + * Add a custom tracking parameter to the specified key. If there is already a parameter at this + * key, the new value replaces the old value. + * + * @param key the parameter's key. Cannot be null + * @param value the parameter's value. May be null + * + * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder#additionalParameters(Map)} instead. + */ + @Deprecated + public void addCustomTrackingParameter(@NonNull String key, @Nullable Object value) { + if (additionalParameters == null) { + additionalParameters = new LinkedHashMap<>(); + } + additionalParameters.put(key, value); + } + + /** + * Removes all custom tracking parameters. + * + * @deprecated Please use {@link MatomoRequest.MatomoRequestBuilder#additionalParameters(Map)} + * instead so that you can manage the map yourself. + */ + @Deprecated + public void clearCustomTrackingParameter() { + additionalParameters.clear(); + } + + /** + * Sets idgoal=0 in the request to track an ecommerce interaction: cart update or an + * ecommerce order. + * + * @deprecated Please use {@link MatomoRequest#setGoalId(Integer)} instead + */ + @Deprecated + public void enableEcommerce() { + setGoalId(0); + } + + /** + * Get the {@link EcommerceItem} at the specified index. + * + * @param index the index of the {@link EcommerceItem} to return + * + * @return the {@link EcommerceItem} at the specified index + * @deprecated Use @link {@link MatomoRequest.MatomoRequestBuilder#ecommerceItems(EcommerceItems)} + * instead + */ + @Nullable + @Deprecated + public EcommerceItem getEcommerceItem(int index) { + if (ecommerceItems == null || ecommerceItems.isEmpty()) { + return null; + } + return ecommerceItems.get(index); + } + + /** + * Add an {@link EcommerceItem} to this order. Ecommerce must be enabled, and EcommerceId and + * EcommerceRevenue must first be set. + * + * @param item the {@link EcommerceItem} to add. Cannot be null + * + * @deprecated Use @link {@link MatomoRequest.MatomoRequestBuilder#ecommerceItems(EcommerceItems)} + * instead + */ + @Deprecated + public void addEcommerceItem(@NonNull EcommerceItem item) { + if (ecommerceItems == null) { + ecommerceItems = new EcommerceItems(); + } + ecommerceItems.add(item); + } + + /** + * Clears all {@link EcommerceItem} from this order. + * + * @deprecated Use @link {@link MatomoRequest.MatomoRequestBuilder#ecommerceItems(EcommerceItems)} + * instead + */ + @Deprecated + public void clearEcommerceItems() { + ecommerceItems.clear(); + } + + /** + * Get the page custom variable at the specified key. + * + * @param key the key of the variable to get + * + * @return the variable at the specified key, null if key is not present + * @deprecated Use the {@link #getPageCustomVariables()} method instead. + */ + @Nullable + @Deprecated + public String getPageCustomVariable(String key) { + if (pageCustomVariables == null) { + return null; + } + return pageCustomVariables.get(key); + } + + /** + * Get the page custom variable at the specified index. + * + * @param index the index of the variable to get. Must be greater than 0 + * + * @return the variable at the specified key, null if nothing at this index + * @deprecated Use {@link MatomoRequest#getPageCustomVariables()} instead + */ + @Deprecated + @Nullable + public CustomVariable getPageCustomVariable(int index) { + return getCustomVariable(pageCustomVariables, index); + } + + @Nullable + @Deprecated + private static CustomVariable getCustomVariable(CustomVariables customVariables, int index) { + if (customVariables == null) { + return null; + } + return customVariables.get(index); + } + + /** + * Set a page custom variable with the specified key and value at the first available index. All + * page custom variables with this key will be overwritten or deleted + * + * @param key the key of the variable to set + * @param value the value of the variable to set at the specified key. A null value will remove + * this custom variable + * + * @deprecated Use {@link MatomoRequest#getPageCustomVariables()} instead + */ + @Deprecated + public void setPageCustomVariable( + @NonNull String key, @Nullable String value + ) { + if (value == null) { + if (pageCustomVariables == null) { + return; + } + pageCustomVariables.remove(key); + } else { + CustomVariable variable = new CustomVariable(key, value); + if (pageCustomVariables == null) { + pageCustomVariables = new CustomVariables(); + } + pageCustomVariables.add(variable); + } + } + + /** + * Set a page custom variable at the specified index. + * + * @param customVariable the CustomVariable to set. A null value will remove the CustomVariable + * at the specified index + * @param index the index of he CustomVariable to set + * + * @deprecated Use {@link #getPageCustomVariables()} instead + */ + @Deprecated + public void setPageCustomVariable( + @Nullable CustomVariable customVariable, int index + ) { + if (pageCustomVariables == null) { + if (customVariable == null) { + return; + } + pageCustomVariables = new CustomVariables(); + } + setCustomVariable(pageCustomVariables, customVariable, index); + } + + @Deprecated + private static void setCustomVariable( + CustomVariables customVariables, @Nullable CustomVariable customVariable, int index + ) { + if (customVariable == null) { + customVariables.remove(index); + } else { + customVariables.add(customVariable, index); + } + } + + /** + * Get the datetime of the request. + * + * @return the datetime of the request + * @deprecated Use {@link #getRequestTimestamp()} instead + */ + @Deprecated + @Nullable + public MatomoDate getRequestDatetime() { + return requestTimestamp == null ? null : new MatomoDate(requestTimestamp.toEpochMilli()); + } + + /** + * Set the datetime of the request (normally the current time is used). This can be used to record + * visits and page views in the past. The datetime must be sent in UTC timezone. Note: if you + * record data in the past, you will need to force + * Matomo to re-process reports for the past dates. If you set the Request + * Datetime to a datetime older than four hours then Auth Token must be set. If you + * set + * Request Datetime with a datetime in the last four hours then you + * don't need to pass Auth Token. + * + * @param matomoDate the datetime of the request to set. A null value will remove this parameter + * + * @deprecated Use {@link #setRequestTimestamp(Instant)} instead + */ + @Deprecated + public void setRequestDatetime(MatomoDate matomoDate) { + if (matomoDate == null) { + requestTimestamp = null; + } else { + setRequestTimestamp(matomoDate.getZonedDateTime().toInstant()); + } + } + + + /** + * Get the visit custom variable at the specified key. + * + * @param key the key of the variable to get + * + * @return the variable at the specified key, null if key is not present + * @deprecated Use the {@link #getVisitCustomVariables()} method instead. + */ + @Nullable + @Deprecated + public String getUserCustomVariable(String key) { + if (visitCustomVariables == null) { + return null; + } + return visitCustomVariables.get(key); + } + + /** + * Get the visit custom variable at the specified index. + * + * @param index the index of the variable to get + * + * @return the variable at the specified index, null if nothing at this index + * @deprecated Use {@link #getVisitCustomVariables()} instead + */ + @Nullable + @Deprecated + public CustomVariable getVisitCustomVariable(int index) { + return getCustomVariable(visitCustomVariables, index); + } + + /** + * Set a visit custom variable with the specified key and value at the first available index. All + * visit custom variables with this key will be overwritten or deleted + * + * @param key the key of the variable to set + * @param value the value of the variable to set at the specified key. A null value will remove + * this parameter + * + * @deprecated Use {@link #setVisitCustomVariables(CustomVariables)} instead + */ + @Deprecated + public void setUserCustomVariable( + @NonNull String key, @Nullable String value + ) { + if (value == null) { + if (visitCustomVariables == null) { + return; + } + visitCustomVariables.remove(key); + } else { + CustomVariable variable = new CustomVariable(key, value); + if (visitCustomVariables == null) { + visitCustomVariables = new CustomVariables(); + } + visitCustomVariables.add(variable); + } + } + + /** + * Set a user custom variable at the specified key. + * + * @param customVariable the CustomVariable to set. A null value will remove the custom variable + * at the specified index + * @param index the index to set the customVariable at. + * + * @deprecated Use {@link #setVisitCustomVariables(CustomVariables)} instead + */ + @Deprecated + public void setVisitCustomVariable( + @Nullable CustomVariable customVariable, int index + ) { + if (visitCustomVariables == null) { + if (customVariable == null) { + return; + } + visitCustomVariables = new CustomVariables(); + } + setCustomVariable(visitCustomVariables, customVariable, index); + } + + /** + * Sets a custom parameter to append to the Matomo tracking parameters. + * + *

Attention: If a parameter with the same name already exists, it will be appended twice! + * + * @param parameterName The name of the query parameter to append. Must not be null or empty. + * @param value The value of the query parameter to append. To remove the parameter, pass + * null. + * + * @deprecated Use @link {@link MatomoRequest.MatomoRequestBuilder#additionalParameters(Map)} + * instead + */ + @Deprecated + public void setParameter(@NonNull String parameterName, Object value) { + if (parameterName.trim().isEmpty()) { + throw new IllegalArgumentException("Parameter name must not be empty"); + } + if (additionalParameters == null) { + if (value == null) { + return; + } + additionalParameters = new LinkedHashMap<>(); + } + if (value == null) { + additionalParameters.remove(parameterName); + } else { + additionalParameters.put(parameterName, value); + } + } + + /** + * Creates a new {@link MatomoRequestBuilder} instance. Only here for backwards compatibility. + * + * @deprecated Use {@link MatomoRequest#request()} instead. + */ + @Deprecated + public static org.matomo.java.tracking.MatomoRequestBuilder builder() { + return new org.matomo.java.tracking.MatomoRequestBuilder(); + } + + /** + * Parses the given device resolution string and sets the {@link #deviceResolution} field. + * + * @param deviceResolution the device resolution string to parse. Format: "WIDTHxHEIGHT" + * + * @deprecated Use {@link #setDeviceResolution(DeviceResolution)} instead. + */ + @Tolerate + @Deprecated + public void setDeviceResolution(@Nullable String deviceResolution) { + if (deviceResolution == null || deviceResolution.trim().isEmpty()) { + this.deviceResolution = null; + } else { + this.deviceResolution = DeviceResolution.fromString(deviceResolution); + } + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java b/core/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java new file mode 100644 index 00000000..01c43cd2 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java @@ -0,0 +1,49 @@ +package org.matomo.java.tracking; + +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import org.matomo.java.tracking.parameters.AcceptLanguage; + +/** + * The former MatomoRequestBuilder class has been moved to MatomoRequest.MatomoRequestBuilder. + * This class is only here for backwards compatibility. + * + * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder} instead. + */ +@Deprecated +public class MatomoRequestBuilder extends MatomoRequest.MatomoRequestBuilder { + + + /** + * Sets the tracking parameter for the accept languages of a user. Only here for backwards + * compatibility. + * + * @param headerAcceptLanguage The accept language header of a user. Must be in the format + * specified in RFC 2616. + * @return This builder + * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder#headerAcceptLanguage(AcceptLanguage)} + * in combination with {@link AcceptLanguage#fromHeader(String)} instead. + */ + @Deprecated + public MatomoRequestBuilder headerAcceptLanguage(@Nullable String headerAcceptLanguage) { + headerAcceptLanguage(AcceptLanguage.fromHeader(headerAcceptLanguage)); + return this; + } + + /** + * Sets the custom tracking parameters to the given parameters. + * + *

This converts the given map to a map of collections. Only included for backwards + * compatibility. + * + * @param parameters The custom tracking parameters to set + * @return This builder + * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder#additionalParameters(Map)}} + */ + @Deprecated + public MatomoRequestBuilder customTrackingParameters(@Nullable Map parameters) { + additionalParameters(parameters); + return this; + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/MatomoRequests.java b/core/src/main/java/org/matomo/java/tracking/MatomoRequests.java new file mode 100644 index 00000000..c5b57c09 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/MatomoRequests.java @@ -0,0 +1,359 @@ +package org.matomo.java.tracking; + +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Optional; +import lombok.NonNull; + +/** + * This class contains static methods for common tracking items to create {@link MatomoRequest} + * objects. + * + *

The intention of this class is to bundle common tracking items in a single place to make + * tracking easier. The methods contain the typical parameters for the tracking item and return a + * {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters, like the visitor + * ID, a user ID or custom dimensions. + */ +public class MatomoRequests { + + /** + * Creates a {@link MatomoRequest} object for a download or a link action. + * + * @param url The URL of the download or link. Must not be null. + * @param type The type of the action. Either {@link ActionType#DOWNLOAD} or + * {@link ActionType#LINK}. + * + * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters. + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder action( + @NonNull String url, @NonNull ActionType type + ) { + return type.applyUrl(MatomoRequest.request(), url); + } + + /** + * Creates a {@link MatomoRequest} object for a content impression. + * + *

A content impression is a view of a content piece. The content piece can be a product, an + * article, a video, a banner, etc. The content piece can be specified by the parameters + * {@code piece} and {@code target}. The {@code name} parameter is required and should be a + * descriptive name of the content piece. + * + * @param name The name of the content piece, like the name of a product or an article. Must not + * be null. Example: "SuperPhone". + * @param piece The content piece. Can be null. Example: "Smartphone". + * @param target The target of the content piece, like the URL of a product or an article. Can be + * null. Example: "https://example.com/superphone". + * + * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters. + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder contentImpression( + @NonNull String name, @Nullable String piece, @Nullable String target + ) { + return MatomoRequest.request().contentName(name).contentPiece(piece).contentTarget(target); + } + + /** + * Creates a {@link MatomoRequest} object for a content interaction. + * + *

Make sure you have tracked a content impression using the same content name and + * content piece, otherwise it will not count. + * + *

A content interaction is an interaction with a content piece. The content piece can be a + * product, an article, a video, a banner, etc. The content piece can be specified by the + * parameters {@code piece} and {@code target}. The {@code name} parameter is required and should + * be a descriptive name of the content piece. The {@code interaction} parameter is required and + * should be the type of the interaction, like "click" or "add-to-cart". + * + * @param interaction The type of the interaction. Must not be null. Example: "click". + * @param name The name of the content piece, like the name of a product or an article. + * @param piece The content piece. Can be null. Example: "Blog Article XYZ". + * @param target The target of the content piece, like the URL of a product or an article. + * Can be null. Example: "https://example.com/blog/article-xyz". + * + * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters. + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder contentInteraction( + @NonNull String interaction, + @NonNull String name, + @Nullable String piece, + @Nullable String target + ) { + return MatomoRequest + .request() + .contentInteraction(interaction) + .contentName(name) + .contentPiece(piece) + .contentTarget(target); + } + + /** + * Creates a {@link MatomoRequest} object for a crash. + * + *

Requires Crash Analytics plugin to be enabled in the target Matomo instance. + * + *

A crash is an error that causes the application to stop working. The parameters {@code + * message} and {@code stackTrace} are required. The other parameters are optional. The + * {@code type} parameter can be used to specify the type of the crash, like + * {@code NullPointerException}. The {@code category} parameter can be used to specify the + * category of the crash, like payment failure. The {@code location}, {@code line} and + * {@code column} can be used to specify the location of the crash. The {@code location} parameter + * should be the name of the file where the crash occurred. The {@code line} and {@code column} + * parameters should be the line and column number of the crash. + * + * @param message The message of the crash. Must not be null. + * @param type The type of the crash. Can be null. Example: + * {@code java.lang.NullPointerException} + * @param category The category of the crash. Can be null. Example: "payment failure". + * @param stackTrace The stack trace of the crash. Must not be null. + * @param location The location of the crash. Can be null. Example: "MainActivity.java". + * @param line The line number of the crash. Can be null. Example: 42. + * @param column The column number of the crash. Can be null. Example: 23. + * + * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters. + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder crash( + @NonNull String message, + @Nullable String type, + @Nullable String category, + @Nullable String stackTrace, + @Nullable String location, + @Nullable Integer line, + @Nullable Integer column + ) { + return MatomoRequest.request() + .crashMessage(message) + .crashType(type) + .crashCategory(category) + .crashStackTrace(stackTrace) + .crashLocation(location) + .crashLine(line) + .crashColumn(column); + } + + /** + * Creates a {@link MatomoRequest} object for a crash with information from a {@link Throwable}. + * + *

Requires Crash Analytics plugin to be enabled in the target Matomo instance. + * + *

The {@code category} parameter can be used to specify the category of the crash, like + * payment failure. + * + * @param throwable The throwable that caused the crash. Must not be null. + * @param category The category of the crash. Can be null. Example: "payment failure". + * + * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters. + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder crash( + @NonNull Throwable throwable, @Nullable String category + ) { + return MatomoRequest + .request() + .crashMessage(throwable.getMessage()) + .crashCategory(category) + .crashStackTrace(formatStackTrace(throwable)) + .crashType(throwable.getClass().getName()) + .crashLocation( + getFirstStackTraceElement(throwable) + .map(StackTraceElement::getFileName) + .orElse(null)) + .crashLine( + getFirstStackTraceElement(throwable) + .map(StackTraceElement::getLineNumber) + .orElse(null)); + } + + @edu.umd.cs.findbugs.annotations.NonNull + private static String formatStackTrace(@Nullable Throwable throwable) { + StringWriter writer = new StringWriter(); + throwable.printStackTrace(new PrintWriter(writer)); + return writer.toString().trim(); + } + + @edu.umd.cs.findbugs.annotations.NonNull + private static Optional getFirstStackTraceElement( + @edu.umd.cs.findbugs.annotations.NonNull Throwable throwable + ) { + StackTraceElement[] stackTrace = throwable.getStackTrace(); + if (stackTrace == null || stackTrace.length == 0) { + return Optional.empty(); + } + return Optional.of(stackTrace[0]); + } + + /** + * Creates a {@link MatomoRequest} object for a ecommerce cart update (add item, remove item, + * update item). + * + *

The {@code revenue} parameter is required and should be the total revenue of the cart. + * + * @param revenue The total revenue of the cart. Must not be null. + * + * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters. + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder ecommerceCartUpdate( + @NonNull Double revenue + ) { + return MatomoRequest.request().ecommerceRevenue(revenue); + } + + /** + * Creates a {@link MatomoRequest} object for a ecommerce order. + * + *

All revenues (revenue, subtotal, tax, shippingCost, discount) will be individually summed + * and reported in Matomo reports. + * + *

The {@code id} and {@code revenue} parameters are required and should be the order ID and + * the total revenue of the order. The other parameters are optional. The {@code subtotal}, + * {@code tax}, {@code shippingCost} and {@code discount} parameters should be the subtotal, tax, + * shipping cost and discount of the order. + * + *

If the Ecommerce order contains items (products), you must call + * {@link MatomoRequest.MatomoRequestBuilder#ecommerceItems(EcommerceItems)} to add the items to + * the request. + * + * @param id An order ID. Can be a stock keeping unit (SKU) or a unique ID. Must not be + * null. + * @param revenue The total revenue of the order. Must not be null. + * @param subtotal The subtotal of the order. Can be null. + * @param tax The tax of the order. Can be null. + * @param shippingCost The shipping cost of the order. Can be null. + * @param discount The discount of the order. Can be null. + * + * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters. + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder ecommerceOrder( + @NonNull String id, + @NonNull Double revenue, + @Nullable Double subtotal, + @Nullable Double tax, + @Nullable Double shippingCost, + @Nullable Double discount + ) { + return MatomoRequest.request() + .ecommerceId(id) + .ecommerceRevenue(revenue) + .ecommerceSubtotal(subtotal) + .ecommerceTax(tax) + .ecommerceShippingCost(shippingCost) + .ecommerceDiscount(discount); + } + + /** + * Creates a {@link MatomoRequest} object for an event. + * + *

The {@code category} and {@code action} parameters are required and should be the category + * and action of the event. The {@code name} and {@code value} parameters are optional. The + * {@code category} parameter should be a category of the event, like "Travel". The {@code action} + * parameter should be an action of the event, like "Book flight". The {@code name} parameter + * should be the name of the event, like "Flight to Berlin". The {@code value} parameter should be + * the value of the event, like the price of the flight. + * + * @param category The category of the event. Must not be null. Example: "Music" + * @param action The action of the event. Must not be null. Example: "Play" + * @param name The name of the event. Can be null. Example: "Edvard Grieg - The Death of Ase" + * @param value The value of the event. Can be null. Example: 9.99 + * + * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters. + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder event( + @NonNull String category, + @NonNull String action, + @Nullable String name, + @Nullable Double value + ) { + return MatomoRequest.request() + .eventCategory(category) + .eventAction(action) + .eventName(name) + .eventValue(value); + } + + /** + * Creates a {@link MatomoRequest} object for a conversion of a goal of the website. + * + *

The {@code id} parameter is required and should be the ID of the goal. The {@code revenue}, + * {@code name} and {@code value} parameters are optional. The {@code revenue} parameter should be + * the revenue of the conversion. The {@code name} parameter should be the name of the conversion. + * The {@code value} parameter should be the value of the conversion. + * + * @param id The ID of the goal. Must not be null. Example: 1 + * @param revenue The revenue of the conversion. Can be null. Example: 9.99 + * + * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters. + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder goal( + int id, @Nullable Double revenue + ) { + return MatomoRequest.request().goalId(id).ecommerceRevenue(revenue); + } + + /** + * Creates a {@link MatomoRequest} object for a page view. + * + *

The {@code name} parameter is required and should be the name of the page. + * + * @param name The name of the page. Must not be null. Example: "Home" + * + * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters. + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder pageView( + @NonNull String name + ) { + return MatomoRequest.request().actionName(name); + } + + /** + * Creates a {@link MatomoRequest} object for a search. + * + *

These are used to populate reports in Actions > Site Search. + * + *

The {@code query} parameter is required and should be the search query. The {@code + * category} and {@code resultsCount} parameters are optional. The {@code category} parameter + * should be the category of the search, like "Music". The {@code resultsCount} parameter should + * be the number of results of the search. + * + * @param query The search query. Must not be null. Example: "Edvard Grieg" + * @param category The category of the search. Can be null. Example: "Music" + * @param resultsCount The number of results of the search. Can be null. Example: 42 + * + * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters. + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder siteSearch( + @NonNull String query, @Nullable String category, @Nullable Long resultsCount + ) { + return MatomoRequest.request() + .searchQuery(query) + .searchCategory(category) + .searchResultsCount(resultsCount); + } + + /** + * Creates a {@link MatomoRequest} object for a ping. + * + *

Ping requests do not track new actions. If they are sent within the standard visit + * length (see global.ini.php), they will extend the existing visit and the current last action + * for the visit. If after the standard visit length, ping requests will create a new visit using + * the last action in the last known visit. + * + * @return A {@link MatomoRequest.MatomoRequestBuilder} object to add additional parameters. + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder ping() { + return MatomoRequest.request().ping(true); + } + + +} diff --git a/core/src/main/java/org/matomo/java/tracking/MatomoTracker.java b/core/src/main/java/org/matomo/java/tracking/MatomoTracker.java new file mode 100644 index 00000000..3c38d017 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/MatomoTracker.java @@ -0,0 +1,376 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import edu.umd.cs.findbugs.annotations.Nullable; +import java.net.URI; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; +import lombok.AccessLevel; +import lombok.NonNull; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * The main class that sends {@link MatomoRequest}s to a specified Matomo server. + * + *

Contains several methods to send requests synchronously and asynchronously. The asynchronous methods return a + * {@link CompletableFuture} that can be used to wait for the request to finish. The synchronous methods block until + * the request is finished. The asynchronous methods are more efficient if you want to send multiple requests at once. + * + *

Configure this tracker using the {@link TrackerConfiguration} class. You can use the + * {@link TrackerConfiguration#builder()} to create a new configuration. The configuration is immutable and can be + * reused for multiple trackers. + * + *

The tracker is thread-safe and can be used by multiple threads at once. + * + * @author brettcsorba + */ +@Slf4j +public class MatomoTracker implements AutoCloseable { + + private final TrackerConfiguration trackerConfiguration; + + @Setter(AccessLevel.PROTECTED) + private SenderFactory senderFactory = new ServiceLoaderSenderFactory(); + + private Sender sender; + + /** + * Creates a tracker that will send {@link MatomoRequest}s to the specified + * Tracking HTTP API endpoint. + * + * @param hostUrl url endpoint to send requests to. Usually in the format + * https://your-matomo-domain.tld/matomo.php. Must not be null + * @deprecated Please use {@link MatomoTracker#MatomoTracker(TrackerConfiguration)} + */ + @Deprecated + public MatomoTracker( + @NonNull String hostUrl + ) { + this(hostUrl, 0); + } + + /** + * Creates a tracker that will send {@link MatomoRequest}s to the specified + * Tracking HTTP API endpoint. + * + * @param hostUrl url endpoint to send requests to. Usually in the format + * https://your-matomo-domain.tld/matomo.php. + * @param timeout the timeout of the sent request in milliseconds or -1 if not set + * @deprecated Please use {@link MatomoTracker#MatomoTracker(TrackerConfiguration)} + */ + @Deprecated + public MatomoTracker( + @NonNull String hostUrl, int timeout + ) { + this(hostUrl, null, 0, timeout); + } + + /** + * Creates a tracker that will send {@link MatomoRequest}s to the specified + * Tracking HTTP API endpoint. + * + * @param hostUrl url endpoint to send requests to. Usually in the format + * https://your-matomo-domain.tld/matomo.php. + * @param proxyHost The hostname or IP address of an optional HTTP proxy, null allowed + * @param proxyPort The port of an HTTP proxy or -1 if not set + * @param timeout the timeout of the request in milliseconds or -1 if not set + * @deprecated Please use {@link MatomoTracker#MatomoTracker(TrackerConfiguration)} + */ + @Deprecated + public MatomoTracker( + @NonNull String hostUrl, @Nullable String proxyHost, int proxyPort, int timeout + ) { + this(TrackerConfiguration + .builder() + .enabled(true) + .apiEndpoint(URI.create(hostUrl)) + .proxyHost(proxyHost) + .proxyPort(proxyPort) + .connectTimeout(timeout == -1 ? Duration.ofSeconds(5L) : Duration.ofSeconds(timeout)) + .socketTimeout(timeout == -1 ? Duration.ofSeconds(5L) : Duration.ofSeconds(timeout)) + .build()); + } + + /** + * Creates a new Matomo Tracker instance. + * + * @param trackerConfiguration Configurations parameters (you can use a builder) + */ + public MatomoTracker( + @NonNull TrackerConfiguration trackerConfiguration + ) { + trackerConfiguration.validate(); + this.trackerConfiguration = trackerConfiguration; + } + + /** + * Creates a tracker that will send {@link MatomoRequest}s to the specified + * Tracking HTTP API endpoint via the provided proxy. + * + * @param hostUrl url endpoint to send requests to. Usually in the format + * https://your-matomo-domain.tld/matomo.php. + * @param proxyHost url endpoint for the proxy, null allowed + * @param proxyPort proxy server port number or -1 if not set + * @deprecated Please use {@link MatomoTracker#MatomoTracker(TrackerConfiguration)} + */ + @Deprecated + public MatomoTracker( + @NonNull String hostUrl, @Nullable String proxyHost, int proxyPort + ) { + this(hostUrl, proxyHost, proxyPort, -1); + } + + /** + * Sends a tracking request to Matomo using the HTTP GET method. + * + *

Use this method if you want to send a single request. If you want to send multiple requests at once, use + * {@link #sendBulkRequest(Iterable)} instead. If you want to send multiple requests asynchronously, use + * {@link #sendRequestAsync(MatomoRequest)} or {@link #sendBulkRequestAsync(Collection)} instead. + * + * @param request request to send. must not be null + */ + public void sendRequest(@NonNull MatomoRequest request) { + if (trackerConfiguration.isEnabled()) { + log.debug("Sending request via GET: {}", request); + applyGoalIdAndCheckSiteId(request); + initializeSender(); + sender.sendSingle(request); + } else { + log.warn("Not sending request, because tracker is disabled"); + } + } + + private void initializeSender() { + if (sender == null) { + sender = senderFactory.createSender(trackerConfiguration, new QueryCreator(trackerConfiguration)); + } + } + + /** + * Send a request asynchronously via HTTP GET. + * + *

Use this method if you want to send a single request. If you want to send multiple requests at once, use + * {@link #sendBulkRequestAsync(Collection)} instead. If you want to send multiple requests synchronously, use + * {@link #sendRequest(MatomoRequest)} or {@link #sendBulkRequest(Iterable)} instead. + * + * @param request request to send + * @return completable future to let you know when the request is done. Contains the request. + */ + public CompletableFuture sendRequestAsync( + @NonNull MatomoRequest request + ) { + return sendRequestAsync(request, Function.identity()); + } + + /** + * Send a request asynchronously via HTTP GET and specify a callback that gets executed when the response arrives. + * + *

Use this method if you want to send a single request. If you want to send multiple requests at once, use + * {@link #sendBulkRequestAsync(Collection, Consumer)} instead. If you want to send multiple requests synchronously, + * use {@link #sendRequest(MatomoRequest)} or {@link #sendBulkRequest(Iterable)} instead. + * + * @param request request to send + * @param callback callback that gets executed when response arrives, must not be null + * @return a completable future to let you know when the request is done. The future contains + * the callback result. + * @deprecated Please use {@link MatomoTracker#sendRequestAsync(MatomoRequest)} in combination with + * {@link CompletableFuture#thenAccept(Consumer)} instead + */ + @Deprecated + public CompletableFuture sendRequestAsync( + @NonNull MatomoRequest request, + @NonNull Function callback + ) { + if (trackerConfiguration.isEnabled()) { + applyGoalIdAndCheckSiteId(request); + log.debug("Sending async request via GET: {}", request); + initializeSender(); + CompletableFuture future = sender.sendSingleAsync(request); + return future.thenApply(callback); + } + log.warn("Not sending request, because tracker is disabled"); + return CompletableFuture.completedFuture(null); + } + + private void applyGoalIdAndCheckSiteId( + @NonNull MatomoRequest request + ) { + if (request.getGoalId() == null && ( + request.getEcommerceId() != null || request.getEcommerceRevenue() != null + || request.getEcommerceDiscount() != null || request.getEcommerceItems() != null + || request.getEcommerceLastOrderTimestamp() != null + || request.getEcommerceShippingCost() != null || request.getEcommerceSubtotal() != null + || request.getEcommerceTax() != null)) { + request.setGoalId(0); + } + if (trackerConfiguration.getDefaultSiteId() == null && request.getSiteId() == null) { + throw new IllegalArgumentException("No default site ID and no request site ID is given"); + } + } + + /** + * Send multiple requests in a single HTTP POST call. + * + *

More efficient than sending several individual requests. If you want to send a single request, use + * {@link #sendRequest(MatomoRequest)} instead. If you want to send multiple requests asynchronously, use + * {@link #sendBulkRequestAsync(Collection)} instead. + * + * @param requests the requests to send + */ + public void sendBulkRequest(MatomoRequest... requests) { + sendBulkRequest(Arrays.asList(requests), null); + } + + /** + * Send multiple requests in a single HTTP POST call. + * + *

More efficient than sending several individual requests. If you want to send a single request, use + * {@link #sendRequest(MatomoRequest)} instead. If you want to send multiple requests asynchronously, use + * {@link #sendBulkRequestAsync(Collection)} instead. + * + * @param requests the requests to send + */ + public void sendBulkRequest(@NonNull Iterable 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 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 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 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 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 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 requests, @Nullable String overrideAuthToken + ); + + @NonNull + CompletableFuture sendBulkAsync( + @NonNull Collection 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"]}} + * + *

Example: {@code {"1":["OS","Windows"],"2":["Browser","Firefox"]}} + * + *

This is mainly used to parse the custom variables from the cookie. + * + * @param value The JSON representation of the custom variables to parse or null + * @return The parsed custom variables or null if the given value is null or empty + */ + @Nullable + public static CustomVariables parse(@Nullable String value) { + if (value == null || value.isEmpty()) { + return null; + } + + CustomVariables customVariables = new CustomVariables(); + StringTokenizer tokenizer = new StringTokenizer(value, ":{}\""); + + Integer key = null; + String customVariableKey = null; + String customVariableValue = null; + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken().trim(); + if (!token.isEmpty()) { + if (token.matches("\\d+")) { + key = Integer.parseInt(token); + } else if (token.startsWith("[") && key != null) { + customVariableKey = tokenizer.nextToken(); + tokenizer.nextToken(); + customVariableValue = tokenizer.nextToken(); + } else if (key != null && customVariableKey != null && customVariableValue != null) { + customVariables.add(new CustomVariable(customVariableKey, customVariableValue), key); + } else if (token.equals(",")) { + key = null; + customVariableKey = null; + customVariableValue = null; + } + } + } + return customVariables; + } + + /** + * Creates a JSON representation of the custom variables. The format is as follows: + * {@code {"1":["key1","value1"],"2":["key2","value2"]}} + * + * @return A JSON representation of the custom variables + */ + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder("{"); + Iterator> iterator = variables.entrySet().iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + stringBuilder + .append('"') + .append(entry.getKey()) + .append("\":[\"") + .append(entry.getValue().getKey()) + .append("\",\"") + .append(entry.getValue().getValue()) + .append("\"]"); + if (iterator.hasNext()) { + stringBuilder.append(','); + } + } + stringBuilder.append('}'); + return stringBuilder.toString(); + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/DeviceResolution.java b/core/src/main/java/org/matomo/java/tracking/parameters/DeviceResolution.java new file mode 100644 index 00000000..6729443c --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/parameters/DeviceResolution.java @@ -0,0 +1,59 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import edu.umd.cs.findbugs.annotations.Nullable; +import lombok.Builder; +import lombok.RequiredArgsConstructor; + +/** + * The resolution (width and height) of the user's output device (monitor / phone). + */ +@Builder +@RequiredArgsConstructor +public class DeviceResolution { + + private final int width; + + private final int height; + + /** + * Creates a device resolution from a string representation. + * + *

The string must be in the format "widthxheight", e.g. "1920x1080". + * + * @param deviceResolution The string representation of the device resolution, e.g. "1920x1080" + * @return The device resolution representation + */ + @Nullable + public static DeviceResolution fromString( + @Nullable + String deviceResolution + ) { + if (deviceResolution == null || deviceResolution.trim().isEmpty()) { + return null; + } + if (deviceResolution.length() < 3) { + throw new IllegalArgumentException("Wrong device resolution size"); + } + String[] dimensions = deviceResolution.split("x"); + if (dimensions.length != 2) { + throw new IllegalArgumentException("Wrong dimension size"); + } + return builder() + .width(Integer.parseInt(dimensions[0])) + .height(Integer.parseInt(dimensions[1])) + .build(); + } + + @Override + public String toString() { + return String.format("%dx%d", width, height); + } + +} diff --git a/src/main/java/org/matomo/java/tracking/EcommerceItem.java b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java similarity index 53% rename from src/main/java/org/matomo/java/tracking/EcommerceItem.java rename to core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java index f7b5bf23..7f5ba051 100644 --- a/src/main/java/org/matomo/java/tracking/EcommerceItem.java +++ b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java @@ -4,7 +4,8 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ -package org.matomo.java.tracking; + +package org.matomo.java.tracking.parameters; import lombok.AllArgsConstructor; import lombok.Builder; @@ -13,19 +14,28 @@ /** * Represents an item in an ecommerce order. - * - * @author brettcsorba */ -@Setter -@Getter -@AllArgsConstructor @Builder +@AllArgsConstructor +@Getter +@Setter public class EcommerceItem { private String sku; - private String name; - private String category; - private Double price; - private Integer quantity; + @Builder.Default + private String name = ""; + + @Builder.Default + private String category = ""; + + @Builder.Default + private Double price = 0.0; + + @Builder.Default + private Integer quantity = 0; + + public String toString() { + return String.format("[\"%s\",\"%s\",\"%s\",%s,%d]", sku, name, category, price, quantity); + } } diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItems.java b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItems.java new file mode 100644 index 00000000..7000ded7 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItems.java @@ -0,0 +1,41 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.Singular; +import lombok.experimental.Delegate; + +/** + * Multiple things that you can buy online. + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode +@Getter +@Setter +public class EcommerceItems { + + @Delegate + @Singular + private List items = new ArrayList<>(); + + public String toString() { + return items.stream().map(String::valueOf).collect(Collectors.joining(",", "[", "]")); + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/Hex.java b/core/src/main/java/org/matomo/java/tracking/parameters/Hex.java new file mode 100644 index 00000000..13ab4d98 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/parameters/Hex.java @@ -0,0 +1,26 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import lombok.NonNull; + +final class Hex { + + private Hex() { + // utility class + } + + static String fromBytes(@NonNull byte[] bytes) { + StringBuilder result = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/RandomValue.java b/core/src/main/java/org/matomo/java/tracking/parameters/RandomValue.java new file mode 100644 index 00000000..2b000b6e --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/parameters/RandomValue.java @@ -0,0 +1,55 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import java.security.SecureRandom; +import java.util.Random; + +/** + * A random value to avoid the tracking request being cached by the browser or a proxy. + */ +public class RandomValue { + + private static final Random RANDOM = new SecureRandom(); + + private final byte[] representation = new byte[10]; + + private String override; + + /** + * Static factory to generate a random value. + * + * @return A randomly generated value + */ + public static RandomValue random() { + RandomValue randomValue = new RandomValue(); + RANDOM.nextBytes(randomValue.representation); + return randomValue; + } + + /** + * Static factory to generate a random value from a given string. The string will be used as is and not hashed. + * + * @param override The string to use as random value + * @return A random value from the given string + */ + public static RandomValue fromString(String override) { + RandomValue randomValue = new RandomValue(); + randomValue.override = override; + return randomValue; + } + + @Override + public String toString() { + if (override != null) { + return override; + } + return Hex.fromBytes(representation); + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/UniqueId.java b/core/src/main/java/org/matomo/java/tracking/parameters/UniqueId.java new file mode 100644 index 00000000..06a82be6 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/parameters/UniqueId.java @@ -0,0 +1,58 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import java.security.SecureRandom; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +/** + * A six character unique ID consisting of the characters [0-9a-Z]. + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class UniqueId { + + private static final String CHARS = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + private static final Random RANDOM = new SecureRandom(); + + private final long value; + + /** + * Static factory to generate a random unique id. + * + * @return A randomly generated unique id + */ + public static UniqueId random() { + return fromValue(RANDOM.nextLong()); + } + + /** + * Creates a unique id from a number. + * + * @param value A number to create this unique id from + * @return The unique id for the given value + */ + public static UniqueId fromValue(long value) { + return new UniqueId(value); + } + + @Override + public String toString() { + return IntStream + .range(0, 6) + .map(i -> (int) (value >> i * 8)) + .mapToObj(codePoint -> String.valueOf(CHARS.charAt(Math.abs(codePoint % CHARS.length())))) + .collect(Collectors.joining()); + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/VisitorId.java b/core/src/main/java/org/matomo/java/tracking/parameters/VisitorId.java new file mode 100644 index 00000000..39b54e30 --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/parameters/VisitorId.java @@ -0,0 +1,132 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import edu.umd.cs.findbugs.annotations.Nullable; +import java.security.SecureRandom; +import java.util.Random; +import java.util.UUID; +import java.util.regex.Pattern; +import lombok.NonNull; + +/** + * The unique visitor ID, must be a 16 characters hexadecimal string. Every unique visitor must be assigned a different + * ID and this ID must not change after it is assigned. If this value is not set Matomo will still track visits, but the + * unique visitors metric might be less accurate. + */ +public class VisitorId { + + private static final Random RANDOM = new SecureRandom(); + private static final Pattern HEX_DIGITS = Pattern.compile("[0-9a-fA-F]+"); + + private final byte[] representation = new byte[8]; + + /** + * Static factory to generate a random visitor id. + * + *

Please consider creating a fixed id for each visitor by getting a hash code from e.g. the username and + * using {@link #fromHash(long)} or {@link #fromString(String)} instead of using this method. + * + * @return A randomly generated visitor id + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static VisitorId random() { + VisitorId visitorId = new VisitorId(); + RANDOM.nextBytes(visitorId.representation); + return visitorId; + } + + /** + * Creates always the same visitor id for the given input. + * + *

You can use e.g. {@link Object#hashCode()} to generate a hash code for an object, e.g. a username + * string as input. + * + * @param hash A number (a hash code) to create the visitor id from + * @return Always the same visitor id for the same input + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static VisitorId fromHash(long hash) { + VisitorId visitorId = new VisitorId(); + long remainingHash = hash; + for (int i = visitorId.representation.length - 1; i >= 0; i--) { + visitorId.representation[i] = (byte) (remainingHash & 0xFF); + remainingHash >>= Byte.SIZE; + } + return visitorId; + } + + /** + * Creates a visitor id from a UUID. + * + *

Uses the most significant bits of the UUID to create the visitor id. + * + * @param uuid A UUID to create the visitor id from + * @return The visitor id for the given UUID + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static VisitorId fromUUID(@NonNull UUID uuid) { + return fromHash(uuid.getMostSignificantBits()); + } + + /** + * Creates a visitor id from a hexadecimal string. + * + *

The input must be a valid hexadecimal string with a maximum length of 16 characters. If the input is shorter + * than 16 characters it will be padded with zeros.

+ * + * @param inputHex A hexadecimal string to create the visitor id from + * @return The visitor id for the given input + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static VisitorId fromHex(@NonNull String inputHex) { + if (inputHex.trim().isEmpty()) { + throw new IllegalArgumentException("Hex string must not be null or empty"); + } + if (inputHex.length() > 16) { + throw new IllegalArgumentException("Hex string must not be longer than 16 characters"); + } + if (!HEX_DIGITS.matcher(inputHex).matches()) { + throw new IllegalArgumentException("Input must be a valid hex string"); + } + VisitorId visitorId = new VisitorId(); + for (int charIndex = inputHex.length() - 1, representationIndex = + visitorId.representation.length - 1; + charIndex >= 0; + charIndex -= 2, representationIndex--) { + String hex = inputHex.substring(Math.max(0, charIndex - 1), charIndex + 1); + try { + visitorId.representation[representationIndex] = (byte) Integer.parseInt(hex, 16); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Input must be a valid hex string", e); + } + } + return visitorId; + } + + /** + * Creates a visitor id from a string. The string will be hashed to create the visitor id. + * + * @param str A string to create the visitor id from + * @return The visitor id for the given string or null if the string is null or empty + */ + @Nullable + public static VisitorId fromString(@Nullable String str) { + if (str == null || str.trim().isEmpty()) { + return null; + } + return fromHash(str.hashCode()); + } + + @Override + @edu.umd.cs.findbugs.annotations.NonNull + public String toString() { + return Hex.fromBytes(representation); + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/parameters/package-info.java b/core/src/main/java/org/matomo/java/tracking/parameters/package-info.java new file mode 100644 index 00000000..f709edce --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/parameters/package-info.java @@ -0,0 +1,9 @@ +/** + * Contains types for Matomo Tracking Parameters according to + * the Matomo Tracking HTTP API. + * + *

The types help you to use the correct format for the tracking parameters. The package was introduced in Matomo + * Java Tracker version 3 to let the tracker be more self-explanatory and better maintainable. + */ + +package org.matomo.java.tracking.parameters; diff --git a/core/src/main/java/org/matomo/java/tracking/servlet/CookieWrapper.java b/core/src/main/java/org/matomo/java/tracking/servlet/CookieWrapper.java new file mode 100644 index 00000000..4c29ae3d --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/servlet/CookieWrapper.java @@ -0,0 +1,15 @@ +package org.matomo.java.tracking.servlet; + +import lombok.Value; + +/** + * Wrapper for the cookie name and value. + */ +@Value +public class CookieWrapper { + + String name; + + String value; + +} diff --git a/core/src/main/java/org/matomo/java/tracking/servlet/HttpServletRequestWrapper.java b/core/src/main/java/org/matomo/java/tracking/servlet/HttpServletRequestWrapper.java new file mode 100644 index 00000000..b72c087b --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/servlet/HttpServletRequestWrapper.java @@ -0,0 +1,60 @@ +package org.matomo.java.tracking.servlet; + +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +/** + * Wraps a HttpServletRequest to be compatible with both the Jakarta and the Java EE API. + */ +@Builder +@Value +public class HttpServletRequestWrapper { + + @Nullable + StringBuffer requestURL; + + @Nullable + String remoteAddr; + + @Nullable + String remoteUser; + + @Nullable + Map headers; + + @Nullable + CookieWrapper[] cookies; + + /** + * Returns an enumeration of all the header names this request contains. If the request has no + * headers, this method returns an empty enumeration. + * + * @return an enumeration of all the header names sent with this request + */ + public Enumeration getHeaderNames() { + return headers == null ? Collections.emptyEnumeration() : + Collections.enumeration(headers.keySet()); + } + + /** + * Returns the value of the specified request header as a String. If the request did not include a + * header of the specified name, this method returns null. If there are multiple headers with the + * same name, this method returns the last header in the request. The header name is case + * insensitive. You can use this method with any request header. + * + * @param name a String specifying the header name (case insensitive) - must not be {@code null}. + * @return a String containing the value of the requested header, or null if the request does not + * have a header of that name + */ + @Nullable + public String getHeader(@NonNull String name) { + return headers == null ? null : headers.get(name.toLowerCase(Locale.ROOT)); + } + +} diff --git a/core/src/main/java/org/matomo/java/tracking/servlet/ServletMatomoRequest.java b/core/src/main/java/org/matomo/java/tracking/servlet/ServletMatomoRequest.java new file mode 100644 index 00000000..ef81608f --- /dev/null +++ b/core/src/main/java/org/matomo/java/tracking/servlet/ServletMatomoRequest.java @@ -0,0 +1,170 @@ +package org.matomo.java.tracking.servlet; + +import static java.util.Arrays.asList; + +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import lombok.NonNull; +import org.matomo.java.tracking.MatomoRequest; +import org.matomo.java.tracking.parameters.CustomVariables; +import org.matomo.java.tracking.parameters.VisitorId; + + +/** + * Adds the headers from a {@link HttpServletRequestWrapper} to a + * {@link MatomoRequest.MatomoRequestBuilder}. + * + *

Use #fromServletRequest(HttpServletRequestWrapper) to create a new builder with the headers from the + * request or #addServletRequestHeaders(MatomoRequest.MatomoRequestBuilder, HttpServletRequestWrapper) to + * add the headers to an existing builder. + */ +public final class ServletMatomoRequest { + + /** + * Please ensure these values are always lower case. + */ + private static final Set RESTRICTED_HEADERS = + Collections.unmodifiableSet(new HashSet<>(asList( + "connection", + "content-length", + "expect", + "host", + "upgrade" + ))); + + private ServletMatomoRequest() { + // should not be instantiated + } + + /** + * Creates a new builder with the headers from the request. + * + *

Use #addServletRequestHeaders(MatomoRequest.MatomoRequestBuilder, HttpServletRequestWrapper) to + * add the headers to an existing builder. + * + * @param request the request to get the headers from (must not be null) + * + * @return a new builder with the headers from the request (never null) + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder fromServletRequest(@NonNull HttpServletRequestWrapper request) { + return addServletRequestHeaders(MatomoRequest.request(), request); + } + + /** + * Adds the headers from the request to an existing builder. + * + *

Use #fromServletRequest(HttpServletRequestWrapper) to create a new builder with the headers from + * the request. + * + * @param builder the builder to add the headers to (must not be null) + * @param request the request to get the headers from (must not be null) + * + * @return the builder with the headers added (never null) + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static MatomoRequest.MatomoRequestBuilder addServletRequestHeaders( + @NonNull MatomoRequest.MatomoRequestBuilder builder, @NonNull HttpServletRequestWrapper request + ) { + return builder + .actionUrl(request.getRequestURL() == null ? null : request.getRequestURL().toString()) + .headers(collectHeaders(request)) + .visitorIp(determineVisitorIp(request)) + .userId(request.getRemoteUser()) + .cookies(processCookies(builder, request)); + } + + @edu.umd.cs.findbugs.annotations.NonNull + private static Map collectHeaders( + @edu.umd.cs.findbugs.annotations.NonNull HttpServletRequestWrapper request + ) { + Map headers = new HashMap<>(10); + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + if (headerName != null && !headerName.trim().isEmpty() && !RESTRICTED_HEADERS.contains( + headerName.toLowerCase( + Locale.ROOT))) { + headers.put(headerName, request.getHeader(headerName)); + } + } + return headers; + } + + @Nullable + private static String determineVisitorIp( + @edu.umd.cs.findbugs.annotations.NonNull HttpServletRequestWrapper request + ) { + String forwardedForHeader = request.getHeader("X-Forwarded-For"); + if (isNotEmpty(forwardedForHeader)) { + return forwardedForHeader; + } + if (isNotEmpty(request.getRemoteAddr())) { + return request.getRemoteAddr(); + } + return null; + } + + @edu.umd.cs.findbugs.annotations.NonNull + private static Map processCookies( + @edu.umd.cs.findbugs.annotations.NonNull MatomoRequest.MatomoRequestBuilder builder, + @edu.umd.cs.findbugs.annotations.NonNull HttpServletRequestWrapper request + ) { + Map cookies = new LinkedHashMap<>(3); + if (request.getCookies() != null) { + builder.supportsCookies(Boolean.TRUE); + for (CookieWrapper cookie : request.getCookies()) { + if (isNotEmpty(cookie.getValue())) { + processCookie(builder, cookies, cookie.getName(), cookie.getValue()); + } + } + } + return cookies; + } + + private static boolean isNotEmpty(String forwardedForHeader) { + return forwardedForHeader != null && !forwardedForHeader.trim().isEmpty(); + } + + private static void processCookie( + @edu.umd.cs.findbugs.annotations.NonNull MatomoRequest.MatomoRequestBuilder builder, + @edu.umd.cs.findbugs.annotations.NonNull Map cookies, + @edu.umd.cs.findbugs.annotations.NonNull String cookieName, + @edu.umd.cs.findbugs.annotations.NonNull String cookieValue + ) { + if (cookieName.toLowerCase(Locale.ROOT).startsWith("_pk_id")) { + extractVisitorId(builder, cookies, cookieValue, cookieName); + } + if (cookieName.toLowerCase(Locale.ROOT).equalsIgnoreCase("MATOMO_SESSID")) { + builder.sessionId(cookieValue); + } + if (cookieName.toLowerCase(Locale.ROOT).startsWith("_pk_ses") + || cookieName.toLowerCase(Locale.ROOT).startsWith("_pk_ref") + || cookieName.toLowerCase(Locale.ROOT).startsWith("_pk_hsr")) { + cookies.put(cookieName, cookieValue); + } + if (cookieName.toLowerCase(Locale.ROOT).startsWith("_pk_cvar")) { + builder.visitCustomVariables(CustomVariables.parse(cookieValue)); + } + } + + private static void extractVisitorId( + @edu.umd.cs.findbugs.annotations.NonNull MatomoRequest.MatomoRequestBuilder builder, + @edu.umd.cs.findbugs.annotations.NonNull Map cookies, + @edu.umd.cs.findbugs.annotations.NonNull String cookieValue, + @edu.umd.cs.findbugs.annotations.NonNull String cookieName + ) { + String[] cookieValues = cookieValue.split("\\."); + if (cookieValues.length > 0) { + builder.visitorId(VisitorId.fromHex(cookieValues[0])).newVisitor(Boolean.FALSE); + cookies.put(cookieName, cookieValue); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/org/piwik/java/tracking/CustomVariable.java b/core/src/main/java/org/piwik/java/tracking/CustomVariable.java new file mode 100644 index 00000000..20845bed --- /dev/null +++ b/core/src/main/java/org/piwik/java/tracking/CustomVariable.java @@ -0,0 +1,32 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.piwik.java.tracking; + +/** + * A user defined custom variable. + * + *

Renamed to {@link org.matomo.java.tracking.parameters.CustomVariable} in 3.0.0. + * + * @author brettcsorba + * @deprecated Use {@link org.matomo.java.tracking.parameters.CustomVariable} instead. + */ +@Deprecated +public class CustomVariable extends org.matomo.java.tracking.parameters.CustomVariable { + + /** + * Instantiates a new custom variable. + * + * @param key the key of the custom variable (required) + * @param value the value of the custom variable (required) + * @deprecated Use {@link org.matomo.java.tracking.parameters.CustomVariable} instead. + */ + @Deprecated + public CustomVariable(String key, String value) { + super(key, value); + } +} diff --git a/core/src/main/java/org/piwik/java/tracking/EcommerceItem.java b/core/src/main/java/org/piwik/java/tracking/EcommerceItem.java new file mode 100644 index 00000000..49c3a83c --- /dev/null +++ b/core/src/main/java/org/piwik/java/tracking/EcommerceItem.java @@ -0,0 +1,34 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.piwik.java.tracking; + +/** + * Describes an item in an ecommerce transaction. + * + * @author brettcsorba + * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead. + */ +@Deprecated +public class EcommerceItem extends org.matomo.java.tracking.parameters.EcommerceItem { + + /** + * Creates a new ecommerce item. + * + * @param sku the sku (Stock Keeping Unit) of the item. + * @param name the name of the item. + * @param category the category of the item. + * @param price the price of the item. + * @param quantity the quantity of the item. + * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead. + */ + @Deprecated + public EcommerceItem(String sku, String name, String category, Double price, Integer quantity) { + super(sku, name, category, price, quantity); + } + +} diff --git a/core/src/main/java/org/piwik/java/tracking/PiwikDate.java b/core/src/main/java/org/piwik/java/tracking/PiwikDate.java new file mode 100644 index 00000000..d8029269 --- /dev/null +++ b/core/src/main/java/org/piwik/java/tracking/PiwikDate.java @@ -0,0 +1,55 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.piwik.java.tracking; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.TimeZone; +import org.matomo.java.tracking.MatomoDate; + +/** + * A date object that can be used to send dates to Matomo. This class is deprecated and will be removed in a future. + * + * @author brettcsorba + * @deprecated Please use {@link Instant} + */ +@Deprecated +public class PiwikDate extends MatomoDate { + + /** + * Creates a new date object with the current time. + * + * @deprecated Use {@link Instant} instead. + */ + @Deprecated + public PiwikDate() { + } + + /** + * Creates a new date object with the specified time. The time is specified in milliseconds since the epoch. + * + * @param epochMilli The time in milliseconds since the epoch + * @deprecated Use {@link Instant} instead. + */ + @Deprecated + public PiwikDate(long epochMilli) { + super(epochMilli); + } + + /** + * Sets the time zone for this date object. This is used to convert the date to UTC before sending it to Matomo. + * + * @param zone the time zone to use + * @deprecated Use {@link ZonedDateTime#toInstant()} instead. + */ + @Deprecated + public void setTimeZone(TimeZone zone) { + setTimeZone(zone.toZoneId()); + } + +} diff --git a/core/src/main/java/org/piwik/java/tracking/PiwikLocale.java b/core/src/main/java/org/piwik/java/tracking/PiwikLocale.java new file mode 100644 index 00000000..34f156d6 --- /dev/null +++ b/core/src/main/java/org/piwik/java/tracking/PiwikLocale.java @@ -0,0 +1,33 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.piwik.java.tracking; + +import java.util.Locale; +import org.matomo.java.tracking.parameters.Country; + +/** + * A locale object that can be used to send visitor country to Matomo. This class is deprecated and will be removed in + * the future. + * + * @author brettcsorba + * @deprecated Use {@link Country} instead. + */ +@Deprecated +public class PiwikLocale extends Country { + + /** + * Creates a new Piwik locale object with the specified locale. + * + * @param locale the locale to use + * @deprecated Use {@link Country} instead. + */ + @Deprecated + public PiwikLocale(Locale locale) { + super(locale); + } +} diff --git a/src/main/java/org/piwik/java/tracking/PiwikRequest.java b/core/src/main/java/org/piwik/java/tracking/PiwikRequest.java similarity index 50% rename from src/main/java/org/piwik/java/tracking/PiwikRequest.java rename to core/src/main/java/org/piwik/java/tracking/PiwikRequest.java index bab72b01..e4d64fe4 100644 --- a/src/main/java/org/piwik/java/tracking/PiwikRequest.java +++ b/core/src/main/java/org/piwik/java/tracking/PiwikRequest.java @@ -4,14 +4,18 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ + package org.piwik.java.tracking; -import lombok.NonNull; -import org.matomo.java.tracking.MatomoRequest; +import static java.util.Objects.requireNonNull; import java.net.URL; +import org.matomo.java.tracking.MatomoRequest; /** + * A request object that can be used to send requests to Matomo. This class is deprecated and will be removed in the + * future. + * * @author brettcsorba * @deprecated Use {@link MatomoRequest} instead. */ @@ -19,10 +23,14 @@ public class PiwikRequest extends MatomoRequest { /** + * Creates a new request object with the specified site ID and action URL. + * + * @param siteId the site ID + * @param actionUrl the action URL. Must not be null. * @deprecated Use {@link MatomoRequest} instead. */ @Deprecated - public PiwikRequest(int siteId, @NonNull URL actionUrl) { - super(siteId, actionUrl.toString()); + public PiwikRequest(int siteId, URL actionUrl) { + super(siteId, requireNonNull(actionUrl, "Action URL must not be null").toString()); } } diff --git a/core/src/main/java/org/piwik/java/tracking/PiwikTracker.java b/core/src/main/java/org/piwik/java/tracking/PiwikTracker.java new file mode 100644 index 00000000..02e18b13 --- /dev/null +++ b/core/src/main/java/org/piwik/java/tracking/PiwikTracker.java @@ -0,0 +1,74 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.piwik.java.tracking; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import org.matomo.java.tracking.MatomoTracker; + +/** + * Creates a new PiwikTracker instance. This class is deprecated and will be removed in the future. + * + * @author brettcsorba + * @deprecated Use {@link MatomoTracker} instead. + */ +@Deprecated +public class PiwikTracker extends MatomoTracker { + + /** + * Creates a new PiwikTracker instance with the given host URL. + * + * @param hostUrl the host URL of the Matomo server + * @deprecated Use {@link MatomoTracker} instead. + */ + @Deprecated + public PiwikTracker(@NonNull String hostUrl) { + super(hostUrl); + } + + /** + * Creates a new PiwikTracker instance with the given host URL and timeout in milliseconds. Use -1 for no timeout. + * + * @param hostUrl the host URL of the Matomo server + * @param timeout the timeout in milliseconds or -1 for no timeout + * @deprecated Use {@link MatomoTracker} instead. + */ + @Deprecated + public PiwikTracker(@NonNull String hostUrl, int timeout) { + super(hostUrl, timeout); + } + + /** + * Creates a new PiwikTracker instance with the given host URL and proxy settings. + * + * @param hostUrl the host URL of the Matomo server + * @param proxyHost the proxy host + * @param proxyPort the proxy port + * @deprecated Use {@link MatomoTracker} instead. + */ + @Deprecated + public PiwikTracker(@NonNull String hostUrl, @Nullable String proxyHost, int proxyPort) { + super(hostUrl, proxyHost, proxyPort); + } + + /** + * Creates a new PiwikTracker instance with the given host URL, proxy settings and timeout in milliseconds. Use -1 for + * no timeout. + * + * @param hostUrl the host URL of the Matomo server + * @param proxyHost the proxy host + * @param proxyPort the proxy port + * @param timeout the timeout in milliseconds or -1 for no timeout + * @deprecated Use {@link MatomoTracker} instead. + */ + @Deprecated + public PiwikTracker(@NonNull String hostUrl, @Nullable String proxyHost, int proxyPort, int timeout) { + super(hostUrl, proxyHost, proxyPort, timeout); + } + +} diff --git a/core/src/main/java/org/piwik/java/tracking/package-info.java b/core/src/main/java/org/piwik/java/tracking/package-info.java new file mode 100644 index 00000000..eb647ae7 --- /dev/null +++ b/core/src/main/java/org/piwik/java/tracking/package-info.java @@ -0,0 +1,5 @@ +/** + * Piwik Java Tracking API. Renamed to {@link org.matomo.java.tracking} in 3.0.0. + */ + +package org.piwik.java.tracking; diff --git a/core/src/test/java/org/matomo/java/tracking/AuthTokenTest.java b/core/src/test/java/org/matomo/java/tracking/AuthTokenTest.java new file mode 100644 index 00000000..169a83e6 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/AuthTokenTest.java @@ -0,0 +1,73 @@ +package org.matomo.java.tracking; + +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import org.junit.jupiter.api.Test; + +class AuthTokenTest { + + @Test + void determineAuthTokenReturnsAuthTokenFromRequest() { + + MatomoRequest request = + MatomoRequests + .event("Inbox", "Open", null, null) + .authToken("bdeca231a312ab12cde124131bedfa23").build(); + + String authToken = AuthToken.determineAuthToken(null, singleton(request), null); + + assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23"); + + } + + @Test + void determineAuthTokenReturnsAuthTokenFromTrackerConfiguration() { + + TrackerConfiguration trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create("https://your-matomo-domain.example/matomo.")) + .defaultAuthToken("bdeca231a312ab12cde124131bedfa23") + .build(); + + String authToken = AuthToken.determineAuthToken(null, null, trackerConfiguration); + + assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23"); + } + + @Test + void determineAuthTokenFromTrackerConfigurationIfRequestTokenIsEmpty() { + + MatomoRequest request = MatomoRequests.ping().authToken("").build(); + + TrackerConfiguration trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create("https://your-matomo-domain.example/matomo")) + .defaultAuthToken("bdeca231a312ab12cde124131bedfa23") + .build(); + + String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration); + + assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23"); + + } + + @Test + void determineAuthTokenFromTrackerConfigurationIfRequestTokenIsBlank() { + + MatomoRequest request = MatomoRequests.pageView("Help").authToken(" ").build(); + + TrackerConfiguration trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create("https://your-matomo-domain.example/matomo")) + .defaultAuthToken("bdeca231a312ab12cde124131bedfa23") + .build(); + + String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration); + + assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23"); + + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/BulkRequestTest.java b/core/src/test/java/org/matomo/java/tracking/BulkRequestTest.java new file mode 100644 index 00000000..535986a0 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/BulkRequestTest.java @@ -0,0 +1,35 @@ +package org.matomo.java.tracking; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class BulkRequestTest { + + @Test + void formatsQueriesAsJson() { + BulkRequest bulkRequest = BulkRequest.builder() + .queries(singleton("idsite=1&rec=1&action_name=TestBulkRequest")) + .authToken("token") + .build(); + + byte[] bytes = bulkRequest.toBytes(); + + assertThat(new String(bytes)).isEqualTo("{\"requests\":[\"?idsite=1&rec=1&action_name=TestBulkRequest\"],\"token_auth\":\"token\"}"); + } + + @Test + void failsIfQueriesAreEmpty() { + + BulkRequest bulkRequest = BulkRequest.builder().queries(emptyList()).build(); + + assertThatThrownBy(bulkRequest::toBytes) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Queries must not be empty"); + + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/matomo/java/tracking/CustomVariableTest.java b/core/src/test/java/org/matomo/java/tracking/CustomVariableTest.java new file mode 100644 index 00000000..e58a10dd --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/CustomVariableTest.java @@ -0,0 +1,43 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class CustomVariableTest { + + @Test + void createsCustomVariable() { + CustomVariable customVariable = new CustomVariable("key", "value"); + + assertThat(customVariable.getKey()).isEqualTo("key"); + assertThat(customVariable.getValue()).isEqualTo("value"); + } + + @Test + void failsOnNullKey() { + assertThatThrownBy(() -> new CustomVariable( + null, + "value" + )).isInstanceOf(NullPointerException.class); + } + + @Test + void failsOnNullValue() { + assertThatThrownBy(() -> new CustomVariable( + "key", + null + )).isInstanceOf(NullPointerException.class); + } + + @Test + void failsOnNullKeyAndValue() { + assertThatThrownBy(() -> new CustomVariable( + null, + null + )).isInstanceOf(NullPointerException.class); + } + + +} diff --git a/core/src/test/java/org/matomo/java/tracking/DaemonThreadFactoryTest.java b/core/src/test/java/org/matomo/java/tracking/DaemonThreadFactoryTest.java new file mode 100644 index 00000000..787693db --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/DaemonThreadFactoryTest.java @@ -0,0 +1,27 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class DaemonThreadFactoryTest { + + private final DaemonThreadFactory daemonThreadFactory = new DaemonThreadFactory(); + + @Test + void createsNewThreadAsDaemonThread() { + Thread thread = daemonThreadFactory.newThread(() -> { + // do nothing + }); + assertThat(thread.isDaemon()).isTrue(); + } + + @Test + void createsNewThreadWithMatomoJavaTrackerName() { + Thread thread = daemonThreadFactory.newThread(() -> { + // do nothing + }); + assertThat(thread.getName()).startsWith("MatomoJavaTracker-"); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java b/core/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java new file mode 100644 index 00000000..71e9fcc0 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java @@ -0,0 +1,68 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class EcommerceItemTest { + + private EcommerceItem ecommerceItem = new EcommerceItem(null, null, null, null, null); + + /** + * Test of constructor, of class EcommerceItem. + */ + @Test + void testConstructor() { + EcommerceItem ecommerceItem = new EcommerceItem("sku", "name", "category", 2.0, 2); + assertThat(ecommerceItem.getSku()).isEqualTo("sku"); + assertThat(ecommerceItem.getName()).isEqualTo("name"); + assertThat(ecommerceItem.getCategory()).isEqualTo("category"); + assertThat(ecommerceItem.getPrice()).isEqualTo(2.0); + assertThat(ecommerceItem.getQuantity()).isEqualTo(2); + } + + /** + * Test of getSku method, of class EcommerceItem. + */ + @Test + void testGetSku() { + ecommerceItem.setSku("sku"); + assertThat(ecommerceItem.getSku()).isEqualTo("sku"); + } + + /** + * Test of getName method, of class EcommerceItem. + */ + @Test + void testGetName() { + ecommerceItem.setName("name"); + assertThat(ecommerceItem.getName()).isEqualTo("name"); + } + + /** + * Test of getCategory method, of class EcommerceItem. + */ + @Test + void testGetCategory() { + ecommerceItem.setCategory("category"); + assertThat(ecommerceItem.getCategory()).isEqualTo("category"); + } + + /** + * Test of getPrice method, of class EcommerceItem. + */ + @Test + void testGetPrice() { + ecommerceItem.setPrice(2.0); + assertThat(ecommerceItem.getPrice()).isEqualTo(2.0); + } + + /** + * Test of getQuantity method, of class EcommerceItem. + */ + @Test + void testGetQuantity() { + ecommerceItem.setQuantity(2); + assertThat(ecommerceItem.getQuantity()).isEqualTo(2); + } +} diff --git a/core/src/test/java/org/matomo/java/tracking/ExecutorServiceCloserTest.java b/core/src/test/java/org/matomo/java/tracking/ExecutorServiceCloserTest.java new file mode 100644 index 00000000..e0d167a9 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/ExecutorServiceCloserTest.java @@ -0,0 +1,46 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; + +class ExecutorServiceCloserTest { + + @Test + void shutsDownExecutorService() { + + ExecutorService executorService = Executors.newFixedThreadPool(2, new DaemonThreadFactory()); + + ExecutorServiceCloser.close(executorService); + + assertThat(executorService.isTerminated()).isTrue(); + assertThat(executorService.isShutdown()).isTrue(); + + } + + @Test + void shutsDownExecutorServiceImmediately() throws Exception { + + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.submit(() -> { + try { + Thread.sleep(10000L); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + Thread thread = new Thread(() -> { + ExecutorServiceCloser.close(executorService); + }); + thread.start(); + Thread.sleep(1000L); + thread.interrupt(); + + assertThat(executorService.isShutdown()).isTrue(); + + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/matomo/java/tracking/InvalidUrlExceptionTest.java b/core/src/test/java/org/matomo/java/tracking/InvalidUrlExceptionTest.java new file mode 100644 index 00000000..17605f04 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/InvalidUrlExceptionTest.java @@ -0,0 +1,18 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class InvalidUrlExceptionTest { + + @Test + void createsInvalidUrlException() { + InvalidUrlException invalidUrlException = new InvalidUrlException(new Throwable()); + + assertThat(invalidUrlException).isNotNull(); + assertThat(invalidUrlException.getCause()).isNotNull(); + + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/MatomoExceptionTest.java b/core/src/test/java/org/matomo/java/tracking/MatomoExceptionTest.java new file mode 100644 index 00000000..9c998a37 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/MatomoExceptionTest.java @@ -0,0 +1,25 @@ +package org.matomo.java.tracking; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class MatomoExceptionTest { + + @Test + void createsMatomoExceptionWithMessage() { + MatomoException matomoException = new MatomoException("message"); + + assertEquals("message", matomoException.getMessage()); + } + + @Test + void createsMatomoExceptionWithMessageAndCause() { + Throwable cause = new Throwable(); + MatomoException matomoException = new MatomoException("message", cause); + + assertEquals("message", matomoException.getMessage()); + assertEquals(cause, matomoException.getCause()); + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/MatomoLocaleTest.java b/core/src/test/java/org/matomo/java/tracking/MatomoLocaleTest.java new file mode 100644 index 00000000..7822acd3 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/MatomoLocaleTest.java @@ -0,0 +1,24 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Locale; +import org.junit.jupiter.api.Test; + +class MatomoLocaleTest { + + @Test + void createsMatomoLocaleFromLocale() { + MatomoLocale locale = new MatomoLocale(Locale.US); + assertThat(locale.toString()).isEqualTo("us"); + } + + @Test + void failsIfLocaleIsNull() { + assertThatThrownBy(() -> new MatomoLocale(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("Locale must not be null"); + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java b/core/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java new file mode 100644 index 00000000..87dab6d5 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java @@ -0,0 +1,88 @@ +package org.matomo.java.tracking; + +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.parameters.CustomVariables; + + +class MatomoRequestBuilderTest { + + @Test + void buildsRequest() { + CustomVariable pageCustomVariable = + new CustomVariable("pageCustomVariableName", "pageCustomVariableValue"); + CustomVariable visitCustomVariable = + new CustomVariable("visitCustomVariableName", "visitCustomVariableValue"); + + MatomoRequest matomoRequest = new MatomoRequestBuilder() + .siteId(42) + .actionName("ACTION_NAME") + .actionUrl("https://www.your-domain.tld/some/page?query=foo") + .referrerUrl("https://referrer.com") + .additionalParameters(singletonMap( + "trackingParameterName", + "trackingParameterValue" + )) + .pageCustomVariables(new CustomVariables().add(pageCustomVariable, 2)) + .visitCustomVariables(new CustomVariables().add(visitCustomVariable, 3)) + .customAction(true) + .build(); + + assertThat(matomoRequest.getSiteId()).isEqualTo(42); + assertThat(matomoRequest.getActionName()).isEqualTo("ACTION_NAME"); + assertThat(matomoRequest.getApiVersion()).isEqualTo("1"); + assertThat(matomoRequest.getActionUrl()).isEqualTo( + "https://www.your-domain.tld/some/page?query=foo"); + assertThat(matomoRequest.getVisitorId().toString()).hasSize(16).isHexadecimal(); + assertThat(matomoRequest.getRandomValue().toString()).hasSize(20).isHexadecimal(); + assertThat(matomoRequest.getResponseAsImage()).isFalse(); + assertThat(matomoRequest.getRequired()).isTrue(); + assertThat(matomoRequest.getReferrerUrl()).isEqualTo("https://referrer.com"); + assertThat(matomoRequest.getCustomTrackingParameter("trackingParameterName")).isEqualTo( + "trackingParameterValue"); + assertThat(matomoRequest.getPageCustomVariables()).hasToString( + "{\"2\":[\"pageCustomVariableName\",\"pageCustomVariableValue\"]}"); + assertThat(matomoRequest.getVisitCustomVariables()).hasToString( + "{\"3\":[\"visitCustomVariableName\",\"visitCustomVariableValue\"]}"); + assertThat(matomoRequest.getCustomAction()).isTrue(); + + } + + @Test + void setCustomTrackingParameters() { + MatomoRequest matomoRequest = new MatomoRequestBuilder() + .customTrackingParameters(singletonMap("foo", "bar")) + .siteId(42) + .actionName("ACTION_NAME") + .actionUrl("https://www.your-domain.tld/some/page?query=foo") + .referrerUrl("https://referrer.com") + .build(); + + assertThat(matomoRequest.getCustomTrackingParameter("foo")).isEqualTo("bar"); + } + + @Test + void setCustomTrackingParametersWithCollectopm() { + MatomoRequest matomoRequest = new MatomoRequestBuilder() + .customTrackingParameters(singletonMap("foo", "bar")) + .siteId(42) + .actionName("ACTION_NAME") + .actionUrl("https://www.your-domain.tld/some/page?query=foo") + .referrerUrl("https://referrer.com") + .build(); + + assertThat(matomoRequest.getCustomTrackingParameter("foo")).isEqualTo("bar"); + } + + @Test + void acceptsNullAsHeaderAcceptLanguage() { + MatomoRequest matomoRequest = new MatomoRequestBuilder() + .headerAcceptLanguage((String) null) + .build(); + + assertThat(matomoRequest.getHeaderAcceptLanguage()).isNull(); + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/MatomoRequestTest.java b/core/src/test/java/org/matomo/java/tracking/MatomoRequestTest.java new file mode 100644 index 00000000..b4ad0e04 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/MatomoRequestTest.java @@ -0,0 +1,134 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class MatomoRequestTest { + + private MatomoRequest request = new MatomoRequest(); + + @Test + void returnsEmptyListWhenCustomTrackingParametersDoesNotContainKey() { + + request.setCustomTrackingParameter("foo", "bar"); + + assertThat(request.getCustomTrackingParameter("baz")).isNull(); + assertThat(request.getAdditionalParameters()).isNotEmpty(); + assertThat(request.getCustomTrackingParameter("foo")).isEqualTo("bar"); + } + + @Test + void getPageCustomVariableReturnsNullIfPageCustomVariablesIsNull() { + assertThat(request.getPageCustomVariable("foo")).isNull(); + } + + @Test + void getPageCustomVariableReturnsValueIfPageCustomVariablesIsNotNull() { + request.setPageCustomVariable("foo", "bar"); + assertThat(request.getPageCustomVariable("foo")).isEqualTo("bar"); + } + + @Test + void setPageCustomVariableRequiresNonNullKey() { + assertThatThrownBy(() -> request.setPageCustomVariable(null, "bar")).isInstanceOf( + NullPointerException.class); + } + + @Test + void setPageCustomVariableDoesNothingIfValueIsNull() { + request.setPageCustomVariable("foo", null); + assertThat(request.getPageCustomVariable("foo")).isNull(); + } + + @Test + void setPageCustomVariableRemovesValueIfValueIsNull() { + request.setPageCustomVariable("foo", "bar"); + request.setPageCustomVariable("foo", null); + assertThat(request.getPageCustomVariable("foo")).isNull(); + } + + @Test + void setPageCustomVariableAddsCustomVariableIfValueIsNotNull() { + request.setPageCustomVariable("foo", "bar"); + assertThat(request.getPageCustomVariable("foo")).isEqualTo("bar"); + } + + @Test + void setPageCustomVariableDoesNothingIfCustomVariableParameterIsNullAndIndexIsPositive() { + request.setPageCustomVariable(null, 1); + assertThat(request.getPageCustomVariable(1)).isNull(); + } + + @Test + void setPageCustomVariableInitializesPageCustomVariablesIfCustomVariableParameterIsNullAndIndexIsPositive() { + request.setPageCustomVariable(new CustomVariable("key", "value"), 1); + assertThat(request.getPageCustomVariables()).isNotNull(); + } + + @Test + void setUserCustomVariableDoesNothingIfValueIsNull() { + request.setUserCustomVariable("foo", null); + assertThat(request.getUserCustomVariable("foo")).isNull(); + } + + @Test + void setUserCustomVariableRemovesValueIfValueIsNull() { + request.setUserCustomVariable("foo", "bar"); + request.setUserCustomVariable("foo", null); + assertThat(request.getUserCustomVariable("foo")).isNull(); + } + + @Test + void setVisitCustomVariableDoesNothingIfCustomVariableParameterIsNullAndIndexIsPositive() { + request.setVisitCustomVariable(null, 1); + assertThat(request.getVisitCustomVariable(1)).isNull(); + } + + @Test + void setVisitCustomVariableInitializesVisitCustomVariablesIfCustomVariableParameterIsNullAndIndexIsPositive() { + request.setVisitCustomVariable(new CustomVariable("key", "value"), 1); + assertThat(request.getVisitCustomVariables()).isNotNull(); + } + + @Test + void setsCustomParameter() { + request.setParameter("foo", 1); + assertThat(request.getCustomTrackingParameter("foo")).isEqualTo(1); + } + + @Test + void failsToSetCustomParameterIfKeyIsNull() { + assertThatThrownBy(() -> request.setParameter( + null, + 1 + )).isInstanceOf(NullPointerException.class); + } + + @Test + void doesNothingWhenSettingCustomParameterIfValueIsNull() { + request.setParameter("foo", null); + assertThat(request.getAdditionalParameters()).isNull(); + } + + @Test + void removesCustomParameter() { + request.setParameter("foo", 1); + request.setParameter("foo", null); + assertThat(request.getAdditionalParameters()).isEmpty(); + } + + @Test + void setsDeviceResolutionString() { + request.setDeviceResolution("1920x1080"); + assertThat(request.getDeviceResolution().toString()).isEqualTo("1920x1080"); + } + + @Test + void failsIfSetParameterParameterNameIsBlank() { + assertThatThrownBy(() -> request.setParameter(" ", "bar")).isInstanceOf( + IllegalArgumentException.class); + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/MatomoRequestsTest.java b/core/src/test/java/org/matomo/java/tracking/MatomoRequestsTest.java new file mode 100644 index 00000000..3aa058d2 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/MatomoRequestsTest.java @@ -0,0 +1,318 @@ +package org.matomo.java.tracking; + + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import org.junit.jupiter.api.Test; + +class MatomoRequestsTest { + + @Test + void actionRequestBuilderContainsDownloadUrl() { + MatomoRequest.MatomoRequestBuilder builder = + MatomoRequests.action("https://example.com", ActionType.DOWNLOAD); + MatomoRequest request = builder.build(); + assertThat(request.getDownloadUrl()) + .isEqualTo("https://example.com"); + } + + @Test + void actionRequestBuilderContainsOutlinkUrl() { + MatomoRequest.MatomoRequestBuilder builder = + MatomoRequests.action("https://example.com", ActionType.LINK); + MatomoRequest request = builder.build(); + assertThat(request.getOutlinkUrl()) + .isEqualTo("https://example.com"); + } + + @Test + void contentImpressionRequestBuilderContainsContentInformation() { + MatomoRequest.MatomoRequestBuilder builder = + MatomoRequests.contentImpression("Product", "Smartphone", "https://example.com/product"); + MatomoRequest request = builder.build(); + assertThat(request) + .isNotNull() + .extracting( + MatomoRequest::getContentName, + MatomoRequest::getContentPiece, + MatomoRequest::getContentTarget + ) + .containsExactly("Product", "Smartphone", "https://example.com/product"); + } + + @Test + void contentInteractionRequestBuilderContainsInteractionAndContentInformation() { + MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.contentInteraction( + "click", + "Product", + "Smartphone", + "https://example.com/product" + ); + MatomoRequest request = builder.build(); + assertThat(request) + .isNotNull() + .extracting( + MatomoRequest::getContentInteraction, + MatomoRequest::getContentName, + MatomoRequest::getContentPiece, + MatomoRequest::getContentTarget + ) + .containsExactly("click", "Product", "Smartphone", "https://example.com/product"); + } + + @Test + void crashRequestBuilderContainsCrashInformation() { + MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.crash( + "Error", + "NullPointerException", + "payment failure", + "stackTrace", + "MainActivity.java", + 42, + 23 + ); + MatomoRequest request = builder.build(); + assertThat(request) + .isNotNull() + .extracting( + MatomoRequest::getCrashMessage, + MatomoRequest::getCrashType, + MatomoRequest::getCrashCategory, + MatomoRequest::getCrashStackTrace, + MatomoRequest::getCrashLocation, + MatomoRequest::getCrashLine, + MatomoRequest::getCrashColumn + ) + .containsExactly( + "Error", + "NullPointerException", + "payment failure", + "stackTrace", + "MainActivity.java", + 42, + 23 + ); + } + + @Test + void crashWithThrowableRequestBuilderContainsCrashInformationFromThrowable() { + Throwable throwable = new NullPointerException("Test NullPointerException"); + MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.crash(throwable, "payment failure"); + MatomoRequest request = builder.build(); + assertThat(request) + .isNotNull() + .extracting(MatomoRequest::getCrashMessage) // Additional assertions for other properties + .isEqualTo("Test NullPointerException"); + } + + @Test + void ecommerceCartUpdateRequestBuilderContainsEcommerceRevenue() { + MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.ecommerceCartUpdate(100.0); + MatomoRequest request = builder.build(); + assertThat(request) + .isNotNull() + .extracting(MatomoRequest::getEcommerceRevenue) + .isEqualTo(100.0); + } + + @Test + void ecommerceOrderRequestBuilderContainsEcommerceOrderInformation() { + MatomoRequest.MatomoRequestBuilder builder = + MatomoRequests.ecommerceOrder("123", 200.0, 180.0, 10.0, 5.0, 5.0); + MatomoRequest request = builder.build(); + assertThat(request) + .isNotNull() + .extracting( + MatomoRequest::getEcommerceId, + MatomoRequest::getEcommerceRevenue, + MatomoRequest::getEcommerceSubtotal, + MatomoRequest::getEcommerceTax, + MatomoRequest::getEcommerceShippingCost, + MatomoRequest::getEcommerceDiscount + ) + .containsExactly("123", 200.0, 180.0, 10.0, 5.0, 5.0); + } + + @Test + void eventRequestBuilderContainsEventInformation() { + MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.event( + "Music", + "Play", + "Edvard Grieg - The Death of Ase", + 9.99 + ); + MatomoRequest request = builder.build(); + assertThat(request) + .isNotNull() + .extracting( + MatomoRequest::getEventCategory, + MatomoRequest::getEventAction, + MatomoRequest::getEventName, + MatomoRequest::getEventValue + ) + .containsExactly("Music", "Play", "Edvard Grieg - The Death of Ase", 9.99); + } + + @Test + void goalRequestBuilderContainsGoalInformation() { + MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.goal( + 1, + 9.99 + ); + MatomoRequest request = builder.build(); + assertThat(request) + .isNotNull() + .extracting( + MatomoRequest::getGoalId, + MatomoRequest::getEcommerceRevenue + ) + .containsExactly(1, 9.99); + } + + @Test + void pageViewRequestBuilderContainsPageViewInformation() { + MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.pageView("About"); + MatomoRequest request = builder.build(); + assertThat(request.getActionName()) + .isEqualTo("About"); + } + + @Test + void searchRequestBuilderContainsSearchInformation() { + MatomoRequest.MatomoRequestBuilder builder = + MatomoRequests.siteSearch("Matomo", "Download", 42L); + MatomoRequest request = builder.build(); + assertThat(request) + .isNotNull() + .extracting( + MatomoRequest::getSearchQuery, + MatomoRequest::getSearchCategory, + MatomoRequest::getSearchResultsCount + ) + .containsExactly("Matomo", "Download", 42L); + } + + @Test + void pingRequestBuilderContainsPingInformation() { + MatomoRequest.MatomoRequestBuilder builder = MatomoRequests.ping(); + MatomoRequest request = builder.build(); + assertThat(request.getPing()).isTrue(); + } + + @Test + void nullParametersThrowNullPointerExceptionForInvalidInput() { + assertThatNullPointerException() + .isThrownBy(() -> MatomoRequests.action(null, ActionType.DOWNLOAD)) + .withMessage("url is marked non-null but is null"); + assertThatNullPointerException() + .isThrownBy(() -> MatomoRequests.contentImpression(null, null, null)) + .withMessage("name is marked non-null but is null"); + // Add similar checks for other methods + } + + @Test + void actionNullUrlThrowsNullPointerException() { + assertThatNullPointerException() + .isThrownBy(() -> MatomoRequests.action(null, ActionType.DOWNLOAD)) + .withMessage("url is marked non-null but is null"); + } + + @Test + void actionNullTypeThrowsNullPointerException() { + assertThatNullPointerException() + .isThrownBy(() -> MatomoRequests.action("https://example.com", null)) + .withMessage("type is marked non-null but is null"); + } + + @Test + void contentImpressionNullNameThrowsNullPointerException() { + assertThatNullPointerException() + .isThrownBy(() -> MatomoRequests.contentImpression( + null, + "Smartphone", + "https://example.com/product" + )) + .withMessage("name is marked non-null but is null"); + } + + // Add similar null checks for other methods... + + @Test + void crashNullMessageThrowsNullPointerException() { + assertThatNullPointerException() + .isThrownBy(() -> MatomoRequests.crash( + null, + "NullPointerException", + "payment failure", + "stackTrace", + "MainActivity.java", + 42, + 23 + )) + .withMessage("message is marked non-null but is null"); + } + + @Test + void crashWithThrowableNullThrowableThrowsNullPointerException() { + assertThatNullPointerException() + .isThrownBy(() -> MatomoRequests.crash(null, "payment failure")) + .withMessage("throwable is marked non-null but is null"); + } + + @Test + void ecommerceCartUpdateNullRevenueThrowsNullPointerException() { + assertThatNullPointerException() + .isThrownBy(() -> MatomoRequests.ecommerceCartUpdate(null)) + .withMessage("revenue is marked non-null but is null"); + } + + @Test + void ecommerceOrderNullIdThrowsNullPointerException() { + assertThatNullPointerException() + .isThrownBy(() -> MatomoRequests.ecommerceOrder(null, 200.0, 180.0, 10.0, 5.0, 5.0)) + .withMessage("id is marked non-null but is null"); + } + + @Test + void eventNullCategoryThrowsNullPointerException() { + assertThatNullPointerException() + .isThrownBy(() -> MatomoRequests.event( + null, + "Play", + "Edvard Grieg - The Death of Ase", + 9.99 + )) + .withMessage("category is marked non-null but is null"); + } + + @Test + void pageViewNullNameThrowsNullPointerException() { + assertThatNullPointerException() + .isThrownBy(() -> MatomoRequests.pageView(null)) + .withMessage("name is marked non-null but is null"); + } + + @Test + void siteSearchNullQueryThrowsNullPointerException() { + assertThatNullPointerException() + .isThrownBy(() -> MatomoRequests.siteSearch(null, "Music", 42L)) + .withMessage("query is marked non-null but is null"); + } + + @Test + void crashDoesNotIncludeStackTraceIfStackTraceOfThrowableIsEmpty() { + MatomoRequest.MatomoRequestBuilder builder = + MatomoRequests.crash(new TestThrowable(), "payment failure"); + MatomoRequest request = builder.build(); + assertThat(request.getCrashMessage()).isEqualTo("message"); + assertThat(request.getCrashType()).isEqualTo("org.matomo.java.tracking.TestThrowable"); + assertThat(request.getCrashCategory()).isEqualTo("payment failure"); + assertThat(request.getCrashStackTrace()).isEqualTo( + "org.matomo.java.tracking.TestThrowable: message"); + assertThat(request.getCrashLocation()).isNull(); + assertThat(request.getCrashLine()).isNull(); + assertThat(request.getCrashColumn()).isNull(); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java b/core/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java new file mode 100644 index 00000000..c312c717 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java @@ -0,0 +1,234 @@ +package org.matomo.java.tracking; + +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.parameters.RandomValue; +import org.matomo.java.tracking.parameters.VisitorId; + +class MatomoTrackerIT { + + private static final String HOST_URL = "http://localhost:8080/matomo.php"; + public static final String QUERY = + "rec=1&idsite=1&action_name=test&apiv=1&_id=00000000343efaf5&send_image=0&rand=test-random"; + private MatomoTracker matomoTracker; + private final TestSenderFactory senderFactory = new TestSenderFactory(); + private final MatomoRequest request = MatomoRequest + .request() + .siteId(1) + .visitorId(VisitorId.fromString("test-visitor-id")) + .randomValue(RandomValue.fromString("test-random")) + .actionName("test") + .build(); + + @Test + void sendsRequest() { + + matomoTracker = new MatomoTracker(HOST_URL); + matomoTracker.setSenderFactory(senderFactory); + + matomoTracker.sendRequest(request); + + TestSender testSender = senderFactory.getTestSender(); + thenContainsRequest(testSender, QUERY); + assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL); + + } + + @Test + void validatesRequest() { + + matomoTracker = new MatomoTracker(HOST_URL); + matomoTracker.setSenderFactory(senderFactory); + request.setSiteId(null); + + assertThatThrownBy(() -> matomoTracker.sendRequest(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("No default site ID and no request site ID is given"); + + assertThat(senderFactory.getTestSender()).isNull(); + + } + + @Test + void doesNotSendRequestIfNotEnabled() { + + matomoTracker = + new MatomoTracker(TrackerConfiguration.builder().apiEndpoint(URI.create(HOST_URL)).enabled(false).build()); + matomoTracker.setSenderFactory(senderFactory); + + matomoTracker.sendRequest(request); + + assertThat(senderFactory.getTestSender()).isNull(); + + } + + @Test + void sendsRequestUsingProxy() { + + matomoTracker = new MatomoTracker(HOST_URL, "localhost", 8081); + matomoTracker.setSenderFactory(senderFactory); + + matomoTracker.sendRequest(request); + + TestSender testSender = senderFactory.getTestSender(); + thenContainsRequest(testSender, QUERY); + TrackerConfiguration trackerConfiguration = testSender.getTrackerConfiguration(); + assertThat(trackerConfiguration.getProxyHost()).isEqualTo("localhost"); + assertThat(trackerConfiguration.getProxyPort()).isEqualTo(8081); + + } + + @Test + void sendsRequestAsync() { + + matomoTracker = new MatomoTracker(HOST_URL, 1000); + matomoTracker.setSenderFactory(senderFactory); + + matomoTracker.sendRequestAsync(request); + + TestSender testSender = senderFactory.getTestSender(); + thenContainsRequest(testSender, QUERY); + assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL); + + } + + @Test + void sendsRequestAsyncWithCallback() { + + matomoTracker = new MatomoTracker(HOST_URL, 1000); + matomoTracker.setSenderFactory(senderFactory); + AtomicBoolean callbackCalled = new AtomicBoolean(); + matomoTracker.sendRequestAsync(request, request -> { + assertThat(request).isEqualTo(request); + callbackCalled.set(true); + return null; + }); + + TestSender testSender = senderFactory.getTestSender(); + thenContainsRequest(testSender, QUERY); + assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL); + assertThat(callbackCalled).isTrue(); + + } + + @Test + void doesNotSendRequestAsyncIfNotEnabled() { + + matomoTracker = + new MatomoTracker(TrackerConfiguration.builder().apiEndpoint(URI.create(HOST_URL)).enabled(false).build()); + matomoTracker.setSenderFactory(senderFactory); + + matomoTracker.sendRequestAsync(request); + + assertThat(senderFactory.getTestSender()).isNull(); + + } + + @Test + void sendsBulkRequests() { + + matomoTracker = new MatomoTracker(HOST_URL); + matomoTracker.setSenderFactory(senderFactory); + + matomoTracker.sendBulkRequest(request); + + TestSender testSender = senderFactory.getTestSender(); + thenContainsRequest(testSender, QUERY); + assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL); + + } + + @Test + void doesNotSendBulkRequestsIfNotEnabled() { + + matomoTracker = + new MatomoTracker(TrackerConfiguration.builder().apiEndpoint(URI.create(HOST_URL)).enabled(false).build()); + matomoTracker.setSenderFactory(senderFactory); + + matomoTracker.sendBulkRequest(request); + + assertThat(senderFactory.getTestSender()).isNull(); + + } + + @Test + void sendsBulkRequestsAsync() { + + matomoTracker = new MatomoTracker(HOST_URL, 1000); + matomoTracker.setSenderFactory(senderFactory); + + matomoTracker.sendBulkRequestAsync(request); + + TestSender testSender = senderFactory.getTestSender(); + thenContainsRequest(testSender, QUERY); + assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL); + + } + + @Test + void doesNotSendBulkRequestsAsyncIfNotEnabled() { + + matomoTracker = + new MatomoTracker(TrackerConfiguration.builder().apiEndpoint(URI.create(HOST_URL)).enabled(false).build()); + matomoTracker.setSenderFactory(senderFactory); + + matomoTracker.sendBulkRequestAsync(request); + + assertThat(senderFactory.getTestSender()).isNull(); + + } + + @Test + void sendsBulkRequestAsyncWithCallback() { + + matomoTracker = new MatomoTracker(HOST_URL, 1000); + matomoTracker.setSenderFactory(senderFactory); + AtomicBoolean callbackCalled = new AtomicBoolean(); + matomoTracker.sendBulkRequestAsync(singleton(request), v -> callbackCalled.set(true)); + + TestSender testSender = senderFactory.getTestSender(); + thenContainsRequest(testSender, QUERY); + assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL); + assertThat(callbackCalled).isTrue(); + + } + + @Test + void sendsBulkRequestAsyncWithAuthToken() { + + matomoTracker = new MatomoTracker(HOST_URL, 1000); + matomoTracker.setSenderFactory(senderFactory); + matomoTracker.sendBulkRequestAsync(singleton(request), "abc123def456abc123def456abc123de"); + + TestSender testSender = senderFactory.getTestSender(); + thenContainsRequest(testSender, "token_auth=abc123def456abc123def456abc123de&rec=1&idsite=1&action_name=test&apiv=1&_id=00000000343efaf5&send_image=0&rand=test-random"); + assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL); + + } + + @Test + void appliesGoalId() { + + matomoTracker = new MatomoTracker(HOST_URL); + matomoTracker.setSenderFactory(senderFactory); + request.setEcommerceId("some-id"); + + matomoTracker.sendRequest(request); + + TestSender testSender = senderFactory.getTestSender(); + thenContainsRequest(testSender, "rec=1&idsite=1&action_name=test&apiv=1&_id=00000000343efaf5&idgoal=0&ec_id=some-id&send_image=0&rand=test-random"); + assertThat(testSender.getTrackerConfiguration().getApiEndpoint()).hasToString(HOST_URL); + + } + + private void thenContainsRequest(TestSender testSender, String query) { + assertThat(testSender.getRequests()).containsExactly(request); + assertThat(testSender.getQueries()).containsExactly(query); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/matomo/java/tracking/PiwikDateTest.java b/core/src/test/java/org/matomo/java/tracking/PiwikDateTest.java new file mode 100644 index 00000000..40c8bd80 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/PiwikDateTest.java @@ -0,0 +1,43 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.TimeZone; +import org.junit.jupiter.api.Test; +import org.piwik.java.tracking.PiwikDate; + + +class PiwikDateTest { + + /** + * Test of constructor, of class PiwikDate. + */ + @Test + void testConstructor0() { + PiwikDate date = new PiwikDate(); + assertThat(date).isNotNull(); + } + + @Test + void testConstructor1() { + PiwikDate date = new PiwikDate(1433186085092L); + assertThat(date).isNotNull(); + assertThat(date.getTime()).isEqualTo(1433186085092L); + } + + @Test + void testConstructor2() { + PiwikDate date = new PiwikDate(1467437553000L); + assertThat(date.getTime()).isEqualTo(1467437553000L); + } + + /** + * Test of setTimeZone method, of class PiwikDate. + */ + @Test + void testSetTimeZone() { + PiwikDate date = new PiwikDate(1433186085092L); + date.setTimeZone(TimeZone.getTimeZone("America/New_York")); + assertThat(date.getTime()).isEqualTo(1433186085092L); + } +} diff --git a/core/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java b/core/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java new file mode 100644 index 00000000..46e1c563 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java @@ -0,0 +1,29 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Locale; +import org.junit.jupiter.api.Test; +import org.piwik.java.tracking.PiwikLocale; + +class PiwikLocaleTest { + + private final PiwikLocale locale = new PiwikLocale(Locale.US); + + /** + * Test of setLocale method, of class PiwikLocale. + */ + @Test + void testLocale() { + locale.setLocale(Locale.GERMANY); + assertThat(locale.getLocale()).isEqualTo(Locale.GERMAN); + } + + /** + * Test of toString method, of class PiwikLocale. + */ + @Test + void testToString() { + assertThat(locale).hasToString("us"); + } +} diff --git a/core/src/test/java/org/matomo/java/tracking/PiwikRequestTest.java b/core/src/test/java/org/matomo/java/tracking/PiwikRequestTest.java new file mode 100644 index 00000000..c88c7570 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/PiwikRequestTest.java @@ -0,0 +1,940 @@ +package org.matomo.java.tracking; + +import static java.time.temporal.ChronoUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.within; + +import java.net.URL; +import java.nio.charset.Charset; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.Locale; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.parameters.AcceptLanguage; +import org.matomo.java.tracking.parameters.DeviceResolution; +import org.matomo.java.tracking.parameters.RandomValue; +import org.matomo.java.tracking.parameters.VisitorId; +import org.piwik.java.tracking.PiwikDate; +import org.piwik.java.tracking.PiwikLocale; +import org.piwik.java.tracking.PiwikRequest; + +class PiwikRequestTest { + + private PiwikRequest request; + + @BeforeEach + void setUp() throws Exception { + request = new PiwikRequest(3, new URL("https://test.com")); + } + + @Test + void testConstructor() throws Exception { + request = new PiwikRequest(3, new URL("https://test.com")); + assertThat(request.getSiteId()).isEqualTo(Integer.valueOf(3)); + assertThat(request.getRequired()).isTrue(); + assertThat(request.getActionUrl()).isEqualTo("https://test.com"); + assertThat(request.getVisitorId()).isNotNull(); + assertThat(request.getRandomValue()).isNotNull(); + assertThat(request.getApiVersion()).isEqualTo("1"); + assertThat(request.getResponseAsImage()).isFalse(); + } + + /** + * Test of getActionName method, of class PiwikRequest. + */ + @Test + void testActionName() { + request.setActionName("action"); + assertThat(request.getActionName()).isEqualTo("action"); + request.setActionName(null); + assertThat(request.getActionName()).isNull(); + } + + /** + * Test of getActionUrl method, of class PiwikRequest. + */ + @Test + void testActionUrl() { + request.setActionUrl(null); + assertThat(request.getActionUrl()).isNull(); + request.setActionUrl("https://action.com"); + assertThat(request.getActionUrl()).isEqualTo("https://action.com"); + } + + /** + * Test of getApiVersion method, of class PiwikRequest. + */ + @Test + void testApiVersion() { + request.setApiVersion("2"); + assertThat(request.getApiVersion()).isEqualTo("2"); + } + + @Test + void testAuthTokenTF() { + request.setAuthToken("12345678901234567890123456789012"); + assertThat(request.getAuthToken()).isEqualTo("12345678901234567890123456789012"); + } + + @Test + void testAuthTokenF() { + request.setAuthToken("12345678901234567890123456789012"); + request.setAuthToken(null); + assertThat(request.getAuthToken()).isNull(); + } + + /** + * Test of getCampaignKeyword method, of class PiwikRequest. + */ + @Test + void testCampaignKeyword() { + request.setCampaignKeyword("keyword"); + assertThat(request.getCampaignKeyword()).isEqualTo("keyword"); + } + + /** + * Test of getCampaignName method, of class PiwikRequest. + */ + @Test + void testCampaignName() { + request.setCampaignName("name"); + assertThat(request.getCampaignName()).isEqualTo("name"); + } + + /** + * Test of getCharacterSet method, of class PiwikRequest. + */ + @Test + void testCharacterSet() { + Charset charset = Charset.defaultCharset(); + request.setCharacterSet(charset); + assertThat(request.getCharacterSet()).isEqualTo(charset); + } + + /** + * Test of getContentInteraction method, of class PiwikRequest. + */ + @Test + void testContentInteraction() { + request.setContentInteraction("interaction"); + assertThat(request.getContentInteraction()).isEqualTo("interaction"); + } + + /** + * Test of getContentName method, of class PiwikRequest. + */ + @Test + void testContentName() { + request.setContentName("name"); + assertThat(request.getContentName()).isEqualTo("name"); + } + + /** + * Test of getContentPiece method, of class PiwikRequest. + */ + @Test + void testContentPiece() { + request.setContentPiece("piece"); + assertThat(request.getContentPiece()).isEqualTo("piece"); + } + + /** + * Test of getContentTarget method, of class PiwikRequest. + */ + @Test + void testContentTarget() { + request.setContentTarget("https://target.com"); + assertThat(request.getContentTarget()).isEqualTo("https://target.com"); + } + + /** + * Test of getCurrentHour method, of class PiwikRequest. + */ + @Test + void testCurrentHour() { + request.setCurrentHour(1); + assertThat(request.getCurrentHour()).isEqualTo(Integer.valueOf(1)); + } + + /** + * Test of getCurrentMinute method, of class PiwikRequest. + */ + @Test + void testCurrentMinute() { + request.setCurrentMinute(2); + assertThat(request.getCurrentMinute()).isEqualTo(Integer.valueOf(2)); + } + + /** + * Test of getCurrentSecond method, of class PiwikRequest. + */ + @Test + void testCurrentSecond() { + request.setCurrentSecond(3); + assertThat(request.getCurrentSecond()).isEqualTo(Integer.valueOf(3)); + } + + /** + * Test of getCustomTrackingParameter method, of class PiwikRequest. + */ + @Test + void testGetCustomTrackingParameter_T() { + try { + request.getCustomTrackingParameter(null); + fail("Exception should have been thrown."); + } catch (NullPointerException e) { + assertThat(e.getLocalizedMessage()).isEqualTo("key is marked non-null but is null"); + } + } + + @Test + void testGetCustomTrackingParameter_FT() { + assertThat(request.getCustomTrackingParameter("key")).isNull(); + } + + @Test + void testSetCustomTrackingParameter_T() { + try { + request.setCustomTrackingParameter(null, null); + fail("Exception should have been thrown."); + } catch (NullPointerException e) { + assertThat(e.getLocalizedMessage()).isEqualTo("key is marked non-null but is null"); + } + } + + @Test + void testSetCustomTrackingParameter1() { + request.setCustomTrackingParameter("key", "value"); + Object l = request.getCustomTrackingParameter("key"); + assertThat(l).isEqualTo("value"); + request.setCustomTrackingParameter("key", "value2"); + } + + @Test + void testSetCustomTrackingParameter2() { + request.setCustomTrackingParameter("key", "value2"); + Object l = request.getCustomTrackingParameter("key"); + assertThat(l).isEqualTo("value2"); + request.setCustomTrackingParameter("key", null); + l = request.getCustomTrackingParameter("key"); + assertThat(l).isNull(); + } + + @Test + void testSetCustomTrackingParameter3() { + request.setCustomTrackingParameter("key", null); + Object l = request.getCustomTrackingParameter("key"); + assertThat(l).isNull(); + } + + @Test + void testAddCustomTrackingParameter_T() { + try { + request.addCustomTrackingParameter(null, null); + fail("Exception should have been thrown."); + } catch (NullPointerException e) { + assertThat(e.getLocalizedMessage()).isEqualTo("key is marked non-null but is null"); + } + } + + @Test + void testAddCustomTrackingParameter1() { + request.addCustomTrackingParameter("key", "value"); + Object l = request.getCustomTrackingParameter("key"); + assertThat(l).isEqualTo("value"); + } + + @Test + void testAddCustomTrackingParameter2() { + request.addCustomTrackingParameter("key", "value"); + request.addCustomTrackingParameter("key", "value2"); + Object l = request.getCustomTrackingParameter("key"); + assertThat(l).isEqualTo("value2"); + } + + @Test + void testClearCustomTrackingParameter() { + request.setCustomTrackingParameter("key", "value"); + request.clearCustomTrackingParameter(); + Object l = request.getCustomTrackingParameter("key"); + assertThat(l).isNull(); + } + + /** + * Test of getDeviceResolution method, of class PiwikRequest. + */ + @Test + void testDeviceResolution() { + request.setDeviceResolution(DeviceResolution.fromString("100x200")); + assertThat(request.getDeviceResolution()).hasToString("100x200"); + } + + /** + * Test of getDownloadUrl method, of class PiwikRequest. + */ + @Test + void testDownloadUrl() { + + request.setDownloadUrl("https://download.com"); + assertThat(request.getDownloadUrl()).isEqualTo("https://download.com"); + } + + /** + * Test of enableEcommerce method, of class PiwikRequest. + */ + @Test + void testEnableEcommerce() { + request.enableEcommerce(); + assertThat(request.getGoalId()).isEqualTo(Integer.valueOf(0)); + } + + /** + * Test of getEcommerceDiscount method, of class PiwikRequest. + */ + @Test + void testEcommerceDiscountT() { + request.enableEcommerce(); + request.setEcommerceId("1"); + request.setEcommerceRevenue(2.0); + request.setEcommerceDiscount(1.0); + assertThat(request.getEcommerceDiscount()).isEqualTo(Double.valueOf(1.0)); + } + + + @Test + void testEcommerceDiscountF() { + request.setEcommerceDiscount(null); + assertThat(request.getEcommerceDiscount()).isNull(); + } + + /** + * Test of getEcommerceId method, of class PiwikRequest. + */ + @Test + void testEcommerceIdT() { + request.enableEcommerce(); + request.setEcommerceId("1"); + assertThat(request.getEcommerceId()).isEqualTo("1"); + } + + @Test + void testEcommerceIdF() { + request.setEcommerceId(null); + assertThat(request.getEcommerceId()).isNull(); + } + + @Test + void testEcommerceItemE2() { + try { + request.enableEcommerce(); + request.setEcommerceId("1"); + request.setEcommerceRevenue(2.0); + request.addEcommerceItem(null); + fail("Exception should have been thrown."); + } catch (NullPointerException e) { + assertThat(e.getLocalizedMessage()).isEqualTo("item is marked non-null but is null"); + } + } + + @Test + void testEcommerceItem() { + assertThat(request.getEcommerceItem(0)).isNull(); + EcommerceItem item = new EcommerceItem("sku", "name", "category", 1.0, 2); + request.enableEcommerce(); + request.setEcommerceId("1"); + request.setEcommerceRevenue(2.0); + request.addEcommerceItem(item); + assertThat(request.getEcommerceItem(0)).isEqualTo(item); + request.clearEcommerceItems(); + assertThat(request.getEcommerceItem(0)).isNull(); + } + + /** + * Test of getEcommerceLastOrderTimestamp method, of class PiwikRequest. + */ + @Test + void testEcommerceLastOrderTimestampT() { + request.enableEcommerce(); + request.setEcommerceId("1"); + request.setEcommerceRevenue(2.0); + request.setEcommerceLastOrderTimestamp(Instant.ofEpochSecond(1000L)); + assertThat(request.getEcommerceLastOrderTimestamp()).isEqualTo("1970-01-01T00:16:40Z"); + } + + @Test + void testEcommerceLastOrderTimestampF() { + request.setEcommerceLastOrderTimestamp(null); + assertThat(request.getEcommerceLastOrderTimestamp()).isNull(); + } + + /** + * Test of getEcommerceRevenue method, of class PiwikRequest. + */ + @Test + void testEcommerceRevenueT() { + request.enableEcommerce(); + request.setEcommerceId("1"); + request.setEcommerceRevenue(20.0); + assertThat(request.getEcommerceRevenue()).isEqualTo(Double.valueOf(20.0)); + } + + + @Test + void testEcommerceRevenueF() { + request.setEcommerceRevenue(null); + assertThat(request.getEcommerceRevenue()).isNull(); + } + + /** + * Test of getEcommerceShippingCost method, of class PiwikRequest. + */ + @Test + void testEcommerceShippingCostT() { + request.enableEcommerce(); + request.setEcommerceId("1"); + request.setEcommerceRevenue(2.0); + request.setEcommerceShippingCost(20.0); + assertThat(request.getEcommerceShippingCost()).isEqualTo(Double.valueOf(20.0)); + } + + @Test + void testEcommerceShippingCostF() { + request.setEcommerceShippingCost(null); + assertThat(request.getEcommerceShippingCost()).isNull(); + } + + /** + * Test of getEcommerceSubtotal method, of class PiwikRequest. + */ + @Test + void testEcommerceSubtotalT() { + request.enableEcommerce(); + request.setEcommerceId("1"); + request.setEcommerceRevenue(2.0); + request.setEcommerceSubtotal(20.0); + assertThat(request.getEcommerceSubtotal()).isEqualTo(Double.valueOf(20.0)); + } + + @Test + void testEcommerceSubtotalF() { + request.setEcommerceSubtotal(null); + assertThat(request.getEcommerceSubtotal()).isNull(); + } + + /** + * Test of getEcommerceTax method, of class PiwikRequest. + */ + @Test + void testEcommerceTaxT() { + request.enableEcommerce(); + request.setEcommerceId("1"); + request.setEcommerceRevenue(2.0); + request.setEcommerceTax(20.0); + assertThat(request.getEcommerceTax()).isEqualTo(Double.valueOf(20.0)); + } + + @Test + void testEcommerceTaxF() { + request.setEcommerceTax(null); + assertThat(request.getEcommerceTax()).isNull(); + } + + /** + * Test of getEventAction method, of class PiwikRequest. + */ + @Test + void testEventAction() { + request.setEventAction("action"); + assertThat(request.getEventAction()).isEqualTo("action"); + request.setEventAction(null); + assertThat(request.getEventAction()).isNull(); + } + + /** + * Test of getEventCategory method, of class PiwikRequest. + */ + @Test + void testEventCategory() { + request.setEventCategory("category"); + assertThat(request.getEventCategory()).isEqualTo("category"); + } + + /** + * Test of getEventName method, of class PiwikRequest. + */ + @Test + void testEventName() { + request.setEventName("name"); + assertThat(request.getEventName()).isEqualTo("name"); + } + + /** + * Test of getEventValue method, of class PiwikRequest. + */ + @Test + void testEventValue() { + request.setEventValue(1.0); + assertThat(request.getEventValue()).isOne(); + } + + /** + * Test of getGoalId method, of class PiwikRequest. + */ + @Test + void testGoalId() { + request.setGoalId(1); + assertThat(request.getGoalId()).isEqualTo(Integer.valueOf(1)); + } + + /** + * Test of getHeaderAcceptLanguage method, of class PiwikRequest. + */ + @Test + void testHeaderAcceptLanguage() { + request.setHeaderAcceptLanguage(AcceptLanguage.fromHeader("en")); + assertThat(request.getHeaderAcceptLanguage()).hasToString("en"); + } + + /** + * Test of getHeaderUserAgent method, of class PiwikRequest. + */ + @Test + void testHeaderUserAgent() { + request.setHeaderUserAgent("agent"); + assertThat(request.getHeaderUserAgent()).isEqualTo("agent"); + } + + /** + * Test of getNewVisit method, of class PiwikRequest. + */ + @Test + void testNewVisit() { + request.setNewVisit(true); + assertThat(request.getNewVisit()).isTrue(); + request.setNewVisit(null); + assertThat(request.getNewVisit()).isNull(); + } + + /** + * Test of getOutlinkUrl method, of class PiwikRequest. + */ + @Test + void testOutlinkUrl() { + request.setOutlinkUrl("https://outlink.com"); + assertThat(request.getOutlinkUrl()).isEqualTo("https://outlink.com"); + } + + /** + * Test of getPageCustomVariable method, of class PiwikRequest. + */ + @Test + void testPageCustomVariableStringStringE() { + assertThatThrownBy(() -> request.setPageCustomVariable(null, null)); + } + + @Test + void testPageCustomVariableStringStringE2() { + assertThatThrownBy(() -> request.setPageCustomVariable(null, "pageVal")); + } + + @Test + void testPageCustomVariableCustomVariable() { + assertThat(request.getPageCustomVariable(1)).isNull(); + CustomVariable cv = new CustomVariable("pageKey", "pageVal"); + request.setPageCustomVariable(cv, 1); + assertThat(request.getPageCustomVariable(1)).isEqualTo(cv); + request.setPageCustomVariable(null, 1); + assertThat(request.getPageCustomVariable(1)).isNull(); + request.setPageCustomVariable(cv, 2); + assertThat(request.getPageCustomVariable(2)).isEqualTo(cv); + } + + /** + * Test of getPluginDirector method, of class PiwikRequest. + */ + @Test + void testPluginDirector() { + request.setPluginDirector(true); + assertThat(request.getPluginDirector()).isTrue(); + } + + /** + * Test of getPluginFlash method, of class PiwikRequest. + */ + @Test + void testPluginFlash() { + request.setPluginFlash(true); + assertThat(request.getPluginFlash()).isTrue(); + } + + /** + * Test of getPluginGears method, of class PiwikRequest. + */ + @Test + void testPluginGears() { + request.setPluginGears(true); + assertThat(request.getPluginGears()).isTrue(); + } + + /** + * Test of getPluginJava method, of class PiwikRequest. + */ + @Test + void testPluginJava() { + request.setPluginJava(true); + assertThat(request.getPluginJava()).isTrue(); + } + + /** + * Test of getPluginPDF method, of class PiwikRequest. + */ + @Test + void testPluginPDF() { + request.setPluginPDF(true); + assertThat(request.getPluginPDF()).isTrue(); + } + + /** + * Test of getPluginQuicktime method, of class PiwikRequest. + */ + @Test + void testPluginQuicktime() { + request.setPluginQuicktime(true); + assertThat(request.getPluginQuicktime()).isTrue(); + } + + /** + * Test of getPluginRealPlayer method, of class PiwikRequest. + */ + @Test + void testPluginRealPlayer() { + request.setPluginRealPlayer(true); + assertThat(request.getPluginRealPlayer()).isTrue(); + } + + /** + * Test of getPluginSilverlight method, of class PiwikRequest. + */ + @Test + void testPluginSilverlight() { + request.setPluginSilverlight(true); + assertThat(request.getPluginSilverlight()).isTrue(); + } + + /** + * Test of getPluginWindowsMedia method, of class PiwikRequest. + */ + @Test + void testPluginWindowsMedia() { + request.setPluginWindowsMedia(true); + assertThat(request.getPluginWindowsMedia()).isTrue(); + } + + /** + * Test of getRandomValue method, of class PiwikRequest. + */ + @Test + void testRandomValue() { + request.setRandomValue(RandomValue.fromString("value")); + assertThat(request.getRandomValue()).hasToString("value"); + } + + /** + * Test of setReferrerUrl method, of class PiwikRequest. + */ + @Test + void testReferrerUrl() { + request.setReferrerUrl("https://referrer.com"); + assertThat(request.getReferrerUrl()).isEqualTo("https://referrer.com"); + } + + /** + * Test of getRequestDatetime method, of class PiwikRequest. + */ + @Test + void testRequestDatetimeTTT() { + request.setAuthToken("12345678901234567890123456789012"); + PiwikDate date = new PiwikDate(1000L); + request.setRequestDatetime(date); + assertThat(request.getRequestDatetime().getTime()).isEqualTo(1000L); + } + + + @Test + void testRequestDatetimeTF() { + request.setRequestDatetime(new PiwikDate()); + assertThat(request.getRequestDatetime().getZonedDateTime()).isCloseTo( + ZonedDateTime.now(), + within(2, MINUTES) + ); + } + + @Test + void testRequestDatetimeF() { + PiwikDate date = new PiwikDate(); + request.setRequestDatetime(date); + request.setRequestDatetime(null); + assertThat(request.getRequestDatetime()).isNull(); + } + + /** + * Test of getRequired method, of class PiwikRequest. + */ + @Test + void testRequired() { + request.setRequired(false); + assertThat(request.getRequired()).isFalse(); + } + + /** + * Test of getResponseAsImage method, of class PiwikRequest. + */ + @Test + void testResponseAsImage() { + request.setResponseAsImage(true); + assertThat(request.getResponseAsImage()).isTrue(); + } + + @Test + void testSearchCategoryTF() { + request.setSearchQuery("query"); + request.setSearchCategory("category"); + assertThat(request.getSearchCategory()).isEqualTo("category"); + } + + @Test + void testSearchCategoryF() { + request.setSearchCategory(null); + assertThat(request.getSearchCategory()).isNull(); + } + + /** + * Test of getSearchQuery method, of class PiwikRequest. + */ + @Test + void testSearchQuery() { + request.setSearchQuery("query"); + assertThat(request.getSearchQuery()).isEqualTo("query"); + } + + @Test + void testSearchResultsCountTF() { + request.setSearchQuery("query"); + request.setSearchResultsCount(100L); + assertThat(request.getSearchResultsCount()).isEqualTo(Long.valueOf(100L)); + } + + @Test + void testSearchResultsCountF() { + request.setSearchResultsCount(null); + assertThat(request.getSearchResultsCount()).isNull(); + } + + /** + * Test of getSiteId method, of class PiwikRequest. + */ + @Test + void testSiteId() { + request.setSiteId(2); + assertThat(request.getSiteId()).isEqualTo(Integer.valueOf(2)); + } + + /** + * Test of setTrackBotRequest method, of class PiwikRequest. + */ + @Test + void testTrackBotRequests() { + request.setTrackBotRequests(true); + assertThat(request.getTrackBotRequests()).isTrue(); + } + + /** + * Test of getUserCustomVariable method, of class PiwikRequest. + */ + @Test + void testUserCustomVariableStringString() { + request.setUserCustomVariable("userKey", "userValue"); + assertThat(request.getUserCustomVariable("userKey")).isEqualTo("userValue"); + } + + + /** + * Test of getUserId method, of class PiwikRequest. + */ + @Test + void testUserId() { + request.setUserId("id"); + assertThat(request.getUserId()).isEqualTo("id"); + } + + /** + * Test of getVisitorCity method, of class PiwikRequest. + */ + @Test + void testVisitorCityT() { + request.setAuthToken("12345678901234567890123456789012"); + request.setVisitorCity("city"); + assertThat(request.getVisitorCity()).isEqualTo("city"); + } + + @Test + void testVisitorCityF() { + request.setVisitorCity(null); + assertThat(request.getVisitorCity()).isNull(); + } + + /** + * Test of getVisitorCountry method, of class PiwikRequest. + */ + @Test + void testVisitorCountryT() { + PiwikLocale country = new PiwikLocale(Locale.US); + request.setAuthToken("12345678901234567890123456789012"); + request.setVisitorCountry(country); + assertThat(request.getVisitorCountry()).isEqualTo(country); + } + + @Test + void testVisitorCountryF() { + request.setVisitorCountry(null); + assertThat(request.getVisitorCountry()).isNull(); + } + + @Test + void testVisitorCustomTF() { + request.setVisitorCustomId(VisitorId.fromHex("1234567890abcdef")); + assertThat(request.getVisitorCustomId()).hasToString("1234567890abcdef"); + } + + @Test + void testVisitorCustomIdF() { + request.setVisitorCustomId(VisitorId.fromHex("1234567890abcdef")); + request.setVisitorCustomId(null); + assertThat(request.getVisitorCustomId()).isNull(); + } + + /** + * Test of getVisitorFirstVisitTimestamp method, of class PiwikRequest. + */ + @Test + void testVisitorFirstVisitTimestamp() { + request.setVisitorFirstVisitTimestamp(Instant.parse("2021-03-10T10:22:22.123Z")); + assertThat(request.getVisitorFirstVisitTimestamp()).isEqualTo("2021-03-10T10:22:22.123Z"); + } + + @Test + void testVisitorIdTFT() { + try { + request.setVisitorId(VisitorId.fromHex("1234567890abcdeg")); + fail("Exception should have been thrown."); + } catch (IllegalArgumentException e) { + assertThat(e.getLocalizedMessage()).isEqualTo("Input must be a valid hex string"); + } + } + + @Test + void testVisitorIdTFF() { + request.setVisitorId(VisitorId.fromHex("1234567890abcdef")); + assertThat(request.getVisitorId()).hasToString("1234567890abcdef"); + } + + @Test + void testVisitorIdF() { + request.setVisitorId(VisitorId.fromHex("1234567890abcdef")); + request.setVisitorId(null); + assertThat(request.getVisitorId()).isNull(); + } + + /** + * Test of getVisitorIp method, of class PiwikRequest. + */ + @Test + void testVisitorIpT() { + request.setAuthToken("12345678901234567890123456789012"); + request.setVisitorIp("ip"); + assertThat(request.getVisitorIp()).isEqualTo("ip"); + } + + @Test + void testVisitorIpF() { + request.setVisitorIp(null); + assertThat(request.getVisitorIp()).isNull(); + } + + /** + * Test of getVisitorLatitude method, of class PiwikRequest. + */ + @Test + void testVisitorLatitudeT() { + request.setAuthToken("12345678901234567890123456789012"); + request.setVisitorLatitude(10.5); + assertThat(request.getVisitorLatitude()).isEqualTo(Double.valueOf(10.5)); + } + + @Test + void testVisitorLatitudeF() { + request.setVisitorLatitude(null); + assertThat(request.getVisitorLatitude()).isNull(); + } + + /** + * Test of getVisitorLongitude method, of class PiwikRequest. + */ + @Test + void testVisitorLongitudeT() { + request.setAuthToken("12345678901234567890123456789012"); + request.setVisitorLongitude(20.5); + assertThat(request.getVisitorLongitude()).isEqualTo(Double.valueOf(20.5)); + } + + @Test + void testVisitorLongitudeF() { + request.setVisitorLongitude(null); + assertThat(request.getVisitorLongitude()).isNull(); + } + + /** + * Test of getVisitorPreviousVisitTimestamp method, of class PiwikRequest. + */ + @Test + void testVisitorPreviousVisitTimestamp() { + request.setVisitorPreviousVisitTimestamp(Instant.ofEpochSecond(1000L)); + assertThat(request.getVisitorPreviousVisitTimestamp()).isEqualTo("1970-01-01T00:16:40Z"); + } + + /** + * Test of getVisitorRegion method, of class PiwikRequest. + */ + @Test + void testVisitorRegionT() { + request.setAuthToken("12345678901234567890123456789012"); + request.setVisitorRegion("region"); + assertThat(request.getVisitorRegion()).isEqualTo("region"); + } + + @Test + void testVisitorRegionF() { + request.setVisitorRegion(null); + assertThat(request.getVisitorRegion()).isNull(); + } + + /** + * Test of getVisitorVisitCount method, of class PiwikRequest. + */ + @Test + void testVisitorVisitCount() { + request.setVisitorVisitCount(100); + assertThat(request.getVisitorVisitCount()).isEqualTo(Integer.valueOf(100)); + } + + @Test + void failsIfActionUrlIsNull() { + assertThatThrownBy(() -> new PiwikRequest(3, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("Action URL must not be null"); + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/ProxyAuthenticatorTest.java b/core/src/test/java/org/matomo/java/tracking/ProxyAuthenticatorTest.java new file mode 100644 index 00000000..08893677 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/ProxyAuthenticatorTest.java @@ -0,0 +1,61 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.Authenticator; +import java.net.Authenticator.RequestorType; +import java.net.InetAddress; +import java.net.PasswordAuthentication; +import java.net.URL; +import org.junit.jupiter.api.Test; + +class ProxyAuthenticatorTest { + + private PasswordAuthentication passwordAuthentication; + + @Test + void createsPasswordAuthentication() throws Exception { + + ProxyAuthenticator proxyAuthenticator = new ProxyAuthenticator("user", "password"); + Authenticator.setDefault(proxyAuthenticator); + givenPasswordAuthentication(RequestorType.PROXY); + + assertThat(passwordAuthentication.getUserName()).isEqualTo("user"); + assertThat(passwordAuthentication.getPassword()).contains( + 'p', + 'a', + 's', + 's', + 'w', + 'o', + 'r', + 'd' + ); + + } + + private void givenPasswordAuthentication(RequestorType proxy) throws Exception { + passwordAuthentication = Authenticator.requestPasswordAuthentication("host", + InetAddress.getLocalHost(), + 8080, + "http", + "prompt", + "https", + new URL("https://www.daniel-heid.de"), + proxy + ); + } + + @Test + void returnsNullIfNoPasswordAuthentication() throws Exception { + + ProxyAuthenticator proxyAuthenticator = new ProxyAuthenticator("user", "password"); + Authenticator.setDefault(proxyAuthenticator); + givenPasswordAuthentication(RequestorType.SERVER); + + assertThat(passwordAuthentication).isNull(); + + } + + +} diff --git a/core/src/test/java/org/matomo/java/tracking/QueryCreatorTest.java b/core/src/test/java/org/matomo/java/tracking/QueryCreatorTest.java new file mode 100644 index 00000000..a4e87e5b --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/QueryCreatorTest.java @@ -0,0 +1,604 @@ +package org.matomo.java.tracking; + +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Locale.LanguageRange; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.parameters.AcceptLanguage; +import org.matomo.java.tracking.parameters.Country; +import org.matomo.java.tracking.parameters.CustomVariable; +import org.matomo.java.tracking.parameters.CustomVariables; +import org.matomo.java.tracking.parameters.DeviceResolution; +import org.matomo.java.tracking.parameters.EcommerceItem; +import org.matomo.java.tracking.parameters.EcommerceItems; +import org.matomo.java.tracking.parameters.RandomValue; +import org.matomo.java.tracking.parameters.UniqueId; +import org.matomo.java.tracking.parameters.VisitorId; + +class QueryCreatorTest { + + private final MatomoRequest.MatomoRequestBuilder matomoRequestBuilder = MatomoRequest + .request() + .visitorId(VisitorId.fromHash(1234567890123456789L)) + .randomValue(RandomValue.fromString("random-value")); + + private String defaultAuthToken = "876de1876fb2cda2816c362a61bfc712"; + + private String query; + + private MatomoRequest request; + + @Test + void usesDefaultSiteId() { + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value"); + + } + + private void whenCreatesQuery() { + request = matomoRequestBuilder.build(); + TrackerConfiguration trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create("http://localhost")) + .defaultSiteId(42) + .defaultAuthToken(defaultAuthToken) + .build(); + String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration); + query = new QueryCreator(trackerConfiguration).createQuery(request, authToken); + } + + @Test + void overridesDefaultSiteId() { + + matomoRequestBuilder.siteId(123); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&idsite=123&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value"); + + } + + @Test + void usesDefaultTokenAuth() { + + defaultAuthToken = "f123bfc9a46de0bb5453afdab6f93200"; + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=f123bfc9a46de0bb5453afdab6f93200&rec=1&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value"); + + } + + @Test + void overridesDefaultTokenAuth() { + + defaultAuthToken = "f123bfc9a46de0bb5453afdab6f93200"; + matomoRequestBuilder.authToken("e456bfc9a46de0bb5453afdab6f93200"); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=e456bfc9a46de0bb5453afdab6f93200&rec=1&apiv=1&_id=112210f47de98115&token_auth=e456bfc9a46de0bb5453afdab6f93200&send_image=0&rand=random-value"); + + } + + @Test + void validatesTokenAuth() { + + matomoRequestBuilder.authToken("invalid-token-auth"); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + + } + + @Test + void convertsTrueBooleanTo1() { + + matomoRequestBuilder.pluginFlash(true); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&fla=1&send_image=0&rand=random-value"); + + } + + @Test + void convertsFalseBooleanTo0() { + + matomoRequestBuilder.pluginJava(false); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&java=0&send_image=0&rand=random-value"); + + } + + @Test + void encodesUrl() { + + matomoRequestBuilder.actionUrl("https://www.daniel-heid.de/some/page?foo=bar"); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&url=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fpage%3Ffoo%3Dbar&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value"); + + } + + @Test + void encodesReferrerUrl() { + + matomoRequestBuilder.referrerUrl("https://www.daniel-heid.de/some/referrer?foo2=bar2"); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&urlref=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Freferrer%3Ffoo2%3Dbar2&send_image=0&rand=random-value"); + + } + + @Test + void encodesLink() { + + matomoRequestBuilder.outlinkUrl("https://www.daniel-heid.de/some/external/link#"); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&link=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fexternal%2Flink%23&send_image=0&rand=random-value"); + + } + + @Test + void encodesDownloadUrl() { + + matomoRequestBuilder.downloadUrl("https://www.daniel-heid.de/some/download.pdf"); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&download=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fdownload.pdf&send_image=0&rand=random-value"); + + } + + @Test + void tracksMinimalRequest() { + + matomoRequestBuilder + .actionName("Help / Feedback") + .actionUrl("https://www.daniel-heid.de/portfolio") + .visitorId(VisitorId.fromHash(3434343434343434343L)) + .referrerUrl("https://www.daniel-heid.de/referrer") + .visitCustomVariables(new CustomVariables() + .add(new CustomVariable("customVariable1Key", "customVariable1Value"), 5) + .add(new CustomVariable("customVariable2Key", "customVariable2Value"), 6)) + .visitorVisitCount(2) + .visitorPreviousVisitTimestamp(Instant.parse("2022-08-09T18:34:12Z")) + .deviceResolution(DeviceResolution.builder().width(1024).height(768).build()) + .headerAcceptLanguage(AcceptLanguage + .builder() + .languageRange(new LanguageRange("de")) + .languageRange(new LanguageRange("de-DE", 0.9)) + .languageRange(new LanguageRange("en", 0.8)) + .build()) + .pageViewId(UniqueId.fromValue(999999999999999999L)) + .goalId(0) + .ecommerceRevenue(12.34) + .ecommerceItems(EcommerceItems + .builder() + .item(EcommerceItem.builder().sku("SKU").build()) + .item(EcommerceItem + .builder() + .sku("SKU") + .name("NAME") + .category("CATEGORY") + .price(123.4) + .build()) + .build()) + .authToken("fdf6e8461ea9de33176b222519627f78") + .visitorCountry(Country.fromLanguageRanges("en-GB;q=0.7,de,de-DE;q=0.9,en;q=0.8,en-US;q=0.6")); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=fdf6e8461ea9de33176b222519627f78&rec=1&action_name=Help+%2F+Feedback&url=https%3A%2F%2Fwww.daniel-heid.de%2Fportfolio&apiv=1&_id=2fa93d2858bc4867&urlref=https%3A%2F%2Fwww.daniel-heid.de%2Freferrer&_cvar=%7B%225%22%3A%5B%22customVariable1Key%22%2C%22customVariable1Value%22%5D%2C%226%22%3A%5B%22customVariable2Key%22%2C%22customVariable2Value%22%5D%7D&_idvc=2&_viewts=1660070052&res=1024x768&lang=de%2Cde-de%3Bq%3D0.9%2Cen%3Bq%3D0.8&pv_id=lbBbxG&idgoal=0&revenue=12.34&ec_items=%5B%5B%22SKU%22%2C%22%22%2C%22%22%2C0.0%2C0%5D%2C%5B%22SKU%22%2C%22NAME%22%2C%22CATEGORY%22%2C123.4%2C0%5D%5D&token_auth=fdf6e8461ea9de33176b222519627f78&country=de&send_image=0&rand=random-value"); + + } + + @Test + void testGetQueryString() { + matomoRequestBuilder + .siteId(3) + .actionUrl("http://test.com") + .randomValue(RandomValue.fromString("random")) + .visitorId(VisitorId.fromHex("1234567890123456")); + defaultAuthToken = null; + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random"); + matomoRequestBuilder.pageCustomVariables(new CustomVariables().add(new CustomVariable( + "key", + "val" + ), 7)); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random"); + matomoRequestBuilder.additionalParameters(singletonMap("key", singleton("test"))); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random&key=%5Btest%5D"); + matomoRequestBuilder.additionalParameters(singletonMap("key", asList("test", "test2"))); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random&key=%5Btest%2C+test2%5D"); + Map customTrackingParameters = new HashMap<>(); + customTrackingParameters.put("key", "test2"); + customTrackingParameters.put("key2", "test3"); + matomoRequestBuilder.additionalParameters(customTrackingParameters); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random&key2=test3&key=test2"); + customTrackingParameters.put("key", "test4"); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random&key2=test3&key=test4"); + matomoRequestBuilder.randomValue(null); + matomoRequestBuilder.siteId(null); + matomoRequestBuilder.required(null); + matomoRequestBuilder.apiVersion(null); + matomoRequestBuilder.responseAsImage(null); + matomoRequestBuilder.visitorId(null); + matomoRequestBuilder.actionUrl(null); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "idsite=42&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&key2=test3&key=test4"); + } + + @Test + void testGetQueryString2() { + matomoRequestBuilder + .actionUrl("http://test.com") + .randomValue(RandomValue.fromString("random")) + .visitorId(VisitorId.fromHex("1234567890123456")) + .siteId(3); + defaultAuthToken = null; + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random"); + } + + @Test + void testGetUrlEncodedQueryString() { + defaultAuthToken = null; + matomoRequestBuilder + .actionUrl("http://test.com") + .randomValue(RandomValue.fromString("random")) + .visitorId(VisitorId.fromHex("1234567890123456")) + .siteId(3); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random"); + Map customTrackingParameters = new HashMap<>(); + customTrackingParameters.put("ke/y", "te:st"); + matomoRequestBuilder.additionalParameters(customTrackingParameters); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random&ke%2Fy=te%3Ast"); + customTrackingParameters.put("ke/y", "te:st2"); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random&ke%2Fy=te%3Ast2"); + customTrackingParameters.put("ke/y2", "te:st3"); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random&ke%2Fy=te%3Ast2&ke%2Fy2=te%3Ast3"); + customTrackingParameters.put("ke/y", "te:st3"); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random&ke%2Fy=te%3Ast3&ke%2Fy2=te%3Ast3"); + matomoRequestBuilder + .randomValue(null) + .siteId(null) + .required(null) + .apiVersion(null) + .responseAsImage(null) + .visitorId(null) + .actionUrl(null); + whenCreatesQuery(); + assertThat(query).isEqualTo("idsite=42&ke%2Fy=te%3Ast3&ke%2Fy2=te%3Ast3"); + + } + + @Test + void testGetUrlEncodedQueryString2() { + matomoRequestBuilder + .actionUrl("http://test.com") + .randomValue(RandomValue.fromString("random")) + .visitorId(VisitorId.fromHex("1234567890123456")); + defaultAuthToken = null; + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&rec=1&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random"); + + } + + @Test + void testVisitCustomVariableCustomVariable() { + matomoRequestBuilder + .randomValue(RandomValue.fromString("random")) + .visitorId(VisitorId.fromHex("1234567890123456")) + .siteId(3); + org.matomo.java.tracking.CustomVariable cv = + new org.matomo.java.tracking.CustomVariable("visitKey", "visitVal"); + matomoRequestBuilder.visitCustomVariables(new CustomVariables().add(cv, 8)); + defaultAuthToken = null; + + whenCreatesQuery(); + + assertThat(request.getVisitCustomVariable(1)).isNull(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&apiv=1&_id=1234567890123456&_cvar=%7B%228%22%3A%5B%22visitKey%22%2C%22visitVal%22%5D%7D&send_image=0&rand=random"); + } + + @Test + void doesNotAppendEmptyString() { + + matomoRequestBuilder.eventAction(""); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&e_a=&send_image=0&rand=random-value"); + + } + + @Test + void testAuthTokenTT() { + + matomoRequestBuilder.authToken("1234"); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + } + + @Test + void createsQueryWithDimensions() { + Map dimensions = new LinkedHashMap<>(); + dimensions.put(1L, "firstDimension"); + dimensions.put(3L, "thirdDimension"); + matomoRequestBuilder.dimensions(dimensions); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value&dimension1=firstDimension&dimension3=thirdDimension"); + } + + @Test + void appendsCharsetParameters() { + matomoRequestBuilder.characterSet(StandardCharsets.ISO_8859_1); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&cs=ISO-8859-1&send_image=0&rand=random-value"); + } + + @Test + void failsIfIdSiteIsNegative() { + matomoRequestBuilder.siteId(-1); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not append parameter") + .hasRootCauseInstanceOf(MatomoException.class) + .hasRootCauseMessage("Invalid value for idsite. Must be greater or equal than 1"); + } + + @Test + void failsIfIdSiteIsZero() { + matomoRequestBuilder.siteId(0); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not append parameter") + .hasRootCauseInstanceOf(MatomoException.class) + .hasRootCauseMessage("Invalid value for idsite. Must be greater or equal than 1"); + } + + @Test + void failsIfCurrentHourIsNegative() { + matomoRequestBuilder.currentHour(-1); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not append parameter") + .hasRootCauseInstanceOf(MatomoException.class) + .hasRootCauseMessage("Invalid value for h. Must be greater or equal than 0"); + } + + @Test + void failsIfCurrentHourIsGreaterThan23() { + matomoRequestBuilder.currentHour(24); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not append parameter") + .hasRootCauseInstanceOf(MatomoException.class) + .hasRootCauseMessage("Invalid value for h. Must be less or equal than 23"); + } + + @Test + void failsIfCurrentMinuteIsNegative() { + matomoRequestBuilder.currentMinute(-1); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not append parameter") + .hasRootCauseInstanceOf(MatomoException.class) + .hasRootCauseMessage("Invalid value for m. Must be greater or equal than 0"); + } + + @Test + void failsIfCurrentMinuteIsGreaterThan59() { + matomoRequestBuilder.currentMinute(60); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not append parameter") + .hasRootCauseInstanceOf(MatomoException.class) + .hasRootCauseMessage("Invalid value for m. Must be less or equal than 59"); + } + + @Test + void failsIfCurrentSecondIsNegative() { + matomoRequestBuilder.currentSecond(-1); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not append parameter") + .hasRootCauseInstanceOf(MatomoException.class) + .hasRootCauseMessage("Invalid value for s. Must be greater or equal than 0"); + } + + @Test + void failsIfCurrentSecondIsGreaterThan59() { + matomoRequestBuilder.currentSecond(60); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not append parameter") + .hasRootCauseInstanceOf(MatomoException.class) + .hasRootCauseMessage("Invalid value for s. Must be less or equal than 59"); + } + + @Test + void failsIfLatitudeIsLessThanMinus90() { + matomoRequestBuilder.visitorLatitude(-90.1); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not append parameter") + .hasRootCauseInstanceOf(MatomoException.class) + .hasRootCauseMessage("Invalid value for lat. Must be greater or equal than -90"); + } + + @Test + void failsIfLatitudeIsGreaterThan90() { + matomoRequestBuilder.visitorLatitude(90.1); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not append parameter") + .hasRootCauseInstanceOf(MatomoException.class) + .hasRootCauseMessage("Invalid value for lat. Must be less or equal than 90"); + } + + @Test + void failsIfLongitudeIsLessThanMinus180() { + matomoRequestBuilder.visitorLongitude(-180.1); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not append parameter") + .hasRootCauseMessage("Invalid value for long. Must be greater or equal than -180"); + } + + @Test + void failsIfLongitudeIsGreaterThan180() { + matomoRequestBuilder.visitorLongitude(180.1); + + assertThatThrownBy(this::whenCreatesQuery) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not append parameter") + .hasRootCauseMessage("Invalid value for long. Must be less or equal than 180"); + } + + @Test + void tracksEvent() { + matomoRequestBuilder.eventName("Event Name") + .eventValue(23.456) + .eventAction("Event Action") + .eventCategory("Event Category"); + + whenCreatesQuery(); + + assertThat(query).isEqualTo("idsite=42&token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&e_c=Event+Category&e_a=Event+Action&e_n=Event+Name&e_v=23.456&send_image=0&rand=random-value"); + } + + @Test + void allowsZeroForEventValue() { + matomoRequestBuilder.eventName("Event Name") + .eventValue(0.0) + .eventAction("Event Action") + .eventCategory("Event Category"); + + whenCreatesQuery(); + + assertThat(query) + .isEqualTo("idsite=42&" + + "token_auth=876de1876fb2cda2816c362a61bfc712&" + + "rec=1&" + + "apiv=1&" + + "_id=112210f47de98115&" + + "e_c=Event+Category&" + + "e_a=Event+Action&" + + "e_n=Event+Name&" + + "e_v=0.0&" + + "send_image=0&" + + "rand=random-value" + ); + } + + @Test + void allowsZeroForEcommerceValues() { + matomoRequestBuilder + .ecommerceRevenue(0.0) + .ecommerceSubtotal(0.0) + .ecommerceTax(0.0) + .ecommerceShippingCost(0.0) + .ecommerceDiscount(0.0); + + whenCreatesQuery(); + + assertThat(query) + .isEqualTo("idsite=42&" + + "token_auth=876de1876fb2cda2816c362a61bfc712&" + + "rec=1&" + + "apiv=1&" + + "_id=112210f47de98115&" + + "revenue=0.0&" + + "ec_st=0.0&" + + "ec_tx=0.0&" + + "ec_sh=0.0&" + + "ec_dt=0.0&" + + "send_image=0&" + + "rand=random-value" + ); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/matomo/java/tracking/RequestValidatorTest.java b/core/src/test/java/org/matomo/java/tracking/RequestValidatorTest.java new file mode 100644 index 00000000..6d1ab348 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/RequestValidatorTest.java @@ -0,0 +1,97 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Locale; +import org.junit.jupiter.api.Test; +import org.piwik.java.tracking.PiwikDate; +import org.piwik.java.tracking.PiwikLocale; + +class RequestValidatorTest { + + private final MatomoRequest request = new MatomoRequest(); + + + @Test + void testSearchResultsCount() { + + request.setSearchResultsCount(100L); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Search query must be set if search results count is set"); + + } + + @Test + void testVisitorLongitude() { + request.setVisitorLongitude(20.5); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage( + "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set"); + } + + @Test + void testVisitorLatitude() { + request.setVisitorLatitude(10.5); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage( + "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set"); + } + + @Test + void testVisitorCity() { + request.setVisitorCity("city"); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage( + "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set"); + } + + @Test + void testVisitorRegion() { + request.setVisitorRegion("region"); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage( + "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set"); + } + + @Test + void testVisitorCountryTE() { + PiwikLocale country = new PiwikLocale(Locale.US); + request.setVisitorCountry(country); + + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage( + "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set"); + } + + @Test + void testRequestDatetime() { + + PiwikDate date = new PiwikDate(1000L); + request.setRequestDatetime(date); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Auth token must be present if request timestamp is more than four hours ago"); + + } + + @Test + void failsIfAuthTokenIsNot32CharactersLong() { + assertThatThrownBy(() -> RequestValidator.validate(request, "123456789012345678901234567890")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/ServiceLoaderSenderFactoryTest.java b/core/src/test/java/org/matomo/java/tracking/ServiceLoaderSenderFactoryTest.java new file mode 100644 index 00000000..0a5d2253 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/ServiceLoaderSenderFactoryTest.java @@ -0,0 +1,24 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; +import org.junit.jupiter.api.Test; + +class ServiceLoaderSenderFactoryTest { + + @Test + void failsIfNoImplementationFound() { + ServiceLoaderSenderFactory serviceLoaderSenderFactory = new ServiceLoaderSenderFactory(); + + TrackerConfiguration trackerConfiguration = + TrackerConfiguration.builder().apiEndpoint(URI.create("http://localhost/matomo.php")).build(); + + assertThatThrownBy(() -> serviceLoaderSenderFactory.createSender(trackerConfiguration, + new QueryCreator(trackerConfiguration) + )) + .isInstanceOf(MatomoException.class) + .hasMessage("No SenderProvider found"); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/matomo/java/tracking/TestSender.java b/core/src/test/java/org/matomo/java/tracking/TestSender.java new file mode 100644 index 00000000..f000bc8d --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/TestSender.java @@ -0,0 +1,76 @@ +package org.matomo.java.tracking; + +import static java.util.Collections.singleton; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * A {@link Sender} implementation that does not send anything but stores the requests and queries. + * + *

This class is intended for testing purposes only. It does not send anything to the Matomo server. Instead, it + * stores the requests and queries in collections that can be accessed via {@link #getRequests()} and {@link + * #getQueries()}. + */ +@RequiredArgsConstructor +@Getter +class TestSender implements Sender { + + private final Collection requests = new ArrayList<>(); + + private final Collection queries = new ArrayList<>(); + + private final TrackerConfiguration trackerConfiguration; + + private final QueryCreator queryCreator; + + @NonNull + @Override + public CompletableFuture sendSingleAsync(@NonNull MatomoRequest request) { + createQueryAndAddRequest(request, null); + return CompletableFuture.completedFuture(request); + } + + @Override + public void sendSingle(@NonNull MatomoRequest request) { + createQueryAndAddRequest(request, null); + } + + @Override + public void sendBulk( + @NonNull Iterable requests, @Nullable String overrideAuthToken + ) { + for (MatomoRequest request : requests) { + createQueryAndAddRequest(request, overrideAuthToken); + } + } + + @NonNull + @Override + public CompletableFuture sendBulkAsync( + @NonNull Collection requests, @Nullable String overrideAuthToken + ) { + for (MatomoRequest request : requests) { + createQueryAndAddRequest(request, overrideAuthToken); + } + return CompletableFuture.completedFuture(null); + } + + + + private void createQueryAndAddRequest(@lombok.NonNull MatomoRequest request, @Nullable String overrideAuthToken) { + String authToken = AuthToken.determineAuthToken(overrideAuthToken, singleton(request), trackerConfiguration); + queries.add(queryCreator.createQuery(request, authToken)); + requests.add(request); + } + + @Override + public void close() { + // do nothing + } +} diff --git a/core/src/test/java/org/matomo/java/tracking/TestSenderFactory.java b/core/src/test/java/org/matomo/java/tracking/TestSenderFactory.java new file mode 100644 index 00000000..fafafa90 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/TestSenderFactory.java @@ -0,0 +1,16 @@ +package org.matomo.java.tracking; + +import lombok.Getter; + +class TestSenderFactory implements SenderFactory { + + @Getter + private TestSender testSender; + + @Override + public Sender createSender(TrackerConfiguration trackerConfiguration, QueryCreator queryCreator) { + TestSender testSender = new TestSender(trackerConfiguration, queryCreator); + this.testSender = testSender; + return testSender; + } +} diff --git a/core/src/test/java/org/matomo/java/tracking/TestThrowable.java b/core/src/test/java/org/matomo/java/tracking/TestThrowable.java new file mode 100644 index 00000000..daf4a69a --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/TestThrowable.java @@ -0,0 +1,9 @@ +package org.matomo.java.tracking; + +class TestThrowable extends Throwable { + + TestThrowable() { + super("message", null, false, false); + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/TrackerConfigurationTest.java b/core/src/test/java/org/matomo/java/tracking/TrackerConfigurationTest.java new file mode 100644 index 00000000..593aa7b7 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/TrackerConfigurationTest.java @@ -0,0 +1,412 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; +import org.junit.jupiter.api.Test; + +class TrackerConfigurationTest { + + private final TrackerConfiguration.TrackerConfigurationBuilder trackerConfigurationBuilder + = TrackerConfiguration.builder(); + + @Test + void validateDoesNotFailIfDefaultAuthTokenIsNull() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken(null) + .build(); + whenValidates(); + } + + @Test + void validateFailsIfDefaultAuthTokenIsEmpty() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("") + .build(); + + thenFailsOnValidation("Auth token must be exactly 32 characters long"); + } + + @Test + void validateFailsIfDefaultAuthTokenIsTooLong() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("123456789012345678901234567890123") + .build(); + + thenFailsOnValidation("Auth token must be exactly 32 characters long"); + } + + @Test + void validateFailsIfDefaultAuthTokenIsTooShort() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("1234567890123456789012345678901") + .build(); + + thenFailsOnValidation("Auth token must be exactly 32 characters long"); + } + + @Test + void validateFailsIfDefaultAuthTokenContainsInvalidCharacters() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("1234567890123456789012345678901!") + .build(); + + thenFailsOnValidation("Auth token must contain only lowercase letters and numbers"); + } + + @Test + void validateDoesNotFailIfDefaultSiteIdIsNull() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .defaultSiteId(null) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + + @Test + void validateDoesNotFailIfDefaultSiteIdIsPositive() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateDoesNotFailIfDefaultSiteIdIsZero() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .defaultSiteId(0) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + + whenValidates(); + } + + @Test + void validateFailsIfDefaultSiteIdIsNegative() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .defaultSiteId(-1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + + thenFailsOnValidation("Default site ID must not be negative"); + } + + @Test + void validateDoesNotFailIfApiEndpointIsSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateFailsIfApiEndpointIsNotSet() { + trackerConfigurationBuilder + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + + thenFailsOnValidation("API endpoint must not be null"); + } + + @Test + void validateDoesNotFailIfProxyHostIsSetAndProxyPortIsPositive() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyHost("proxy.example") + .proxyPort(1234) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateFailsIfProxyPortIsSetAndProxyHostIsNotSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyPort(1234) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + thenFailsOnValidation("Proxy host must be set if port is set"); + } + + @Test + void validateFailsIfProxyPasswordIsSetAndProxyHostIsNotSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyPassword("password") + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + thenFailsOnValidation("Proxy host must be set if password is set"); + } + + @Test + void validateFailsIfProxyHostIsSetAndProxyPortIsZero() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyHost("proxy.example") + .proxyPort(0) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + + thenFailsOnValidation("Proxy port must be greater than 0"); + } + + @Test + void validateFailsIfProxyHostIsSetAndProxyPortIsNegative() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyHost("proxy.example") + .proxyPort(-1) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + + thenFailsOnValidation("Proxy port must be greater than 0"); + } + + @Test + void validateFailsIfProxyUsernameIsSetAndProxyPasswordIsNotSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyHost("proxy.example") + .proxyPort(1234) + .proxyUsername("user") + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + + thenFailsOnValidation("Proxy password must be set if username is set"); + } + + @Test + void validateFailsIfProxyPasswordIsSetAndProxyUsernameIsNotSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyHost("proxy.example") + .proxyPort(1234) + .proxyPassword("password") + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + + thenFailsOnValidation("Proxy username must be set if password is set"); + } + + @Test + void validateDoesNotFailIfProxyUsernameAndProxyPasswordAreSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyHost("proxy.example") + .proxyPort(1234) + .proxyUsername("user") + .proxyPassword("password") + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateDoesNotFailIfProxyUsernameAndProxyPasswordAreNotSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyHost("proxy.example") + .proxyPort(1234) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateDoesNotFailIfProxyUsernameIsSetAndProxyPasswordIsSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyHost("proxy.example") + .proxyPort(1234) + .proxyUsername("user") + .proxyPassword("password") + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateDoesNotFailIfProxyUsernameIsNotSetAndProxyPasswordIsNotSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyHost("proxy.example") + .proxyPort(1234) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateFailsIfProxyUsernameIsSetAndProxyPasswordIsNotSetAndProxyHostIsNotSetAndProxyPortIsNotSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyUsername("user") + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + thenFailsOnValidation("Proxy host must be set if username is set"); + } + + @Test + void validateDoesNotFailIfProxyUsernameIsNotSetAndProxyPasswordIsNotSetAndProxyHostIsNotSetAndProxyPortIsNotSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateFailsIfProxyUsernameIsSetAndProxyPasswordIsSetAndProxyHostIsNotSetAndProxyPortIsNotSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .proxyUsername("user") + .proxyPassword("password") + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + thenFailsOnValidation("Proxy host must be set if username is set"); + } + + @Test + void validateDoesNotFailIfSocketTimeoutIsNotSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .socketTimeout(null) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateFailsIfSocketTimeoutIsNegative() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .socketTimeout(java.time.Duration.ofSeconds(-1)) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + + thenFailsOnValidation("Socket timeout must not be negative"); + } + + @Test + void validateDoesNotFailIfConnectTimeoutIsNotSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .connectTimeout(null) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateFailsIfConnectTimeoutIsNegative() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .connectTimeout(java.time.Duration.ofSeconds(-1)) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + + thenFailsOnValidation("Connect timeout must not be negative"); + } + + @Test + void validateDoesNotFailIfThreadPoolSizeIsOne() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .threadPoolSize(1) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateFailsIfThreadPoolSizeIsLessThanOne() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .threadPoolSize(0) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + + thenFailsOnValidation("Thread pool size must be greater than 0"); + } + + @Test + void validateDoesNotFailIfThreadPoolSizeIsGreaterThanOne() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .threadPoolSize(2) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateDoesNotFailIfThreadPoolSizeIsNotSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + @Test + void validateDoesNotFailIfThreadPoolSizeIsGreaterThanOneAndSet() { + trackerConfigurationBuilder + .apiEndpoint(URI.create("https://matomo.example/matomo.php")) + .threadPoolSize(2) + .defaultSiteId(1) + .defaultAuthToken("12345678901234567890123456789012") + .build(); + whenValidates(); + } + + void whenValidates() { + trackerConfigurationBuilder.build().validate(); + } + + private void thenFailsOnValidation(String message) { + assertThatThrownBy(this::whenValidates).isInstanceOf(IllegalArgumentException.class).hasMessage(message); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/matomo/java/tracking/TrackingParameterMethodTest.java b/core/src/test/java/org/matomo/java/tracking/TrackingParameterMethodTest.java new file mode 100644 index 00000000..8b68c950 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/TrackingParameterMethodTest.java @@ -0,0 +1,89 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; + +class TrackingParameterMethodTest { + + @Test + void validateParameterValueFailsIfPatternDoesNotMatch() { + TrackingParameterMethod trackingParameterMethod = TrackingParameterMethod + .builder() + .parameterName("foo") + .pattern(Pattern.compile("bar")) + .build(); + + assertThatThrownBy(() -> trackingParameterMethod.validateParameterValue("baz")) + .isInstanceOf(MatomoException.class) + .hasMessage("Invalid value for foo. Must match regex bar"); + } + + @Test + void doNothingIfPatternIsNull() { + TrackingParameterMethod trackingParameterMethod = + TrackingParameterMethod.builder().parameterName("foo").maxLength(255).build(); + + trackingParameterMethod.validateParameterValue("baz"); + } + + @Test + void doNothingIfParameterValueIsNotCharSequence() { + TrackingParameterMethod trackingParameterMethod = TrackingParameterMethod + .builder() + .parameterName("foo") + .pattern(Pattern.compile("bar")) + .maxLength(255) + .min(1) + .max(1) + .build(); + + trackingParameterMethod.validateParameterValue(1); + } + + @Test + void failIfParameterValueIsNull() { + TrackingParameterMethod trackingParameterMethod = TrackingParameterMethod + .builder() + .parameterName("foo") + .pattern(Pattern.compile("bar")) + .maxLength(255) + .build(); + + assertThatThrownBy(() -> trackingParameterMethod.validateParameterValue(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("parameterValue is marked non-null but is null"); + } + + @Test + void validateParameterValueFailsIfMaxLengthIsExceeded() { + TrackingParameterMethod trackingParameterMethod = + TrackingParameterMethod.builder().parameterName("foo").maxLength(3).build(); + + assertThatThrownBy(() -> trackingParameterMethod.validateParameterValue("foobar")) + .isInstanceOf(MatomoException.class) + .hasMessage("Invalid value for foo. Must be less or equal than 3 characters"); + } + + @Test + void failIfParameterValueIsLessThanMin() { + TrackingParameterMethod trackingParameterMethod = + TrackingParameterMethod.builder().parameterName("foo").min(3.0).build(); + + assertThatThrownBy(() -> trackingParameterMethod.validateParameterValue(1)) + .isInstanceOf(MatomoException.class) + .hasMessage("Invalid value for foo. Must be greater or equal than 3"); + } + + @Test + void failIfParameterValueIsGreaterThanMax() { + TrackingParameterMethod trackingParameterMethod = + TrackingParameterMethod.builder().parameterName("foo").max(3.0).build(); + + assertThatThrownBy(() -> trackingParameterMethod.validateParameterValue(4)) + .isInstanceOf(MatomoException.class) + .hasMessage("Invalid value for foo. Must be less or equal than 3"); + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/TrustingX509TrustManagerTest.java b/core/src/test/java/org/matomo/java/tracking/TrustingX509TrustManagerTest.java new file mode 100644 index 00000000..895017de --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/TrustingX509TrustManagerTest.java @@ -0,0 +1,28 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.security.cert.X509Certificate; +import org.junit.jupiter.api.Test; + +class TrustingX509TrustManagerTest { + + private final TrustingX509TrustManager trustingX509TrustManager = new TrustingX509TrustManager(); + + @Test + void acceptedIssuersIsAlwaysNull() { + X509Certificate[] acceptedIssuers = trustingX509TrustManager.getAcceptedIssuers(); + assertThat(acceptedIssuers).isNull(); + } + + @Test + void checkClientTrustedDoesNothing() { + trustingX509TrustManager.checkClientTrusted(null, null); + } + + @Test + void checkServerTrustedDoesNothing() { + trustingX509TrustManager.checkServerTrusted(null, null); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/matomo/java/tracking/parameters/AcceptLanguageTest.java b/core/src/test/java/org/matomo/java/tracking/parameters/AcceptLanguageTest.java new file mode 100644 index 00000000..6235949d --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/parameters/AcceptLanguageTest.java @@ -0,0 +1,48 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class AcceptLanguageTest { + + @Test + void fromHeader() { + + AcceptLanguage acceptLanguage = + AcceptLanguage.fromHeader("de,de-DE;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6"); + + assertThat(acceptLanguage).hasToString( + "de,de-de;q=0.9,de-dd;q=0.9,en;q=0.8,en-gb;q=0.7,en-us;q=0.6"); + + } + + @ParameterizedTest + @NullAndEmptySource + void fromHeaderToleratesNull(String header) { + + AcceptLanguage acceptLanguage = AcceptLanguage.fromHeader(header); + + assertThat(acceptLanguage).isNull(); + + } + + @Test + void failsOnNullLanguageRange() { + assertThat(AcceptLanguage + .builder() + .languageRanges(singletonList(null)) + .build()).hasToString(""); + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/parameters/CountryTest.java b/core/src/test/java/org/matomo/java/tracking/parameters/CountryTest.java new file mode 100644 index 00000000..097357c1 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/parameters/CountryTest.java @@ -0,0 +1,154 @@ +/* + * 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 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; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class CountryTest { + + @Test + void createsCountryFromCode() { + + Country country = Country.fromCode("DE"); + + assertThat(country).hasToString("de"); + + } + + @Test + void createsCountryFromAcceptLanguageHeader() { + + Country country = Country.fromLanguageRanges("en-GB;q=0.7,de,de-DE;q=0.9,en;q=0.8,en-US;q=0.6"); + + assertThat(country).hasToString("de"); + + } + + @ParameterizedTest + @NullAndEmptySource + void returnsNullOnEmptyRanges(String ranges) { + + Country country = Country.fromLanguageRanges(ranges); + + assertThat(country).isNull(); + + } + + @Test + void failsOnInvalidCountryCode() { + + assertThatThrownBy(() -> Country.fromCode("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid country code"); + + } + + @Test + void failsOnInvalidCountryCodeLength() { + + assertThatThrownBy(() -> Country.fromCode("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid country code"); + + } + + @Test + void returnsNullOnNullCode() { + + Country country = Country.fromCode(null); + + assertThat(country).isNull(); + + } + + @Test + void returnsNullOnEmptyCode() { + + Country country = Country.fromCode(""); + + assertThat(country).isNull(); + + } + + @Test + void returnsNullOnBlankCode() { + + Country country = Country.fromCode(" "); + + assertThat(country).isNull(); + + } + + @Test + void returnsNullOnNullRanges() { + + Country country = Country.fromLanguageRanges(null); + + assertThat(country).isNull(); + + } + + @Test + void returnsNullOnBlankRanges() { + + Country country = Country.fromLanguageRanges(" "); + + assertThat(country).isNull(); + + } + + @Test + void failsOnInvalidRanges() { + + assertThatThrownBy(() -> Country.fromLanguageRanges("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid country code"); + + } + + @Test + void failsOnLocaleWithoutCountryCode() { + + assertThatThrownBy(() -> new Country(Locale.forLanguageTag("de"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid locale"); + + } + + @Test + void setLocaleFailsOnNullLocale() { + + assertThatThrownBy(() -> new Country(Locale.forLanguageTag("de")).setLocale(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid locale"); + + } + + @Test + void setLocaleFailsOnNullCountryCode() { + + assertThatThrownBy(() -> new Country(Locale.forLanguageTag("de")).setLocale(Locale.forLanguageTag( + "de"))).isInstanceOf(IllegalArgumentException.class).hasMessage("Invalid locale"); + + } + + @Test + void setLocaleFailsOnEmptyCountryCode() { + + assertThatThrownBy(() -> new Country(Locale.forLanguageTag("de")).setLocale(Locale.forLanguageTag( + "de"))).isInstanceOf(IllegalArgumentException.class).hasMessage("Invalid locale"); + + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/parameters/CustomVariableTest.java b/core/src/test/java/org/matomo/java/tracking/parameters/CustomVariableTest.java new file mode 100644 index 00000000..f072d09e --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/parameters/CustomVariableTest.java @@ -0,0 +1,69 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CustomVariableTest { + + private CustomVariable customVariable; + + @BeforeEach + void setUp() { + customVariable = new CustomVariable("key", "value"); + } + + @Test + void testConstructorNullKey() { + try { + new CustomVariable(null, null); + fail("Exception should have been throw."); + } catch (NullPointerException e) { + assertThat(e.getLocalizedMessage()).isEqualTo("key is marked non-null but is null"); + } + } + + @Test + void testConstructorNullValue() { + try { + new CustomVariable("key", null); + fail("Exception should have been throw."); + } catch (NullPointerException e) { + assertThat(e.getLocalizedMessage()).isEqualTo("value is marked non-null but is null"); + } + } + + @Test + void testGetKey() { + assertThat(customVariable.getKey()).isEqualTo("key"); + } + + @Test + void testGetValue() { + assertThat(customVariable.getValue()).isEqualTo("value"); + } + + @Test + void equalsCustomVariable() { + CustomVariable variableA = new CustomVariable("a", "b"); + CustomVariable variableB = new CustomVariable("a", "b"); + assertThat(variableA).isEqualTo(variableB); + assertThat(variableA.hashCode()).isEqualTo(variableB.hashCode()); + CustomVariable c = new CustomVariable("a", "c"); + assertThat(variableA).isNotEqualTo(c); + assertThat(variableA.hashCode()).isNotEqualTo(c.hashCode()); + CustomVariable d = new CustomVariable("d", "b"); + assertThat(variableA).isNotEqualTo(d); + assertThat(variableA.hashCode()).isNotEqualTo(d.hashCode()); + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/parameters/CustomVariablesTest.java b/core/src/test/java/org/matomo/java/tracking/parameters/CustomVariablesTest.java new file mode 100644 index 00000000..0cf25d80 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/parameters/CustomVariablesTest.java @@ -0,0 +1,178 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; + +import org.junit.jupiter.api.Test; + +class CustomVariablesTest { + + private final CustomVariables customVariables = new CustomVariables(); + + @Test + void testAdd_CustomVariable() { + CustomVariable a = new CustomVariable("a", "b"); + assertThat(customVariables.isEmpty()).isTrue(); + customVariables.add(a); + assertThat(customVariables.isEmpty()).isFalse(); + assertThat(customVariables.get("a")).isEqualTo("b"); + assertThat(customVariables.get(1)).isEqualTo(a); + assertThat(customVariables).hasToString("{\"1\":[\"a\",\"b\"]}"); + CustomVariable b = new CustomVariable("c", "d"); + customVariables.add(b); + assertThat(customVariables.get("c")).isEqualTo("d"); + assertThat(customVariables.get(2)).isEqualTo(b); + assertThat(customVariables).hasToString("{\"1\":[\"a\",\"b\"],\"2\":[\"c\",\"d\"]}"); + CustomVariable c = new CustomVariable("a", "e"); + customVariables.add(c, 5); + assertThat(customVariables.get("a")).isEqualTo("b"); + assertThat(customVariables.get(5)).isEqualTo(c); + assertThat(customVariables.get(3)).isNull(); + assertThat(customVariables).hasToString("{\"1\":[\"a\",\"b\"],\"2\":[\"c\",\"d\"],\"5\":[\"a\",\"e\"]}"); + CustomVariable d = new CustomVariable("a", "f"); + customVariables.add(d); + assertThat(customVariables.get("a")).isEqualTo("f"); + assertThat(customVariables.get(1)).isEqualTo(d); + assertThat(customVariables.get(5)).isEqualTo(d); + assertThat(customVariables).hasToString("{\"1\":[\"a\",\"f\"],\"2\":[\"c\",\"d\"],\"5\":[\"a\",\"f\"]}"); + customVariables.remove("a"); + assertThat(customVariables.get("a")).isNull(); + assertThat(customVariables.get(1)).isNull(); + assertThat(customVariables.get(5)).isNull(); + assertThat(customVariables).hasToString("{\"2\":[\"c\",\"d\"]}"); + customVariables.remove(2); + assertThat(customVariables.get("c")).isNull(); + assertThat(customVariables.get(2)).isNull(); + assertThat(customVariables.isEmpty()).isTrue(); + assertThat(customVariables).hasToString("{}"); + } + + @Test + void testAddCustomVariableIndexLessThan1() { + try { + customVariables.add(new CustomVariable("a", "b"), 0); + fail("Exception should have been throw."); + } catch (IllegalArgumentException e) { + assertThat(e.getLocalizedMessage()).isEqualTo("Index must be greater than 0"); + } + } + + @Test + void equalCustomVariables() { + CustomVariables customVariables = new CustomVariables(); + customVariables.add(new CustomVariable("a", "b")); + customVariables.add(new CustomVariable("c", "d")); + customVariables.add(new CustomVariable("a", "e")); + customVariables.add(new CustomVariable("a", "f")); + assertThat(customVariables).isEqualTo(customVariables); + assertThat(customVariables).hasSameHashCodeAs(customVariables); + } + + @Test + void notEqualCustomVariables() { + CustomVariables customVariablesA = new CustomVariables(); + customVariablesA.add(new CustomVariable("a", "b")); + customVariablesA.add(new CustomVariable("c", "d")); + customVariablesA.add(new CustomVariable("a", "e")); + customVariablesA.add(new CustomVariable("a", "f")); + CustomVariables customVariablesB = new CustomVariables(); + customVariablesB.add(new CustomVariable("a", "b")); + customVariablesB.add(new CustomVariable("c", "d")); + customVariablesB.add(new CustomVariable("a", "e")); + assertThat(customVariablesA).isNotEqualTo(customVariablesB); + assertThat(customVariablesA).doesNotHaveSameHashCodeAs(customVariablesB); + } + + @Test + void testAddCustomVariableNull() { + assertThatThrownBy(() -> customVariables.add(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("variable" + " is marked non-null but is null") + .hasNoCause(); + } + + @Test + void testAddCustomVariableKeyEmpty() { + assertThatThrownBy(() -> customVariables.add(new CustomVariable("", "b"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Custom variable key must not be null or empty") + .hasNoCause(); + } + + @Test + void testAddCustomVariableValueEmpty() { + assertThatThrownBy(() -> customVariables.add(new CustomVariable("a", ""))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Custom variable value must not be null or empty") + .hasNoCause(); + } + + @Test + void testAddCustomVariableNullIndex() { + assertThatThrownBy(() -> customVariables.add(new CustomVariable("a", "b"), 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Index must be greater than 0") + .hasNoCause(); + } + + + @Test + void testAddNullCustomVariableIndex() { + assertThatThrownBy(() -> customVariables.add(null, 1)) + .isInstanceOf(NullPointerException.class) + .hasMessage("cv is marked non-null but is null") + .hasNoCause(); + } + + @Test + void testGetCustomVariableIntegerNull() { + assertThatThrownBy(() -> customVariables.get(0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Index must be greater than 0") + .hasNoCause(); + } + + @Test + void testGetCustomVariableKeyNull() { + assertThatThrownBy(() -> customVariables.get(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("key is marked non-null but is null") + .hasNoCause(); + } + + @Test + void testGetCustomVariableKeyEmpty() { + assertThatThrownBy(() -> customVariables.get("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("key must not be null or empty") + .hasNoCause(); + } + + @Test + void testRemoveCustomVariableKeyNull() { + assertThatThrownBy(() -> customVariables.remove(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("key is marked non-null but is null") + .hasNoCause(); + } + + @Test + void parseCustomVariables() { + CustomVariables customVariables = + CustomVariables.parse( + "{\"1\":[\"VAR 1 set, var 2 not set\",\"yes\"],\"3\":[\"var 3 set\",\"yes!!!!\"]}"); + assertThat(customVariables.get(1).getKey()).isEqualTo("VAR 1 set, var 2 not set"); + assertThat(customVariables.get(1).getValue()).isEqualTo("yes"); + assertThat(customVariables.get(3).getKey()).isEqualTo("var 3 set"); + assertThat(customVariables.get(3).getValue()).isEqualTo("yes!!!!"); + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/parameters/DeviceResolutionTest.java b/core/src/test/java/org/matomo/java/tracking/parameters/DeviceResolutionTest.java new file mode 100644 index 00000000..829c915f --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/parameters/DeviceResolutionTest.java @@ -0,0 +1,54 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class DeviceResolutionTest { + + @Test + void formatsDeviceResolution() { + + DeviceResolution deviceResolution = DeviceResolution.builder().width(1280).height(1080).build(); + + assertThat(deviceResolution).hasToString("1280x1080"); + + } + + @Test + void returnsNullOnNull() { + + DeviceResolution deviceResolution = DeviceResolution.fromString(null); + + assertThat(deviceResolution).isNull(); + + } + + @Test + void failsOnWrongDimensionSize() { + assertThatThrownBy(() -> DeviceResolution.fromString("1920x1080x720")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Wrong dimension size"); + } + + @Test + void failsIfDeviceResolutionIsTooShort() { + assertThatThrownBy(() -> DeviceResolution.fromString("1")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Wrong device resolution size"); + } + + @Test + void returnsNullIfDeviceResolutionIsEmpty() { + assertThat(DeviceResolution.fromString("")).isNull(); + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/parameters/EcommerceItemsTest.java b/core/src/test/java/org/matomo/java/tracking/parameters/EcommerceItemsTest.java new file mode 100644 index 00000000..9d13a5d8 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/parameters/EcommerceItemsTest.java @@ -0,0 +1,35 @@ +package org.matomo.java.tracking.parameters; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + + +class EcommerceItemsTest { + + @Test + void formatsJson() { + EcommerceItems 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.1) + .quantity(1) + .build()) + .build(); + assertThat(ecommerceItems).hasToString("[[\"XYZ12345\",\"Matomo - The big book about web analytics\",\"Education & Teaching\",23.1,2],[\"B0C2WV3MRJ\",\"Matomo for data visualization\",\"Education & Teaching\",15.1,1]]"); + } + + +} diff --git a/core/src/test/java/org/matomo/java/tracking/parameters/HexTest.java b/core/src/test/java/org/matomo/java/tracking/parameters/HexTest.java new file mode 100644 index 00000000..5f07f27d --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/parameters/HexTest.java @@ -0,0 +1,37 @@ +package org.matomo.java.tracking.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class HexTest { + + + + @Test + void failsIfBytesAreNull() { + assertThatThrownBy(() -> Hex.fromBytes(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("bytes is marked non-null but is null"); + } + + private static Stream testBytes() { + return Stream.of( + Arguments.of(new byte[] {0x00, 0x01, 0x02, 0x03}, "00010203"), + Arguments.of(new byte[] {(byte) 0xFF, (byte) 0xFE, (byte) 0xFD, (byte) 0xFC}, "fffefdfc"), + Arguments.of(new byte[0], "") + ); + } + + @ParameterizedTest + @MethodSource("testBytes") + void convertsBytesIntoHex(byte[] bytes, String expected) { + assertThat(Hex.fromBytes(bytes)).isEqualTo(expected); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/matomo/java/tracking/parameters/UniqueIdTest.java b/core/src/test/java/org/matomo/java/tracking/parameters/UniqueIdTest.java new file mode 100644 index 00000000..00d1bca9 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/parameters/UniqueIdTest.java @@ -0,0 +1,28 @@ +package org.matomo.java.tracking.parameters; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class UniqueIdTest { + + @Test + void createsRandomUniqueId() { + + UniqueId uniqueId = UniqueId.random(); + + assertThat(uniqueId.toString()).matches("[0-9a-zA-Z]{6}"); + + } + + @Test + void createsSameUniqueIds() { + + UniqueId uniqueId1 = UniqueId.fromValue(868686868L); + UniqueId uniqueId2 = UniqueId.fromValue(868686868); + + assertThat(uniqueId1).hasToString(uniqueId2.toString()); + + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/parameters/VisitorIdTest.java b/core/src/test/java/org/matomo/java/tracking/parameters/VisitorIdTest.java new file mode 100644 index 00000000..45aa34de --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/parameters/VisitorIdTest.java @@ -0,0 +1,189 @@ +package org.matomo.java.tracking.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class VisitorIdTest { + + private static Stream validHexStrings() { + return Stream.of( + Arguments.of("0", "0000000000000000"), + Arguments.of("0000", "0000000000000000"), + Arguments.of("1", "0000000000000001"), + Arguments.of("a", "000000000000000a"), + Arguments.of("1a", "000000000000001a"), + Arguments.of("01a", "000000000000001a"), + Arguments.of("1a2b", "0000000000001a2b"), + Arguments.of("1a2b3c", "00000000001a2b3c"), + Arguments.of("1a2b3c4d", "000000001a2b3c4d"), + Arguments.of("1a2b3c4d5e", "0000001a2b3c4d5e"), + Arguments.of("1A2B3C4D5E", "0000001a2b3c4d5e"), + Arguments.of("1a2b3c4d5e6f", "00001a2b3c4d5e6f"), + Arguments.of("1a2b3c4d5e6f7a", "001a2b3c4d5e6f7a") + ); + } + + @Test + void hasCorrectFormat() { + + VisitorId visitorId = VisitorId.random(); + + assertThat(visitorId.toString()).matches("^[a-z0-9]{16}$"); + + } + + @Test + void createsRandomVisitorId() { + + VisitorId first = VisitorId.random(); + VisitorId second = VisitorId.random(); + + assertThat(first).doesNotHaveToString(second.toString()); + + } + + @Test + void fixedVisitorIdForLongHash() { + + VisitorId visitorId = VisitorId.fromHash(987654321098765432L); + + assertThat(visitorId).hasToString("0db4da5f49f8b478"); + + } + + @Test + void fixedVisitorIdForIntHash() { + + VisitorId visitorId = VisitorId.fromHash(777777777); + + assertThat(visitorId).hasToString("000000002e5bf271"); + + } + + @Test + void sameVisitorIdForSameHash() { + + VisitorId first = VisitorId.fromHash(1234567890L); + VisitorId second = VisitorId.fromHash(1234567890); + + assertThat(first).hasToString(second.toString()); + + } + + @Test + void alwaysTheSameToString() { + + VisitorId visitorId = VisitorId.random(); + + assertThat(visitorId).hasToString(visitorId.toString()); + + } + + @Test + void createsVisitorIdFrom16CharacterHex() { + + VisitorId visitorId = VisitorId.fromHex("1234567890abcdef"); + + assertThat(visitorId).hasToString("1234567890abcdef"); + + } + + @Test + void createsVisitorIdFrom1CharacterHex() { + + VisitorId visitorId = VisitorId.fromHex("a"); + + assertThat(visitorId).hasToString("000000000000000a"); + + } + + @Test + void createsVisitorIdFrom2CharacterHex() { + + VisitorId visitorId = VisitorId.fromHex("12"); + + assertThat(visitorId).hasToString("0000000000000012"); + + } + + @Test + void failsOnInvalidHexString() { + + assertThatThrownBy(() -> VisitorId.fromHex("invalid123456789")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Input must be a valid hex string"); + + } + + @ParameterizedTest + @ValueSource(strings = + {"g", "gh", "ghi", "ghij", "ghijk", "ghijkl", "ghijklm", "ghijklmn", "ghijklmn", "-1"}) + void failsOnInvalidHexString(String hex) { + assertThatThrownBy(() -> VisitorId.fromHex(hex)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Input must be a valid hex string"); + } + + @ParameterizedTest + @MethodSource("validHexStrings") + void createsVisitorIdFromHex( + String hex, String expected + ) { + + VisitorId visitorId = VisitorId.fromHex(hex); + + assertThat(visitorId).hasToString(expected); + + } + + @ParameterizedTest + @ValueSource(strings = {"", " "}) + void failsOnEmptyStrings(String hex) { + assertThatThrownBy(() -> VisitorId.fromHex(hex)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Hex string must not be null or empty"); + } + + @ParameterizedTest + @ValueSource(strings = {"1234567890abcdefg", "1234567890abcdeff"}) + void failsOnInvalidHexStringLength(String hex) { + assertThatThrownBy(() -> VisitorId.fromHex(hex)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Hex string must not be longer than 16 characters"); + } + + @Test + void createsVisitorIdFromUUID() { + + VisitorId visitorId = VisitorId.fromUUID( + java.util.UUID.fromString("12345678-90ab-cdef-1234-567890abcdef") + ); + + assertThat(visitorId).hasToString("1234567890abcdef"); + + } + + @Test + void failsOnNullUUID() { + assertThatThrownBy(() -> VisitorId.fromUUID(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("uuid is marked non-null but is null"); + } + + @Test + void createsVisitorIdFromString() { + + VisitorId visitorId = VisitorId.fromString("test"); + + assertThat(visitorId).hasToString("0000000000364492"); + + } + +} diff --git a/core/src/test/java/org/matomo/java/tracking/servlet/ServletMatomoRequestTest.java b/core/src/test/java/org/matomo/java/tracking/servlet/ServletMatomoRequestTest.java new file mode 100644 index 00000000..8f11d935 --- /dev/null +++ b/core/src/test/java/org/matomo/java/tracking/servlet/ServletMatomoRequestTest.java @@ -0,0 +1,201 @@ +package org.matomo.java.tracking.servlet; + +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.matomo.java.tracking.MatomoRequest; + +class ServletMatomoRequestTest { + + private MatomoRequest.MatomoRequestBuilder requestBuilder; + + private HttpServletRequestWrapper.HttpServletRequestWrapperBuilder wrapperBuilder = + HttpServletRequestWrapper.builder(); + + @Test + void addsServletRequestHeaders() { + + wrapperBuilder + .headers(singletonMap("headername", "headerValue")) + .build(); + + whenBuildsRequest(); + + MatomoRequest matomoRequest = requestBuilder.build(); + assertThat(matomoRequest.getHeaders()).hasSize(1).containsEntry("headername", "headerValue"); + } + + @Test + void skipsEmptyHeaderNames() { + + wrapperBuilder + .headers(singletonMap("", "headerValue")) + .build(); + + whenBuildsRequest(); + + MatomoRequest matomoRequest = requestBuilder.build(); + assertThat(matomoRequest.getHeaders()).isEmpty(); + + } + + @Test + void skipsBlankHeaderNames() { + + wrapperBuilder + .headers(singletonMap(" ", "headerValue")) + .build(); + + whenBuildsRequest(); + + MatomoRequest matomoRequest = requestBuilder.build(); + assertThat(matomoRequest.getHeaders()).isEmpty(); + + } + + @ParameterizedTest + @ValueSource(strings = {"connection", "content-length", "expect", "host", "upgrade"}) + void doesNotAddRestrictedHeaders(String restrictedHeader) { + wrapperBuilder + .headers(singletonMap(restrictedHeader, "headerValue")) + .build(); + + whenBuildsRequest(); + + MatomoRequest matomoRequest = requestBuilder.build(); + assertThat(matomoRequest.getHeaders()).isEmpty(); + } + + @Test + void failsIfServletRequestIsNull() { + assertThatThrownBy(() -> ServletMatomoRequest.fromServletRequest(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("request is marked non-null but is null"); + } + + @Test + void failsIfBuilderIsNull() { + assertThatThrownBy(() -> ServletMatomoRequest.addServletRequestHeaders( + null, + HttpServletRequestWrapper.builder().build() + )) + .isInstanceOf(NullPointerException.class) + .hasMessage("builder is marked non-null but is null"); + } + + @Test + void extractsVisitorIdFromCookie() { + wrapperBuilder + .cookies(new CookieWrapper[] { + new CookieWrapper("_pk_id.1.1fff", "be40d677d6c7270b.1699801331.") + }) + .build(); + + whenBuildsRequest(); + + MatomoRequest matomoRequest = requestBuilder.build(); + assertThat(matomoRequest.getVisitorId()).hasToString("be40d677d6c7270b"); + assertThat(matomoRequest.getCookies()) + .hasSize(1) + .containsEntry("_pk_id.1.1fff", "be40d677d6c7270b.1699801331."); + } + + @ParameterizedTest + @ValueSource( + strings = {"_pk_ses.1.1fff", "_pk_ref.1.1fff", "_pk_hsr.1.1fff"} + ) + void extractsMatomoCookies(String cookieName) { + wrapperBuilder + .cookies(new CookieWrapper[] {new CookieWrapper(cookieName, "anything")}) + .build(); + + whenBuildsRequest(); + + MatomoRequest matomoRequest = requestBuilder.build(); + assertThat(matomoRequest.getCookies()).hasSize(1).containsEntry(cookieName, "anything"); + } + + @Test + void extractsSessionIdFromMatomoSessIdCookie() { + wrapperBuilder + .cookies(new CookieWrapper[] { + new CookieWrapper( + "MATOMO_SESSID", + "2cbf8b5ba00fbf9ba70853308cd0944a" + ) + }) + .build(); + + whenBuildsRequest(); + + MatomoRequest matomoRequest = requestBuilder.build(); + assertThat(matomoRequest.getSessionId()).isEqualTo("2cbf8b5ba00fbf9ba70853308cd0944a"); + } + + @Test + void parsesVisitCustomVariablesFromCookie() { + wrapperBuilder + .cookies(new CookieWrapper[] { + new CookieWrapper( + "_pk_cvar.1.1fff", + "{\"1\":[\"VAR 1 set, var 2 not set\",\"yes\"],\"3\":[\"var 3 set\",\"yes!!!!\"]}" + ) + }) + .build(); + + whenBuildsRequest(); + + MatomoRequest matomoRequest = requestBuilder.build(); + assertThat(matomoRequest.getVisitCustomVariables().get(1).getKey()).isEqualTo( + "VAR 1 set, var 2 not set"); + assertThat(matomoRequest.getVisitCustomVariables().get(1).getValue()).isEqualTo("yes"); + assertThat(matomoRequest.getVisitCustomVariables().get(3).getKey()).isEqualTo("var 3 set"); + assertThat(matomoRequest.getVisitCustomVariables().get(3).getValue()).isEqualTo("yes!!!!"); + + } + + @Test + void determinerVisitorIpFromXForwardedForHeader() { + wrapperBuilder + .headers(singletonMap("x-forwarded-for", "44.55.66.77")) + .build(); + + whenBuildsRequest(); + + MatomoRequest matomoRequest = requestBuilder.build(); + assertThat(matomoRequest.getVisitorIp()).isEqualTo("44.55.66.77"); + } + + @Test + void setsActionUrlFromRequestURL() { + wrapperBuilder + .requestURL(new StringBuffer("https://localhost/test")) + .build(); + + whenBuildsRequest(); + + MatomoRequest matomoRequest = requestBuilder.build(); + assertThat(matomoRequest.getActionUrl()).isEqualTo("https://localhost/test"); + } + + @Test + void setsUserIdFromRemoteUser() { + wrapperBuilder + .remoteUser("remote-user") + .build(); + + whenBuildsRequest(); + + MatomoRequest matomoRequest = requestBuilder.build(); + assertThat(matomoRequest.getUserId()).isEqualTo("remote-user"); + } + + private void whenBuildsRequest() { + requestBuilder = ServletMatomoRequest.fromServletRequest(wrapperBuilder.build()); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/piwik/java/tracking/CustomVariableTest.java b/core/src/test/java/org/piwik/java/tracking/CustomVariableTest.java new file mode 100644 index 00000000..4492d0a2 --- /dev/null +++ b/core/src/test/java/org/piwik/java/tracking/CustomVariableTest.java @@ -0,0 +1,17 @@ +package org.piwik.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +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"); + } + +} diff --git a/core/src/test/java/org/piwik/java/tracking/EcommerceItemTest.java b/core/src/test/java/org/piwik/java/tracking/EcommerceItemTest.java new file mode 100644 index 00000000..78a311f5 --- /dev/null +++ b/core/src/test/java/org/piwik/java/tracking/EcommerceItemTest.java @@ -0,0 +1,19 @@ +package org.piwik.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class EcommerceItemTest { + + @Test + void createsEcItem() { + EcommerceItem item = new EcommerceItem("sku", "name", "category", 1.0, 1); + + assertThat(item.getSku()).isEqualTo("sku"); + assertThat(item.getName()).isEqualTo("name"); + assertThat(item.getCategory()).isEqualTo("category"); + assertThat(item.getPrice()).isEqualTo(1.0); + assertThat(item.getQuantity()).isEqualTo(1); + } +} diff --git a/core/src/test/java/org/piwik/java/tracking/PiwikTrackerIT.java b/core/src/test/java/org/piwik/java/tracking/PiwikTrackerIT.java new file mode 100644 index 00000000..c4ce73e6 --- /dev/null +++ b/core/src/test/java/org/piwik/java/tracking/PiwikTrackerIT.java @@ -0,0 +1,47 @@ +package org.piwik.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class PiwikTrackerIT { + + private PiwikTracker piwikTracker; + + @Test + void createsNewPiwikTrackerInstanceWithHostUrl() { + + piwikTracker = new PiwikTracker("http://localhost:8080"); + + assertThat(piwikTracker).isNotNull(); + + } + + @Test + void createsNewPiwikTrackerInstanceWithHostUrlAndTimeout() { + + piwikTracker = new PiwikTracker("http://localhost:8080", 1000); + + assertThat(piwikTracker).isNotNull(); + + } + + @Test + void createsNewPiwikTrackerInstanceWithHostUrlAndProxySettings() { + + piwikTracker = new PiwikTracker("http://localhost:8080", "localhost", 8080); + + assertThat(piwikTracker).isNotNull(); + + } + + @Test + void createsNewPiwikTrackerInstanceWithHostUrlAndProxySettingsAndTimeout() { + + piwikTracker = new PiwikTracker("http://localhost:8080", "localhost", 8080, 1000); + + assertThat(piwikTracker).isNotNull(); + + } + +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..4064bcf8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.8' +services: + database: + image: mariadb:10.11.5-jammy + command: --max-allowed-packet=64MB + environment: + - MYSQL_ROOT_PASSWORD=matomo + - MYSQL_PASSWORD=matomo + - MYSQL_DATABASE=matomo + - MYSQL_USER=matomo + matomo: + image: matomo:4.15.1-apache + environment: + - MATOMO_DATABASE_HOST=database + - MATOMO_DATABASE_ADAPTER=mysql + - MATOMO_DATABASE_TABLES_PREFIX=matomo_ + - MATOMO_DATABASE_USERNAME=matomo + - MATOMO_DATABASE_PASSWORD=matomo + - MATOMO_DATABASE_DBNAME=matomo + ports: + - '8080:80' diff --git a/java11/pom.xml b/java11/pom.xml new file mode 100644 index 00000000..fb03eedf --- /dev/null +++ b/java11/pom.xml @@ -0,0 +1,75 @@ + + 4.0.0 + + + org.piwik.java.tracking + matomo-java-tracker-parent + 3.4.1-SNAPSHOT + ../pom.xml + + + matomo-java-tracker-java11 + 3.4.1-SNAPSHOT + jar + + Matomo Java Tracker Java 11 + Official Java implementation of the Matomo Tracking HTTP API for Java 11. + + + 11 + 11 + + + + + org.piwik.java.tracking + matomo-java-tracker-core + ${project.version} + + + org.projectlombok + lombok + provided + + + com.github.spotbugs + spotbugs-annotations + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + org.wiremock + wiremock + 3.13.2 + test + + + org.slf4j + slf4j-simple + test + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + true + + + + + + + diff --git a/java11/src/main/java/org/matomo/java/tracking/Java11Sender.java b/java11/src/main/java/org/matomo/java/tracking/Java11Sender.java new file mode 100644 index 00000000..abb6181f --- /dev/null +++ b/java11/src/main/java/org/matomo/java/tracking/Java11Sender.java @@ -0,0 +1,244 @@ +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.net.CookieStore; +import java.net.HttpCookie; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * A {@link Sender} implementation that uses the Java 11 HTTP client. + */ +@RequiredArgsConstructor +@Slf4j +public class Java11Sender implements Sender { + + @lombok.NonNull + private final TrackerConfiguration trackerConfiguration; + + @lombok.NonNull + private final QueryCreator queryCreator; + + @lombok.NonNull + private final HttpClient httpClient; + + @lombok.NonNull + private final CookieStore cookieStore; + + @lombok.NonNull + private final ExecutorService executorService; + + @NonNull + @Override + public CompletableFuture sendSingleAsync( + @NonNull @lombok.NonNull MatomoRequest request + ) { + return sendAsyncAndCheckResponse(buildHttpGetRequest(request), request); + } + + @Override + public void sendSingle(@NonNull @lombok.NonNull MatomoRequest request) { + sendAndCheckResponse(buildHttpGetRequest(request)); + } + + private void sendAndCheckResponse(@NonNull HttpRequest httpRequest) { + checkResponse( + send( + httpRequest, + () -> httpClient.send(httpRequest, HttpResponse.BodyHandlers.discarding()) + ), + httpRequest + ); + } + + @Override + public void sendBulk( + @NonNull @lombok.NonNull Iterable requests, + @Nullable String overrideAuthToken + ) { + sendAndCheckResponse(buildHttpPostRequest(requests, overrideAuthToken)); + } + + @NonNull + private HttpRequest buildHttpPostRequest( + @NonNull Iterable requests, @Nullable String overrideAuthToken + ) { + String authToken = + AuthToken.determineAuthToken(overrideAuthToken, requests, trackerConfiguration); + Collection queries = new ArrayList<>(); + Map headers = new LinkedHashMap<>(10); + String headerUserAgent = null; + cookieStore.removeAll(); + for (MatomoRequest request : requests) { + RequestValidator.validate(request, authToken); + if (request.getHeaders() != null && !request.getHeaders().isEmpty()) { + headers.putAll(request.getHeaders()); + } + if (request.getHeaderUserAgent() != null && !request.getHeaderUserAgent().trim().isEmpty()) { + headerUserAgent = request.getHeaderUserAgent(); + } + queries.add(queryCreator.createQuery(request, null)); + addCookies(request); + } + HttpRequest.Builder builder = HttpRequest + .newBuilder() + .uri(trackerConfiguration.getApiEndpoint()) + .header("Accept", "*/*") + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofByteArray(BulkRequest + .builder() + .queries(queries) + .authToken(authToken) + .build() + .toBytes())); + applyTrackerConfiguration(builder); + setUserAgentHeader(builder, headerUserAgent, headers); + addHeaders(builder, headers); + return builder.build(); + } + + @NonNull + @Override + public CompletableFuture sendBulkAsync( + @NonNull @lombok.NonNull Collection requests, + @Nullable String overrideAuthToken + ) { + return sendAsyncAndCheckResponse(buildHttpPostRequest(requests, overrideAuthToken), null); + } + + @NonNull + private CompletableFuture sendAsyncAndCheckResponse( + @NonNull HttpRequest httpRequest, @Nullable T result + ) { + return send( + httpRequest, + () -> httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding()) + .thenApply(response -> { + checkResponse(response, httpRequest); + return result; + }) + ); + } + + @NonNull + private HttpRequest buildHttpGetRequest(@NonNull MatomoRequest request) { + String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration); + RequestValidator.validate(request, authToken); + cookieStore.removeAll(); + addCookies(request); + URI apiEndpoint = trackerConfiguration.getApiEndpoint(); + HttpRequest.Builder builder = HttpRequest + .newBuilder() + .uri(apiEndpoint.resolve(String.format( + "%s?%s", + apiEndpoint.getPath(), + queryCreator.createQuery(request, authToken) + ))); + applyTrackerConfiguration(builder); + setUserAgentHeader(builder, request.getHeaderUserAgent(), request.getHeaders()); + addHeaders(builder, request.getHeaders()); + return builder.build(); + } + + private T send( + @NonNull HttpRequest httpRequest, @NonNull Callable callable + ) { + try { + log.debug("Sending request to Matomo: {}", httpRequest); + log.debug("Headers: {}", httpRequest.headers()); + log.debug("Cookies: {}", cookieStore.getCookies()); + return callable.call(); + } catch (Exception e) { + if (trackerConfiguration.isLogFailedTracking()) { + log.error("Could not send request to Matomo: {}", httpRequest.uri(), e); + } + throw new MatomoException("Could not send request to Matomo", e); + } + } + + private void checkResponse( + @NonNull HttpResponse response, + @NonNull HttpRequest httpRequest + ) { + if (response.statusCode() > 399) { + if (trackerConfiguration.isLogFailedTracking()) { + log.error( + "Received HTTP error code {} for URL {}", + response.statusCode(), + httpRequest.uri() + ); + } + throw new MatomoException(String.format( + "Tracking endpoint responded with code %d", + response.statusCode() + )); + } + } + + private void addCookies(MatomoRequest request) { + if (request.getSessionId() != null && !request.getSessionId().isEmpty()) { + cookieStore.add(null, new HttpCookie("MATOMO_SESSID", request.getSessionId())); + } + if (request.getCookies() != null) { + for (Map.Entry entry : request.getCookies().entrySet()) { + cookieStore.add(null, new HttpCookie(entry.getKey(), entry.getValue())); + } + } + } + + private void applyTrackerConfiguration(@NonNull HttpRequest.Builder builder) { + if (trackerConfiguration.getSocketTimeout() != null + && trackerConfiguration.getSocketTimeout().toMillis() > 0L) { + builder.timeout(trackerConfiguration.getSocketTimeout()); + } + } + + private void setUserAgentHeader( + HttpRequest.Builder builder, + @Nullable String headerUserAgent, + @Nullable Map headers + ) { + String userAgentHeader = null; + if ((headerUserAgent == null || headerUserAgent.trim().isEmpty()) && headers != null) { + TreeMap caseInsensitiveMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + caseInsensitiveMap.putAll(headers); + userAgentHeader = caseInsensitiveMap.get("User-Agent"); + } + if ((userAgentHeader == null || userAgentHeader.trim().isEmpty()) && ( + headerUserAgent == null || headerUserAgent.trim().isEmpty()) + && trackerConfiguration.getUserAgent() != null && !trackerConfiguration.getUserAgent().isEmpty()) { + builder.header("User-Agent", trackerConfiguration.getUserAgent()); + } + } + + private void addHeaders( + @NonNull HttpRequest.Builder builder, + @Nullable Map headers + ) { + if (headers != null) { + for (Map.Entry header : headers.entrySet()) { + builder.header(header.getKey(), header.getValue()); + } + } + } + + @Override + public void close() { + ExecutorServiceCloser.close(executorService); + } +} diff --git a/java11/src/main/java/org/matomo/java/tracking/Java11SenderProvider.java b/java11/src/main/java/org/matomo/java/tracking/Java11SenderProvider.java new file mode 100644 index 00000000..d6145198 --- /dev/null +++ b/java11/src/main/java/org/matomo/java/tracking/Java11SenderProvider.java @@ -0,0 +1,79 @@ +package org.matomo.java.tracking; + +import edu.umd.cs.findbugs.annotations.Nullable; +import java.net.CookieManager; +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.security.SecureRandom; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +/** + * Provides a {@link Sender} implementation based on Java 11. + */ +public class Java11SenderProvider implements SenderProvider { + + private static final TrustManager[] TRUST_ALL_MANAGERS = {new TrustingX509TrustManager()}; + + @Override + public Sender provideSender( + TrackerConfiguration trackerConfiguration, QueryCreator queryCreator + ) { + CookieManager cookieManager = new CookieManager(); + ExecutorService executorService = Executors.newFixedThreadPool( + trackerConfiguration.getThreadPoolSize(), + new DaemonThreadFactory() + ); + HttpClient.Builder builder = HttpClient + .newBuilder() + .cookieHandler(cookieManager) + .executor(executorService); + if (trackerConfiguration.getConnectTimeout() != null + && trackerConfiguration.getConnectTimeout().toMillis() > 0L) { + builder.connectTimeout(trackerConfiguration.getConnectTimeout()); + } + if (!isEmpty(trackerConfiguration.getProxyHost()) && trackerConfiguration.getProxyPort() > 0) { + builder.proxy(ProxySelector.of(new InetSocketAddress( + trackerConfiguration.getProxyHost(), + trackerConfiguration.getProxyPort() + ))); + if (!isEmpty(trackerConfiguration.getProxyUsername()) + && !isEmpty(trackerConfiguration.getProxyPassword())) { + builder.authenticator(new ProxyAuthenticator( + trackerConfiguration.getProxyUsername(), + trackerConfiguration.getProxyPassword() + )); + } + } + if (trackerConfiguration.isDisableSslCertValidation()) { + try { + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, TRUST_ALL_MANAGERS, new SecureRandom()); + builder.sslContext(sslContext); + } catch (Exception e) { + throw new MatomoException("Could not disable SSL certification validation", e); + } + } + if (trackerConfiguration.isDisableSslHostVerification()) { + throw new MatomoException("Please disable SSL hostname verification manually using the system parameter -Djdk.internal.httpclient.disableHostnameVerification=true"); + } + + return new Java11Sender( + trackerConfiguration, + queryCreator, + builder.build(), + cookieManager.getCookieStore(), + executorService + ); + } + + private static boolean isEmpty( + @Nullable String str + ) { + return str == null || str.isEmpty() || str.trim().isEmpty(); + } + +} diff --git a/java11/src/main/resources/META-INF/services/org.matomo.java.tracking.SenderProvider b/java11/src/main/resources/META-INF/services/org.matomo.java.tracking.SenderProvider new file mode 100644 index 00000000..55c8186e --- /dev/null +++ b/java11/src/main/resources/META-INF/services/org.matomo.java.tracking.SenderProvider @@ -0,0 +1 @@ +org.matomo.java.tracking.Java11SenderProvider \ No newline at end of file diff --git a/java11/src/test/java/org/matomo/java/tracking/Java11SenderIT.java b/java11/src/test/java/org/matomo/java/tracking/Java11SenderIT.java new file mode 100644 index 00000000..6ce1a63b --- /dev/null +++ b/java11/src/test/java/org/matomo/java/tracking/Java11SenderIT.java @@ -0,0 +1,396 @@ +package org.matomo.java.tracking; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.status; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import java.net.CookieManager; +import java.net.URI; +import java.net.http.HttpClient; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@WireMockTest(httpsEnabled = true) +class Java11SenderIT { + + private Sender sender; + + private TrackerConfiguration trackerConfiguration; + + @BeforeEach + void disableSslHostnameVerification() { + System.setProperty("jdk.internal.httpclient.disableHostnameVerification", "true"); + } + + @Test + void failsIfTrackerConfigurationIsNotSet() { + CookieManager cookieManager = new CookieManager(); + assertThatThrownBy(() -> new Java11Sender( + null, + new QueryCreator(TrackerConfiguration.builder() + .apiEndpoint(URI.create("http://localhost")) + .build()), + HttpClient.newBuilder().cookieHandler(cookieManager).build(), + cookieManager.getCookieStore(), + Executors.newFixedThreadPool(2, new DaemonThreadFactory()) + )).isInstanceOf(NullPointerException.class) + .hasMessage("trackerConfiguration is marked non-null but is null"); + } + + @Test + void failsIfQueryCreatorIsNotSet() { + CookieManager cookieManager = new CookieManager(); + assertThatThrownBy(() -> new Java11Sender( + TrackerConfiguration.builder().apiEndpoint(URI.create("http://localhost")).build(), + null, + HttpClient.newBuilder().cookieHandler(cookieManager).build(), + cookieManager.getCookieStore(), + Executors.newFixedThreadPool(2, new DaemonThreadFactory()) + )).isInstanceOf(NullPointerException.class) + .hasMessage("queryCreator is marked non-null but is null"); + } + + @Test + void failsIfHttpClientIsNotSet() { + CookieManager cookieManager = new CookieManager(); + assertThatThrownBy(() -> new Java11Sender( + TrackerConfiguration.builder().apiEndpoint(URI.create("http://localhost")).build(), + new QueryCreator(TrackerConfiguration.builder() + .apiEndpoint(URI.create("http://localhost")) + .build()), + null, + cookieManager.getCookieStore(), + Executors.newFixedThreadPool(2, new DaemonThreadFactory()) + )).isInstanceOf(NullPointerException.class) + .hasMessage("httpClient is marked non-null but is null"); + } + + @Test + void failsIfCookieStoreIsNotSet() { + CookieManager cookieManager = new CookieManager(); + assertThatThrownBy(() -> new Java11Sender( + TrackerConfiguration.builder().apiEndpoint(URI.create("http://localhost")).build(), + new QueryCreator(TrackerConfiguration.builder() + .apiEndpoint(URI.create("http://localhost")) + .build()), + HttpClient.newBuilder().cookieHandler(cookieManager).build(), + null, + Executors.newFixedThreadPool(2, new DaemonThreadFactory()) + )).isInstanceOf(NullPointerException.class) + .hasMessage("cookieStore is marked non-null but is null"); + } + + @Test + void sendSingleFailsIfQueryIsMalformedWithSocketTimeout() { + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create("telnet://localhost")) + .socketTimeout(Duration.ofSeconds(30L)) + .build(); + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(MatomoRequests.ecommerceCartUpdate(50.0) + .goalId(0).build())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("invalid URI scheme telnet"); + } + + @Test + void sendSingleFailsIfQueryIsMalformedWithoutSocketTimeout() { + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create("telnet://localhost")) + .build(); + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("invalid URI scheme telnet"); + } + + private void givenSender() { + sender = new Java11SenderProvider().provideSender( + trackerConfiguration, + new QueryCreator(trackerConfiguration) + ); + } + + @Test + void failsIfEndpointReturnsNotFound(WireMockRuntimeInfo wireMockRuntimeInfo) { + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(wireMockRuntimeInfo.getHttpBaseUrl())) + .disableSslCertValidation(true) + .socketTimeout(Duration.ofSeconds(0L)) + .build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(MatomoException.class) + .hasMessage("Tracking endpoint responded with code 404"); + } + + @Test + void failsAndLogsIfCouldNotConnectToEndpoint() { + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create("http://localhost:1234")) + .userAgent("") + .logFailedTracking(true) + .build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not send request to Matomo"); + } + + @Test + void failsAndDoesNotLogIfCouldNotConnectToEndpoint() { + trackerConfiguration = + TrackerConfiguration.builder() + .apiEndpoint(URI.create("http://localhost:1234")) + .userAgent("") + .build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not send request to Matomo"); + } + + @Test + void connectsViaProxy(WireMockRuntimeInfo wireMockRuntimeInfo) { + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(wireMockRuntimeInfo.getHttpBaseUrl())) + .disableSslCertValidation(true) + .proxyHost("localhost") + .proxyPort(wireMockRuntimeInfo.getHttpPort()) + .userAgent(null) + .build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(MatomoException.class) + .hasMessage("Tracking endpoint responded with code 400"); + } + + @Test + void connectsViaProxyWithProxyUserNameAndPassword(WireMockRuntimeInfo wireMockRuntimeInfo) { + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(wireMockRuntimeInfo.getHttpBaseUrl())) + .disableSslCertValidation(true) + .proxyHost("localhost") + .proxyPort(wireMockRuntimeInfo.getHttpPort()) + .proxyUsername("user") + .proxyPassword("password") + .build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(MatomoException.class) + .hasMessage("Tracking endpoint responded with code 400"); + } + + @Test + void logsFailedTracking(WireMockRuntimeInfo wireMockRuntimeInfo) { + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(wireMockRuntimeInfo.getHttpsBaseUrl())) + .disableSslCertValidation(true) + .logFailedTracking(true) + .build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(MatomoException.class) + .hasMessage("Tracking endpoint responded with code 404"); + } + + @Test + void skipSslCertificationValidation(WireMockRuntimeInfo wireMockRuntimeInfo) { + stubFor(get(urlPathEqualTo("/matomo_ssl.php")).willReturn(status(204))); + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(String.format( + "https://localhost:%d/matomo_ssl.php", + wireMockRuntimeInfo.getHttpsPort() + ))) + .disableSslCertValidation(true) + .build(); + + givenSender(); + + sender.sendSingle(MatomoRequests.goal(2, 60.0).build()); + + verify(getRequestedFor(urlPathEqualTo("/matomo_ssl.php"))); + + } + + @Test + void addsHeadersToSingleRequest(WireMockRuntimeInfo wireMockRuntimeInfo) { + stubFor(get(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(wireMockRuntimeInfo.getHttpBaseUrl() + "/matomo.php")) + .build(); + + givenSender(); + + sender.sendSingle(MatomoRequests.ping() + .headers(singletonMap("headerName", "headerValue")) + .build()); + + verify(getRequestedFor(urlPathEqualTo("/matomo.php")).withHeader( + "headerName", + equalTo("headerValue") + )); + } + + @Test + void addsHeadersToBulkRequest(WireMockRuntimeInfo wireMockRuntimeInfo) { + stubFor(post(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(wireMockRuntimeInfo.getHttpBaseUrl() + "/matomo.php")) + .build(); + + givenSender(); + + sender.sendBulk(List.of(MatomoRequests.goal(1, 23.50).headers(singletonMap( + "headerName", + "headerValue" + )).build()), null); + + verify(postRequestedFor(urlPathEqualTo("/matomo.php")).withHeader( + "headerName", + equalTo("headerValue") + )); + } + + @Test + void doesNotAddEmptyHeaders(WireMockRuntimeInfo wireMockRuntimeInfo) { + stubFor(post(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(wireMockRuntimeInfo.getHttpBaseUrl() + "/matomo.php")) + .build(); + + givenSender(); + + sender.sendBulk(List.of(MatomoRequests.pageView("Contact").headers(emptyMap()).build()), null); + + verify(postRequestedFor(urlPathEqualTo("/matomo.php")).withoutHeader("headerName")); + } + + @Test + void addsHeadersToBulkAsyncRequest(WireMockRuntimeInfo wireMockRuntimeInfo) { + stubFor(post(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(wireMockRuntimeInfo.getHttpBaseUrl() + "/matomo.php")) + .build(); + + givenSender(); + + CompletableFuture future = sender.sendBulkAsync(List.of(MatomoRequest + .request() + .headers(singletonMap("headerName", "headerValue")) + .build()), null); + + future.join(); + verify(postRequestedFor(urlPathEqualTo("/matomo.php")).withHeader( + "headerName", + equalTo("headerValue") + )); + } + + @Test + void failsOnSendSingleAsyncIfRequestIsNull() { + trackerConfiguration = + TrackerConfiguration.builder().apiEndpoint(URI.create("http://localhost:1234")).build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingleAsync(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("request is marked non-null but is null"); + } + + @Test + void failsOnSendSingleIfRequestIsNull() { + trackerConfiguration = + TrackerConfiguration.builder().apiEndpoint(URI.create("http://localhost:1234")).build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("request is marked non-null but is null"); + } + + @Test + void failsOnSendBulkAsyncIfRequestsIsNull() { + trackerConfiguration = + TrackerConfiguration.builder().apiEndpoint(URI.create("http://localhost:1234")).build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendBulkAsync(null, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("requests is marked non-null but is null"); + } + + @Test + void failsOnSendBulkAsyncIfRequestIsNull() { + trackerConfiguration = + TrackerConfiguration.builder().apiEndpoint(URI.create("http://localhost:1234")).build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendBulk(null, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("requests is marked non-null but is null"); + } + + @Test + void failsOnSendBulkAsyncIfOverrideAuthTokenIsMalformed() { + trackerConfiguration = + TrackerConfiguration.builder().apiEndpoint(URI.create("http://localhost:1234")).build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendBulkAsync( + List.of(MatomoRequests + .siteSearch("Special offers", "Products", 5L).build()), + "telnet://localhost" + )) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + } + + +} diff --git a/java11/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java b/java11/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java new file mode 100644 index 00000000..734d69e0 --- /dev/null +++ b/java11/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java @@ -0,0 +1,501 @@ +package org.matomo.java.tracking; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.resetAllRequests; +import static com.github.tomakehurst.wiremock.client.WireMock.status; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static java.util.Collections.singleton; +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import java.net.URI; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Locale.LanguageRange; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.MatomoRequest.MatomoRequestBuilder; +import org.matomo.java.tracking.TrackerConfiguration.TrackerConfigurationBuilder; +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; + +@WireMockTest +class MatomoTrackerIT { + + private static final int SITE_ID = 42; + + private final TrackerConfigurationBuilder trackerConfigurationBuilder = TrackerConfiguration.builder(); + + private final MatomoRequestBuilder requestBuilder = MatomoRequest + .builder() + .visitorId(VisitorId.fromHex("bbccddeeff1122")) + .randomValue(RandomValue.fromString("someRandom")); + + private CompletableFuture future; + + private MatomoTracker matomoTracker; + + @BeforeEach + void givenStub() { + resetAllRequests(); + stubFor(post(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + stubFor(get(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + } + + @Test + void requiresSiteId(WireMockRuntimeInfo wireMockRuntimeInfo) { + + trackerConfigurationBuilder.apiEndpoint(URI.create(wireMockRuntimeInfo.getHttpBaseUrl() + "/matomo.php")).build(); + + assertThatThrownBy(this::whenSendsRequestAsync) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("No default site ID and no request site ID is given"); + + } + + private void whenSendsRequestAsync() { + matomoTracker = new MatomoTracker(trackerConfigurationBuilder.build()); + future = matomoTracker.sendRequestAsync(requestBuilder.build()); + } + + @Test + void usesDefaultSiteId(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + + whenSendsRequestAsync(); + + thenGetsRequest("idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom"); + + } + + private void givenTrackerConfigurationWithDefaultSiteId(WireMockRuntimeInfo wireMockRuntimeInfo) { + trackerConfigurationBuilder + .apiEndpoint(URI.create(wireMockRuntimeInfo.getHttpBaseUrl() + "/matomo.php")) + .defaultSiteId(SITE_ID); + } + + private void thenGetsRequest(String expectedQuery) { + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(getRequestedFor(urlEqualTo(String.format("/matomo.php?%s", expectedQuery))).withHeader("User-Agent", + equalTo("MatomoJavaClient") + )); + }); + } + + @Test + void overridesDefaultSiteId(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + requestBuilder.siteId(123); + + whenSendsRequestAsync(); + + thenGetsRequest("rec=1&idsite=123&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom"); + + } + + @Test + void validatesTokenAuth(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + requestBuilder.authToken("invalid-token-auth"); + + assertThatThrownBy(this::whenSendsRequestAsync) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + + } + + @Test + void convertsTrueBooleanTo1(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + requestBuilder.pluginFlash(true); + + whenSendsRequestAsync(); + + thenGetsRequest("idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&fla=1&send_image=0&rand=someRandom"); + + } + + @Test + void convertsFalseBooleanTo0(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + requestBuilder.pluginJava(false); + + whenSendsRequestAsync(); + + thenGetsRequest("idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&java=0&send_image=0&rand=someRandom"); + + } + + @Test + void encodesUrl(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + requestBuilder.actionUrl("https://www.daniel-heid.de/some/page?foo=bar"); + + whenSendsRequestAsync(); + + thenGetsRequest( + "idsite=42&rec=1&url=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fpage%3Ffoo%3Dbar&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom"); + + } + + @Test + void encodesReferrerUrl(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + requestBuilder.referrerUrl("https://www.daniel-heid.de/some/referrer?foo2=bar2"); + + whenSendsRequestAsync(); + + thenGetsRequest( + "idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&urlref=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Freferrer%3Ffoo2%3Dbar2&send_image=0&rand=someRandom"); + + } + + @Test + void encodesLink(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + requestBuilder.outlinkUrl("https://www.daniel-heid.de/some/external/link#"); + + whenSendsBulkRequestAsync(); + + thenPostsRequestWithoutAuthToken( + "idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&link=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fexternal%2Flink%23&send_image=0&rand=someRandom", + "156" + ); + + } + + @Test + void sendsRequestsBulkAsynchronously(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + matomoTracker = new MatomoTracker(trackerConfigurationBuilder.build()); + + future = matomoTracker.sendBulkRequestAsync(requestBuilder.build()); + + thenPostsRequestWithoutAuthToken("idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom", "90"); + + } + + private void whenSendsBulkRequestAsync() { + future = + new MatomoTracker(trackerConfigurationBuilder.build()).sendBulkRequestAsync(singleton(requestBuilder.build())); + } + + private void thenPostsRequestWithoutAuthToken(String expectedQuery, String contentLength) { + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo(contentLength)) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson("{\"requests\":[\"?" + expectedQuery + "\"]}"))); + }); + + } + + @Test + void encodesDownloadUrl(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + requestBuilder.downloadUrl("https://www.daniel-heid.de/some/download.pdf"); + + whenSendsBulkRequestAsync(); + + thenPostsRequestWithoutAuthToken( + "idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&download=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fdownload.pdf&send_image=0&rand=someRandom", + "154" + ); + + } + + @Test + void getContainsHeaders(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + + whenSendsRequestAsync(); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(getRequestedFor(urlPathEqualTo("/matomo.php")).withHeader("User-Agent", equalTo("MatomoJavaClient"))); + }); + + } + + @Test + void postContainsHeaders(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + + whenSendsBulkRequestAsync(); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(postRequestedFor(urlPathEqualTo("/matomo.php")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Length", equalTo("90")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient"))); + }); + + } + + @Test + void allowsToOverrideUserAgent(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + trackerConfigurationBuilder.userAgent("Mozilla/5.0"); + + whenSendsRequestAsync(); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(getRequestedFor(urlPathEqualTo("/matomo.php")).withHeader("User-Agent", equalTo("Mozilla/5.0"))); + }); + + } + + @Test + void tracksMinimalRequest(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + requestBuilder + .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"), 4) + .add(new CustomVariable("customVariable2Key", "customVariable2Value"), 5)) + .visitorVisitCount(2) + .visitorFirstVisitTimestamp(LocalDateTime.of(2022, 8, 9, 18, 34, 12).toInstant(ZoneOffset.UTC)) + .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")); + + whenSendsBulkRequestAsync(); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("711")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson("{\"requests\":[\"?" + + "idsite=42&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%224%22%3A%5B%22customVariable1Key%22%2C%22customVariable1Value%22%5D%2C%225%22%3A%5B%22customVariable2Key%22%2C%22customVariable2Value%22%5D%7D&_idvc=2&_idts=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=someRandom" + + "\"],\"token_auth\" : \"" + "fdf6e8461ea9de33176b222519627f78" + "\"}"))); + + }); + + } + + @Test + void doesNothingIfNotEnabled(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + trackerConfigurationBuilder.enabled(false); + + whenSendsRequestAsync(); + future.get(); + + verify(0, postRequestedFor(urlPathEqualTo("/matomo.php"))); + + } + + @Test + void reportsErrors(WireMockRuntimeInfo wireMockRuntimeInfo) { + + stubFor(get(urlPathEqualTo("/failing")).willReturn(status(500))); + trackerConfigurationBuilder + .apiEndpoint(URI.create(wireMockRuntimeInfo.getHttpBaseUrl() + "/failing")) + .defaultSiteId(SITE_ID); + + whenSendsRequestAsync(); + + assertThat(future) + .failsWithin(1, MINUTES) + .withThrowableThat() + .havingRootCause() + .isInstanceOf(MatomoException.class) + .withMessage("Tracking endpoint responded with code 500"); + + } + + @Test + void includesDefaultTokenAuth(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + trackerConfigurationBuilder.defaultAuthToken("fdf6e8461ea9de33176b222519627f78"); + + whenSendsRequestAsync(); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(getRequestedFor(urlEqualTo( + "/matomo.php?idsite=42&token_auth=fdf6e8461ea9de33176b222519627f78&rec=1&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom")).withHeader("User-Agent", + equalTo("MatomoJavaClient") + )); + }); + + } + + @Test + void includesMultipleQueriesInBulkRequest(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + matomoTracker = new MatomoTracker(trackerConfigurationBuilder.build()); + + future = matomoTracker.sendBulkRequestAsync(Arrays.asList(requestBuilder.actionName("First").build(), + requestBuilder.actionName("Second").build(), + requestBuilder.actionName("Third").build() + )); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("297")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\" : [ \"?idsite=42&rec=1&action_name=First&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom\", \"?idsite=42&rec=1&action_name=Second&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom\", \"?idsite=42&rec=1&action_name=Third&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom\" ]}"))); + }); + + } + + @Test + void failsOnNegativeSiteId(WireMockRuntimeInfo wireMockRuntimeInfo) { + + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + requestBuilder.siteId(-1); + + assertThatThrownBy(this::whenSendsRequestAsync) + .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 doesNotSendRequestAsyncIfTrackerConfigurationIsDisabled(WireMockRuntimeInfo wireMockRuntimeInfo) + throws Exception { + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + trackerConfigurationBuilder.enabled(false); + + whenSendsRequestAsync(); + future.get(); + + verify(0, getRequestedFor(urlPathEqualTo("/matomo.php"))); + + } + + @Test + void doesNotSendRequestIfTrackerConfigurationIsDisabled(WireMockRuntimeInfo wireMockRuntimeInfo) { + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + trackerConfigurationBuilder.enabled(false); + + whenSendsSingleRequest(); + + verify(0, getRequestedFor(urlPathEqualTo("/matomo.php"))); + } + + private void whenSendsSingleRequest() { + new MatomoTracker(trackerConfigurationBuilder.build()).sendRequest(requestBuilder.build()); + } + + @Test + void doesNotSendBulkRequestIfTrackerConfigurationIsDisabled(WireMockRuntimeInfo wireMockRuntimeInfo) { + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + trackerConfigurationBuilder.enabled(false); + + whenSendsBulkRequest(); + + verify(0, postRequestedFor(urlPathEqualTo("/matomo.php"))); + } + + private void whenSendsBulkRequest() { + new MatomoTracker(trackerConfigurationBuilder.build()).sendBulkRequest(singleton(requestBuilder.build())); + } + + @Test + void doesNotSendBulkRequestAsyncIfTrackerConfigurationIsDisabled(WireMockRuntimeInfo wireMockRuntimeInfo) + throws Exception { + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + trackerConfigurationBuilder.enabled(false); + + whenSendsBulkRequestAsync(); + + future.get(); + verify(0, postRequestedFor(urlPathEqualTo("/matomo.php"))); + } + + @Test + void sendsRequestAsyncAndAcceptsCallback(WireMockRuntimeInfo wireMockRuntimeInfo) { + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + matomoTracker = new MatomoTracker(trackerConfigurationBuilder.build()); + AtomicBoolean success = new AtomicBoolean(); + future = matomoTracker.sendRequestAsync(requestBuilder.build(), request -> { + success.set(true); + return null; + }); + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(getRequestedFor(urlPathEqualTo("/matomo.php"))); + }); + assertThat(success).isTrue(); + } + + @Test + void sendsRequestsAsyncAndAcceptsCallback(WireMockRuntimeInfo wireMockRuntimeInfo) { + givenTrackerConfigurationWithDefaultSiteId(wireMockRuntimeInfo); + matomoTracker = new MatomoTracker(trackerConfigurationBuilder.build()); + AtomicBoolean success = new AtomicBoolean(); + future = matomoTracker.sendBulkRequestAsync(singleton(requestBuilder.build()), v -> { + success.set(true); + }); + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(postRequestedFor(urlPathEqualTo("/matomo.php"))); + }); + assertThat(success).isTrue(); + } + +} diff --git a/java11/src/test/java/org/matomo/java/tracking/PiwikTrackerIT.java b/java11/src/test/java/org/matomo/java/tracking/PiwikTrackerIT.java new file mode 100644 index 00000000..799af605 --- /dev/null +++ b/java11/src/test/java/org/matomo/java/tracking/PiwikTrackerIT.java @@ -0,0 +1,249 @@ +package org.matomo.java.tracking; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.resetAllRequests; +import static com.github.tomakehurst.wiremock.client.WireMock.status; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.parameters.RandomValue; +import org.matomo.java.tracking.parameters.VisitorId; +import org.piwik.java.tracking.PiwikRequest; +import org.piwik.java.tracking.PiwikTracker; + +@WireMockTest +class PiwikTrackerIT { + + private static final int SITE_ID = 42; + + private PiwikTracker piwikTracker; + + private PiwikRequest request; + + @BeforeEach + void setUp(WireMockRuntimeInfo wireMockRuntimeInfo) throws MalformedURLException { + piwikTracker = new PiwikTracker(wireMockRuntimeInfo.getHttpBaseUrl() + "/matomo.php", -1); + resetAllRequests(); + stubFor(post(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + stubFor(get(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + request = new PiwikRequest(SITE_ID, new URL("https://test.local/test/path?id=123")); + request.setRandomValue(RandomValue.fromString("rand")); + request.setVisitorId(VisitorId.fromHash(999999999999999999L)); + } + + /** + * Test of sendRequest method, of class PiwikTracker. + */ + @Test + void testSendRequest() { + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + piwikTracker.sendRequest(request); + + verify(getRequestedFor(urlEqualTo( + "/matomo.php?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue")).withHeader("User-Agent", + equalTo("MatomoJavaClient") + )); + } + + /** + * Test of sendRequestAsync method, of class PiwikTracker. + */ + @Test + void testSendRequestAsync() { + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + CompletableFuture future = piwikTracker.sendRequestAsync(request); + + assertThat(future).isNotCompletedExceptionally(); + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(getRequestedFor(urlEqualTo( + "/matomo.php?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue")) + .withHeader("User-Agent", equalTo("MatomoJavaClient") + )); + }); + + } + + + /** + * Test of sendBulkRequest method, of class PiwikTracker. + */ + @Test + void testSendBulkRequest_Iterable() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + piwikTracker.sendBulkRequest(requests); + + verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("167")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{ \"requests\" : [ \"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\" ]}"))); + + } + + /** + * Test of sendBulkRequest method, of class PiwikTracker. + */ + @Test + void testSendBulkRequest_Iterable_StringTT() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + assertThatThrownBy(() -> piwikTracker.sendBulkRequest(requests, "1")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + } + + @Test + void testSendBulkRequest_Iterable_StringFF() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + piwikTracker.sendBulkRequest(requests, null); + + verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("167")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\":[\"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\"]}"))); + + } + + @Test + void testSendBulkRequest_Iterable_StringFT() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + piwikTracker.sendBulkRequest(requests, "12345678901234567890123456789012"); + + verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("215")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\":[\"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\" ],\"token_auth\":\"12345678901234567890123456789012\"}"))); + + } + + /** + * Test of sendBulkRequestAsync method, of class PiwikTracker. + */ + @Test + void testSendBulkRequestAsync_Iterable() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + CompletableFuture future = piwikTracker.sendBulkRequestAsync(requests); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("167")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\" : [ \"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\" ]}"))); + + }); + + } + + /** + * Test of sendBulkRequestAsync method, of class PiwikTracker. + */ + @Test + void testSendBulkRequestAsync_Iterable_StringTT() { + + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + assertThatThrownBy(() -> piwikTracker.sendBulkRequestAsync(requests, "1").get()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + + } + + + @Test + void testSendBulkRequestAsync_Iterable_String() { + + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + CompletableFuture future = + piwikTracker.sendBulkRequestAsync(requests, "12345678901234567890123456789012"); + + assertThat(future).isNotCompletedExceptionally(); + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("215")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\":[ \"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\"],\"token_auth\":\"12345678901234567890123456789012\"}"))); + + }); + } + + @Test + void createsPiwikTrackerWithHostUrl(WireMockRuntimeInfo wireMockRuntimeInfo) { + PiwikTracker piwikTracker = new PiwikTracker(wireMockRuntimeInfo.getHttpBaseUrl() + "/matomo.php"); + + piwikTracker.sendRequest(request); + + verify(getRequestedFor(urlEqualTo( + "/matomo.php?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand")).withHeader("User-Agent", + equalTo("MatomoJavaClient") + )); + } + + @Test + void createPiwikTrackerWithHostUrlAndProxyHostAndPort(WireMockRuntimeInfo wireMockRuntimeInfo) { + PiwikTracker piwikTracker = + new PiwikTracker(wireMockRuntimeInfo.getHttpBaseUrl() + "/matomo.php", "localhost", 8080); + + assertThatThrownBy(() -> piwikTracker.sendRequest(request)) + .isInstanceOf(MatomoException.class) + ; + + } + + @Test + void createPiwikTrackerWithHostUrlAndProxyHostAndPortAndTimeout(WireMockRuntimeInfo wireMockRuntimeInfo) { + PiwikTracker piwikTracker = + new PiwikTracker(wireMockRuntimeInfo.getHttpBaseUrl() + "/matomo.php", "localhost", 8080, 1000); + + assertThatThrownBy(() -> piwikTracker.sendRequest(request)) + .isInstanceOf(MatomoException.class) + ; + } + +} diff --git a/java8/pom.xml b/java8/pom.xml new file mode 100644 index 00000000..4b4621ad --- /dev/null +++ b/java8/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + + + org.piwik.java.tracking + matomo-java-tracker-parent + 3.4.1-SNAPSHOT + ../pom.xml + + + matomo-java-tracker + 3.4.1-SNAPSHOT + jar + + Matomo Java Tracker Java 8 + Official Java implementation of the Matomo Tracking HTTP API for Java 8. + + + + org.piwik.java.tracking + matomo-java-tracker-core + ${project.version} + + + org.slf4j + slf4j-api + + + com.github.spotbugs + spotbugs-annotations + + + org.projectlombok + lombok + provided + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + com.github.tomakehurst + wiremock-standalone + 2.27.2 + test + + + org.slf4j + slf4j-simple + test + + + + diff --git a/java8/src/main/java/org/matomo/java/tracking/Java8Sender.java b/java8/src/main/java/org/matomo/java/tracking/Java8Sender.java new file mode 100644 index 00000000..9aca209c --- /dev/null +++ b/java8/src/main/java/org/matomo/java/tracking/Java8Sender.java @@ -0,0 +1,390 @@ +/* + * 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.Collections.singleton; +import static java.util.Objects.requireNonNull; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.IOException; +import java.io.OutputStream; +import java.net.Authenticator; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.URI; +import java.net.URL; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * A {@link Sender} implementation that uses Java 8 {@link HttpURLConnection} to send requests to the Matomo. + * + *

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. + * + *

If you use a newer Java version, please use the newer Java implementation from the Matomo Java Tracker for + * Java 11. + */ +@Slf4j +@RequiredArgsConstructor +class Java8Sender implements Sender { + + private static final TrustManager[] TRUST_ALL_MANAGERS = {new TrustingX509TrustManager()}; + private static final HostnameVerifier TRUSTING_HOSTNAME_VERIFIER = new TrustingHostnameVerifier(); + + private final TrackerConfiguration trackerConfiguration; + + private final QueryCreator queryCreator; + + private final ExecutorService executorService; + + @Override + @NonNull + public CompletableFuture sendSingleAsync( + @NonNull MatomoRequest request + ) { + return CompletableFuture.supplyAsync(() -> { + sendSingle(request); + return request; + }, executorService); + } + + @Override + public void sendSingle( + @NonNull MatomoRequest request + ) { + String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration); + RequestValidator.validate(request, authToken); + HttpURLConnection connection; + URI apiEndpoint = trackerConfiguration.getApiEndpoint(); + try { + connection = openConnection(apiEndpoint + .resolve(String.format("%s?%s", apiEndpoint.getPath(), queryCreator.createQuery(request, authToken))) + .toURL()); + } catch (MalformedURLException e) { + throw new InvalidUrlException(e); + } + applyTrackerConfiguration(connection); + setUserAgentProperty(connection, request.getHeaderUserAgent(), request.getHeaders()); + addHeaders(connection, request.getHeaders()); + addCookies(connection, request.getSessionId(), request.getCookies()); + log.debug("Sending single request using URI {} asynchronously", apiEndpoint); + try { + connection.connect(); + checkResponse(connection); + } catch (IOException e) { + throw new MatomoException("Could not send request via GET", e); + } finally { + connection.disconnect(); + } + } + + private HttpURLConnection openConnection(URL url) { + HttpURLConnection connection; + try { + if (isEmpty(trackerConfiguration.getProxyHost()) || trackerConfiguration.getProxyPort() <= 0) { + log.debug("Proxy host or proxy port not configured. Will create connection without proxy"); + connection = (HttpURLConnection) url.openConnection(); + } else { + connection = openProxiedConnection(url); + } + } catch (IOException e) { + throw new MatomoException("Could not open connection", e); + } + if (connection instanceof HttpsURLConnection) { + applySslConfiguration((HttpsURLConnection) connection); + } + return connection; + } + + private void applyTrackerConfiguration(HttpURLConnection connection) { + connection.setUseCaches(false); + if (trackerConfiguration.getConnectTimeout() != null) { + connection.setConnectTimeout((int) trackerConfiguration.getConnectTimeout().toMillis()); + } + if (trackerConfiguration.getSocketTimeout() != null) { + connection.setReadTimeout((int) trackerConfiguration.getSocketTimeout().toMillis()); + } + } + + private void setUserAgentProperty( + @NonNull HttpURLConnection connection, @Nullable String headerUserAgent, @Nullable Map headers + ) { + String userAgentHeader = null; + if ((headerUserAgent == null || headerUserAgent.trim().isEmpty()) && headers != null) { + TreeMap caseInsensitiveMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + caseInsensitiveMap.putAll(headers); + userAgentHeader = caseInsensitiveMap.get("User-Agent"); + } + if ((userAgentHeader == null || userAgentHeader.trim().isEmpty()) && ( + headerUserAgent == null || headerUserAgent.trim().isEmpty()) + && trackerConfiguration.getUserAgent() != null && !trackerConfiguration.getUserAgent().isEmpty()) { + connection.setRequestProperty("User-Agent", trackerConfiguration.getUserAgent()); + } + } + + private void addHeaders(@NonNull HttpURLConnection connection, @Nullable Map headers) { + if (headers != null) { + for (Map.Entry header : headers.entrySet()) { + connection.setRequestProperty(header.getKey(), header.getValue()); + } + } + } + + private static void addCookies( + HttpURLConnection connection, String sessionId, Map cookies + ) { + StringBuilder cookiesValue = new StringBuilder(); + if (sessionId != null && !sessionId.isEmpty()) { + cookiesValue.append("MATOMO_SESSID=").append(sessionId); + if (cookies != null && !cookies.isEmpty()) { + cookiesValue.append("; "); + } + } + if (cookies != null) { + for (Iterator> iterator = cookies.entrySet().iterator(); iterator.hasNext(); ) { + Map.Entry entry = iterator.next(); + cookiesValue.append(entry.getKey()).append("=").append(entry.getValue()); + if (iterator.hasNext()) { + cookiesValue.append("; "); + } + } + } + if (cookiesValue.length() > 0) { + connection.setRequestProperty("Cookie", cookiesValue.toString()); + } + } + + private void checkResponse(HttpURLConnection connection) throws IOException { + int responseCode = connection.getResponseCode(); + if (responseCode > 399) { + if (trackerConfiguration.isLogFailedTracking()) { + log.error("Received HTTP error code {} for URL {}", responseCode, connection.getURL()); + } + throw new MatomoException(String.format("Tracking endpoint responded with code %d", responseCode)); + } + } + + private static boolean isEmpty( + @Nullable String str + ) { + return str == null || str.isEmpty() || str.trim().isEmpty(); + } + + private HttpURLConnection openProxiedConnection( + @NonNull @lombok.NonNull URL url + ) throws IOException { + requireNonNull(trackerConfiguration.getProxyHost(), "Proxy host must not be null"); + if (trackerConfiguration.getProxyPort() <= 0) { + throw new IllegalArgumentException("Proxy port must be configured"); + } + InetSocketAddress proxyAddress = + new InetSocketAddress(trackerConfiguration.getProxyHost(), trackerConfiguration.getProxyPort()); + Proxy proxy = new Proxy(Proxy.Type.HTTP, proxyAddress); + if (!isEmpty(trackerConfiguration.getProxyUsername()) && !isEmpty(trackerConfiguration.getProxyPassword())) { + Authenticator.setDefault(new ProxyAuthenticator(trackerConfiguration.getProxyUsername(), + trackerConfiguration.getProxyPassword() + )); + } + log.debug("Using proxy {} on port {}", trackerConfiguration.getProxyHost(), trackerConfiguration.getProxyPort()); + return (HttpURLConnection) url.openConnection(proxy); + } + + private void applySslConfiguration( + @NonNull @lombok.NonNull HttpsURLConnection connection + ) { + if (trackerConfiguration.isDisableSslCertValidation()) { + try { + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, TRUST_ALL_MANAGERS, new SecureRandom()); + connection.setSSLSocketFactory(sslContext.getSocketFactory()); + } catch (Exception e) { + throw new MatomoException("Could not disable SSL certification validation", e); + } + } + if (trackerConfiguration.isDisableSslHostVerification()) { + connection.setHostnameVerifier(TRUSTING_HOSTNAME_VERIFIER); + } + } + + @Override + public void sendBulk( + @NonNull @lombok.NonNull Iterable requests, @Nullable String overrideAuthToken + ) { + String authToken = AuthToken.determineAuthToken(overrideAuthToken, requests, trackerConfiguration); + Collection queries = new ArrayList<>(); + Map headers = new LinkedHashMap<>(); + String headerUserAgent = null; + String sessionId = null; + Map cookies = null; + for (MatomoRequest request : requests) { + RequestValidator.validate(request, authToken); + if (request.getHeaders() != null && !request.getHeaders().isEmpty()) { + headers.putAll(request.getHeaders()); + } + if (headerUserAgent == null && request.getHeaderUserAgent() != null && !request + .getHeaderUserAgent() + .trim() + .isEmpty()) { + headerUserAgent = request.getHeaderUserAgent(); + } + queries.add(queryCreator.createQuery(request, null)); + if (request.getSessionId() != null && !request.getSessionId().isEmpty()) { + sessionId = request.getSessionId(); + } + if (request.getCookies() != null && !request.getCookies().isEmpty()) { + cookies = request.getCookies(); + } + } + sendBulk(queries, authToken, headers, headerUserAgent, sessionId, cookies); + } + + private void sendBulk( + @NonNull @lombok.NonNull Collection queries, + @Nullable String authToken, + Map headers, + String headerUserAgent, + String sessionId, + Map cookies + ) { + if (queries.isEmpty()) { + throw new IllegalArgumentException("Queries must not be empty"); + } + HttpURLConnection connection; + try { + connection = openConnection(trackerConfiguration.getApiEndpoint().toURL()); + } catch (MalformedURLException e) { + throw new InvalidUrlException(e); + } + preparePostConnection(connection); + applyTrackerConfiguration(connection); + setUserAgentProperty(connection, headerUserAgent, headers); + addHeaders(connection, headers); + addCookies(connection, sessionId, cookies); + log.debug("Sending bulk request using URI {} asynchronously", trackerConfiguration.getApiEndpoint()); + OutputStream outputStream = null; + try { + connection.connect(); + outputStream = connection.getOutputStream(); + outputStream.write(BulkRequest.builder().queries(queries).authToken(authToken).build().toBytes()); + outputStream.flush(); + checkResponse(connection); + } catch (IOException e) { + throw new MatomoException("Could not send requests via POST", e); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + // ignore + } + } + connection.disconnect(); + } + } + + private static void preparePostConnection(HttpURLConnection connection) { + try { + connection.setRequestMethod("POST"); + } catch (ProtocolException e) { + throw new MatomoException("Could not set request method", e); + } + connection.setDoOutput(true); + connection.setRequestProperty("Accept", "*/*"); + connection.setRequestProperty("Content-Type", "application/json"); + + } + + @Override + @NonNull + public CompletableFuture sendBulkAsync( + @NonNull Collection requests, @Nullable String overrideAuthToken + ) { + String authToken = AuthToken.determineAuthToken(overrideAuthToken, requests, trackerConfiguration); + Map headers = new LinkedHashMap<>(); + String headerUserAgent = findHeaderUserAgent(requests); + String sessionId = findSessionId(requests); + Map cookies = findCookies(requests); + List queries = new ArrayList<>(requests.size()); + for (MatomoRequest request : requests) { + RequestValidator.validate(request, authToken); + if (request.getHeaders() != null && !request.getHeaders().isEmpty()) { + headers.putAll(request.getHeaders()); + } + queries.add(queryCreator.createQuery(request, null)); + } + return CompletableFuture.supplyAsync(() -> + sendBulkAsync(queries, authToken, headers, headerUserAgent, sessionId, cookies), + executorService); + } + + @Nullable + private Void sendBulkAsync( + List queries, + @Nullable String authToken, + Map headers, + String headerUserAgent, + String sessionId, + Map cookies + ) { + sendBulk(queries, authToken, headers, headerUserAgent, sessionId, cookies); + return null; + } + + @Nullable + private static String findHeaderUserAgent(@NonNull Iterable requests) { + for (MatomoRequest request : requests) { + if (request.getHeaderUserAgent() != null && !request.getHeaderUserAgent().trim().isEmpty()) { + return request.getHeaderUserAgent(); + } + } + return null; + } + + private String findSessionId(Iterable requests) { + for (MatomoRequest request : requests) { + if (request.getHeaderUserAgent() != null && !request.getHeaderUserAgent().trim().isEmpty()) { + return request.getHeaderUserAgent(); + } + } + return null; + } + + private Map findCookies(Iterable requests) { + for (MatomoRequest request : requests) { + if (request.getCookies() != null && !request.getCookies().isEmpty()) { + return request.getCookies(); + } + } + return null; + } + + @Override + public void close() { + ExecutorServiceCloser.close(executorService); + } +} diff --git a/java8/src/main/java/org/matomo/java/tracking/Java8SenderProvider.java b/java8/src/main/java/org/matomo/java/tracking/Java8SenderProvider.java new file mode 100644 index 00000000..0a3b40bf --- /dev/null +++ b/java8/src/main/java/org/matomo/java/tracking/Java8SenderProvider.java @@ -0,0 +1,20 @@ +package org.matomo.java.tracking; + +import java.util.concurrent.Executors; + +/** + * Provides a {@link Sender} implementation based on Java 8. + */ +public class Java8SenderProvider implements SenderProvider { + + @Override + public Sender provideSender( + TrackerConfiguration trackerConfiguration, QueryCreator queryCreator + ) { + return new Java8Sender( + trackerConfiguration, + queryCreator, + Executors.newFixedThreadPool(trackerConfiguration.getThreadPoolSize(), new DaemonThreadFactory()) + ); + } +} diff --git a/java8/src/main/java/org/matomo/java/tracking/TrustingHostnameVerifier.java b/java8/src/main/java/org/matomo/java/tracking/TrustingHostnameVerifier.java new file mode 100644 index 00000000..341487d6 --- /dev/null +++ b/java8/src/main/java/org/matomo/java/tracking/TrustingHostnameVerifier.java @@ -0,0 +1,18 @@ +package org.matomo.java.tracking; + +import edu.umd.cs.findbugs.annotations.Nullable; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +class TrustingHostnameVerifier implements HostnameVerifier { + + @Override + public boolean verify( + @Nullable + String hostname, + @Nullable + SSLSession session + ) { + return true; + } +} diff --git a/java8/src/main/resources/META-INF/services/org.matomo.java.tracking.SenderProvider b/java8/src/main/resources/META-INF/services/org.matomo.java.tracking.SenderProvider new file mode 100644 index 00000000..25145902 --- /dev/null +++ b/java8/src/main/resources/META-INF/services/org.matomo.java.tracking.SenderProvider @@ -0,0 +1 @@ +org.matomo.java.tracking.Java8SenderProvider \ No newline at end of file diff --git a/java8/src/test/java/org/matomo/java/tracking/Java8SenderIT.java b/java8/src/test/java/org/matomo/java/tracking/Java8SenderIT.java new file mode 100644 index 00000000..dcaeafa4 --- /dev/null +++ b/java8/src/test/java/org/matomo/java/tracking/Java8SenderIT.java @@ -0,0 +1,250 @@ +package org.matomo.java.tracking; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.status; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.net.MalformedURLException; +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class Java8SenderIT { + + private static final WireMockServer wireMockServer = + new WireMockServer(WireMockConfiguration.options().dynamicPort().dynamicHttpsPort()); + + private Sender sender; + + private TrackerConfiguration trackerConfiguration; + + @BeforeAll + static void beforeAll() { + wireMockServer.start(); + } + + @Test + void sendSingleFailsIfQueryIsMalformed() { + trackerConfiguration = + TrackerConfiguration.builder().apiEndpoint(URI.create("telnet://localhost")).build(); + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(InvalidUrlException.class) + .hasRootCause(new MalformedURLException("unknown protocol: telnet")); + } + + + @Test + void failsIfEndpointReturnsNotFound() { + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(wireMockServer.baseUrl())) + .disableSslHostVerification(true) + .disableSslCertValidation(true) + .build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(MatomoException.class) + .hasMessage("Tracking endpoint responded with code 404"); + } + + @Test + void failsIfCouldNotConnectToEndpoint() { + trackerConfiguration = + TrackerConfiguration.builder().apiEndpoint(URI.create("http://localhost:1234")).build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not send request via GET"); + } + + @Test + void connectsViaProxy() { + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(wireMockServer.baseUrl())) + .disableSslCertValidation(true) + .disableSslHostVerification(true) + .proxyHost("localhost") + .proxyPort(wireMockServer.port()) + .build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not send request via GET"); + } + + @Test + void connectsViaProxyWithProxyUserNameAndPassword() { + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(wireMockServer.baseUrl())) + .disableSslCertValidation(true) + .disableSslHostVerification(true) + .proxyHost("localhost") + .proxyPort(wireMockServer.port()) + .proxyUsername("user") + .proxyPassword("password") + .build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not send request via GET"); + } + + @Test + void logsFailedTracking() { + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(wireMockServer.baseUrl())) + .disableSslCertValidation(true) + .disableSslHostVerification(true) + .logFailedTracking(true) + .build(); + + givenSender(); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())) + .isInstanceOf(MatomoException.class) + .hasMessage("Tracking endpoint responded with code 404"); + } + + @Test + void skipSslCertificationValidation() { + wireMockServer.stubFor(get(urlPathEqualTo("/matomo_ssl.php")).willReturn(status(204))); + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(String.format( + "https://localhost:%d/matomo_ssl.php", + wireMockServer.httpsPort() + ))) + .disableSslCertValidation(true) + .disableSslHostVerification(true) + .build(); + + givenSender(); + + sender.sendSingle(MatomoRequests.pageView("Join Us").build()); + + wireMockServer.verify(getRequestedFor(urlPathEqualTo("/matomo_ssl.php"))); + + } + + @Test + void addsHeadersToSingleRequest() { + wireMockServer.stubFor(get(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(String.format( + "http://localhost:%d/matomo.php", + wireMockServer.port() + ))) + .disableSslCertValidation(true) + .disableSslHostVerification(true) + .build(); + + givenSender(); + + sender.sendSingle(MatomoRequests + .action("http://localhost/example", ActionType.LINK) + .headers(singletonMap( + "headerName", + "headerValue" + )) + .build()); + + wireMockServer.verify(getRequestedFor(urlPathEqualTo("/matomo.php")).withHeader( + "headerName", + equalTo("headerValue") + )); + + } + + @Test + void addsHeadersToBulkRequest() { + wireMockServer.stubFor(post(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(String.format( + "http://localhost:%d/matomo.php", + wireMockServer.port() + ))) + .disableSslCertValidation(true) + .disableSslHostVerification(true) + .build(); + + givenSender(); + + sender.sendBulk( + singleton(MatomoRequests.ecommerceCartUpdate(50.0).goalId(0) + .headers(singletonMap("headerName", "headerValue")) + .build()), + null + ); + + wireMockServer.verify(postRequestedFor(urlPathEqualTo("/matomo.php")).withHeader( + "headerName", + equalTo("headerValue") + )); + + } + + + @Test + void addsHeadersToBulkAsyncRequest() { + wireMockServer.stubFor(post(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + trackerConfiguration = TrackerConfiguration + .builder() + .apiEndpoint(URI.create(String.format( + "http://localhost:%d/matomo.php", + wireMockServer.port() + ))) + .disableSslCertValidation(true) + .disableSslHostVerification(true) + .build(); + + givenSender(); + + CompletableFuture future = sender.sendBulkAsync(singleton(MatomoRequest + .request() + .headers(singletonMap("headerName", "headerValue")) + .build()), null); + + future.join(); + wireMockServer.verify(postRequestedFor(urlPathEqualTo("/matomo.php")).withHeader( + "headerName", + equalTo("headerValue") + )); + + } + + + private void givenSender() { + sender = new Java8Sender( + trackerConfiguration, + new QueryCreator(trackerConfiguration), + Executors.newFixedThreadPool(2, new DaemonThreadFactory()) + ); + } + +} diff --git a/java8/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java b/java8/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java new file mode 100644 index 00000000..04500061 --- /dev/null +++ b/java8/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java @@ -0,0 +1,525 @@ +package org.matomo.java.tracking; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.status; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singleton; +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.net.URI; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Locale.LanguageRange; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.MatomoRequest.MatomoRequestBuilder; +import org.matomo.java.tracking.TrackerConfiguration.TrackerConfigurationBuilder; +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 MatomoTrackerIT { + + private static final WireMockServer wireMockServer = + new WireMockServer(WireMockConfiguration.options().dynamicPort()); + + private static final int SITE_ID = 42; + + private final TrackerConfigurationBuilder trackerConfigurationBuilder = TrackerConfiguration.builder(); + + private final MatomoRequestBuilder requestBuilder = MatomoRequest + .builder() + .visitorId(VisitorId.fromHex("bbccddeeff1122")) + .randomValue(RandomValue.fromString("someRandom")); + + private CompletableFuture future; + + private MatomoTracker matomoTracker; + + @BeforeAll + static void beforeAll() { + wireMockServer.start(); + } + + @BeforeEach + void givenStub() { + wireMockServer.resetRequests(); + wireMockServer.stubFor(post(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + wireMockServer.stubFor(get(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + } + + @AfterEach + void closeTracker() throws Exception { + if (matomoTracker != null) { + matomoTracker.close(); + } + } + + @Test + void requiresSiteId() { + + trackerConfigurationBuilder.apiEndpoint(URI.create(wireMockServer.baseUrl() + "/matomo.php")).build(); + + assertThatThrownBy(this::whenSendsRequestAsync) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("No default site ID and no request site ID is given"); + + } + + private void whenSendsRequestAsync() { + matomoTracker = new MatomoTracker(trackerConfigurationBuilder.build()); + future = matomoTracker.sendRequestAsync(requestBuilder.build()); + } + + @Test + void usesDefaultSiteId() { + + givenTrackerConfigurationWithDefaultSiteId(); + + whenSendsRequestAsync(); + + thenGetsRequest("idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom"); + + } + + private void givenTrackerConfigurationWithDefaultSiteId() { + trackerConfigurationBuilder + .apiEndpoint(URI.create(wireMockServer.baseUrl() + "/matomo.php")) + .defaultSiteId(SITE_ID); + } + + private void thenGetsRequest(String expectedQuery) { + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(getRequestedFor(urlEqualTo( + String.format("/matomo.php?%s", expectedQuery))).withHeader("User-Agent", + equalTo("MatomoJavaClient") + )); + }); + } + + @Test + void overridesDefaultSiteId() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.siteId(123); + + whenSendsRequestAsync(); + + thenGetsRequest("rec=1&idsite=123&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom"); + + } + + @Test + void validatesTokenAuth() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.authToken("invalid-token-auth"); + + whenSendsRequestAsync(); + + assertThat(future) + .failsWithin(1, MINUTES) + .withThrowableThat() + .havingRootCause() + .isInstanceOf(IllegalArgumentException.class) + .withMessage("Auth token must be exactly 32 characters long"); + + } + + @Test + void convertsTrueBooleanTo1() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.pluginFlash(true); + + whenSendsRequestAsync(); + + thenGetsRequest("idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&fla=1&send_image=0&rand=someRandom"); + + } + + @Test + void convertsFalseBooleanTo0() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.pluginJava(false); + + whenSendsRequestAsync(); + + thenGetsRequest("idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&java=0&send_image=0&rand=someRandom"); + + } + + @Test + void encodesUrl() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.actionUrl("https://www.daniel-heid.de/some/page?foo=bar"); + + whenSendsRequestAsync(); + + thenGetsRequest( + "idsite=42&rec=1&url=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fpage%3Ffoo%3Dbar&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom"); + + } + + @Test + void encodesReferrerUrl() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.referrerUrl("https://www.daniel-heid.de/some/referrer?foo2=bar2"); + + whenSendsRequestAsync(); + + thenGetsRequest( + "idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&urlref=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Freferrer%3Ffoo2%3Dbar2&send_image=0&rand=someRandom"); + + } + + @Test + void encodesLink() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.outlinkUrl("https://www.daniel-heid.de/some/external/link#"); + + whenSendsBulkRequestAsync(); + + thenPostsRequestWithoutAuthToken( + "idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&link=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fexternal%2Flink%23&send_image=0&rand=someRandom", + "156" + ); + + } + + @Test + void sendsRequestsBulkAsynchronously() { + + givenTrackerConfigurationWithDefaultSiteId(); + matomoTracker = new MatomoTracker(trackerConfigurationBuilder.build()); + + future = matomoTracker.sendBulkRequestAsync(requestBuilder.build()); + + thenPostsRequestWithoutAuthToken("idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom", "90"); + + } + + private void whenSendsBulkRequestAsync() { + future = + new MatomoTracker(trackerConfigurationBuilder.build()).sendBulkRequestAsync(singleton(requestBuilder.build())); + } + + private void thenPostsRequestWithoutAuthToken(String expectedQuery, String contentLength) { + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo(contentLength)) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson("{\"requests\":[\"?" + expectedQuery + "\"]}"))); + }); + + } + + @Test + void encodesDownloadUrl() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.downloadUrl("https://www.daniel-heid.de/some/download.pdf"); + + whenSendsBulkRequestAsync(); + + thenPostsRequestWithoutAuthToken( + "idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&download=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fdownload.pdf&send_image=0&rand=someRandom", + "154" + ); + + } + + @Test + void getContainsHeaders() { + + givenTrackerConfigurationWithDefaultSiteId(); + + whenSendsRequestAsync(); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(getRequestedFor(urlPathEqualTo("/matomo.php")).withHeader("User-Agent", + equalTo("MatomoJavaClient") + )); + }); + + } + + @Test + void postContainsHeaders() { + + givenTrackerConfigurationWithDefaultSiteId(); + + whenSendsBulkRequestAsync(); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(postRequestedFor(urlPathEqualTo("/matomo.php")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Length", equalTo("90")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient"))); + }); + + } + + @Test + void allowsToOverrideUserAgent() { + + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.userAgent("Mozilla/5.0"); + + whenSendsRequestAsync(); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(getRequestedFor(urlPathEqualTo("/matomo.php")).withHeader("User-Agent", + equalTo("Mozilla/5.0") + )); + }); + + } + + @Test + void tracksMinimalRequest() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder + .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"), 4) + .add(new CustomVariable("customVariable2Key", "customVariable2Value"), 5)) + .visitorVisitCount(2) + .visitorFirstVisitTimestamp(LocalDateTime.of(2022, 8, 9, 18, 34, 12).toInstant(ZoneOffset.UTC)) + .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(org.matomo.java.tracking.parameters.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")); + + whenSendsBulkRequestAsync(); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("711")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson("{\"requests\":[\"?" + + "idsite=42&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%224%22%3A%5B%22customVariable1Key%22%2C%22customVariable1Value%22%5D%2C%225%22%3A%5B%22customVariable2Key%22%2C%22customVariable2Value%22%5D%7D&_idvc=2&_idts=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=someRandom" + + "\"],\"token_auth\" : \"" + "fdf6e8461ea9de33176b222519627f78" + "\"}"))); + + }); + + } + + @Test + void doesNothingIfNotEnabled() throws Exception { + + wireMockServer.resetRequests(); + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.enabled(false); + + whenSendsRequestAsync(); + future.get(); + + wireMockServer.verify(0, postRequestedFor(urlPathEqualTo("/matomo.php"))); + + } + + @Test + void reportsErrors() { + + wireMockServer.stubFor(get(urlPathEqualTo("/failing")).willReturn(status(500))); + trackerConfigurationBuilder.apiEndpoint(URI.create(wireMockServer.baseUrl() + "/failing")).defaultSiteId(SITE_ID); + + whenSendsRequestAsync(); + + assertThat(future) + .failsWithin(1, MINUTES) + .withThrowableThat() + .havingRootCause() + .isInstanceOf(MatomoException.class) + .withMessage("Tracking endpoint responded with code 500"); + + } + + @Test + void includesDefaultTokenAuth() { + + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.defaultAuthToken("fdf6e8461ea9de33176b222519627f78"); + + whenSendsRequestAsync(); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(getRequestedFor(urlEqualTo( + "/matomo.php?idsite=42&token_auth=fdf6e8461ea9de33176b222519627f78&rec=1&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom")).withHeader("User-Agent", + equalTo("MatomoJavaClient") + )); + }); + + } + + @Test + void includesMultipleQueriesInBulkRequest() { + + givenTrackerConfigurationWithDefaultSiteId(); + matomoTracker = new MatomoTracker(trackerConfigurationBuilder.build()); + + future = matomoTracker.sendBulkRequestAsync(Arrays.asList(requestBuilder.actionName("First").build(), + requestBuilder.actionName("Second").build(), + requestBuilder.actionName("Third").build() + )); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("297")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\" : [ \"?idsite=42&rec=1&action_name=First&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom\", \"?idsite=42&rec=1&action_name=Second&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom\", \"?idsite=42&rec=1&action_name=Third&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom\" ]}"))); + }); + + } + + @Test + void failsOnNegativeSiteId() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.siteId(-1); + + whenSendsRequestAsync(); + + assertThat(future) + .failsWithin(1, MINUTES) + .withThrowableThat() + .havingRootCause() + .isInstanceOf(MatomoException.class) + .withMessage("Invalid value for idsite. Must be greater or equal than 1"); + + ; + } + + @Test + void doesNotSendRequestAsyncIfTrackerConfigurationIsDisabled() throws Exception { + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.enabled(false); + + whenSendsRequestAsync(); + future.get(); + + wireMockServer.verify(0, getRequestedFor(urlPathEqualTo("/matomo.php"))); + + } + + @Test + void doesNotSendRequestIfTrackerConfigurationIsDisabled() { + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.enabled(false); + + whenSendsSingleRequest(); + + wireMockServer.verify(0, getRequestedFor(urlPathEqualTo("/matomo.php"))); + } + + private void whenSendsSingleRequest() { + new MatomoTracker(trackerConfigurationBuilder.build()).sendRequest(requestBuilder.build()); + } + + @Test + void doesNotSendBulkRequestIfTrackerConfigurationIsDisabled() { + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.enabled(false); + + whenSendsBulkRequest(); + + wireMockServer.verify(0, postRequestedFor(urlPathEqualTo("/matomo.php"))); + } + + private void whenSendsBulkRequest() { + new MatomoTracker(trackerConfigurationBuilder.build()).sendBulkRequest(singleton(requestBuilder.build())); + } + + @Test + void doesNotSendBulkRequestAsyncIfTrackerConfigurationIsDisabled() throws Exception { + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.enabled(false); + + whenSendsBulkRequestAsync(); + + future.get(); + wireMockServer.verify(0, postRequestedFor(urlPathEqualTo("/matomo.php"))); + } + + @Test + void sendsRequestAsyncAndAcceptsCallback() { + givenTrackerConfigurationWithDefaultSiteId(); + matomoTracker = new MatomoTracker(trackerConfigurationBuilder.build()); + AtomicBoolean success = new AtomicBoolean(); + future = matomoTracker.sendRequestAsync(requestBuilder.build(), request -> { + success.set(true); + return null; + }); + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(getRequestedFor(urlPathEqualTo("/matomo.php"))); + }); + assertThat(success).isTrue(); + } + + @Test + void sendsRequestsAsyncAndAcceptsCallback() { + givenTrackerConfigurationWithDefaultSiteId(); + matomoTracker = new MatomoTracker(trackerConfigurationBuilder.build()); + AtomicBoolean success = new AtomicBoolean(); + future = matomoTracker.sendBulkRequestAsync(singleton(requestBuilder.build()), v -> { + success.set(true); + }); + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(postRequestedFor(urlPathEqualTo("/matomo.php"))); + }); + assertThat(success).isTrue(); + } + +} diff --git a/java8/src/test/java/org/matomo/java/tracking/PiwikTrackerIT.java b/java8/src/test/java/org/matomo/java/tracking/PiwikTrackerIT.java new file mode 100644 index 00000000..523b9f4a --- /dev/null +++ b/java8/src/test/java/org/matomo/java/tracking/PiwikTrackerIT.java @@ -0,0 +1,255 @@ +package org.matomo.java.tracking; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.status; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.parameters.RandomValue; +import org.matomo.java.tracking.parameters.VisitorId; +import org.piwik.java.tracking.PiwikRequest; +import org.piwik.java.tracking.PiwikTracker; + +class PiwikTrackerIT { + + private static final WireMockServer wireMockServer = + new WireMockServer(WireMockConfiguration.options().dynamicPort()); + + + private static final int SITE_ID = 42; + + private PiwikTracker piwikTracker; + + private PiwikRequest request; + + @BeforeAll + static void beforeAll() { + wireMockServer.start(); + } + + @BeforeEach + void setUp() throws MalformedURLException { + piwikTracker = new PiwikTracker(wireMockServer.baseUrl() + "/matomo.php", -1); + wireMockServer.resetRequests(); + wireMockServer.stubFor(post(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + wireMockServer.stubFor(get(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + request = new PiwikRequest(SITE_ID, new URL("https://test.local/test/path?id=123")); + request.setRandomValue(RandomValue.fromString("rand")); + request.setVisitorId(VisitorId.fromHash(999999999999999999L)); + } + + /** + * Test of sendRequest method, of class PiwikTracker. + */ + @Test + void testSendRequest() { + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + piwikTracker.sendRequest(request); + + wireMockServer.verify(getRequestedFor(urlEqualTo( + "/matomo.php?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue")).withHeader("User-Agent", + equalTo("MatomoJavaClient") + )); + } + + /** + * Test of sendRequestAsync method, of class PiwikTracker. + */ + @Test + void testSendRequestAsync() { + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + CompletableFuture future = piwikTracker.sendRequestAsync(request); + + assertThat(future).isNotCompletedExceptionally(); + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(getRequestedFor(urlEqualTo( + "/matomo.php?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue")) + .withHeader("User-Agent", equalTo("MatomoJavaClient") + )); + }); + + } + + + /** + * Test of sendBulkRequest method, of class PiwikTracker. + */ + @Test + void testSendBulkRequest_Iterable() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + piwikTracker.sendBulkRequest(requests); + + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("167")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{ \"requests\" : [ \"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\" ]}"))); + + } + + /** + * Test of sendBulkRequest method, of class PiwikTracker. + */ + @Test + void testSendBulkRequest_Iterable_StringTT() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + assertThatThrownBy(() -> piwikTracker.sendBulkRequest(requests, "1")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + } + + @Test + void testSendBulkRequest_Iterable_StringFF() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + piwikTracker.sendBulkRequest(requests, null); + + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("167")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\":[\"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\"]}"))); + + } + + @Test + void testSendBulkRequest_Iterable_StringFT() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + piwikTracker.sendBulkRequest(requests, "12345678901234567890123456789012"); + + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("215")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\":[\"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\" ],\"token_auth\":\"12345678901234567890123456789012\"}"))); + + } + + /** + * Test of sendBulkRequestAsync method, of class PiwikTracker. + */ + @Test + void testSendBulkRequestAsync_Iterable() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + CompletableFuture future = piwikTracker.sendBulkRequestAsync(requests); + + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("167")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\" : [ \"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\" ]}"))); + + }); + + } + + /** + * Test of sendBulkRequestAsync method, of class PiwikTracker. + */ + @Test + void testSendBulkRequestAsync_Iterable_StringTT() { + + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + assertThatThrownBy(() -> piwikTracker.sendBulkRequestAsync(requests, "1").get()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + + } + + + @Test + void testSendBulkRequestAsync_Iterable_String() { + + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + CompletableFuture future = + piwikTracker.sendBulkRequestAsync(requests, "12345678901234567890123456789012"); + + assertThat(future).isNotCompletedExceptionally(); + assertThat(future).succeedsWithin(1, MINUTES).satisfies(v -> { + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("215")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\":[ \"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\"],\"token_auth\":\"12345678901234567890123456789012\"}"))); + + }); + } + + @Test + void createsPiwikTrackerWithHostUrl() { + PiwikTracker piwikTracker = new PiwikTracker(wireMockServer.baseUrl() + "/matomo.php"); + + piwikTracker.sendRequest(request); + + wireMockServer.verify(getRequestedFor(urlEqualTo( + "/matomo.php?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand")).withHeader("User-Agent", + equalTo("MatomoJavaClient") + )); + } + + @Test + void createPiwikTrackerWithHostUrlAndProxyHostAndPort() { + PiwikTracker piwikTracker = + new PiwikTracker(wireMockServer.baseUrl() + "/matomo.php", "localhost", 8080); + + assertThatThrownBy(() -> piwikTracker.sendRequest(request)) + .isInstanceOf(MatomoException.class) + ; + + } + + @Test + void createPiwikTrackerWithHostUrlAndProxyHostAndPortAndTimeout() { + PiwikTracker piwikTracker = + new PiwikTracker(wireMockServer.baseUrl() + "/matomo.php", "localhost", 8080, 1000); + + assertThatThrownBy(() -> piwikTracker.sendRequest(request)) + .isInstanceOf(MatomoException.class) + ; + } + +} diff --git a/java8/src/test/java/org/matomo/java/tracking/TrustingHostnameVerifierTest.java b/java8/src/test/java/org/matomo/java/tracking/TrustingHostnameVerifierTest.java new file mode 100644 index 00000000..e3e61969 --- /dev/null +++ b/java8/src/test/java/org/matomo/java/tracking/TrustingHostnameVerifierTest.java @@ -0,0 +1,17 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class TrustingHostnameVerifierTest { + + @Test + void verifyAlwaysReturnsTrue() { + + boolean verified = new TrustingHostnameVerifier().verify(null, null); + + assertThat(verified).isTrue(); + } + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 526a673b..f4c62914 100644 --- a/pom.xml +++ b/pom.xml @@ -1,201 +1,397 @@ - - 4.0.0 + + 4.0.0 - org.piwik.java.tracking - matomo-java-tracker - 2.0-SNAPSHOT - jar + org.piwik.java.tracking + matomo-java-tracker-parent + 3.4.1-SNAPSHOT + pom - Matomo Java Tracker - Official Java implementation of the Matomo Tracking HTTP API. - https://github.com/matomo-org/matomo-java-tracker + Matomo Java Tracker Parent + Parent for Matomo Java Tracker modules. + https://github.com/matomo-org/matomo-java-tracker - - - BSD 3-Clause License - https://github.com/matomo-org/matomo-java-tracker/blob/master/LICENSE - - + + + BSD 3-Clause License + https://github.com/matomo-org/matomo-java-tracker/blob/master/LICENSE + + - - - bcsorba - Brett Csorba - brett.csorba@gmail.com - - - tholu - Thomas Lutz - thomaslutz.de@gmail.com - - + + + bcsorba + Brett Csorba + brett.csorba@gmail.com + + + tholu + Thomas Lutz + thomaslutz.de@gmail.com + + + dheid + Daniel Heid + mail@daniel-heid.de + Freelancer + https://www.daniel-heid.de/ + + - - scm:git@github.com:matomo-org/matomo-java-tracker.git - scm:git@github.com:matomo-org/matomo-java-tracker.git - git@github.com:matomo-org/matomo-java-tracker.git - + + scm:git:https://@github.com/matomo-org/matomo-java-tracker.git + scm:git:https://github.com/matomo-org/matomo-java-tracker.git + https://github.com/matomo-org/matomo-java-tracker + HEAD + - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + - - UTF-8 - 2.0.7 - + + UTF-8 + UTF-8 + UTF-8 + 1.8 + 1.8 + ${project.build.outputDirectory}/delombok + 1.18.36 + 2.0.17 + - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.5.0 - - - - - - attach-javadocs - - jar - - - - - - org.pitest - pitest-maven - 1.13.1 - - - org.matomo.java.tracking* - - - org.matomo.java.tracking* - - - - - org.eluder.coveralls - coveralls-maven-plugin - 4.3.0 - - - org.jacoco - jacoco-maven-plugin - 0.8.10 - - - prepare-agent - - prepare-agent - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 3.1.0 - - - sign-artifacts - verify - - sign - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 - true - - ossrh - https://oss.sonatype.org/ - false - - - - + + + + com.github.spotbugs + spotbugs-annotations + 4.9.8 + provided + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.projectlombok + lombok + ${lombok.version} + provided + + + org.junit.jupiter + junit-jupiter + 5.11.4 + test + + + org.assertj + assertj-core + 3.27.7 + test + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + org.eclipse.jetty.ee10 + jetty-ee10-servlet + 12.0.16 + + + - - - org.apache.httpcomponents - httpasyncclient - 4.1.5 - - - com.google.guava - guava - 31.1-jre - - - com.fasterxml.jackson.core - jackson-databind - 2.15.0 - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.mockito - mockito-core - 4.11.0 - test - - - junit - junit - 4.13.2 - test - - - org.slf4j - slf4j-simple - ${slf4j.version} - test - - - org.projectlombok - lombok - 1.18.26 - provided - - + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.8.0 + + + org.apache.maven.plugins + maven-clean-plugin + 3.5.0 + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.4 + + + org.apache.maven.plugins + maven-install-plugin + 3.1.3 + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + org.apache.maven.plugins + maven-resources-plugin + 3.5.0 + + + org.apache.maven.plugins + maven-site-plugin + 3.21.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.5 + + + org.apache.maven.plugins + maven-release-plugin + 3.3.1 + + true + false + release + deploy + [ci skip] + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.5 + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar-no-fork + + + + + + org.projectlombok + lombok-maven-plugin + 1.18.20.0 + + + org.projectlombok + lombok + ${lombok.version} + + + + src/main/java + ${delombok.output} + false + + + + generate-sources + + delombok + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.2 + + ${delombok.output} + none + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.5.0 + + + enforce-maven + + enforce + + + + + 3.2.5 + + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.14 + + + prepare-agent + + prepare-agent + + + + report + + report + + + + check + + check + + + + + CLASS + + + LINE + COVEREDRATIO + 0.8 + + + BRANCH + COVEREDRATIO + 0.5 + + + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + warning + checkstyle.xml + true + + + + + validate + + + check + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.9.8.2 + + + org.owasp + dependency-check-maven + 12.2.0 + + true + + + + + + + + release + + + + org.apache.maven.plugins + maven-gpg-plugin + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + + + + + + core + java8 + java11 + servlet-jakarta + servlet-javax + spring + test + diff --git a/servlet-jakarta/pom.xml b/servlet-jakarta/pom.xml new file mode 100644 index 00000000..0c31328f --- /dev/null +++ b/servlet-jakarta/pom.xml @@ -0,0 +1,71 @@ + + 4.0.0 + + + org.piwik.java.tracking + matomo-java-tracker-parent + 3.4.1-SNAPSHOT + ../pom.xml + + + matomo-java-tracker-servlet-jakarta + 3.4.1-SNAPSHOT + jar + + Matomo Java Tracker Servlet Jakarta + Integrates Matomo Java Tracker into your Jakarta servlet based application + + + 11 + 11 + + + + + org.piwik.java.tracking + matomo-java-tracker-java11 + ${project.version} + + + jakarta.servlet + jakarta.servlet-api + 6.1.0 + provided + + + org.slf4j + slf4j-api + + + com.github.spotbugs + spotbugs-annotations + + + org.projectlombok + lombok + provided + + + org.junit.jupiter + junit-jupiter + test + + + org.eclipse.jetty.ee10 + jetty-ee10-servlet + 12.0.16 + test + + + org.assertj + assertj-core + test + + + org.slf4j + slf4j-simple + test + + + + diff --git a/servlet-jakarta/src/main/java/org/matomo/java/tracking/servlet/JakartaHttpServletWrapper.java b/servlet-jakarta/src/main/java/org/matomo/java/tracking/servlet/JakartaHttpServletWrapper.java new file mode 100644 index 00000000..11a89de7 --- /dev/null +++ b/servlet-jakarta/src/main/java/org/matomo/java/tracking/servlet/JakartaHttpServletWrapper.java @@ -0,0 +1,50 @@ +package org.matomo.java.tracking.servlet; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.NonNull; + +/** + * Converts a Jakarta {@link HttpServletRequest} to a {@link HttpServletRequestWrapper}. + */ +public final class JakartaHttpServletWrapper { + + private JakartaHttpServletWrapper() { + // utility + } + + /** + * Takes a Jakarta {@link HttpServletRequest} and converts it to a + * {@link HttpServletRequestWrapper}. + * + * @param request the request to convert to a wrapper object (must not be {@code null}). + * @return the wrapper object (never {@code null}). + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static HttpServletRequestWrapper fromHttpServletRequest(@NonNull HttpServletRequest request) { + Map headers = new LinkedHashMap<>(); + request.getHeaderNames() + .asIterator() + .forEachRemaining(name -> headers.put(name.toLowerCase(Locale.ROOT), request.getHeader(name))); + List cookies = null; + if (request.getCookies() != null) { + cookies = Stream.of(request.getCookies()) + .map(cookie -> new CookieWrapper(cookie.getName(), cookie.getValue())) + .collect(Collectors.toList()); + } + return HttpServletRequestWrapper + .builder() + .requestURL(request.getRequestURL()) + .remoteAddr(request.getRemoteAddr()) + .remoteUser(request.getRemoteUser()) + .headers(headers) + .cookies(cookies == null ? null : cookies.toArray(new CookieWrapper[0])) + .build(); + } + +} diff --git a/servlet-jakarta/src/main/java/org/matomo/java/tracking/servlet/MatomoTrackerFilter.java b/servlet-jakarta/src/main/java/org/matomo/java/tracking/servlet/MatomoTrackerFilter.java new file mode 100644 index 00000000..11ca4494 --- /dev/null +++ b/servlet-jakarta/src/main/java/org/matomo/java/tracking/servlet/MatomoTrackerFilter.java @@ -0,0 +1,44 @@ +package org.matomo.java.tracking.servlet; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.matomo.java.tracking.MatomoRequest; +import org.matomo.java.tracking.MatomoTracker; + +/** + * This filter can be used to automatically send a request to the Matomo server for every request + * that is received by the servlet container. + */ +@RequiredArgsConstructor +@Slf4j +public class MatomoTrackerFilter extends HttpFilter { + + private final MatomoTracker tracker; + + @Override + protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) + throws IOException, ServletException { + MatomoRequest matomoRequest = ServletMatomoRequest + .fromServletRequest(JakartaHttpServletWrapper.fromHttpServletRequest(req)).build(); + log.debug("Sending request {}", matomoRequest); + tracker.sendRequestAsync(matomoRequest); + super.doFilter(req, res, chain); + } + + @Override + public void destroy() { + if (tracker != null) { + try { + tracker.close(); + } catch (Exception e) { + throw new RuntimeException("Could not close tracker", e); + } + } + } +} diff --git a/servlet-jakarta/src/test/java/org/matomo/java/tracking/MatomoTrackerFilterIT.java b/servlet-jakarta/src/test/java/org/matomo/java/tracking/MatomoTrackerFilterIT.java new file mode 100644 index 00000000..fac0d472 --- /dev/null +++ b/servlet-jakarta/src/test/java/org/matomo/java/tracking/MatomoTrackerFilterIT.java @@ -0,0 +1,63 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import org.eclipse.jetty.ee10.servlet.FilterHolder; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.server.Server; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.servlet.MatomoTrackerFilter; + +class MatomoTrackerFilterIT { + + @Test + void sendsAnAsyncRequestOnFilter() throws Exception { + + + TestSenderFactory senderFactory = new TestSenderFactory(); + + MatomoTracker tracker = new MatomoTracker( + TrackerConfiguration + .builder() + .apiEndpoint(URI.create("http://localhost:8080/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") + .logFailedTracking(true) + .build()); + tracker.setSenderFactory(senderFactory); + + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + context.addFilter(new FilterHolder(new MatomoTrackerFilter(tracker)), "/*", null); + Server server = new Server(0); + server.setHandler(context); + + server.start(); + URI uri = server.getURI(); + HttpClient.newHttpClient().send( + HttpRequest.newBuilder() + .header("Accept-Language", "en-US,en;q=0.9,de;q=0.8") + .uri(uri) + .build(), + HttpResponse.BodyHandlers.discarding() + ); + server.stop(); + + TestSender testSender = senderFactory.getTestSender(); + assertThat(testSender.getRequests()).hasSize(1).satisfiesExactly(matomoRequest -> { + assertThat(matomoRequest.getActionUrl()).isEqualTo(uri.toString()); + assertThat(matomoRequest.getVisitorId()).isNotNull(); + assertThat(matomoRequest.getVisitorIp()).isNotNull(); + assertThat(matomoRequest.getHeaders()).containsEntry( + "accept-language", + "en-US,en;q=0.9,de;q=0.8" + ); + }); + + } + +} \ No newline at end of file diff --git a/servlet-jakarta/src/test/java/org/matomo/java/tracking/TestSender.java b/servlet-jakarta/src/test/java/org/matomo/java/tracking/TestSender.java new file mode 100644 index 00000000..14f8cde6 --- /dev/null +++ b/servlet-jakarta/src/test/java/org/matomo/java/tracking/TestSender.java @@ -0,0 +1,58 @@ +package org.matomo.java.tracking; + +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. + * + *

This class is intended for testing purposes only. It does not send anything to the Matomo server. Instead, it + * stores the requests and queries in collections that can be accessed via {@link #getRequests()}. + */ +@RequiredArgsConstructor +@Getter +class TestSender implements Sender { + + private final Collection requests = new ArrayList<>(); + + private final TrackerConfiguration trackerConfiguration; + + private final QueryCreator queryCreator; + + @NonNull + @Override + public CompletableFuture sendSingleAsync(@NonNull MatomoRequest request) { + requests.add(request); + return CompletableFuture.completedFuture(request); + } + + @Override + public void sendSingle(@NonNull MatomoRequest request) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendBulk( + @NonNull Iterable requests, @Nullable String overrideAuthToken + ) { + throw new UnsupportedOperationException(); + } + + @NonNull + @Override + public CompletableFuture sendBulkAsync( + @NonNull Collection requests, @Nullable String overrideAuthToken + ) { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + // Do nothing + } +} diff --git a/servlet-jakarta/src/test/java/org/matomo/java/tracking/TestSenderFactory.java b/servlet-jakarta/src/test/java/org/matomo/java/tracking/TestSenderFactory.java new file mode 100644 index 00000000..fafafa90 --- /dev/null +++ b/servlet-jakarta/src/test/java/org/matomo/java/tracking/TestSenderFactory.java @@ -0,0 +1,16 @@ +package org.matomo.java.tracking; + +import lombok.Getter; + +class TestSenderFactory implements SenderFactory { + + @Getter + private TestSender testSender; + + @Override + public Sender createSender(TrackerConfiguration trackerConfiguration, QueryCreator queryCreator) { + TestSender testSender = new TestSender(trackerConfiguration, queryCreator); + this.testSender = testSender; + return testSender; + } +} diff --git a/servlet-jakarta/src/test/java/org/matomo/java/tracking/servlet/JakartaHttpServletWrapperTest.java b/servlet-jakarta/src/test/java/org/matomo/java/tracking/servlet/JakartaHttpServletWrapperTest.java new file mode 100644 index 00000000..d15dbcbf --- /dev/null +++ b/servlet-jakarta/src/test/java/org/matomo/java/tracking/servlet/JakartaHttpServletWrapperTest.java @@ -0,0 +1,37 @@ +package org.matomo.java.tracking.servlet; + +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.servlet.http.Cookie; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JakartaHttpServletWrapperTest { + + @Test + void wrapsHttpServletRequest() { + + MockHttpServletRequest servlet = new MockHttpServletRequest(); + servlet.setRequestURL(new StringBuffer("http://localhost")); + servlet.setRemoteUser("remote-user"); + servlet.setHeaders(singletonMap("Accept-Language", "en-US,en;q=0.9,de;q=0.8")); + servlet.setCookies(List.of(new Cookie("foo", "bar"))); + + HttpServletRequestWrapper httpServletRequestWrapper = + JakartaHttpServletWrapper.fromHttpServletRequest(servlet); + + assertThat(httpServletRequestWrapper.getRequestURL()).hasToString("http://localhost"); + assertThat(httpServletRequestWrapper.getRemoteUser()).hasToString("remote-user"); + assertThat(httpServletRequestWrapper.getHeaders()) + .containsEntry("accept-language", "en-US,en;q=0.9,de;q=0.8"); + assertThat(httpServletRequestWrapper.getCookies()) + .hasSize(1) + .satisfiesExactly(cookieWrapper -> { + assertThat(cookieWrapper.getName()).isEqualTo("foo"); + assertThat(cookieWrapper.getValue()).isEqualTo("bar"); + }); + } + +} + diff --git a/servlet-jakarta/src/test/java/org/matomo/java/tracking/servlet/MockHttpServletRequest.java b/servlet-jakarta/src/test/java/org/matomo/java/tracking/servlet/MockHttpServletRequest.java new file mode 100644 index 00000000..856c76db --- /dev/null +++ b/servlet-jakarta/src/test/java/org/matomo/java/tracking/servlet/MockHttpServletRequest.java @@ -0,0 +1,383 @@ +package org.matomo.java.tracking.servlet; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletConnection; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpUpgradeHandler; +import jakarta.servlet.http.Part; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +class MockHttpServletRequest implements HttpServletRequest { + + private StringBuffer requestURL; + + private String remoteUser; + + private Map headers = new LinkedHashMap<>(); + + private Collection cookies; + + @Override + public String getAuthType() { + return null; + } + + @Override + public Cookie[] getCookies() { + return cookies == null ? null : cookies.toArray(new Cookie[0]); + } + + @Override + public long getDateHeader(String name) { + return 0; + } + + @Override + public String getHeader(String name) { + return headers.get(name); + } + + @Override + public Enumeration getHeaders(String name) { + return null; + } + + @Override + public Enumeration getHeaderNames() { + return headers == null ? Collections.emptyEnumeration() : Collections.enumeration(headers.keySet()); + } + + @Override + public int getIntHeader(String name) { + return 0; + } + + @Override + public String getMethod() { + return null; + } + + @Override + public String getPathInfo() { + return null; + } + + @Override + public String getPathTranslated() { + return null; + } + + @Override + public String getContextPath() { + return null; + } + + @Override + public String getQueryString() { + return null; + } + + @Override + public boolean isUserInRole(String role) { + return false; + } + + @Override + public Principal getUserPrincipal() { + return null; + } + + @Override + public String getRequestedSessionId() { + return null; + } + + @Override + public String getRequestURI() { + return null; + } + + @Override + public String getServletPath() { + return null; + } + + @Override + public HttpSession getSession(boolean create) { + return null; + } + + @Override + public HttpSession getSession() { + return null; + } + + @Override + public String changeSessionId() { + return null; + } + + @Override + public boolean isRequestedSessionIdValid() { + return false; + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + return false; + } + + @Override + public boolean isRequestedSessionIdFromURL() { + return false; + } + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + return false; + } + + @Override + public void login(String username, String password) throws ServletException { + + } + + @Override + public void logout() throws ServletException { + + } + + @Override + public Collection getParts() throws IOException, ServletException { + return null; + } + + @Override + public Part getPart(String name) throws IOException, ServletException { + return null; + } + + @Override + public T upgrade(Class handlerClass) throws IOException, ServletException { + return null; + } + + @Override + public Object getAttribute(String name) { + return null; + } + + @Override + public Enumeration getAttributeNames() { + return null; + } + + @Override + public String getCharacterEncoding() { + return null; + } + + @Override + public void setCharacterEncoding(String env) throws UnsupportedEncodingException { + + } + + @Override + public int getContentLength() { + return 0; + } + + @Override + public long getContentLengthLong() { + return 0; + } + + @Override + public String getContentType() { + return null; + } + + @Override + public ServletInputStream getInputStream() throws IOException { + return null; + } + + @Override + public String getParameter(String name) { + return null; + } + + @Override + public Enumeration getParameterNames() { + return null; + } + + @Override + public String[] getParameterValues(String name) { + return new String[0]; + } + + @Override + public Map getParameterMap() { + return null; + } + + @Override + public String getProtocol() { + return null; + } + + @Override + public String getScheme() { + return null; + } + + @Override + public String getServerName() { + return null; + } + + @Override + public int getServerPort() { + return 0; + } + + @Override + public BufferedReader getReader() throws IOException { + return null; + } + + @Override + public String getRemoteAddr() { + return null; + } + + @Override + public String getRemoteHost() { + return null; + } + + @Override + public void setAttribute(String name, Object o) { + + } + + @Override + public void removeAttribute(String name) { + + } + + @Override + public Locale getLocale() { + return null; + } + + @Override + public Enumeration getLocales() { + return null; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + return null; + } + + @Override + public int getRemotePort() { + return 0; + } + + @Override + public String getLocalName() { + return null; + } + + @Override + public String getLocalAddr() { + return null; + } + + @Override + public int getLocalPort() { + return 0; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) + throws IllegalStateException { + return null; + } + + @Override + public boolean isAsyncStarted() { + return false; + } + + @Override + public boolean isAsyncSupported() { + return false; + } + + @Override + public AsyncContext getAsyncContext() { + return null; + } + + @Override + public DispatcherType getDispatcherType() { + return null; + } + + @Override + public String getRequestId() { + return null; + } + + @Override + public String getProtocolRequestId() { + return null; + } + + @Override + public ServletConnection getServletConnection() { + return null; + } +} diff --git a/servlet-javax/pom.xml b/servlet-javax/pom.xml new file mode 100644 index 00000000..7e76f0a5 --- /dev/null +++ b/servlet-javax/pom.xml @@ -0,0 +1,71 @@ + + 4.0.0 + + + org.piwik.java.tracking + matomo-java-tracker-parent + 3.4.1-SNAPSHOT + ../pom.xml + + + matomo-java-tracker-servlet-javax + 3.4.1-SNAPSHOT + jar + + Matomo Java Tracker Servlet Javax + Integrates Matomo Java Tracker into your javax servlet based application + + + 1.8 + 1.8 + + + + + org.piwik.java.tracking + matomo-java-tracker + ${project.version} + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + org.slf4j + slf4j-api + + + com.github.spotbugs + spotbugs-annotations + + + org.projectlombok + lombok + provided + + + org.junit.jupiter + junit-jupiter + test + + + org.eclipse.jetty + jetty-servlet + 10.0.24 + test + + + org.assertj + assertj-core + test + + + org.slf4j + slf4j-simple + test + + + + diff --git a/servlet-javax/src/main/java/org/matomo/java/tracking/servlet/JavaxHttpServletWrapper.java b/servlet-javax/src/main/java/org/matomo/java/tracking/servlet/JavaxHttpServletWrapper.java new file mode 100644 index 00000000..77c9838b --- /dev/null +++ b/servlet-javax/src/main/java/org/matomo/java/tracking/servlet/JavaxHttpServletWrapper.java @@ -0,0 +1,50 @@ +package org.matomo.java.tracking.servlet; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.servlet.http.HttpServletRequest; +import lombok.NonNull; + +/** + * Converts a javax {@link HttpServletRequest} to a {@link HttpServletRequestWrapper}. + */ +public final class JavaxHttpServletWrapper { + + private JavaxHttpServletWrapper() { + // utility + } + + /** + * Takes a javax {@link HttpServletRequest} and converts it to a + * {@link HttpServletRequestWrapper}. + * + * @param request the request to convert to a wrapper object (must not be {@code null}). + * @return the wrapper object (never {@code null}). + */ + @edu.umd.cs.findbugs.annotations.NonNull + public static HttpServletRequestWrapper fromHttpServletRequest(@NonNull HttpServletRequest request) { + Map headers = new LinkedHashMap<>(); + request.getHeaderNames() + .asIterator() + .forEachRemaining(name -> headers.put(name.toLowerCase(Locale.ROOT), request.getHeader(name))); + List cookies = null; + if (request.getCookies() != null) { + cookies = Stream.of(request.getCookies()) + .map(cookie -> new CookieWrapper(cookie.getName(), cookie.getValue())) + .collect(Collectors.toList()); + } + return HttpServletRequestWrapper + .builder() + .requestURL(request.getRequestURL()) + .remoteAddr(request.getRemoteAddr()) + .remoteUser(request.getRemoteUser()) + .headers(headers) + .cookies(cookies == null ? null : cookies.toArray(new CookieWrapper[0])) + .build(); + } + +} diff --git a/servlet-javax/src/main/java/org/matomo/java/tracking/servlet/MatomoTrackerFilter.java b/servlet-javax/src/main/java/org/matomo/java/tracking/servlet/MatomoTrackerFilter.java new file mode 100644 index 00000000..abf0849d --- /dev/null +++ b/servlet-javax/src/main/java/org/matomo/java/tracking/servlet/MatomoTrackerFilter.java @@ -0,0 +1,35 @@ +package org.matomo.java.tracking.servlet; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpFilter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.matomo.java.tracking.MatomoRequest; +import org.matomo.java.tracking.MatomoTracker; + +/** + * This filter can be used to automatically send a request to the Matomo server for every request + * that is received by the servlet container. + */ +@RequiredArgsConstructor +@Slf4j +public class MatomoTrackerFilter extends HttpFilter { + + private final MatomoTracker tracker; + + @Override + protected void doFilter(@NonNull HttpServletRequest req, @NonNull HttpServletResponse res, + @NonNull FilterChain chain) + throws IOException, ServletException { + MatomoRequest matomoRequest = ServletMatomoRequest + .fromServletRequest(JavaxHttpServletWrapper.fromHttpServletRequest(req)).build(); + log.debug("Sending request {}", matomoRequest); + tracker.sendRequestAsync(matomoRequest); + super.doFilter(req, res, chain); + } +} diff --git a/servlet-javax/src/test/java/org/matomo/java/tracking/MatomoTrackerFilterIT.java b/servlet-javax/src/test/java/org/matomo/java/tracking/MatomoTrackerFilterIT.java new file mode 100644 index 00000000..676524c2 --- /dev/null +++ b/servlet-javax/src/test/java/org/matomo/java/tracking/MatomoTrackerFilterIT.java @@ -0,0 +1,65 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.servlet.MatomoTrackerFilter; + +class MatomoTrackerFilterIT { + + @Test + void sendsAnAsyncRequestOnFilter() throws Exception { + + + TestSenderFactory senderFactory = new TestSenderFactory(); + + MatomoTracker tracker = new MatomoTracker( + TrackerConfiguration + .builder() + .apiEndpoint(URI.create("http://localhost:8080/matomo.php")) + .defaultSiteId(1) + .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") + .logFailedTracking(true) + .build()); + tracker.setSenderFactory(senderFactory); + + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + context.addFilter(new FilterHolder(new MatomoTrackerFilter(tracker)), "/*", null); + Server server = new Server(0); + server.setHandler(context); + + server.start(); + URI uri = server.getURI(); + HttpClient.newHttpClient().send( + HttpRequest.newBuilder() + .header("Accept-Language", "en-US,en;q=0.9,de;q=0.8") + .uri(uri) + .build(), + HttpResponse.BodyHandlers.discarding() + ); + server.stop(); + + TestSender testSender = senderFactory.getTestSender(); + assertThat(testSender.getRequests()).hasSize(1).satisfiesExactly(matomoRequest -> { + assertThat(matomoRequest.getActionUrl()).isEqualTo(uri.toString()); + assertThat(matomoRequest.getVisitorId()).isNotNull(); + assertThat(matomoRequest.getVisitorIp()).isNotNull(); + assertThat(matomoRequest.getHeaders()).containsEntry( + "accept-language", + "en-US,en;q=0.9,de;q=0.8" + ); + }); + + tracker.close(); + + } + +} \ No newline at end of file diff --git a/servlet-javax/src/test/java/org/matomo/java/tracking/TestSender.java b/servlet-javax/src/test/java/org/matomo/java/tracking/TestSender.java new file mode 100644 index 00000000..0e1704f1 --- /dev/null +++ b/servlet-javax/src/test/java/org/matomo/java/tracking/TestSender.java @@ -0,0 +1,58 @@ +package org.matomo.java.tracking; + +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. + * + *

This class is intended for testing purposes only. It does not send anything to the Matomo server. Instead, it + * stores the requests and queries in collections that can be accessed via {@link #getRequests()}. + */ +@RequiredArgsConstructor +@Getter +class TestSender implements Sender { + + private final Collection requests = new ArrayList<>(); + + private final TrackerConfiguration trackerConfiguration; + + private final QueryCreator queryCreator; + + @NonNull + @Override + public CompletableFuture sendSingleAsync(@NonNull MatomoRequest request) { + requests.add(request); + return CompletableFuture.completedFuture(request); + } + + @Override + public void sendSingle(@NonNull MatomoRequest request) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendBulk( + @NonNull Iterable requests, @Nullable String overrideAuthToken + ) { + throw new UnsupportedOperationException(); + } + + @NonNull + @Override + public CompletableFuture sendBulkAsync( + @NonNull Collection requests, @Nullable String overrideAuthToken + ) { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + // do nothing + } +} diff --git a/servlet-javax/src/test/java/org/matomo/java/tracking/TestSenderFactory.java b/servlet-javax/src/test/java/org/matomo/java/tracking/TestSenderFactory.java new file mode 100644 index 00000000..fafafa90 --- /dev/null +++ b/servlet-javax/src/test/java/org/matomo/java/tracking/TestSenderFactory.java @@ -0,0 +1,16 @@ +package org.matomo.java.tracking; + +import lombok.Getter; + +class TestSenderFactory implements SenderFactory { + + @Getter + private TestSender testSender; + + @Override + public Sender createSender(TrackerConfiguration trackerConfiguration, QueryCreator queryCreator) { + TestSender testSender = new TestSender(trackerConfiguration, queryCreator); + this.testSender = testSender; + return testSender; + } +} diff --git a/servlet-javax/src/test/java/org/matomo/java/tracking/servlet/JavaxHttpServletWrapperTest.java b/servlet-javax/src/test/java/org/matomo/java/tracking/servlet/JavaxHttpServletWrapperTest.java new file mode 100644 index 00000000..e631d818 --- /dev/null +++ b/servlet-javax/src/test/java/org/matomo/java/tracking/servlet/JavaxHttpServletWrapperTest.java @@ -0,0 +1,38 @@ +package org.matomo.java.tracking.servlet; + +import static java.util.Collections.singleton; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import javax.servlet.http.Cookie; +import org.junit.jupiter.api.Test; + +class JavaxHttpServletWrapperTest { + + @Test + void wrapsHttpServletRequest() { + + MockHttpServletRequest servlet = new MockHttpServletRequest(); + servlet.setRequestURL(new StringBuffer("http://localhost")); + servlet.setRemoteUser("remote-user"); + servlet.setHeaders(singletonMap("Accept-Language", "en-US,en;q=0.9,de;q=0.8")); + servlet.setCookies(singleton(new Cookie("foo", "bar"))); + + HttpServletRequestWrapper httpServletRequestWrapper = + JavaxHttpServletWrapper.fromHttpServletRequest(servlet); + + assertThat(httpServletRequestWrapper.getRequestURL()).hasToString("http://localhost"); + assertThat(httpServletRequestWrapper.getRemoteUser()).hasToString("remote-user"); + assertThat(httpServletRequestWrapper.getHeaders()) + .containsEntry("accept-language", "en-US,en;q=0.9,de;q=0.8"); + assertThat(httpServletRequestWrapper.getCookies()) + .hasSize(1) + .satisfiesExactly(cookieWrapper -> { + assertThat(cookieWrapper.getName()).isEqualTo("foo"); + assertThat(cookieWrapper.getValue()).isEqualTo("bar"); + }); + } + +} + diff --git a/servlet-javax/src/test/java/org/matomo/java/tracking/servlet/MockHttpServletRequest.java b/servlet-javax/src/test/java/org/matomo/java/tracking/servlet/MockHttpServletRequest.java new file mode 100644 index 00000000..e0de55b9 --- /dev/null +++ b/servlet-javax/src/test/java/org/matomo/java/tracking/servlet/MockHttpServletRequest.java @@ -0,0 +1,378 @@ +package org.matomo.java.tracking.servlet; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpUpgradeHandler; +import javax.servlet.http.Part; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +class MockHttpServletRequest implements HttpServletRequest { + + private StringBuffer requestURL; + + private String remoteUser; + + private Map headers = new LinkedHashMap<>(); + + private Collection cookies; + + @Override + public String getAuthType() { + return null; + } + + @Override + public Cookie[] getCookies() { + return cookies == null ? null : cookies.toArray(new Cookie[0]); + } + + @Override + public long getDateHeader(String name) { + return 0; + } + + @Override + public String getHeader(String name) { + return headers.get(name); + } + + @Override + public Enumeration getHeaders(String name) { + return null; + } + + @Override + public Enumeration getHeaderNames() { + return headers == null ? Collections.emptyEnumeration() : Collections.enumeration(headers.keySet()); + } + + @Override + public int getIntHeader(String name) { + return 0; + } + + @Override + public String getMethod() { + return null; + } + + @Override + public String getPathInfo() { + return null; + } + + @Override + public String getPathTranslated() { + return null; + } + + @Override + public String getContextPath() { + return null; + } + + @Override + public String getQueryString() { + return null; + } + + @Override + public boolean isUserInRole(String role) { + return false; + } + + @Override + public Principal getUserPrincipal() { + return null; + } + + @Override + public String getRequestedSessionId() { + return null; + } + + @Override + public String getRequestURI() { + return null; + } + + @Override + public String getServletPath() { + return null; + } + + @Override + public HttpSession getSession(boolean create) { + return null; + } + + @Override + public HttpSession getSession() { + return null; + } + + @Override + public String changeSessionId() { + return null; + } + + @Override + public boolean isRequestedSessionIdValid() { + return false; + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + return false; + } + + @Override + public boolean isRequestedSessionIdFromURL() { + return false; + } + + @Override + public boolean isRequestedSessionIdFromUrl() { + return false; + } + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + return false; + } + + @Override + public void login(String username, String password) throws ServletException { + + } + + @Override + public void logout() throws ServletException { + + } + + @Override + public Collection getParts() throws IOException, ServletException { + return null; + } + + @Override + public Part getPart(String name) throws IOException, ServletException { + return null; + } + + @Override + public T upgrade(Class handlerClass) throws IOException, ServletException { + return null; + } + + @Override + public Object getAttribute(String name) { + return null; + } + + @Override + public Enumeration getAttributeNames() { + return null; + } + + @Override + public String getCharacterEncoding() { + return null; + } + + @Override + public void setCharacterEncoding(String env) throws UnsupportedEncodingException { + + } + + @Override + public int getContentLength() { + return 0; + } + + @Override + public long getContentLengthLong() { + return 0; + } + + @Override + public String getContentType() { + return null; + } + + @Override + public ServletInputStream getInputStream() throws IOException { + return null; + } + + @Override + public String getParameter(String name) { + return null; + } + + @Override + public Enumeration getParameterNames() { + return null; + } + + @Override + public String[] getParameterValues(String name) { + return new String[0]; + } + + @Override + public Map getParameterMap() { + return null; + } + + @Override + public String getProtocol() { + return null; + } + + @Override + public String getScheme() { + return null; + } + + @Override + public String getServerName() { + return null; + } + + @Override + public int getServerPort() { + return 0; + } + + @Override + public BufferedReader getReader() throws IOException { + return null; + } + + @Override + public String getRemoteAddr() { + return null; + } + + @Override + public String getRemoteHost() { + return null; + } + + @Override + public void setAttribute(String name, Object o) { + + } + + @Override + public void removeAttribute(String name) { + + } + + @Override + public Locale getLocale() { + return null; + } + + @Override + public Enumeration getLocales() { + return null; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + return null; + } + + @Override + public String getRealPath(String s) { + return null; + } + + @Override + public int getRemotePort() { + return 0; + } + + @Override + public String getLocalName() { + return null; + } + + @Override + public String getLocalAddr() { + return null; + } + + @Override + public int getLocalPort() { + return 0; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) + throws IllegalStateException { + return null; + } + + @Override + public boolean isAsyncStarted() { + return false; + } + + @Override + public boolean isAsyncSupported() { + return false; + } + + @Override + public AsyncContext getAsyncContext() { + return null; + } + + @Override + public DispatcherType getDispatcherType() { + return null; + } + +} diff --git a/spring/pom.xml b/spring/pom.xml new file mode 100644 index 00000000..c55f8c43 --- /dev/null +++ b/spring/pom.xml @@ -0,0 +1,82 @@ + + 4.0.0 + + + org.piwik.java.tracking + matomo-java-tracker-parent + 3.4.1-SNAPSHOT + ../pom.xml + + + matomo-java-tracker-spring-boot-starter + jar + + Matomo Java Tracker Spring Boot Starter + Spring integration of Matomo Java Tracker + + + 17 + 17 + 3.4.2 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + org.piwik.java.tracking + matomo-java-tracker-java11 + ${project.version} + + + org.piwik.java.tracking + matomo-java-tracker-servlet-jakarta + ${project.version} + + + jakarta.servlet + jakarta.servlet-api + provided + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.projectlombok + lombok + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.slf4j + slf4j-simple + test + + + + diff --git a/spring/src/main/java/org/matomo/java/tracking/spring/MatomoTrackerAutoConfiguration.java b/spring/src/main/java/org/matomo/java/tracking/spring/MatomoTrackerAutoConfiguration.java new file mode 100644 index 00000000..ae7e0376 --- /dev/null +++ b/spring/src/main/java/org/matomo/java/tracking/spring/MatomoTrackerAutoConfiguration.java @@ -0,0 +1,126 @@ +/* + * 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 java.util.List; +import org.matomo.java.tracking.MatomoTracker; +import org.matomo.java.tracking.TrackerConfiguration; +import org.matomo.java.tracking.servlet.MatomoTrackerFilter; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.lang.NonNull; + +/** + * {@link AutoConfiguration Auto configuration} for Matomo Tracker. + * + * @see MatomoTrackerProperties + * @see TrackerConfiguration + * @see MatomoTracker + */ +@AutoConfiguration +@ConditionalOnProperty(prefix = "matomo.tracker", name = "api-endpoint") +@EnableConfigurationProperties(MatomoTrackerProperties.class) +public class MatomoTrackerAutoConfiguration { + + /** + * Creates a {@link TrackerConfiguration.TrackerConfigurationBuilder} and applies all + * {@link TrackerConfigurationBuilderCustomizer}s. Can be overridden by custom beans. + * + * @param customizers the customizers to apply to the builder instance (never {@code null}) + * @return the {@link TrackerConfiguration.TrackerConfigurationBuilder} instance (never {@code null}) + * @see TrackerConfiguration#builder() + */ + @Bean + @ConditionalOnMissingBean + @NonNull + public TrackerConfiguration.TrackerConfigurationBuilder trackerConfigurationBuilder( + @NonNull List customizers + ) { + TrackerConfiguration.TrackerConfigurationBuilder builder = TrackerConfiguration.builder(); + customizers.forEach(customizer -> customizer.customize(builder)); + return builder; + } + + /** + * Creates a {@link TrackerConfiguration} instance based on the current configuration. Can be + * overridden by custom beans. + * + *

If you define your own {@link TrackerConfiguration} bean, please don't forget to set the + * API endpoint. + * + * @param builder the {@link TrackerConfiguration.TrackerConfigurationBuilder} instance (never {@code null}) + * @return the {@link TrackerConfiguration} instance (never {@code null}) + * @see TrackerConfiguration#builder() + * @see TrackerConfiguration.TrackerConfigurationBuilder#apiEndpoint(URI) + */ + @Bean + @ConditionalOnMissingBean + @NonNull + public TrackerConfiguration trackerConfiguration( + TrackerConfiguration.TrackerConfigurationBuilder builder + ) { + return builder.build(); + } + + /** + * Configures the {@link TrackerConfiguration.TrackerConfigurationBuilder} with the properties from + * {@link MatomoTrackerProperties}. + * + * @param properties the {@link MatomoTrackerProperties} instance (never {@code null}) + * @return the {@link StandardTrackerConfigurationBuilderCustomizer} instance (never {@code null}) + * @see MatomoTrackerProperties + * @see TrackerConfiguration.TrackerConfigurationBuilder + */ + @Bean + @NonNull + public StandardTrackerConfigurationBuilderCustomizer standardTrackerConfigurationBuilderCustomizer( + @NonNull MatomoTrackerProperties properties + ) { + return new StandardTrackerConfigurationBuilderCustomizer(properties); + } + + /** + * A {@link MatomoTracker} instance based on the current configuration. Only created if a bean of the same type is not + * already configured. + * + * @param trackerConfiguration the {@link TrackerConfiguration} instance (never {@code null}) + * @return the {@link MatomoTracker} instance (never {@code null}) + * @see MatomoTracker + * @see TrackerConfiguration + */ + @Bean + @ConditionalOnMissingBean + @NonNull + public MatomoTracker matomoTracker(@NonNull TrackerConfiguration trackerConfiguration) { + return new MatomoTracker(trackerConfiguration); + } + + /** + * A {@link FilterRegistrationBean} for the {@link MatomoTrackerFilter}. + * + *

Only created if a bean of the same type is not already configured. The filter is only registered if + * {@code matomo.tracker.filter.enabled} is set to {@code true}. + * + * @param matomoTracker the {@link MatomoTracker} instance (never {@code null}) + * @return the {@link FilterRegistrationBean} instance (never {@code null}) + */ + @Bean + @ConditionalOnProperty(value = "matomo.tracker.filter.enabled", havingValue = "true") + @NonNull + public FilterRegistrationBean matomoTrackerSpringFilter( + @NonNull MatomoTracker matomoTracker + ) { + return new FilterRegistrationBean<>(new MatomoTrackerFilter(matomoTracker)); + } + +} diff --git a/spring/src/main/java/org/matomo/java/tracking/spring/MatomoTrackerProperties.java b/spring/src/main/java/org/matomo/java/tracking/spring/MatomoTrackerProperties.java new file mode 100644 index 00000000..8be4023d --- /dev/null +++ b/spring/src/main/java/org/matomo/java/tracking/spring/MatomoTrackerProperties.java @@ -0,0 +1,148 @@ +/* + * 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.time.Duration; +import lombok.Getter; +import lombok.Setter; +import org.matomo.java.tracking.TrackerConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for the Matomo Tracker. + * + *

These properties can be configured in the application.properties file. For example: + *

+ *   matomo.tracker.api-endpoint=https://your-matomo-domain.example/matomo.php
+ *   matomo.tracker.default-site-id=1
+ *   matomo.tracker.default-auth-token=1234567890abcdef1234567890abcdef
+ *   matomo.tracker.enabled=true
+ *   matomo.tracker.connect-timeout=10s
+ *   matomo.tracker.socket-timeout=30s
+ *   matomo.tracker.proxy-host=proxy.example.com
+ *   matomo.tracker.proxy-port=8080
+ *   matomo.tracker.proxy-username=proxyuser
+ *   matomo.tracker.proxy-password=proxypassword
+ *   matomo.tracker.user-agent=MatomoJavaClient
+ *   matomo.tracker.log-failed-tracking=true
+ *   matomo.tracker.disable-ssl-cert-validation=true
+ *   matomo.tracker.disable-ssl-host-validation=true
+ *   matomo.tracker.thread-pool-size=2
+ * 
+ * + * @see MatomoTrackerAutoConfiguration + * @see TrackerConfiguration + */ +@ConfigurationProperties(prefix = "matomo.tracker") +@Getter +@Setter +public class MatomoTrackerProperties { + + /** + * The Matomo Tracking HTTP API endpoint, for example https://your-matomo-domain.example/matomo.php + */ + private String apiEndpoint; + + /** + * The default ID of the website that will be used if not specified explicitly. + */ + private Integer defaultSiteId; + + /** + * The authorization token (parameter token_auth) to use if not specified explicitly. + */ + private String defaultAuthToken; + + /** + * Allows to stop the tracker to send requests to the Matomo endpoint. + */ + private Boolean enabled = true; + + /** + * The timeout until a connection is established. + * + *

A timeout value of zero is interpreted as an infinite timeout. + * A `null` value is interpreted as undefined (system default if applicable).

+ * + *

Default: 10 seconds

+ */ + private Duration connectTimeout = Duration.ofSeconds(5L); + + /** + * The socket timeout ({@code SO_TIMEOUT}), which is the timeout for waiting for data or, put differently, a maximum + * period inactivity between two consecutive data packets. + * + *

A timeout value of zero is interpreted as an infinite timeout. + * A `null value is interpreted as undefined (system default if applicable).

+ * + *

Default: 30 seconds

+ */ + private Duration socketTimeout = Duration.ofSeconds(5L); + + /** + * The hostname or IP address of an optional HTTP proxy. {@code proxyPort} must be configured as well + */ + private String proxyHost; + + /** + * The port of an HTTP proxy. {@code proxyHost} must be configured as well. + */ + private Integer proxyPort; + + /** + * If the HTTP proxy requires a username for basic authentication, it can be configured here. Proxy host, port and + * password must also be set. + */ + private String proxyUsername; + + /** + * The corresponding password for the basic auth proxy user. The proxy host, port and username must be set as well. + */ + private String proxyPassword; + + /** + * A custom user agent to be set. Defaults to "MatomoJavaClient" + */ + private String userAgent = "MatomoJavaClient"; + + /** + * Logs if the Matomo Tracking API endpoint responds with an erroneous HTTP code. Defaults to + * false. + */ + private Boolean logFailedTracking; + + /** + * Disables SSL certificate validation. This is useful for testing with self-signed certificates. + * Do not use in production environments. Defaults to false. + * + *

Attention: This slows down performance + + * @see #disableSslHostVerification + */ + private Boolean disableSslCertValidation; + + /** + * Disables SSL host verification. This is useful for testing with self-signed certificates. Do + * not use in production environments. Defaults to false. + * + *

Attention: This slows down performance + * + * @see #disableSslCertValidation + */ + private Boolean disableSslHostVerification; + + /** + * The thread pool size for the async sender. Defaults to 2. + * + *

Attention: If you use this library in a web application, make sure that this thread pool + * does not exceed the thread pool of the web application. Otherwise, you might run into + * problems. + */ + private Integer threadPoolSize = 2; + +} diff --git a/spring/src/main/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizer.java b/spring/src/main/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizer.java new file mode 100644 index 00000000..e9f1acd2 --- /dev/null +++ b/spring/src/main/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizer.java @@ -0,0 +1,50 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.spring; + +import java.net.URI; +import org.matomo.java.tracking.TrackerConfiguration; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.core.Ordered; +import org.springframework.lang.NonNull; + +class StandardTrackerConfigurationBuilderCustomizer implements TrackerConfigurationBuilderCustomizer, Ordered { + + private final MatomoTrackerProperties properties; + + StandardTrackerConfigurationBuilderCustomizer(MatomoTrackerProperties properties) { + this.properties = properties; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public void customize(@NonNull TrackerConfiguration.TrackerConfigurationBuilder builder) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getApiEndpoint).as(URI::create).to(builder::apiEndpoint); + map.from(properties::getDefaultSiteId).to(builder::defaultSiteId); + map.from(properties::getDefaultAuthToken).to(builder::defaultAuthToken); + map.from(properties::getEnabled).to(builder::enabled); + map.from(properties::getConnectTimeout).to(builder::connectTimeout); + map.from(properties::getSocketTimeout).to(builder::socketTimeout); + map.from(properties::getProxyHost).to(builder::proxyHost); + map.from(properties::getProxyPort).to(builder::proxyPort); + map.from(properties::getProxyUsername).to(builder::proxyUsername); + map.from(properties::getProxyPassword).to(builder::proxyPassword); + map.from(properties::getUserAgent).to(builder::userAgent); + map.from(properties::getLogFailedTracking).to(builder::logFailedTracking); + map.from(properties::getDisableSslCertValidation).to(builder::disableSslCertValidation); + map.from(properties::getDisableSslHostVerification).to(builder::disableSslHostVerification); + map.from(properties::getThreadPoolSize).to(builder::threadPoolSize); + } + + +} \ No newline at end of file diff --git a/spring/src/main/java/org/matomo/java/tracking/spring/TrackerConfigurationBuilderCustomizer.java b/spring/src/main/java/org/matomo/java/tracking/spring/TrackerConfigurationBuilderCustomizer.java new file mode 100644 index 00000000..e147a64f --- /dev/null +++ b/spring/src/main/java/org/matomo/java/tracking/spring/TrackerConfigurationBuilderCustomizer.java @@ -0,0 +1,34 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.spring; + +import org.matomo.java.tracking.TrackerConfiguration; +import org.springframework.lang.NonNull; + +/** + * Allows to customize the {@link TrackerConfiguration.TrackerConfigurationBuilder} with additional properties. + * + *

Implementations of this interface are detected automatically by the {@link MatomoTrackerAutoConfiguration}. + * + * @see MatomoTrackerAutoConfiguration + * @see TrackerConfiguration + * @see TrackerConfiguration.TrackerConfigurationBuilder + */ +@FunctionalInterface +public interface TrackerConfigurationBuilderCustomizer { + + /** + * Customize the {@link TrackerConfiguration.TrackerConfigurationBuilder}. + * + * @param builder the {@link TrackerConfiguration.TrackerConfigurationBuilder} instance (never {@code null}) + * @see TrackerConfiguration#builder() + * @see MatomoTrackerProperties + */ + void customize(@NonNull TrackerConfiguration.TrackerConfigurationBuilder builder); + +} diff --git a/spring/src/main/java/org/matomo/java/tracking/spring/package-info.java b/spring/src/main/java/org/matomo/java/tracking/spring/package-info.java new file mode 100644 index 00000000..784c9d72 --- /dev/null +++ b/spring/src/main/java/org/matomo/java/tracking/spring/package-info.java @@ -0,0 +1,10 @@ +/** + * Provides Spring specific classes to integrate Matomo tracking into Spring applications. + * + *

See {@link org.matomo.java.tracking.spring.MatomoTrackerProperties} for the available configuration properties. + * + *

See {@link org.matomo.java.tracking.spring.MatomoTrackerAutoConfiguration} for the available configuration + * options. + */ + +package org.matomo.java.tracking.spring; \ No newline at end of file diff --git a/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..dd6d4697 --- /dev/null +++ b/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.matomo.java.tracking.spring.MatomoTrackerAutoConfiguration diff --git a/spring/src/test/java/org/matomo/java/tracking/spring/MatomoTrackerAutoConfigurationIT.java b/spring/src/test/java/org/matomo/java/tracking/spring/MatomoTrackerAutoConfigurationIT.java new file mode 100644 index 00000000..c008dbc8 --- /dev/null +++ b/spring/src/test/java/org/matomo/java/tracking/spring/MatomoTrackerAutoConfigurationIT.java @@ -0,0 +1,70 @@ +package org.matomo.java.tracking.spring; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.MatomoTracker; +import org.matomo.java.tracking.TrackerConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +class MatomoTrackerAutoConfigurationIT { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(MatomoTrackerAutoConfiguration.class)); + + + @Test + void matomoTrackerRegistration() { + contextRunner.withPropertyValues("matomo.tracker.api-endpoint:https://test.com/matomo.php").run(context -> { + assertThat(context).hasSingleBean(MatomoTracker.class).hasBean("matomoTracker"); + }); + } + + @Test + void additionalTrackerConfigurationBuilderCustomization() { + this.contextRunner + .withPropertyValues("matomo.tracker.api-endpoint:https://test.com/matomo.php") + .withUserConfiguration(TrackerConfigurationBuilderCustomizerConfig.class) + .run(context -> { + TrackerConfiguration trackerConfiguration = context.getBean(TrackerConfiguration.class); + assertThat(trackerConfiguration.getConnectTimeout()).isEqualTo(Duration.ofMinutes(1L)); + }); + } + + @Test + void customTrackerConfigurationBuilder() { + this.contextRunner + .withPropertyValues("matomo.tracker.api-endpoint:https://test.com/matomo.php") + .withUserConfiguration(TrackerConfigurationBuilderConfig.class) + .run(context -> { + TrackerConfiguration trackerConfiguration = context.getBean(TrackerConfiguration.class); + assertThat(trackerConfiguration.isDisableSslHostVerification()).isTrue(); + }); + } + + @Configuration + static class TrackerConfigurationBuilderCustomizerConfig { + + @Bean + TrackerConfigurationBuilderCustomizer customConnectTimeout() { + return configurationBuilder -> configurationBuilder.connectTimeout(Duration.ofMinutes(1L)); + } + + } + + @Configuration + static class TrackerConfigurationBuilderConfig { + + @Bean + TrackerConfiguration.TrackerConfigurationBuilder customTrackerConfigurationBuilder() { + return TrackerConfiguration.builder().apiEndpoint(URI.create("https://test.com/matomo.php")).disableSslHostVerification(true); + } + + } + +} \ No newline at end of file diff --git a/spring/src/test/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizerIT.java b/spring/src/test/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizerIT.java new file mode 100644 index 00000000..b62eafec --- /dev/null +++ b/spring/src/test/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizerIT.java @@ -0,0 +1,55 @@ +package org.matomo.java.tracking.spring; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.TrackerConfiguration; + +class StandardTrackerConfigurationBuilderCustomizerIT { + + @Test + void createsStandardTrackerConfigurationBuilderCustomizer() { + MatomoTrackerProperties properties = new MatomoTrackerProperties(); + properties.setApiEndpoint("https://test.com/matomo.php"); + properties.setDefaultSiteId(1); + properties.setDefaultAuthToken("abc123def4563123abc123def4563123"); + properties.setEnabled(true); + properties.setConnectTimeout(Duration.ofMinutes(1L)); + properties.setSocketTimeout(Duration.ofMinutes(2L)); + properties.setProxyHost("proxy.example.com"); + properties.setProxyPort(8080); + properties.setProxyUsername("user"); + properties.setProxyPassword("password"); + properties.setUserAgent("Mozilla/5.0 (compatible; AcmeInc/1.0; +https://example.com/bot.html)"); + properties.setLogFailedTracking(true); + properties.setDisableSslCertValidation(true); + properties.setDisableSslHostVerification(true); + properties.setThreadPoolSize(10); + StandardTrackerConfigurationBuilderCustomizer customizer = + new StandardTrackerConfigurationBuilderCustomizer(properties); + TrackerConfiguration.TrackerConfigurationBuilder builder = TrackerConfiguration.builder(); + + customizer.customize(builder); + + assertThat(customizer.getOrder()).isZero(); + TrackerConfiguration configuration = builder.build(); + assertThat(configuration.getApiEndpoint()).hasToString("https://test.com/matomo.php"); + assertThat(configuration.getDefaultSiteId()).isEqualTo(1); + assertThat(configuration.getDefaultAuthToken()).isEqualTo("abc123def4563123abc123def4563123"); + assertThat(configuration.isEnabled()).isTrue(); + assertThat(configuration.getConnectTimeout()).hasSeconds(60L); + assertThat(configuration.getSocketTimeout()).hasSeconds(120L); + assertThat(configuration.getProxyHost()).isEqualTo("proxy.example.com"); + assertThat(configuration.getProxyPort()).isEqualTo(8080); + assertThat(configuration.getProxyUsername()).isEqualTo("user"); + assertThat(configuration.getProxyPassword()).isEqualTo("password"); + assertThat(configuration.getUserAgent()).isEqualTo( + "Mozilla/5.0 (compatible; AcmeInc/1.0; +https://example.com/bot.html)"); + assertThat(configuration.isLogFailedTracking()).isTrue(); + assertThat(configuration.isDisableSslCertValidation()).isTrue(); + assertThat(configuration.isDisableSslHostVerification()).isTrue(); + assertThat(configuration.getThreadPoolSize()).isEqualTo(10); + } + +} \ No newline at end of file diff --git a/src/main/java/org/matomo/java/tracking/CustomVariable.java b/src/main/java/org/matomo/java/tracking/CustomVariable.java deleted file mode 100644 index 2d52e4d4..00000000 --- a/src/main/java/org/matomo/java/tracking/CustomVariable.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NonNull; -import lombok.ToString; -import lombok.experimental.FieldDefaults; - -/** - * A user defined custom variable. - * - * @author brettcsorba - */ -@Getter -@FieldDefaults(level = AccessLevel.PRIVATE) -@AllArgsConstructor -@ToString -@EqualsAndHashCode -public class CustomVariable { - - @NonNull - String key; - - @NonNull - String value; - -} diff --git a/src/main/java/org/matomo/java/tracking/CustomVariables.java b/src/main/java/org/matomo/java/tracking/CustomVariables.java deleted file mode 100644 index 0bbe94df..00000000 --- a/src/main/java/org/matomo/java/tracking/CustomVariables.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.EqualsAndHashCode; -import lombok.NonNull; - -import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; - -/** - * @author brettcsorba - */ -@EqualsAndHashCode -class CustomVariables { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - private final Map variables = new HashMap<>(); - - void add(@NonNull CustomVariable variable) { - boolean found = false; - for (Map.Entry entry : variables.entrySet()) { - CustomVariable customVariable = entry.getValue(); - if (customVariable.getKey().equals(variable.getKey())) { - variables.put(entry.getKey(), variable); - found = true; - } - } - if (!found) { - int i = 1; - while (variables.putIfAbsent(i, variable) != null) { - i++; - } - } - } - - void add(@NonNull CustomVariable cv, int index) { - if (index <= 0) { - throw new IllegalArgumentException("Index must be greater than 0."); - } - variables.put(index, cv); - } - - @Nullable - CustomVariable get(int index) { - if (index <= 0) { - throw new IllegalArgumentException("Index must be greater than 0."); - } - return variables.get(index); - } - - @Nullable - String get(@NonNull String key) { - return variables.values().stream().filter(variable -> variable.getKey().equals(key)).findFirst().map(CustomVariable::getValue).orElse(null); - } - - void remove(int index) { - variables.remove(index); - } - - void remove(@NonNull String key) { - variables.entrySet().removeIf(entry -> entry.getValue().getKey().equals(key)); - } - - boolean isEmpty() { - return variables.isEmpty(); - } - - @Override - public String toString() { - ObjectNode objectNode = OBJECT_MAPPER.createObjectNode(); - for (Map.Entry entry : variables.entrySet()) { - CustomVariable variable = entry.getValue(); - objectNode.putArray(entry.getKey().toString()).add(variable.getKey()).add(variable.getValue()); - } - return objectNode.toString(); - } -} diff --git a/src/main/java/org/matomo/java/tracking/EcommerceItems.java b/src/main/java/org/matomo/java/tracking/EcommerceItems.java deleted file mode 100644 index a27ae2f2..00000000 --- a/src/main/java/org/matomo/java/tracking/EcommerceItems.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import lombok.EqualsAndHashCode; -import lombok.NonNull; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.List; - -@EqualsAndHashCode -class EcommerceItems { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - private final List ecommerceItems = new ArrayList<>(); - - public void add(@NonNull EcommerceItem ecommerceItem) { - ecommerceItems.add(ecommerceItem); - } - - @Nonnull - public EcommerceItem get(int index) { - return ecommerceItems.get(index); - } - - @Override - public String toString() { - ArrayNode arrayNode = OBJECT_MAPPER.createArrayNode(); - for (EcommerceItem ecommerceItem : ecommerceItems) { - arrayNode.add(OBJECT_MAPPER.createArrayNode() - .add(ecommerceItem.getSku()) - .add(ecommerceItem.getName()) - .add(ecommerceItem.getCategory()) - .add(ecommerceItem.getPrice()) - .add(ecommerceItem.getQuantity()) - ); - } - return arrayNode.toString(); - } -} diff --git a/src/main/java/org/matomo/java/tracking/HttpClientFactory.java b/src/main/java/org/matomo/java/tracking/HttpClientFactory.java deleted file mode 100644 index 86dca94f..00000000 --- a/src/main/java/org/matomo/java/tracking/HttpClientFactory.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.matomo.java.tracking; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import org.apache.http.HttpHost; -import org.apache.http.client.HttpClient; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.conn.DefaultProxyRoutePlanner; -import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; -import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; - -import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; - -/** - * Internal factory for providing instances of HTTP clients. - * Especially {@link org.apache.http.nio.client.HttpAsyncClient} instances are intended to be global resources that share the same lifecycle as the application. - * For details see Apache documentation. - * - * @author norbertroamsys - */ -final class HttpClientFactory { - - private HttpClientFactory() { - // utility - } - - /** - * Internal key class for caching {@link CloseableHttpAsyncClient} instances. - */ - @EqualsAndHashCode - @AllArgsConstructor - private static final class KeyEntry { - - private final String proxyHost; - private final int proxyPort; - private final int timeout; - - } - - private static final Map ASYNC_INSTANCES = new HashMap<>(); - - /** - * Factory for getting a synchronous client by proxy and timeout configuration. - * The clients will be created on each call. - * - * @param proxyHost the proxy host - * @param proxyPort the proxy port - * @param timeout the timeout - * @return the created client - */ - public static HttpClient getInstanceFor(final String proxyHost, final int proxyPort, final int timeout) { - return HttpClientBuilder.create().setRoutePlanner(createRoutePlanner(proxyHost, proxyPort)).setDefaultRequestConfig(createRequestConfig(timeout)).build(); - } - - /** - * Factory for getting a asynchronous client by proxy and timeout configuration. - * The clients will be created and cached as a singleton instance. - * - * @param proxyHost the proxy host - * @param proxyPort the proxy port - * @param timeout the timeout - * @return the created client - */ - public static synchronized CloseableHttpAsyncClient getAsyncInstanceFor(final String proxyHost, final int proxyPort, final int timeout) { - return ASYNC_INSTANCES.computeIfAbsent(new KeyEntry(proxyHost, proxyPort, timeout), key -> - HttpAsyncClientBuilder.create().setRoutePlanner(createRoutePlanner(key.proxyHost, key.proxyPort)).setDefaultRequestConfig(createRequestConfig(key.timeout)).build()); - } - - @Nullable - private static DefaultProxyRoutePlanner createRoutePlanner(final String proxyHost, final int proxyPort) { - if (proxyHost != null && proxyPort != 0) { - final HttpHost proxy = new HttpHost(proxyHost, proxyPort); - return new DefaultProxyRoutePlanner(proxy); - } - return null; - } - - private static RequestConfig createRequestConfig(final int timeout) { - final RequestConfig.Builder config = RequestConfig.custom() - .setConnectTimeout(timeout) - .setConnectionRequestTimeout(timeout) - .setSocketTimeout(timeout); - return config.build(); - } - -} diff --git a/src/main/java/org/matomo/java/tracking/InvalidUrlException.java b/src/main/java/org/matomo/java/tracking/InvalidUrlException.java deleted file mode 100644 index 01d4fbcb..00000000 --- a/src/main/java/org/matomo/java/tracking/InvalidUrlException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.matomo.java.tracking; - -public class InvalidUrlException extends RuntimeException { - - public InvalidUrlException(Throwable cause) { - super(cause); - } -} diff --git a/src/main/java/org/matomo/java/tracking/MatomoBoolean.java b/src/main/java/org/matomo/java/tracking/MatomoBoolean.java deleted file mode 100644 index faa3ee92..00000000 --- a/src/main/java/org/matomo/java/tracking/MatomoBoolean.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import lombok.Value; - -/** - * Object representing a locale required by some Matomo query parameters. - * - * @author brettcsorba - */ -@Value -public class MatomoBoolean { - boolean value; - - /** - * Returns the locale's lowercase country code. - * - * @return the locale's lowercase country code - */ - @Override - public String toString() { - return value ? "1" : "0"; - } -} diff --git a/src/main/java/org/matomo/java/tracking/MatomoException.java b/src/main/java/org/matomo/java/tracking/MatomoException.java deleted file mode 100644 index 39ea492e..00000000 --- a/src/main/java/org/matomo/java/tracking/MatomoException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.matomo.java.tracking; - -public class MatomoException extends RuntimeException { - - private static final long serialVersionUID = 4592083764365938934L; - - public MatomoException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/org/matomo/java/tracking/MatomoLocale.java b/src/main/java/org/matomo/java/tracking/MatomoLocale.java deleted file mode 100644 index a561a17a..00000000 --- a/src/main/java/org/matomo/java/tracking/MatomoLocale.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -import java.util.Locale; - -/** - * Object representing a locale required by some Matomo query parameters. - * - * @author brettcsorba - */ -@Setter -@Getter -@AllArgsConstructor -public class MatomoLocale { - private Locale locale; - - /** - * Returns the locale's lowercase country code. - * - * @return the locale's lowercase country code - */ - @Override - public String toString() { - return locale.getCountry().toLowerCase(Locale.ENGLISH); - } -} diff --git a/src/main/java/org/matomo/java/tracking/MatomoRequest.java b/src/main/java/org/matomo/java/tracking/MatomoRequest.java deleted file mode 100644 index f3681161..00000000 --- a/src/main/java/org/matomo/java/tracking/MatomoRequest.java +++ /dev/null @@ -1,2172 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import com.google.common.collect.LinkedHashMultimap; -import com.google.common.collect.Multimap; -import com.google.common.io.BaseEncoding; -import lombok.NonNull; -import lombok.ToString; -import org.apache.http.client.utils.URIBuilder; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.Charset; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * A class that implements the - * Matomo Tracking HTTP API. These requests can be sent using {@link MatomoTracker}. - * - * @author brettcsorba - */ -@ToString -public class MatomoRequest { - public static final int ID_LENGTH = 16; - public static final int AUTH_TOKEN_LENGTH = 32; - - private static final String ACTION_NAME = "action_name"; - private static final String ACTION_TIME = "gt_ms"; - private static final String ACTION_URL = "url"; - private static final String API_VERSION = "apiv"; - private static final String AUTH_TOKEN = "token_auth"; - private static final String CAMPAIGN_KEYWORD = "_rck"; - private static final String CAMPAIGN_NAME = "_rcn"; - private static final String CHARACTER_SET = "cs"; - private static final String CONTENT_INTERACTION = "c_i"; - private static final String CONTENT_NAME = "c_n"; - private static final String CONTENT_PIECE = "c_p"; - private static final String CONTENT_TARGET = "c_t"; - private static final String CURRENT_HOUR = "h"; - private static final String CURRENT_MINUTE = "m"; - private static final String CURRENT_SECOND = "s"; - - private static final String CUSTOM_ACTION = "ca"; - private static final String DEVICE_RESOLUTION = "res"; - private static final String DOWNLOAD_URL = "download"; - private static final String ECOMMERCE_DISCOUNT = "ec_dt"; - private static final String ECOMMERCE_ID = "ec_id"; - private static final String ECOMMERCE_ITEMS = "ec_items"; - private static final String ECOMMERCE_LAST_ORDER_TIMESTAMP = "_ects"; - private static final String ECOMMERCE_REVENUE = "revenue"; - private static final String ECOMMERCE_SHIPPING_COST = "ec_sh"; - private static final String ECOMMERCE_SUBTOTAL = "ec_st"; - private static final String ECOMMERCE_TAX = "ec_tx"; - private static final String EVENT_ACTION = "e_a"; - private static final String EVENT_CATEGORY = "e_c"; - private static final String EVENT_NAME = "e_n"; - private static final String EVENT_VALUE = "e_v"; - private static final String HEADER_ACCEPT_LANGUAGE = "lang"; - private static final String GOAL_ID = "idgoal"; - private static final String GOAL_REVENUE = "revenue"; - private static final String HEADER_USER_AGENT = "ua"; - private static final String NEW_VISIT = "new_visit"; - private static final String OUTLINK_URL = "link"; - private static final String PAGE_CUSTOM_VARIABLE = "cvar"; - private static final String PLUGIN_DIRECTOR = "dir"; - private static final String PLUGIN_FLASH = "fla"; - private static final String PLUGIN_GEARS = "gears"; - private static final String PLUGIN_JAVA = "java"; - private static final String PLUGIN_PDF = "pdf"; - private static final String PLUGIN_QUICKTIME = "qt"; - private static final String PLUGIN_REAL_PLAYER = "realp"; - private static final String PLUGIN_SILVERLIGHT = "ag"; - private static final String PLUGIN_WINDOWS_MEDIA = "wma"; - private static final String RANDOM_VALUE = "rand"; - private static final String REFERRER_URL = "urlref"; - private static final String REQUEST_DATETIME = "cdt"; - private static final String REQUIRED = "rec"; - private static final String RESPONSE_AS_IMAGE = "send_image"; - private static final String SEARCH_CATEGORY = "search_cat"; - private static final String SEARCH_QUERY = "search"; - private static final String SEARCH_RESULTS_COUNT = "search_count"; - private static final String SITE_ID = "idsite"; - private static final String TRACK_BOT_REQUESTS = "bots"; - private static final String VISIT_CUSTOM_VARIABLE = "_cvar"; - private static final String USER_ID = "uid"; - private static final String VISITOR_CITY = "city"; - private static final String VISITOR_COUNTRY = "country"; - private static final String VISITOR_CUSTOM_ID = "cid"; - private static final String VISITOR_FIRST_VISIT_TIMESTAMP = "_idts"; - private static final String VISITOR_ID = "_id"; - private static final String VISITOR_IP = "cip"; - private static final String VISITOR_LATITUDE = "lat"; - private static final String VISITOR_LONGITUDE = "long"; - private static final String VISITOR_PREVIOUS_VISIT_TIMESTAMP = "_viewts"; - private static final String VISITOR_REGION = "region"; - private static final String VISITOR_VISIT_COUNT = "_idvc"; - - private static final int RANDOM_VALUE_LENGTH = 20; - private static final long REQUEST_DATETIME_AUTH_LIMIT = 14400000L; - private static final Pattern VISITOR_ID_PATTERN = Pattern.compile("[0-9A-Fa-f]+"); - - private final Multimap parameters = LinkedHashMultimap.create(8, 1); - - private final Set customTrackingParameterNames = new HashSet<>(2); - - /** - * Create a new request from the id of the site being tracked and the full - * url for the current action. This constructor also sets: - *

-   * {@code
-   * Required = true
-   * Visior Id = random 16 character hex string
-   * Random Value = random 20 character hex string
-   * API version = 1
-   * Response as Image = false
-   * }
-   * 
- * Overwrite these values yourself as desired. - * - * @param siteId the id of the website we're tracking a visit/action for - * @param actionUrl the full URL for the current action - */ - public MatomoRequest(int siteId, String actionUrl) { - setParameter(SITE_ID, siteId); - setBooleanParameter(REQUIRED, true); - setParameter(ACTION_URL, actionUrl); - setParameter(VISITOR_ID, getRandomHexString(ID_LENGTH)); - setParameter(RANDOM_VALUE, getRandomHexString(RANDOM_VALUE_LENGTH)); - setParameter(API_VERSION, "1"); - setBooleanParameter(RESPONSE_AS_IMAGE, false); - } - - public static MatomoRequestBuilder builder() { - return new MatomoRequestBuilder(); - } - - /** - * Get the title of the action being tracked - * - * @return the title of the action being tracked - */ - @Nullable - public String getActionName() { - return castOrNull(ACTION_NAME); - } - - /** - * Set the title of the action being tracked. It is possible to - * use slashes / - * to set one or several categories for this action. - * For example, Help / Feedback - * will create the Action Feedback in the category Help. - * - * @param actionName the title of the action to set. A null value will remove this parameter - */ - public void setActionName(String actionName) { - setParameter(ACTION_NAME, actionName); - } - - /** - * Get the amount of time it took the server to generate this action, in milliseconds. - * - * @return the amount of time - */ - @Nullable - public Long getActionTime() { - return castOrNull(ACTION_TIME); - } - - /** - * Set the amount of time it took the server to generate this action, in milliseconds. - * This value is used to process the - * Page speed report - * Avg. generation time column in the Page URL and Page Title reports, - * as well as a site wide running average of the speed of your server. - * - * @param actionTime the amount of time to set. A null value will remove this parameter - */ - public void setActionTime(Long actionTime) { - setParameter(ACTION_TIME, actionTime); - } - - /** - * Get the full URL for the current action. - * - * @return the full URL - * @deprecated Please use {@link #getActionUrlAsString} - */ - @Nullable - public URL getActionUrl() { - return castToUrlOrNull(ACTION_URL); - } - - /** - * Get the full URL for the current action. - * - * @return the full URL - */ - @Deprecated - @Nullable - public String getActionUrlAsString() { - return castOrNull(ACTION_URL); - } - - - /** - * Set the full URL for the current action. - * - * @param actionUrl the full URL to set. A null value will remove this parameter - * @deprecated Please use {@link #setActionUrl(String)} - */ - @Deprecated - public void setActionUrl(@NonNull URL actionUrl) { - setActionUrl(actionUrl.toString()); - } - - /** - * Set the full URL for the current action. - * - * @param actionUrl the full URL to set. A null value will remove this parameter - */ - public void setActionUrl(String actionUrl) { - setParameter(ACTION_URL, actionUrl); - } - - /** - * Set the full URL for the current action. - * - * @param actionUrl the full URL to set. A null value will remove this parameter - * @deprecated Please use {@link #setActionUrl(String)} - */ - @Deprecated - public void setActionUrlWithString(String actionUrl) { - setActionUrl(actionUrl); - } - - /** - * Get the api version - * - * @return the api version - */ - @Nullable - public String getApiVersion() { - return castOrNull(API_VERSION); - } - - /** - * Set the api version to use (currently always set to 1) - * - * @param apiVersion the api version to set. A null value will remove this parameter - */ - public void setApiVersion(String apiVersion) { - setParameter(API_VERSION, apiVersion); - } - - /** - * Get the authorization key. - * - * @return the authorization key - */ - @Nullable - public String getAuthToken() { - return castOrNull(AUTH_TOKEN); - } - - /** - * Set the {@value #AUTH_TOKEN_LENGTH} character authorization key used to authenticate the API request. - * - * @param authToken the authorization key to set. A null value will remove this parameter - */ - public void setAuthToken(String authToken) { - if (authToken != null && authToken.length() != AUTH_TOKEN_LENGTH) { - throw new IllegalArgumentException(authToken + " is not " + AUTH_TOKEN_LENGTH + " characters long."); - } - setParameter(AUTH_TOKEN, authToken); - } - - /** - * Verifies that AuthToken has been set for this request. Will throw an - * {@link IllegalStateException} if not. - */ - public void verifyAuthTokenSet() { - if (getAuthToken() == null) { - throw new IllegalStateException("AuthToken must be set before this value can be set."); - } - } - - /** - * Get the campaign keyword - * - * @return the campaign keyword - */ - @Nullable - public String getCampaignKeyword() { - return castOrNull(CAMPAIGN_KEYWORD); - } - - /** - * Set the Campaign Keyword (see - * Tracking Campaigns). - * Used to populate the Referrers > Campaigns report (clicking on a - * campaign loads all keywords for this campaign). Note: this parameter - * will only be used for the first pageview of a visit. - * - * @param campaignKeyword the campaign keyword to set. A null value will remove this parameter - */ - public void setCampaignKeyword(String campaignKeyword) { - setParameter(CAMPAIGN_KEYWORD, campaignKeyword); - } - - /** - * Get the campaign name - * - * @return the campaign name - */ - @Nullable - public String getCampaignName() { - return castOrNull(CAMPAIGN_NAME); - } - - /** - * Set the Campaign Name (see - * Tracking Campaigns). - * Used to populate the Referrers > Campaigns report. Note: this parameter - * will only be used for the first pageview of a visit. - * - * @param campaignName the campaign name to set. A null value will remove this parameter - */ - public void setCampaignName(String campaignName) { - setParameter(CAMPAIGN_NAME, campaignName); - } - - /** - * Get the charset of the page being tracked - * - * @return the charset - */ - @Nullable - public Charset getCharacterSet() { - return castOrNull(CHARACTER_SET); - } - - /** - * The charset of the page being tracked. Specify the charset if the data - * you send to Matomo is encoded in a different character set than the default - * utf-8. - * - * @param characterSet the charset to set. A null value will remove this parameter - */ - public void setCharacterSet(Charset characterSet) { - setParameter(CHARACTER_SET, characterSet); - } - - /** - * Get the name of the interaction with the content - * - * @return the name of the interaction - */ - @Nullable - public String getContentInteraction() { - return castOrNull(CONTENT_INTERACTION); - } - - /** - * Set the name of the interaction with the content. For instance a 'click'. - * - * @param contentInteraction the name of the interaction to set. A null value will remove this parameter - */ - public void setContentInteraction(String contentInteraction) { - setParameter(CONTENT_INTERACTION, contentInteraction); - } - - /** - * Get the name of the content - * - * @return the name - */ - @Nullable - public String getContentName() { - return castOrNull(CONTENT_NAME); - } - - /** - * Set the name of the content. For instance 'Ad Foo Bar'. - * - * @param contentName the name to set. A null value will remove this parameter - */ - public void setContentName(String contentName) { - setParameter(CONTENT_NAME, contentName); - } - - /** - * Get the content piece. - * - * @return the content piece. - */ - @Nullable - public String getContentPiece() { - return castOrNull(CONTENT_PIECE); - } - - /** - * Set the actual content piece. For instance the path to an image, video, audio, any text. - * - * @param contentPiece the content piece to set. A null value will remove this parameter - */ - public void setContentPiece(String contentPiece) { - setParameter(CONTENT_PIECE, contentPiece); - } - - /** - * Get the content target - * - * @return the target - */ - @Nullable - public URL getContentTarget() { - return castToUrlOrNull(CONTENT_TARGET); - } - - @Nullable - private URL castToUrlOrNull(@NonNull String key) { - String url = castOrNull(key); - if (url == null) { - return null; - } - try { - return new URL(url); - } catch (MalformedURLException e) { - throw new InvalidUrlException(e); - } - } - - /** - * Get the content target - * - * @return the target - */ - @Nullable - public String getContentTargetAsString() { - return castOrNull(CONTENT_TARGET); - } - - /** - * Set the target of the content. For instance the URL of a landing page. - * - * @param contentTarget the target to set. A null value will remove this parameter - * @deprecated Please use {@link #setContentTarget(String)} - */ - @Deprecated - public void setContentTarget(@NonNull URL contentTarget) { - setContentTarget(contentTarget.toString()); - } - - /** - * Set the target of the content. For instance the URL of a landing page. - * - * @param contentTarget the target to set. A null value will remove this parameter - */ - public void setContentTarget(String contentTarget) { - setParameter(CONTENT_TARGET, contentTarget); - } - - /** - * Set the target of the content. For instance the URL of a landing page. - * - * @param contentTarget the target to set. A null value will remove this parameter - * @deprecated Please use {@link #setContentTarget(String)} - */ - @Deprecated - public void setContentTargetWithString(String contentTarget) { - setContentTarget(contentTarget); - } - - /** - * Get the current hour. - * - * @return the current hour - */ - @Nullable - public Integer getCurrentHour() { - return castOrNull(CURRENT_HOUR); - } - - /** - * Set the current hour (local time). - * - * @param currentHour the hour to set. A null value will remove this parameter - */ - public void setCurrentHour(Integer currentHour) { - setParameter(CURRENT_HOUR, currentHour); - } - - /** - * Get the current minute. - * - * @return the current minute - */ - @Nullable - public Integer getCurrentMinute() { - return castOrNull(CURRENT_MINUTE); - } - - /** - * Set the current minute (local time). - * - * @param currentMinute the minute to set. A null value will remove this parameter - */ - public void setCurrentMinute(Integer currentMinute) { - setParameter(CURRENT_MINUTE, currentMinute); - } - - /** - * Get the current second - * - * @return the current second - */ - @Nullable - public Integer getCurrentSecond() { - return castOrNull(CURRENT_SECOND); - } - - /** - * Set the current second (local time). - * - * @param currentSecond the second to set. A null value will remove this parameter - */ - public void setCurrentSecond(Integer currentSecond) { - setParameter(CURRENT_SECOND, currentSecond); - } - - /** - * Get the custom action - * - * @return the custom action - */ - @Nullable - public Boolean getCustomAction() { - return getBooleanParameter(CUSTOM_ACTION); - } - - /** - * Set the custom action - * - * @param customAction the second to set. A null value will remove this parameter - */ - public void setCustomAction(Boolean customAction) { - setBooleanParameter(CUSTOM_ACTION, customAction); - } - - /** - * Gets the list of objects currently stored at the specified custom tracking - * parameter. An empty list will be returned if there are no objects set at - * that key. - * - * @param key the key of the parameter whose list of objects to get. Cannot be null - * @return the list of objects currently stored at the specified key - */ - public List getCustomTrackingParameter(@NonNull String key) { - return new ArrayList<>(parameters.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 - */ - public void setCustomTrackingParameter(@NonNull String key, @Nullable T value) { - customTrackingParameterNames.add(key); - setParameter(key, value); - } - - /** - * 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(@NonNull String key, @NonNull Object value) { - customTrackingParameterNames.add(key); - addParameter(key, value); - } - - /** - * Removes all custom tracking parameters - */ - public void clearCustomTrackingParameter() { - for (String customTrackingParameterName : customTrackingParameterNames) { - setParameter(customTrackingParameterName, null); - } - } - - /** - * Get the resolution of the device - * - * @return the resolution - */ - @Nullable - public String getDeviceResolution() { - return castOrNull(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 - */ - @Nullable - public URL getDownloadUrl() { - return castToUrlOrNull(DOWNLOAD_URL); - } - - /** - * Get the url of a file the user had downloaded - * - * @return the url - */ - @Nullable - public String getDownloadUrlAsString() { - return castOrNull(DOWNLOAD_URL); - } - - /** - * 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 - * @deprecated Please use {@link #setDownloadUrl(String)} - */ - @Deprecated - public void setDownloadUrl(@NonNull URL downloadUrl) { - setDownloadUrl(downloadUrl.toString()); - } - - /** - * 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(String 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 - * @deprecated Please use {@link #setDownloadUrl(String)} - */ - @Deprecated - public void setDownloadUrlWithString(String downloadUrl) { - setDownloadUrl(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 - */ - @Nullable - public Double getEcommerceDiscount() { - return castOrNull(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 - */ - @Nullable - public String getEcommerceId() { - return castOrNull(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 - */ - @Nullable - public EcommerceItem getEcommerceItem(int index) { - EcommerceItems ecommerceItems = castOrNull(ECOMMERCE_ITEMS); - if (ecommerceItems == null) { - 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 - */ - public void addEcommerceItem(@NonNull EcommerceItem item) { - verifyEcommerceState(); - EcommerceItems ecommerceItems = castOrNull(ECOMMERCE_ITEMS); - if (ecommerceItems == null) { - ecommerceItems = new EcommerceItems(); - setParameter(ECOMMERCE_ITEMS, ecommerceItems); - } - ecommerceItems.add(item); - } - - /** - * Clears all {@link EcommerceItem} from this order. - */ - public void clearEcommerceItems() { - setParameter(ECOMMERCE_ITEMS, null); - } - - /** - * Get the timestamp of the customer's last ecommerce order - * - * @return the timestamp - */ - @Nullable - public Long getEcommerceLastOrderTimestamp() { - return castOrNull(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 - */ - @Nullable - public Double getEcommerceRevenue() { - return castOrNull(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 - */ - @Nullable - public Double getEcommerceShippingCost() { - return castOrNull(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 - */ - @Nullable - public Double getEcommerceSubtotal() { - return castOrNull(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 - */ - @Nullable - public Double getEcommerceTax() { - return castOrNull(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 - */ - @Nullable - public String getEventAction() { - return castOrNull(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 - */ - @Nullable - public String getEventCategory() { - return castOrNull(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 - */ - @Nullable - public String getEventName() { - return castOrNull(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 - */ - @Nullable - public Number getEventValue() { - return castOrNull(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 - */ - @Nullable - public Integer getGoalId() { - return castOrNull(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 - */ - @Nullable - public Double getGoalRevenue() { - return castOrNull(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 - */ - @Nullable - public String getHeaderAcceptLanguage() { - return castOrNull(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 - */ - @Nullable - public String getHeaderUserAgent() { - return castOrNull(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 - */ - @Nullable - 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 - */ - @Nullable - public URL getOutlinkUrl() { - return castToUrlOrNull(OUTLINK_URL); - } - - /** - * Get the outlink url - * - * @return the outlink url - */ - @Nullable - public String getOutlinkUrlAsString() { - return castOrNull(OUTLINK_URL); - } - - /** - * 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 - * @deprecated Please use {@link #setOutlinkUrl(String)} - */ - @Deprecated - public void setOutlinkUrl(@NonNull URL outlinkUrl) { - setOutlinkUrl(outlinkUrl.toString()); - } - - - /** - * 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(String 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 - * @deprecated Please use {@link #setOutlinkUrl(String)} - */ - @Deprecated - public void setOutlinkUrlWithString(String outlinkUrl) { - setOutlinkUrl(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. - */ - @Nullable - @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 - */ - @Nullable - 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 - */ - @Nullable - 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 - */ - @Nullable - 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 - */ - @Nullable - 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 - */ - @Nullable - 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 - */ - @Nullable - 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 - */ - @Nullable - 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 - */ - @Nullable - 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 - */ - @Nullable - 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 - */ - @Nullable - 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 - */ - @Nullable - 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 - */ - @Nullable - public String getRandomValue() { - return castOrNull(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 - */ - @Nullable - public URL getReferrerUrl() { - return castToUrlOrNull(REFERRER_URL); - } - - /** - * Get the referrer url - * - * @return the referrer url - */ - @Nullable - public String getReferrerUrlAsString() { - return castOrNull(REFERRER_URL); - } - - /** - * 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 - * @deprecated Please use {@link #setReferrerUrl(String)} - */ - @Deprecated - public void setReferrerUrl(@NonNull URL referrerUrl) { - setReferrerUrl(referrerUrl.toString()); - } - - /** - * 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(String 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 - * @deprecated Please use {@link #setReferrerUrl(String)} - */ - @Deprecated - public void setReferrerUrlWithString(String referrerUrl) { - setReferrerUrl(referrerUrl); - } - - /** - * Get the datetime of the request - * - * @return the datetime of the request - */ - @Nullable - public MatomoDate getRequestDatetime() { - return castOrNull(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 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 datetime the datetime of the request to set. A null value will remove this parameter - */ - public void setRequestDatetime(MatomoDate 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 - */ - @Nullable - public Boolean getRequired() { - return getBooleanParameter(REQUIRED); - } - - /** - * Set if this request will be tracked by the Matomo 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 - */ - @Nullable - public Boolean getResponseAsImage() { - return getBooleanParameter(RESPONSE_AS_IMAGE); - } - - /** - * Set if the response will be an image. If set to false, Matomo 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 Matomo 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 - */ - @Nullable - public String getSearchCategory() { - return castOrNull(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 - */ - @Nullable - public String getSearchQuery() { - return castOrNull(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 - */ - @Nullable - public Long getSearchResultsCount() { - return castOrNull(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 - */ - @Nullable - public Integer getSiteId() { - return castOrNull(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 - */ - @Nullable - public Boolean getTrackBotRequests() { - return getBooleanParameter(TRACK_BOT_REQUESTS); - } - - /** - * By default Matomo does not track bots. If you use the Tracking Java API, - * you may be interested in tracking bot requests. To enable Bot Tracking in - * Matomo, 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. - */ - @Nullable - @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 - */ - @Nullable - 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 - */ - @Nullable - public String getUserId() { - return castOrNull(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 Matomo. - * 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 - */ - @Nullable - public String getVisitorCity() { - return castOrNull(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 - */ - @Nullable - public MatomoLocale getVisitorCountry() { - return castOrNull(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(MatomoLocale country) { - if (country != null) { - verifyAuthTokenSet(); - } - setParameter(VISITOR_COUNTRY, country); - } - - /** - * Get the visitor's custom id. - * - * @return the visitor's custom id - */ - @Nullable - public String getVisitorCustomId() { - return castOrNull(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 - if (!VISITOR_ID_PATTERN.matcher(visitorCustomId).matches()) { - 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 - */ - @Nullable - public Long getVisitorFirstVisitTimestamp() { - return castOrNull(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 - */ - @Nullable - public String getVisitorId() { - return castOrNull(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 Matomo 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 - if (!VISITOR_ID_PATTERN.matcher(visitorId).matches()) { - throw new IllegalArgumentException(visitorId + " is not a hexadecimal string."); - } - } - setParameter(VISITOR_ID, visitorId); - } - - /** - * Get the visitor's ip. - * - * @return the visitor's ip - */ - @Nullable - public String getVisitorIp() { - return castOrNull(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 - */ - @Nullable - public Double getVisitorLatitude() { - return castOrNull(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 - */ - @Nullable - public Double getVisitorLongitude() { - return castOrNull(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 - */ - @Nullable - public Long getVisitorPreviousVisitTimestamp() { - return castOrNull(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 - */ - @Nullable - public String getVisitorRegion() { - return castOrNull(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 - */ - @Nullable - public Integer getVisitorVisitCount() { - return castOrNull(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); - } - - public Map> getParameters() { - return parameters.asMap(); - } - - /** - * Get the query string represented by this object. - * - * @return the query string represented by this object - * @deprecated Use {@link URIBuilder} in conjunction with {@link #getParameters()} and {@link QueryParameters#fromMap(Map)} ()} instead - */ - @Nonnull - @Deprecated - public String getQueryString() { - return parameters.entries().stream().map(parameter -> parameter.getKey() + '=' + parameter.getValue().toString()).collect(Collectors.joining("&")); - } - - /** - * Get the url encoded query string represented by this object. - * - * @return the url encoded query string represented by this object - * @deprecated Use {@link URIBuilder} in conjunction with {@link #getParameters()} and {@link QueryParameters#fromMap(Map)} ()} instead - */ - @Nonnull - @Deprecated - public String getUrlEncodedQueryString() { - String queryString = new URIBuilder().setParameters(QueryParameters.fromMap(getParameters())).toString(); - if (queryString.isEmpty()) { - return ""; - } - return queryString.substring(1); - } - - /** - * 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 - */ - @Nonnull - public static String getRandomHexString(int length) { - byte[] bytes = new byte[length / 2]; - new SecureRandom().nextBytes(bytes); - return BaseEncoding.base16().lowerCase().encode(bytes); - } - - /** - * Set a stored parameter. - * - * @param key the parameter's key - * @param value the parameter's value. Removes the parameter if null - */ - public void setParameter(@NonNull String key, @Nullable Object value) { - parameters.removeAll(key); - if (value != null) { - addParameter(key, value); - } - } - - /** - * Add more values to the given parameter - * - * @param key the parameter's key. Must not be null - * @param value the parameter's value. Must not be null - */ - public void addParameter(@NonNull String key, @NonNull Object value) { - parameters.put(key, value); - } - - - /** - * Get a stored parameter that and cast it if present - * - * @param key the parameter's key. Must not be null - * @return the stored parameter's value casted to the requested type or null if no value is present - */ - @Nullable - private T castOrNull(@NonNull String key) { - Collection values = parameters.get(key); - if (values.isEmpty()) { - return null; - } - return (T) values.iterator().next(); - } - - /** - * 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(@NonNull String key, String value) { - if (value != null && value.trim().isEmpty()) { - throw new IllegalArgumentException("Value cannot be empty."); - } - setParameter(key, value); - } - - /** - * Get a stored parameter that is a boolean. - * - * @param key the parameter's key - * @return the stored parameter's value - */ - @Nullable - private Boolean getBooleanParameter(@NonNull String key) { - MatomoBoolean matomoBoolean = castOrNull(key); - if (matomoBoolean == null) { - return null; - } - return matomoBoolean.isValue(); - } - - /** - * Set a stored parameter that is a boolean. - * - * @param key the parameter's key - * @param value the parameter's value. Removes the parameter if null - */ - private void setBooleanParameter(@NonNull String key, @Nullable Boolean value) { - if (value == null) { - setParameter(key, null); - } else { - setParameter(key, new MatomoBoolean(value)); - } - } - - /** - * 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 index the index of the value. - * @return the value at the specified index - */ - @Nullable - private CustomVariable getCustomVariable(@NonNull String parameter, int index) { - CustomVariables customVariables = castOrNull(parameter); - if (customVariables == null) { - return null; - } - return customVariables.get(index); - } - - @Nullable - private String getCustomVariable(@NonNull String parameter, @NonNull String key) { - CustomVariables customVariables = castOrNull(parameter); - if (customVariables == null) { - return null; - } - return customVariables.get(key); - } - - /** - * Store a value in a json object at the specified parameter. - * - * @param parameter the parameter to store the json object at - * @param customVariable the value. Removes the parameter if null - * @param index the custom variable index - */ - private void setCustomVariable(@NonNull String parameter, @Nullable CustomVariable customVariable, Integer index) { - - if (customVariable == null && index == null) { - throw new IllegalArgumentException("Either custom variable or index must be set"); - } - CustomVariables customVariables = castOrNull(parameter); - if (customVariables == null) { - customVariables = new CustomVariables(); - setParameter(parameter, customVariables); - } - if (customVariable == null) { - customVariables.remove(index); - if (customVariables.isEmpty()) { - setParameter(parameter, null); - } - } else if (index == null) { - customVariables.add(customVariable); - } else { - customVariables.add(customVariable, index); - } - } - - private void removeCustomVariable(@NonNull String parameter, @NonNull String key) { - CustomVariables customVariables = castOrNull(parameter); - if (customVariables != null) { - customVariables.remove(key); - if (customVariables.isEmpty()) { - setParameter(parameter, null); - } - } - } - -} diff --git a/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java b/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java deleted file mode 100644 index 712a0be9..00000000 --- a/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java +++ /dev/null @@ -1,643 +0,0 @@ -package org.matomo.java.tracking; - -import java.nio.charset.Charset; -import java.util.List; -import java.util.Map; - -public class MatomoRequestBuilder { - - private int siteId; - - private String actionUrl; - - private String actionName; - private Long actionTime; - private String apiVersion; - private String authToken; - private String campaignKeyword; - private String campaignName; - private Charset characterSet; - private String contentInteraction; - private String contentName; - private String contentPiece; - private String contentTarget; - private Integer currentHour; - private Integer currentMinute; - private Integer currentSecond; - - private Boolean customAction; - private String deviceResolution; - private String downloadUrl; - private Double ecommerceDiscount; - private String ecommerceId; - private Long ecommerceLastOrderTimestamp; - private Double ecommerceRevenue; - private Double ecommerceShippingCost; - private Double ecommerceSubtotal; - private Double ecommerceTax; - private String eventAction; - private String eventCategory; - private String eventName; - private Number eventValue; - private Integer goalId; - private Double goalRevenue; - private String headerAcceptLanguage; - private String headerUserAgent; - private Boolean newVisit; - private String outlinkUrl; - private Boolean pluginDirector; - private Boolean pluginFlash; - private Boolean pluginGears; - private Boolean pluginJava; - private Boolean pluginPDF; - private Boolean pluginQuicktime; - private Boolean pluginRealPlayer; - private Boolean pluginSilverlight; - private Boolean pluginWindowsMedia; - private String randomValue; - private String referrerUrl; - private MatomoDate requestDatetime; - private Boolean required; - private Boolean responseAsImage; - private String searchCategory; - private String searchQuery; - private Long searchResultsCount; - private Boolean trackBotRequests; - private String userId; - private String visitorCity; - private MatomoLocale visitorCountry; - private String visitorCustomId; - private Long visitorFirstVisitTimestamp; - private String visitorId; - private String visitorIp; - private Double visitorLatitude; - private Double visitorLongitude; - private Long visitorPreviousVisitTimestamp; - private String visitorRegion; - private Integer visitorVisitCount; - - private List visitCustomVariables; - - private List pageCustomVariables; - private Map customTrackingParameters; - - public MatomoRequestBuilder siteId(int siteId) { - this.siteId = siteId; - return this; - } - - public MatomoRequestBuilder actionUrl(String actionUrl) { - this.actionUrl = actionUrl; - return this; - } - - public MatomoRequestBuilder actionName(String actionName) { - this.actionName = actionName; - return this; - } - - public MatomoRequestBuilder actionTime(Long actionTime) { - this.actionTime = actionTime; - return this; - } - - public MatomoRequestBuilder apiVersion(String apiVersion) { - this.apiVersion = apiVersion; - return this; - } - - public MatomoRequestBuilder authToken(String authToken) { - this.authToken = authToken; - return this; - } - - public MatomoRequestBuilder campaignKeyword(String campaignKeyword) { - this.campaignKeyword = campaignKeyword; - return this; - } - - public MatomoRequestBuilder campaignName(String campaignName) { - this.campaignName = campaignName; - return this; - } - - public MatomoRequestBuilder characterSet(Charset characterSet) { - this.characterSet = characterSet; - return this; - } - - public MatomoRequestBuilder contentInteraction(String contentInteraction) { - this.contentInteraction = contentInteraction; - return this; - } - - public MatomoRequestBuilder contentName(String contentName) { - this.contentName = contentName; - return this; - } - - public MatomoRequestBuilder contentPiece(String contentPiece) { - this.contentPiece = contentPiece; - return this; - } - - public MatomoRequestBuilder contentTarget(String contentTarget) { - this.contentTarget = contentTarget; - return this; - } - - public MatomoRequestBuilder currentHour(Integer currentHour) { - this.currentHour = currentHour; - return this; - } - - public MatomoRequestBuilder currentMinute(Integer currentMinute) { - this.currentMinute = currentMinute; - return this; - } - - public MatomoRequestBuilder currentSecond(Integer currentSecond) { - this.currentSecond = currentSecond; - return this; - } - - public MatomoRequestBuilder customAction(Boolean customAction) { - this.customAction = customAction; - return this; - } - - public MatomoRequestBuilder deviceResolution(String deviceResolution) { - this.deviceResolution = deviceResolution; - return this; - } - - public MatomoRequestBuilder downloadUrl(String downloadUrl) { - this.downloadUrl = downloadUrl; - return this; - } - - public MatomoRequestBuilder ecommerceDiscount(Double ecommerceDiscount) { - this.ecommerceDiscount = ecommerceDiscount; - return this; - } - - public MatomoRequestBuilder ecommerceId(String ecommerceId) { - this.ecommerceId = ecommerceId; - return this; - } - - public MatomoRequestBuilder ecommerceLastOrderTimestamp(Long ecommerceLastOrderTimestamp) { - this.ecommerceLastOrderTimestamp = ecommerceLastOrderTimestamp; - return this; - } - - public MatomoRequestBuilder ecommerceRevenue(Double ecommerceRevenue) { - this.ecommerceRevenue = ecommerceRevenue; - return this; - } - - public MatomoRequestBuilder ecommerceShippingCost(Double ecommerceShippingCost) { - this.ecommerceShippingCost = ecommerceShippingCost; - return this; - } - - public MatomoRequestBuilder ecommerceSubtotal(Double ecommerceSubtotal) { - this.ecommerceSubtotal = ecommerceSubtotal; - return this; - } - - public MatomoRequestBuilder ecommerceTax(Double ecommerceTax) { - this.ecommerceTax = ecommerceTax; - return this; - } - - public MatomoRequestBuilder eventAction(String eventAction) { - this.eventAction = eventAction; - return this; - } - - public MatomoRequestBuilder eventCategory(String eventCategory) { - this.eventCategory = eventCategory; - return this; - } - - public MatomoRequestBuilder eventName(String eventName) { - this.eventName = eventName; - return this; - } - - public MatomoRequestBuilder eventValue(Number eventValue) { - this.eventValue = eventValue; - return this; - } - - public MatomoRequestBuilder goalId(Integer goalId) { - this.goalId = goalId; - return this; - } - - public MatomoRequestBuilder goalRevenue(Double goalRevenue) { - this.goalRevenue = goalRevenue; - return this; - } - - public MatomoRequestBuilder headerAcceptLanguage(String headerAcceptLanguage) { - this.headerAcceptLanguage = headerAcceptLanguage; - return this; - } - - public MatomoRequestBuilder headerUserAgent(String headerUserAgent) { - this.headerUserAgent = headerUserAgent; - return this; - } - - public MatomoRequestBuilder newVisit(Boolean newVisit) { - this.newVisit = newVisit; - return this; - } - - public MatomoRequestBuilder outlinkUrl(String outlinkUrl) { - this.outlinkUrl = outlinkUrl; - return this; - } - - public MatomoRequestBuilder pluginDirector(Boolean pluginDirector) { - this.pluginDirector = pluginDirector; - return this; - } - - public MatomoRequestBuilder pluginFlash(Boolean pluginFlash) { - this.pluginFlash = pluginFlash; - return this; - } - - public MatomoRequestBuilder pluginGears(Boolean pluginGears) { - this.pluginGears = pluginGears; - return this; - } - - public MatomoRequestBuilder pluginJava(Boolean pluginJava) { - this.pluginJava = pluginJava; - return this; - } - - public MatomoRequestBuilder pluginPDF(Boolean pluginPDF) { - this.pluginPDF = pluginPDF; - return this; - } - - public MatomoRequestBuilder pluginQuicktime(Boolean pluginQuicktime) { - this.pluginQuicktime = pluginQuicktime; - return this; - } - - public MatomoRequestBuilder pluginRealPlayer(Boolean pluginRealPlayer) { - this.pluginRealPlayer = pluginRealPlayer; - return this; - } - - public MatomoRequestBuilder pluginSilverlight(Boolean pluginSilverlight) { - this.pluginSilverlight = pluginSilverlight; - return this; - } - - public MatomoRequestBuilder pluginWindowsMedia(Boolean pluginWindowsMedia) { - this.pluginWindowsMedia = pluginWindowsMedia; - return this; - } - - public MatomoRequestBuilder randomValue(String randomValue) { - this.randomValue = randomValue; - return this; - } - - public MatomoRequestBuilder referrerUrl(String referrerUrl) { - this.referrerUrl = referrerUrl; - return this; - } - - public MatomoRequestBuilder requestDatetime(MatomoDate requestDatetime) { - this.requestDatetime = requestDatetime; - return this; - } - - public MatomoRequestBuilder required(Boolean required) { - this.required = required; - return this; - } - - public MatomoRequestBuilder responseAsImage(Boolean responseAsImage) { - this.responseAsImage = responseAsImage; - return this; - } - - public MatomoRequestBuilder searchCategory(String searchCategory) { - this.searchCategory = searchCategory; - return this; - } - - public MatomoRequestBuilder searchQuery(String searchQuery) { - this.searchQuery = searchQuery; - return this; - } - - public MatomoRequestBuilder searchResultsCount(Long searchResultsCount) { - this.searchResultsCount = searchResultsCount; - return this; - } - - public MatomoRequestBuilder trackBotRequests(Boolean trackBotRequests) { - this.trackBotRequests = trackBotRequests; - return this; - } - - public MatomoRequestBuilder userId(String userId) { - this.userId = userId; - return this; - } - - public MatomoRequestBuilder visitorCity(String visitorCity) { - this.visitorCity = visitorCity; - return this; - } - - public MatomoRequestBuilder visitorCountry(MatomoLocale visitorCountry) { - this.visitorCountry = visitorCountry; - return this; - } - - public MatomoRequestBuilder visitorCustomId(String visitorCustomId) { - this.visitorCustomId = visitorCustomId; - return this; - } - - public MatomoRequestBuilder visitorFirstVisitTimestamp(Long visitorFirstVisitTimestamp) { - this.visitorFirstVisitTimestamp = visitorFirstVisitTimestamp; - return this; - } - - public MatomoRequestBuilder visitorId(String visitorId) { - this.visitorId = visitorId; - return this; - } - - public MatomoRequestBuilder visitorIp(String visitorIp) { - this.visitorIp = visitorIp; - return this; - } - - public MatomoRequestBuilder visitorLatitude(Double visitorLatitude) { - this.visitorLatitude = visitorLatitude; - return this; - } - - public MatomoRequestBuilder visitorLongitude(Double visitorLongitude) { - this.visitorLongitude = visitorLongitude; - return this; - } - - public MatomoRequestBuilder visitorPreviousVisitTimestamp(Long visitorPreviousVisitTimestamp) { - this.visitorPreviousVisitTimestamp = visitorPreviousVisitTimestamp; - return this; - } - - public MatomoRequestBuilder visitorRegion(String visitorRegion) { - this.visitorRegion = visitorRegion; - return this; - } - - public MatomoRequestBuilder visitorVisitCount(Integer visitorVisitCount) { - this.visitorVisitCount = visitorVisitCount; - return this; - } - - public MatomoRequestBuilder visitCustomVariables(List visitCustomVariables) { - this.visitCustomVariables = visitCustomVariables; - return this; - } - - public MatomoRequestBuilder pageCustomVariables(List pageCustomVariables) { - this.pageCustomVariables = pageCustomVariables; - return this; - } - - public MatomoRequestBuilder customTrackingParameters(Map customTrackingParameters) { - this.customTrackingParameters = customTrackingParameters; - return this; - } - - public MatomoRequest build() { - MatomoRequest matomoRequest = new MatomoRequest(siteId, actionUrl); - if (actionName != null) { - matomoRequest.setActionName(actionName); - } - if (actionTime != null) { - matomoRequest.setActionTime(actionTime); - } - if (apiVersion != null) { - matomoRequest.setApiVersion(apiVersion); - } - if (authToken != null) { - matomoRequest.setAuthToken(authToken); - } - if (campaignKeyword != null) { - matomoRequest.setCampaignKeyword(campaignKeyword); - } - if (campaignName != null) { - matomoRequest.setCampaignName(campaignName); - } - if (characterSet != null) { - matomoRequest.setCharacterSet(characterSet); - } - if (contentInteraction != null) { - matomoRequest.setContentInteraction(contentInteraction); - } - if (contentName != null) { - matomoRequest.setContentName(contentName); - } - if (contentPiece != null) { - matomoRequest.setContentPiece(contentPiece); - } - if (contentTarget != null) { - matomoRequest.setContentTarget(contentTarget); - } - if (currentHour != null) { - matomoRequest.setCurrentHour(currentHour); - } - if (currentMinute != null) { - matomoRequest.setCurrentMinute(currentMinute); - } - if (currentSecond != null) { - matomoRequest.setCurrentSecond(currentSecond); - } - if (customAction != null) { - matomoRequest.setCustomAction(customAction); - } - if (customTrackingParameters != null) { - for (Map.Entry customTrackingParameter : customTrackingParameters.entrySet()) { - matomoRequest.addCustomTrackingParameter(customTrackingParameter.getKey(), customTrackingParameter.getValue()); - } - } - if (deviceResolution != null) { - matomoRequest.setDeviceResolution(deviceResolution); - } - if (downloadUrl != null) { - matomoRequest.setDownloadUrl(downloadUrl); - } - if (ecommerceDiscount != null) { - matomoRequest.setEcommerceDiscount(ecommerceDiscount); - } - if (ecommerceId != null) { - matomoRequest.setEcommerceId(ecommerceId); - } - if (ecommerceLastOrderTimestamp != null) { - matomoRequest.setEcommerceLastOrderTimestamp(ecommerceLastOrderTimestamp); - } - if (ecommerceRevenue != null) { - matomoRequest.setEcommerceRevenue(ecommerceRevenue); - } - if (ecommerceShippingCost != null) { - matomoRequest.setEcommerceShippingCost(ecommerceShippingCost); - } - if (ecommerceSubtotal != null) { - matomoRequest.setEcommerceSubtotal(ecommerceSubtotal); - } - if (ecommerceTax != null) { - matomoRequest.setEcommerceTax(ecommerceTax); - } - if (eventAction != null) { - matomoRequest.setEventAction(eventAction); - } - if (eventCategory != null) { - matomoRequest.setEventCategory(eventCategory); - } - if (eventName != null) { - matomoRequest.setEventName(eventName); - } - if (eventValue != null) { - matomoRequest.setEventValue(eventValue); - } - if (goalId != null) { - matomoRequest.setGoalId(goalId); - } - if (goalRevenue != null) { - matomoRequest.setGoalRevenue(goalRevenue); - } - if (headerAcceptLanguage != null) { - matomoRequest.setHeaderAcceptLanguage(headerAcceptLanguage); - } - if (headerUserAgent != null) { - matomoRequest.setHeaderUserAgent(headerUserAgent); - } - if (newVisit != null) { - matomoRequest.setNewVisit(newVisit); - } - if (outlinkUrl != null) { - matomoRequest.setOutlinkUrl(outlinkUrl); - } - if (pageCustomVariables != null) { - for (int i = 0; i < pageCustomVariables.size(); i++) { - CustomVariable pageCustomVariable = pageCustomVariables.get(i); - matomoRequest.setPageCustomVariable(pageCustomVariable, i + 1); - } - } - if (pluginDirector != null) { - matomoRequest.setPluginDirector(pluginDirector); - } - if (pluginFlash != null) { - matomoRequest.setPluginFlash(pluginFlash); - } - if (pluginGears != null) { - matomoRequest.setPluginGears(pluginGears); - } - if (pluginJava != null) { - matomoRequest.setPluginJava(pluginJava); - } - if (pluginPDF != null) { - matomoRequest.setPluginPDF(pluginPDF); - } - if (pluginQuicktime != null) { - matomoRequest.setPluginQuicktime(pluginQuicktime); - } - if (pluginRealPlayer != null) { - matomoRequest.setPluginRealPlayer(pluginRealPlayer); - } - if (pluginSilverlight != null) { - matomoRequest.setPluginSilverlight(pluginSilverlight); - } - if (pluginWindowsMedia != null) { - matomoRequest.setPluginWindowsMedia(pluginWindowsMedia); - } - if (randomValue != null) { - matomoRequest.setRandomValue(randomValue); - } - if (referrerUrl != null) { - matomoRequest.setReferrerUrl(referrerUrl); - } - if (requestDatetime != null) { - matomoRequest.setRequestDatetime(requestDatetime); - } - if (required != null) { - matomoRequest.setRequired(required); - } - if (responseAsImage != null) { - matomoRequest.setResponseAsImage(responseAsImage); - } - if (searchCategory != null) { - matomoRequest.setSearchCategory(searchCategory); - } - if (searchQuery != null) { - matomoRequest.setSearchQuery(searchQuery); - } - if (searchResultsCount != null) { - matomoRequest.setSearchResultsCount(searchResultsCount); - } - if (trackBotRequests != null) { - matomoRequest.setTrackBotRequests(trackBotRequests); - } - if (visitCustomVariables != null) { - for (int i = 0; i < visitCustomVariables.size(); i++) { - CustomVariable visitCustomVariable = visitCustomVariables.get(i); - matomoRequest.setVisitCustomVariable(visitCustomVariable, i + 1); - } - } - if (userId != null) { - matomoRequest.setUserId(userId); - } - if (visitorCity != null) { - matomoRequest.setVisitorCity(visitorCity); - } - if (visitorCountry != null) { - matomoRequest.setVisitorCountry(visitorCountry); - } - if (visitorCustomId != null) { - matomoRequest.setVisitorCustomId(visitorCustomId); - } - if (visitorFirstVisitTimestamp != null) { - matomoRequest.setVisitorFirstVisitTimestamp(visitorFirstVisitTimestamp); - } - if (visitorId != null) { - matomoRequest.setVisitorId(visitorId); - } - if (visitorIp != null) { - matomoRequest.setVisitorIp(visitorIp); - } - if (visitorLatitude != null) { - matomoRequest.setVisitorLatitude(visitorLatitude); - } - if (visitorLongitude != null) { - matomoRequest.setVisitorLongitude(visitorLongitude); - } - if (visitorPreviousVisitTimestamp != null) { - matomoRequest.setVisitorPreviousVisitTimestamp(visitorPreviousVisitTimestamp); - } - if (visitorRegion != null) { - matomoRequest.setVisitorRegion(visitorRegion); - } - if (visitorVisitCount != null) { - matomoRequest.setVisitorVisitCount(visitorVisitCount); - } - return matomoRequest; - } - -} diff --git a/src/main/java/org/matomo/java/tracking/MatomoTracker.java b/src/main/java/org/matomo/java/tracking/MatomoTracker.java deleted file mode 100644 index d5995beb..00000000 --- a/src/main/java/org/matomo/java/tracking/MatomoTracker.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -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.client.methods.HttpUriRequest; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.concurrent.FutureCallback; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.concurrent.Future; - -/** - * A class that sends {@link MatomoRequest}s to a specified Matomo server. - * - * @author brettcsorba - */ -@Slf4j -public class MatomoTracker { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - private static final String AUTH_TOKEN = "token_auth"; - private static final String REQUESTS = "requests"; - private static final int DEFAULT_TIMEOUT = 5000; - private final URI hostUrl; - private final int timeout; - private final String proxyHost; - private final int proxyPort; - - /** - * 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 - * http://your-matomo-domain.tld/matomo.php. Must not be null - */ - public MatomoTracker(@NonNull final String hostUrl) { - this(hostUrl, DEFAULT_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 - * http://your-matomo-domain.tld/matomo.php. - * @param timeout the timeout of the sent request in milliseconds - */ - public MatomoTracker(@NonNull final String hostUrl, final int timeout) { - this(hostUrl, null, 0, timeout); - } - - public MatomoTracker(@NonNull final String hostUrl, @Nullable final String proxyHost, final int proxyPort, final int timeout) { - this.hostUrl = URI.create(hostUrl); - this.proxyHost = proxyHost; - this.proxyPort = proxyPort; - this.timeout = timeout; - } - - /** - * 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 - * http://your-matomo-domain.tld/matomo.php. - * @param proxyHost url endpoint for the proxy - * @param proxyPort proxy server port number - */ - public MatomoTracker(@NonNull final String hostUrl, @Nullable final String proxyHost, final int proxyPort) { - this(hostUrl, proxyHost, proxyPort, DEFAULT_TIMEOUT); - } - - /** - * Sends a tracking request to Matomo - * - * @param request request to send. must not be null - * @return the response from this request - * @deprecated use sendRequestAsync instead - */ - @Deprecated - public HttpResponse sendRequest(@NonNull final MatomoRequest request) { - final HttpClient client = getHttpClient(); - HttpUriRequest get = createGetRequest(request); - log.debug("Sending request via GET: {}", request); - try { - return client.execute(get); - } catch (IOException e) { - throw new MatomoException("Could not send request to Matomo", e); - } - } - - @Nonnull - private HttpUriRequest createGetRequest(@NonNull MatomoRequest request) { - try { - return new HttpGet(new URIBuilder(hostUrl).addParameters(QueryParameters.fromMap(request.getParameters())).build()); - } catch (URISyntaxException e) { - throw new InvalidUrlException(e); - } - } - - /** - * Get a HTTP client. With proxy if a proxy is provided in the constructor. - * - * @return a HTTP client - */ - protected HttpClient getHttpClient() { - return HttpClientFactory.getInstanceFor(proxyHost, proxyPort, timeout); - } - - /** - * Send a request. - * - * @param request request to send - * @return future with response from this request - */ - public Future sendRequestAsync(@NonNull final MatomoRequest request) { - return sendRequestAsync(request, null); - } - - /** - * Send a request. - * - * @param request request to send - * @param callback callback that gets executed when response arrives - * @return future with response from this request - */ - public Future sendRequestAsync(@NonNull final MatomoRequest request, @Nullable FutureCallback callback) { - final CloseableHttpAsyncClient client = getHttpAsyncClient(); - client.start(); - HttpUriRequest get = createGetRequest(request); - log.debug("Sending async request via GET: {}", request); - return client.execute(get, callback); - } - - /** - * Get an async HTTP client. With proxy if a proxy is provided in the constructor. - * - * @return an async HTTP client - */ - protected CloseableHttpAsyncClient getHttpAsyncClient() { - return HttpClientFactory.getAsyncInstanceFor(proxyHost, proxyPort, timeout); - } - - /** - * 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 - * @deprecated use sendBulkRequestAsync instead - */ - @Deprecated - public HttpResponse sendBulkRequest(@NonNull final Iterable requests) { - return sendBulkRequest(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 - * @deprecated use sendBulkRequestAsync instead - */ - @Deprecated - public HttpResponse sendBulkRequest(@NonNull final Iterable requests, @Nullable final String authToken) { - if (authToken != null && authToken.length() != MatomoRequest.AUTH_TOKEN_LENGTH) { - throw new IllegalArgumentException(authToken + " is not " + MatomoRequest.AUTH_TOKEN_LENGTH + " characters long."); - } - HttpPost post = buildPost(requests, authToken); - final HttpClient client = getHttpClient(); - log.debug("Sending requests via POST: {}", requests); - try { - return client.execute(post); - } catch (IOException e) { - throw new MatomoException("Could not send bulk request", e); - } - } - - private HttpPost buildPost(@NonNull Iterable requests, @Nullable String authToken) { - ObjectNode objectNode = OBJECT_MAPPER.createObjectNode(); - ArrayNode requestsNode = objectNode.putArray(REQUESTS); - for (final MatomoRequest request : requests) { - requestsNode.add(new URIBuilder().addParameters(QueryParameters.fromMap(request.getParameters())).toString()); - } - if (authToken != null) { - objectNode.put(AUTH_TOKEN, authToken); - } - HttpPost post = new HttpPost(hostUrl); - post.setEntity(new StringEntity(objectNode.toString(), ContentType.APPLICATION_JSON)); - return post; - } - - /** - * 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 - */ - public Future sendBulkRequestAsync(@NonNull final Iterable 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 - * @param callback callback that gets executed when response arrives - * @return the response from these requests - */ - public Future sendBulkRequestAsync(@NonNull final Iterable requests, @Nullable final String authToken, @Nullable FutureCallback callback) { - if (authToken != null && authToken.length() != MatomoRequest.AUTH_TOKEN_LENGTH) { - throw new IllegalArgumentException(authToken + " is not " + MatomoRequest.AUTH_TOKEN_LENGTH + " characters long."); - } - HttpPost post = buildPost(requests, authToken); - final CloseableHttpAsyncClient client = getHttpAsyncClient(); - client.start(); - log.debug("Sending async requests via POST: {}", requests); - return client.execute(post, callback); - } - - /** - * 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 - * @return future with response from these requests - */ - public Future sendBulkRequestAsync(@NonNull final Iterable requests, @Nullable FutureCallback 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 - * @return the response from these requests - */ - public Future sendBulkRequestAsync(@NonNull final Iterable requests, @Nullable final String authToken) { - return sendBulkRequestAsync(requests, authToken, null); - } -} diff --git a/src/main/java/org/matomo/java/tracking/QueryParameters.java b/src/main/java/org/matomo/java/tracking/QueryParameters.java deleted file mode 100644 index 2e1f94c3..00000000 --- a/src/main/java/org/matomo/java/tracking/QueryParameters.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.matomo.java.tracking; - -import lombok.NonNull; -import org.apache.http.NameValuePair; -import org.apache.http.message.BasicNameValuePair; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - -public final class QueryParameters { - - private QueryParameters() { - // utility - } - - @Nonnull - public static List fromMap(@NonNull Map> map) { - List queryParameters = new ArrayList<>(); - for (Map.Entry> entries : map.entrySet()) { - for (Object value : entries.getValue()) { - queryParameters.add(new BasicNameValuePair(entries.getKey(), value.toString())); - } - } - queryParameters.sort(Comparator.comparing(NameValuePair::getName)); - return queryParameters; - } - -} 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 48b3813a..00000000 --- a/src/main/java/org/piwik/java/tracking/CustomVariable.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.piwik.java.tracking; - -import org.matomo.java.tracking.MatomoRequest; - -/** - * @author brettcsorba - * @deprecated Use {@link org.matomo.java.tracking.CustomVariable} instead. - */ -@Deprecated -public class CustomVariable extends org.matomo.java.tracking.CustomVariable { - - /** - * @deprecated Use {@link MatomoRequest} instead. - */ - @Deprecated - public CustomVariable(String key, String value) { - super(key, value); - } -} 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 65b21446..00000000 --- a/src/main/java/org/piwik/java/tracking/EcommerceItem.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.piwik.java.tracking; - -import org.matomo.java.tracking.MatomoRequest; - -/** - * @author brettcsorba - * @deprecated Use {@link org.matomo.java.tracking.EcommerceItem} instead. - */ -@Deprecated -public class EcommerceItem extends org.matomo.java.tracking.EcommerceItem { - - /** - * @deprecated Use {@link MatomoRequest} instead. - */ - @Deprecated - public EcommerceItem(String sku, String name, String category, Double price, Integer quantity) { - super(sku, name, category, price, quantity); - } -} 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 6105bd8d..00000000 --- a/src/main/java/org/piwik/java/tracking/PiwikDate.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.piwik.java.tracking; - -import org.matomo.java.tracking.MatomoDate; - -import java.time.ZoneId; -import java.util.TimeZone; - -/** - * @author brettcsorba - * @deprecated Use {@link MatomoDate} instead. - */ -@Deprecated -public class PiwikDate extends MatomoDate { - - /** - * @author brettcsorba - * @deprecated Use {@link MatomoDate} instead. - */ - public PiwikDate() { - super(); - } - - /** - * @author brettcsorba - * @deprecated Use {@link MatomoDate} instead. - */ - public PiwikDate(long epochMilli) { - super(epochMilli); - } - - /** - * @author brettcsorba - * @deprecated Use {@link MatomoDate#setTimeZone(ZoneId)} instead. - */ - @Deprecated - public void setTimeZone(TimeZone zone) { - setTimeZone(zone.toZoneId()); - } - -} 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 686bbfd7..00000000 --- a/src/main/java/org/piwik/java/tracking/PiwikLocale.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.piwik.java.tracking; - -import org.matomo.java.tracking.MatomoLocale; - -import java.util.Locale; - -/** - * @author brettcsorba - * @deprecated Use {@link org.matomo.java.tracking.MatomoLocale} instead. - */ -@Deprecated -public class PiwikLocale extends MatomoLocale { - - /** - * @deprecated Use {@link MatomoLocale} instead. - */ - @Deprecated - public PiwikLocale(Locale locale) { - super(locale); - } -} 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 4e91e0b0..00000000 --- a/src/main/java/org/piwik/java/tracking/PiwikTracker.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.piwik.java.tracking; - -import org.matomo.java.tracking.MatomoTracker; - -/** - * @author brettcsorba - * @deprecated Use {@link MatomoTracker} instead. - */ -@Deprecated -public class PiwikTracker extends MatomoTracker { - - /** - * @deprecated Use {@link MatomoTracker} instead. - */ - @Deprecated - public PiwikTracker(final String hostUrl) { - super(hostUrl); - } - - /** - * @deprecated Use {@link MatomoTracker} instead. - */ - @Deprecated - public PiwikTracker(final String hostUrl, final int timeout) { - super(hostUrl, timeout); - } - - /** - * @deprecated Use {@link MatomoTracker} instead. - */ - @Deprecated - public PiwikTracker(final String hostUrl, final String proxyHost, final int proxyPort) { - super(hostUrl, proxyHost, proxyPort); - } - - /** - * @deprecated Use {@link MatomoTracker} instead. - */ - @Deprecated - public PiwikTracker(final String hostUrl, final String proxyHost, final int proxyPort, final int timeout) { - super(hostUrl, proxyHost, proxyPort, timeout); - } - -} diff --git a/src/test/java/org/matomo/java/tracking/CustomVariableTest.java b/src/test/java/org/matomo/java/tracking/CustomVariableTest.java deleted file mode 100644 index 9818bc8d..00000000 --- a/src/test/java/org/matomo/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.matomo.java.tracking; - -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * @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 is marked non-null but is null", e.getLocalizedMessage()); - } - } - - @Test - public void testConstructorNullValue() { - try { - new CustomVariable("key", null); - fail("Exception should have been throw."); - } catch (NullPointerException e) { - assertEquals("value is marked non-null but is 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/matomo/java/tracking/CustomVariablesTest.java b/src/test/java/org/matomo/java/tracking/CustomVariablesTest.java deleted file mode 100644 index bdc4e8d1..00000000 --- a/src/test/java/org/matomo/java/tracking/CustomVariablesTest.java +++ /dev/null @@ -1,85 +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.matomo.java.tracking; - -import org.junit.Test; - -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; - -/** - * @author Katie - */ -public class CustomVariablesTest { - private final CustomVariables customVariables = new CustomVariables(); - - @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(customVariables.isEmpty()); - customVariables.add(a); - assertFalse(customVariables.isEmpty()); - assertEquals("b", customVariables.get("a")); - assertEquals(a, customVariables.get(1)); - assertEquals("{\"1\":[\"a\",\"b\"]}", customVariables.toString()); - - customVariables.add(b); - assertEquals("d", customVariables.get("c")); - assertEquals(b, customVariables.get(2)); - assertEquals("{\"1\":[\"a\",\"b\"],\"2\":[\"c\",\"d\"]}", customVariables.toString()); - - customVariables.add(c, 5); - assertEquals("b", customVariables.get("a")); - assertEquals(c, customVariables.get(5)); - assertNull(customVariables.get(3)); - assertEquals("{\"1\":[\"a\",\"b\"],\"2\":[\"c\",\"d\"],\"5\":[\"a\",\"e\"]}", customVariables.toString()); - - customVariables.add(d); - assertEquals("f", customVariables.get("a")); - assertEquals(d, customVariables.get(1)); - assertEquals(d, customVariables.get(5)); - assertEquals("{\"1\":[\"a\",\"f\"],\"2\":[\"c\",\"d\"],\"5\":[\"a\",\"f\"]}", customVariables.toString()); - - customVariables.remove("a"); - assertNull(customVariables.get("a")); - assertNull(customVariables.get(1)); - assertNull(customVariables.get(5)); - assertEquals("{\"2\":[\"c\",\"d\"]}", customVariables.toString()); - - customVariables.remove(2); - assertNull(customVariables.get("c")); - assertNull(customVariables.get(2)); - assertTrue(customVariables.isEmpty()); - assertEquals("{}", customVariables.toString()); - } - - @Test - public void testAddCustomVariableIndexLessThan1() { - try { - customVariables.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 { - customVariables.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/matomo/java/tracking/EcommerceItemTest.java b/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java deleted file mode 100644 index 986fb305..00000000 --- a/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * @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()); - } - - -} diff --git a/src/test/java/org/matomo/java/tracking/EcommerceItemsTest.java b/src/test/java/org/matomo/java/tracking/EcommerceItemsTest.java deleted file mode 100644 index ee76b7ce..00000000 --- a/src/test/java/org/matomo/java/tracking/EcommerceItemsTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.matomo.java.tracking; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class EcommerceItemsTest { - - @Test - public void formatsJson() { - - EcommerceItems ecommerceItems = new EcommerceItems(); - ecommerceItems.add(new EcommerceItem("sku", "name", "category", 1.0, 1)); - - assertEquals("[[\"sku\",\"name\",\"category\",1.0,1]]", ecommerceItems.toString()); - - } -} diff --git a/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java b/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java deleted file mode 100644 index 9d2804ed..00000000 --- a/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.matomo.java.tracking; - -import org.junit.Test; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -public class MatomoRequestBuilderTest { - - @Test - public void buildsRequest() { - - CustomVariable customVariable = new CustomVariable("pageCustomVariableName", "pageCustomVariableValue"); - MatomoRequest matomoRequest = MatomoRequest.builder() - .siteId(42) - .actionName("ACTION_NAME") - .actionUrl("https://www.your-domain.tld/some/page?query=foo") - .referrerUrl("https://referrer.com") - .customTrackingParameters(Collections.singletonMap("trackingParameterName", "trackingParameterValue")) - .pageCustomVariables(Collections.singletonList(customVariable)) - .visitCustomVariables(Collections.singletonList(customVariable)) - .customAction(true) - .build(); - - Map> parameters = matomoRequest.getParameters(); - assertThat(parameters.get("idsite"), hasItem(42)); - assertThat(parameters.get("action_name"), hasItem("ACTION_NAME")); - assertThat(parameters.get("apiv"), hasItem("1")); - assertThat(parameters.get("url"), hasItem("https://www.your-domain.tld/some/page?query=foo")); - assertThat(parameters.get("_id").isEmpty(), is(false)); - assertThat(parameters.get("rand").isEmpty(), is(false)); - assertThat(parameters.get("send_image"), hasItem(new MatomoBoolean(false))); - assertThat(parameters.get("rec"), hasItem(new MatomoBoolean(true))); - assertThat(parameters.get("urlref"), hasItem("https://referrer.com")); - assertThat(parameters.get("trackingParameterName"), hasItem("trackingParameterValue")); - CustomVariables customVariables = new CustomVariables(); - customVariables.add(customVariable); - assertThat(parameters.get("cvar"), hasItem(customVariables)); - assertThat(parameters.get("_cvar"), hasItem(customVariables)); - assertThat(parameters.get("ca"), hasItem(new MatomoBoolean(true))); - - } - -} diff --git a/src/test/java/org/matomo/java/tracking/PiwikDateTest.java b/src/test/java/org/matomo/java/tracking/PiwikDateTest.java deleted file mode 100644 index 0ba201d5..00000000 --- a/src/test/java/org/matomo/java/tracking/PiwikDateTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import org.junit.Test; -import org.piwik.java.tracking.PiwikDate; - -import java.util.TimeZone; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -/** - * @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/matomo/java/tracking/PiwikLocaleTest.java b/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java deleted file mode 100644 index 1931e5e6..00000000 --- a/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Piwik 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 org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.piwik.java.tracking.PiwikLocale; - -import java.util.Locale; - -import static org.junit.Assert.assertEquals; - -/** - * @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/matomo/java/tracking/PiwikRequestTest.java b/src/test/java/org/matomo/java/tracking/PiwikRequestTest.java deleted file mode 100644 index e50fc017..00000000 --- a/src/test/java/org/matomo/java/tracking/PiwikRequestTest.java +++ /dev/null @@ -1,1537 +0,0 @@ -/* - * Piwik 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 org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.piwik.java.tracking.PiwikDate; -import org.piwik.java.tracking.PiwikLocale; -import org.piwik.java.tracking.PiwikRequest; - -import java.net.URL; -import java.nio.charset.Charset; -import java.util.List; -import java.util.Locale; - -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; - -/** - * @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(Integer.valueOf(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(Long.valueOf(1000L), request.getActionTime()); - } - - /** - * Test of getActionUrl method, of class PiwikRequest. - */ - @Test - public void testActionUrl() throws Exception { - request.setActionUrl((String) null); - assertNull(request.getActionUrl()); - assertNull(request.getActionUrlAsString()); - - URL url = new URL("http://action.com"); - request.setActionUrl(url); - assertEquals(url, request.getActionUrl()); - assertEquals("http://action.com", request.getActionUrlAsString()); - - request.setActionUrlWithString(null); - assertNull(request.getActionUrl()); - assertNull(request.getActionUrlAsString()); - - request.setActionUrlWithString("http://actionstring.com"); - assertEquals("http://actionstring.com", request.getActionUrlAsString()); - assertEquals(new URL("http://actionstring.com"), request.getActionUrl()); - } - - /** - * 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()); - assertEquals("http://target.com", request.getContentTargetAsString()); - - request.setContentTargetWithString("http://targetstring.com"); - assertEquals("http://targetstring.com", request.getContentTargetAsString()); - assertEquals(new URL("http://targetstring.com"), request.getContentTarget()); - - } - - /** - * Test of getCurrentHour method, of class PiwikRequest. - */ - @Test - public void testCurrentHour() { - request.setCurrentHour(1); - assertEquals(Integer.valueOf(1), request.getCurrentHour()); - } - - /** - * Test of getCurrentMinute method, of class PiwikRequest. - */ - @Test - public void testCurrentMinute() { - request.setCurrentMinute(2); - assertEquals(Integer.valueOf(2), request.getCurrentMinute()); - } - - /** - * Test of getCurrentSecond method, of class PiwikRequest. - */ - @Test - public void testCurrentSecond() { - request.setCurrentSecond(3); - assertEquals(Integer.valueOf(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 is marked non-null but is 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 is marked non-null but is 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 is marked non-null but is null", e.getLocalizedMessage()); - } - } - - @Test - public void testAddCustomTrackingParameter_FT() { - try { - request.addCustomTrackingParameter("key", null); - fail("Exception should have been thrown."); - } catch (NullPointerException e) { - assertEquals("value is marked non-null but is null", 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()); - assertEquals("http://download.com", request.getDownloadUrlAsString()); - - request.setDownloadUrlWithString("http://downloadstring.com"); - assertEquals("http://downloadstring.com", request.getDownloadUrlAsString()); - assertEquals(new URL("http://downloadstring.com"), request.getDownloadUrl()); - - } - - /** - * Test of enableEcommerce method, of class PiwikRequest. - */ - @Test - public void testEnableEcommerce() { - request.enableEcommerce(); - assertEquals(Integer.valueOf(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(Double.valueOf(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("item is marked non-null but is 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(Long.valueOf(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(Double.valueOf(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(Double.valueOf(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(Double.valueOf(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(Double.valueOf(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(Integer.valueOf(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(Double.valueOf(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()); - assertEquals("http://outlink.com", request.getOutlinkUrlAsString()); - - request.setOutlinkUrlWithString("http://outlinkstring.com"); - assertEquals("http://outlinkstring.com", request.getOutlinkUrlAsString()); - assertEquals(new URL("http://outlinkstring.com"), request.getOutlinkUrl()); - - } - - /** - * 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 is marked non-null but is null", e.getLocalizedMessage()); - } - } - - @Test - public void testPageCustomVariableStringStringE2() { - try { - request.setPageCustomVariable(null, "pageVal"); - fail("Exception should have been thrown"); - } catch (NullPointerException e) { - assertEquals("key is marked non-null but is null", e.getLocalizedMessage()); - } - } - - @Test - public void testPageCustomVariableStringStringE3() { - try { - request.getPageCustomVariable(null); - fail("Exception should have been thrown"); - } catch (NullPointerException e) { - assertEquals("key is marked non-null but is 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()); - assertEquals("http://referrer.com", request.getReferrerUrlAsString()); - - request.setReferrerUrlWithString("http://referrerstring.com"); - assertEquals("http://referrerstring.com", request.getReferrerUrlAsString()); - assertEquals(new URL("http://referrerstring.com"), request.getReferrerUrl()); - - } - - /** - * 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(Long.valueOf(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(Integer.valueOf(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("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&_cvar={\"1\":[\"visitKey\",\"visitVal\"]}", request.getQueryString()); - - request.setUserCustomVariable("key", "val"); - assertEquals(cv, request.getVisitCustomVariable(1)); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&_cvar={\"1\":[\"visitKey\",\"visitVal\"],\"2\":[\"key\",\"val\"]}", request.getQueryString()); - - request.setVisitCustomVariable(null, 1); - assertNull(request.getVisitCustomVariable(1)); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&_cvar={\"2\":[\"key\",\"val\"]}", request.getQueryString()); - - request.setVisitCustomVariable(cv, 2); - assertEquals(cv, request.getVisitCustomVariable(2)); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&_cvar={\"2\":[\"visitKey\",\"visitVal\"]}", request.getQueryString()); - - request.setUserCustomVariable("visitKey", null); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456", 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(Long.valueOf(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(Double.valueOf(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(Double.valueOf(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(Long.valueOf(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(Integer.valueOf(100), request.getVisitorVisitCount()); - } - - /** - * Test of getQueryString method, of class PiwikRequest. - */ - @Test - public void testGetQueryString() { - request.setRandomValue("random"); - request.setVisitorId("1234567890123456"); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456", request.getQueryString()); - request.setPageCustomVariable("key", "val"); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&cvar={\"1\":[\"key\",\"val\"]}", - request.getQueryString()); - request.setPageCustomVariable("key", null); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456", request.getQueryString()); - request.addCustomTrackingParameter("key", "test"); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&key=test", request.getQueryString()); - request.addCustomTrackingParameter("key", "test2"); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&key=test&key=test2", request.getQueryString()); - request.setCustomTrackingParameter("key2", "test3"); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&key=test&key=test2&key2=test3", request.getQueryString()); - request.setCustomTrackingParameter("key", "test4"); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&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((String) 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("idsite=3&rec=1&apiv=1&send_image=0&url=http://test.com&rand=random&_id=1234567890123456", request.getQueryString()); - } - - /** - * Test of getUrlEncodedQueryString method, of class PiwikRequest. - */ - @Test - public void testGetUrlEncodedQueryString() { - request.setRandomValue("random"); - request.setVisitorId("1234567890123456"); - assertEquals("_id=1234567890123456&apiv=1&idsite=3&rand=random&rec=1&send_image=0&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString()); - request.addCustomTrackingParameter("ke/y", "te:st"); - assertEquals("_id=1234567890123456&apiv=1&idsite=3&ke%2Fy=te%3Ast&rand=random&rec=1&send_image=0&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString()); - request.addCustomTrackingParameter("ke/y", "te:st2"); - assertEquals("_id=1234567890123456&apiv=1&idsite=3&ke%2Fy=te%3Ast&ke%2Fy=te%3Ast2&rand=random&rec=1&send_image=0&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString()); - request.setCustomTrackingParameter("ke/y2", "te:st3"); - assertEquals("_id=1234567890123456&apiv=1&idsite=3&ke%2Fy=te%3Ast&ke%2Fy=te%3Ast2&ke%2Fy2=te%3Ast3&rand=random&rec=1&send_image=0&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString()); - request.setCustomTrackingParameter("ke/y", "te:st4"); - assertEquals("_id=1234567890123456&apiv=1&idsite=3&ke%2Fy=te%3Ast4&ke%2Fy2=te%3Ast3&rand=random&rec=1&send_image=0&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString()); - request.setRandomValue(null); - request.setSiteId(null); - request.setRequired(null); - request.setApiVersion(null); - request.setResponseAsImage(null); - request.setVisitorId(null); - request.setActionUrl((String) 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("_id=1234567890123456&apiv=1&idsite=3&rand=random&rec=1&send_image=0&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/matomo/java/tracking/PiwikTrackerTest.java b/src/test/java/org/matomo/java/tracking/PiwikTrackerTest.java deleted file mode 100644 index b37a9ebc..00000000 --- a/src/test/java/org/matomo/java/tracking/PiwikTrackerTest.java +++ /dev/null @@ -1,441 +0,0 @@ -/* - * Piwik Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import com.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.concurrent.FutureCallback; -import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; -import org.apache.http.util.EntityUtils; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.mockito.ArgumentMatcher; -import org.piwik.java.tracking.PiwikRequest; -import org.piwik.java.tracking.PiwikTracker; - -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.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; - -/** - * @author brettcsorba - */ -public class PiwikTrackerTest { - private static final Map> PARAMETERS = Collections.singletonMap("parameterName", Collections.singleton("parameterValue")); - - // 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(PARAMETERS).when(request).getParameters(); - doReturn(response).when(client) - .execute(argThat(new CorrectGetRequest("http://test.com?parameterName=parameterValue"))); - - 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(PARAMETERS).when(request).getParameters(); - doReturn(response).when(future).get(); - doReturn(true).when(future).isDone(); - doReturn(future).when(client) - .execute(argThat(new CorrectGetRequest("http://test.com?parameterName=parameterValue")), 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); - } - - /** - * Test async API with local server - */ - @Test - public void testWithLocalServerAsyncCallback() throws Exception { - CountDownLatch latch = new CountDownLatch(2); - BlockingQueue responses = new LinkedBlockingQueue<>(); - BlockingQueue exceptions = new LinkedBlockingQueue<>(); - AtomicInteger cancelled = new AtomicInteger(); - - FutureCallback cb = new FutureCallback() { - - @Override - public void completed(HttpResponse httpResponse) { - responses.add(httpResponse); - latch.countDown(); - } - - @Override - public void failed(Exception e) { - exceptions.add(e); - latch.countDown(); - } - - @Override - public void cancelled() { - cancelled.incrementAndGet(); - latch.countDown(); - - } - }; - - // one - PiwikRequest request = new PiwikRequest(3, new URL("http://test.com")); - Future respFuture = localTracker.sendRequestAsync(request, cb); - // bulk - List requests = Collections.singletonList(request); - Future bulkFuture = localTracker.sendBulkRequestAsync(requests, cb); - - assertTrue("Responses not received", latch.await(100, TimeUnit.MILLISECONDS)); - assertEquals("Not expecting cancelled responses", 0, cancelled.get()); - assertEquals("Not expecting exceptions", exceptions.size(), 0); - assertTrue("Single response future not done", respFuture.isDone()); - assertTrue("Bulk response future not done", bulkFuture.isDone()); - HttpResponse response = responses.poll(1, TimeUnit.MILLISECONDS); - assertEquals("OK", EntityUtils.toString(response.getEntity())); - - HttpResponse bulkResponse = responses.poll(1, TimeUnit.MILLISECONDS); - assertEquals("OK", EntityUtils.toString(bulkResponse.getEntity())); - } - - 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() { - 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() { - try { - List requests = new ArrayList<>(); - HttpClient client = mock(HttpClient.class); - PiwikRequest request = mock(PiwikRequest.class); - - doReturn(PARAMETERS).when(request).getParameters(); - 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(PARAMETERS).when(request).getParameters(); - requests.add(request); - doReturn(client).when(piwikTracker).getHttpClient(); - doReturn(response).when(client).execute(argThat(new CorrectPostRequest("{\"requests\":[\"?parameterName=parameterValue\"]}"))); - - 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(PARAMETERS).when(request).getParameters(); - requests.add(request); - doReturn(client).when(piwikTracker).getHttpClient(); - doReturn(response).when(client) - .execute(argThat(new CorrectPostRequest("{\"requests\":[\"?parameterName=parameterValue\"],\"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); - - assertEquals(response, piwikTracker.sendBulkRequestAsync(requests).get()); - } - - /** - * Test of sendBulkRequestAsync method, of class PiwikTracker. - */ - @Test - public void testSendBulkRequestAsync_Iterable_StringTT() { - try { - List requests = new ArrayList<>(); - CloseableHttpAsyncClient client = mock(CloseableHttpAsyncClient.class); - PiwikRequest request = mock(PiwikRequest.class); - - doReturn(PARAMETERS).when(request).getParameters(); - 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(PARAMETERS).when(request).getParameters(); - requests.add(request); - doReturn(client).when(piwikTracker).getHttpAsyncClient(); - doReturn(future).when(client) - .execute(argThat(new CorrectPostRequest("{\"requests\":[\"?parameterName=parameterValue\"]}")), any()); - - assertEquals(response, piwikTracker.sendBulkRequestAsync(requests).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(PARAMETERS).when(request).getParameters(); - requests.add(request); - doReturn(client).when(piwikTracker).getHttpAsyncClient(); - doReturn(future).when(client) - .execute(argThat(new CorrectPostRequest("{\"requests\":[\"?parameterName=parameterValue\"],\"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