diff --git a/marketplace-service/.dockerignore b/marketplace-service/.dockerignore index 0384b84cd..f8271f65a 100644 --- a/marketplace-service/.dockerignore +++ b/marketplace-service/.dockerignore @@ -1,4 +1,5 @@ target/ +data/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ diff --git a/marketplace-service/.editorconfig b/marketplace-service/.editorconfig new file mode 100644 index 000000000..ffd49d3c5 --- /dev/null +++ b/marketplace-service/.editorconfig @@ -0,0 +1,687 @@ +[*] +charset = utf-8 +end_of_line = crlf +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 = true + +[*.dcl] +ij_declarative_keep_indents_on_empty_lines = false + +[*.java] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_deconstruction_list_components = true +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = 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 = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_align_types_in_multi_catch = true +ij_java_annotation_parameter_wrap = normal +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +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 = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_add_space = false +ij_java_block_comment_at_first_column = true +ij_java_builder_methods = +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = normal +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 10 +ij_java_class_names_in_javadoc = 1 +ij_java_deconstruction_list_wrap = normal +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = false +ij_java_do_while_brace_force = never +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_enum_constants_wrap = off +ij_java_enum_field_annotation_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_field_name_prefix = +ij_java_field_name_suffix = +ij_java_finally_on_new_line = false +ij_java_for_brace_force = never +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 = off +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_generate_use_type_annotation_before_type = true +ij_java_if_brace_force = never +ij_java_imports_layout = *,|,javax.**,java.**,|,$* +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 = false +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = true +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 = true +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_local_variable_name_prefix = +ij_java_local_variable_name_suffix = +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = normal +ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal +ij_java_names_count_to_use_import_on_demand = 3 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_deconstruction_pattern = true +ij_java_new_line_after_lparen_in_record_header = false +ij_java_new_line_when_body_is_presented = false +ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* +ij_java_parameter_annotation_wrap = off +ij_java_parameter_name_prefix = +ij_java_parameter_name_suffix = +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_annotations = +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_deconstruction_pattern = true +ij_java_rparen_on_new_line_in_record_header = false +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_deconstruction_list = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_annotation_eq = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_inside_block_braces_when_body_is_present = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_deconstruction_list = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_static_field_name_prefix = +ij_java_static_field_name_suffix = +ij_java_subclass_name_prefix = +ij_java_subclass_name_suffix = Impl +ij_java_switch_expressions_wrap = normal +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_prefix = +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = never +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = true +ij_java_wrap_semicolon_after_call_chain = false + +[*.properties] +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 + +[.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,*.jspx,*.pom,*.rng,*.tagx,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +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 + +[{*.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 + +[{*.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,*.json,*.jsonc}] +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,*.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 + +[{*.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 = normal +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = true +ij_kotlin_call_parameters_right_paren_on_new_line = true +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_continuation_indent_for_chained_calls = false +ij_kotlin_continuation_indent_for_expression_bodies = false +ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = normal +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = true +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +ij_kotlin_indent_before_arrow_on_new_line = true +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.** +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 1 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.markdown,*.md}] +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 + +[{*.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_line_comment_add_space = false +ij_yaml_line_comment_add_space_on_reformat = false +ij_yaml_line_comment_at_first_column = 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/marketplace-service/.gitignore b/marketplace-service/.gitignore index 3ccbd3e32..f11bc4177 100644 --- a/marketplace-service/.gitignore +++ b/marketplace-service/.gitignore @@ -1,4 +1,5 @@ target/ +data/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ diff --git a/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java index 74c7b12d4..872db5c61 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java +++ b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java @@ -1,6 +1,7 @@ package com.axonivy.market; import com.axonivy.market.service.ProductService; +import lombok.AllArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.time.StopWatch; import org.springframework.boot.SpringApplication; @@ -15,14 +16,11 @@ @EnableAsync @EnableScheduling @SpringBootApplication +@AllArgsConstructor public class MarketplaceServiceApplication { private final ProductService productService; - public MarketplaceServiceApplication(ProductService productService) { - this.productService = productService; - } - public static void main(String[] args) { SpringApplication.run(MarketplaceServiceApplication.class, args); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java index 9acbe6fc5..a1b48e0c1 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java @@ -1,22 +1,21 @@ package com.axonivy.market.assembler; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; - -import java.util.Optional; - -import org.apache.commons.lang3.StringUtils; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; - import com.axonivy.market.constants.RequestMappingConstants; import com.axonivy.market.controller.ProductDetailsController; import com.axonivy.market.entity.Product; import com.axonivy.market.model.ProductDetailModel; import com.axonivy.market.util.ImageUtils; import com.axonivy.market.util.VersionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; @Component public class ProductDetailModelAssembler extends RepresentationModelAssemblerSupport { @@ -48,9 +47,9 @@ private ProductDetailModel createModel(Product product, String version, String r String productId = Optional.of(product).map(Product::getId).orElse(StringUtils.EMPTY); if (requestPath.equals(RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION)) { - String bestMatchVersion = VersionUtils.getBestMatchVersion(product.getReleasedVersions(), version); Link link = linkTo( - methodOn(ProductDetailsController.class).findProductJsonContent(productId, bestMatchVersion)).withSelfRel(); + methodOn(ProductDetailsController.class).findProductJsonContent(productId, + product.getBestMatchVersion())).withSelfRel(); model.setMetaProductJsonUrl(link.getHref()); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java b/marketplace-service/src/main/java/com/axonivy/market/bo/ArchivedArtifact.java similarity index 93% rename from marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java rename to marketplace-service/src/main/java/com/axonivy/market/bo/ArchivedArtifact.java index 557a9e4ee..4d8c81023 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java +++ b/marketplace-service/src/main/java/com/axonivy/market/bo/ArchivedArtifact.java @@ -1,4 +1,4 @@ -package com.axonivy.market.github.model; +package com.axonivy.market.bo; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/MavenArtifact.java b/marketplace-service/src/main/java/com/axonivy/market/bo/Artifact.java similarity index 60% rename from marketplace-service/src/main/java/com/axonivy/market/github/model/MavenArtifact.java rename to marketplace-service/src/main/java/com/axonivy/market/bo/Artifact.java index 61e1891e1..2575d7040 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/MavenArtifact.java +++ b/marketplace-service/src/main/java/com/axonivy/market/bo/Artifact.java @@ -1,4 +1,4 @@ -package com.axonivy.market.github.model; +package com.axonivy.market.bo; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; @@ -11,6 +11,7 @@ import java.io.Serial; import java.io.Serializable; import java.util.List; +import java.util.Objects; @Getter @Setter @@ -18,7 +19,7 @@ @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonIgnoreProperties(ignoreUnknown = true) -public class MavenArtifact implements Serializable { +public class Artifact implements Serializable { @Serial private static final long serialVersionUID = 1L; private String repoUrl; @@ -30,4 +31,22 @@ public class MavenArtifact implements Serializable { @Transient private Boolean isProductArtifact; private List archivedArtifacts; + private boolean isInvalidArtifact; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Artifact that = (Artifact) o; + return Objects.equals(groupId, that.groupId) && Objects.equals(artifactId, that.artifactId); + } + + @Override + public int hashCode() { + return Objects.hash(groupId, artifactId); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java b/marketplace-service/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java deleted file mode 100644 index d5c553ed2..000000000 --- a/marketplace-service/src/main/java/com/axonivy/market/comparator/ArchivedArtifactsComparator.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.axonivy.market.comparator; - -import com.axonivy.market.github.model.ArchivedArtifact; - -import java.util.Comparator; - -public class ArchivedArtifactsComparator implements Comparator { - private final LatestVersionComparator comparator = new LatestVersionComparator(); - - @Override - public int compare(ArchivedArtifact artifact1, ArchivedArtifact artifact2) { - return comparator.compare(artifact1.getLastVersion(), artifact2.getLastVersion()); - } -} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java b/marketplace-service/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java index 17d90e397..108036fb0 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java +++ b/marketplace-service/src/main/java/com/axonivy/market/comparator/LatestVersionComparator.java @@ -6,27 +6,6 @@ public class LatestVersionComparator implements Comparator { @Override public int compare(String v1, String v2) { - // Split by "." - String[] parts1 = v1.split("\\."); - String[] parts2 = v2.split("\\."); - - // Compare up to the shorter length - int length = Math.min(parts1.length, parts2.length); - for (int i = 0; i < length; i++) { - try { - int num1 = Integer.parseInt(parts1[i]); - int num2 = Integer.parseInt(parts2[i]); - // Return difference for numeric parts - if (num1 != num2) { - return num2 - num1; - } - // Handle non-numeric parts (e.g., "m229") - } catch (NumberFormatException e) { - return parts2[i].replaceAll("\\D", "").compareTo(parts1[i].replaceAll("\\D", "")); - } - } - - // Versions with more parts are considered larger - return parts2.length - parts1.length; + return MavenVersionComparator.compare(v2, v1); } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/comparator/MavenVersionComparator.java b/marketplace-service/src/main/java/com/axonivy/market/comparator/MavenVersionComparator.java index d7df9ed85..b707019a7 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/comparator/MavenVersionComparator.java +++ b/marketplace-service/src/main/java/com/axonivy/market/comparator/MavenVersionComparator.java @@ -11,112 +11,112 @@ import java.util.regex.Pattern; import static com.axonivy.market.constants.CommonConstants.DASH_SEPARATOR; +import static com.axonivy.market.constants.MavenConstants.MAIN_VERSION_REGEX; import static com.axonivy.market.constants.MavenConstants.SNAPSHOT_VERSION; import static org.apache.commons.lang3.StringUtils.EMPTY; public class MavenVersionComparator { - private static final String MAIN_VERSION_REGEX = "\\."; - private static final int GREATER_THAN = 1; - private static final int EQUAL = 0; - private static final int LESS_THAN = -1; + private static final int GREATER_THAN = 1; + private static final int EQUAL = 0; + private static final int LESS_THAN = -1; - private MavenVersionComparator() { - } + private MavenVersionComparator() { + } - public static GHTag findHighestTag(List ghTags) { - if (CollectionUtils.isEmpty(ghTags)) { - return null; - } - String highestVersion = findHighestMavenVersion(ghTags.stream().map(GHTag::getName).toList()); - return ghTags.stream().filter(tag -> tag.getName().equals(highestVersion)).findAny().orElse(null); + public static GHTag findHighestTag(List ghTags) { + if (CollectionUtils.isEmpty(ghTags)) { + return null; } + String highestVersion = findHighestMavenVersion(ghTags.stream().map(GHTag::getName).toList()); + return ghTags.stream().filter(tag -> tag.getName().equals(highestVersion)).findAny().orElse(null); + } - public static String findHighestMavenVersion(List versions) { - if (CollectionUtils.isEmpty(versions)) { - return null; - } - - String highestVersion = versions.get(0); - for (var version : versions) { - if (compare(version, highestVersion) > EQUAL) { - highestVersion = version; - } - } - return highestVersion; + public static String findHighestMavenVersion(List versions) { + if (CollectionUtils.isEmpty(versions)) { + return null; } - private static int compare(String version, String otherVersion) { - version = stripLeadingChars(version); - otherVersion = stripLeadingChars(otherVersion); - String[] versionParts = createMainAndQualifierArray(version); - String[] otherVersionParts = createMainAndQualifierArray(otherVersion); - - // Compare main version parts - int mainComparison = compareMainVersion(versionParts[0], otherVersionParts[0]); - if (mainComparison != EQUAL) { - return mainComparison; - } - - // Compare qualifiers - String qualifier1 = getQualifierPart(versionParts); - String qualifier2 = getQualifierPart(otherVersionParts); - // Consider versions without a qualifier higher than those with qualifiers - if (qualifier1.isEmpty() && !qualifier2.isEmpty()) { - return GREATER_THAN; - } - if (!qualifier1.isEmpty() && qualifier2.isEmpty()) { - return LESS_THAN; - } - return compareQualifier(qualifier1, qualifier2); + String highestVersion = versions.get(0); + for (var version : versions) { + if (compare(version, highestVersion) > EQUAL) { + highestVersion = version; + } } - - private static String stripLeadingChars(String version) { - Pattern pattern = Pattern.compile(CommonConstants.DIGIT_REGEX); - Matcher matcher = pattern.matcher(version); - if (matcher.find()) { - return matcher.group(1); - } - return version; + return highestVersion; + } + + public static int compare(String version, String otherVersion) { + version = stripLeadingChars(version); + otherVersion = stripLeadingChars(otherVersion); + String[] versionParts = createMainAndQualifierArray(version); + String[] otherVersionParts = createMainAndQualifierArray(otherVersion); + + // Compare main version parts + int mainComparison = compareMainVersion(versionParts[0], otherVersionParts[0]); + if (mainComparison != EQUAL) { + return mainComparison; } - private static int compareMainVersion(String mainVersion, String otherMainVersion) { - String[] parts1 = mainVersion.split(MAIN_VERSION_REGEX); - String[] parts2 = otherMainVersion.split(MAIN_VERSION_REGEX); - - int length = Math.max(parts1.length, parts2.length); - for (int i = 0; i < length; i++) { - int num1 = parseToNumber(parts1, i); - int num2 = parseToNumber(parts2, i); - if (num1 != num2) { - return num1 - num2; - } - } - return EQUAL; + // Compare qualifiers + String qualifier1 = getQualifierPart(versionParts); + String qualifier2 = getQualifierPart(otherVersionParts); + // Consider versions without a qualifier higher than those with qualifiers + if (qualifier1.isEmpty() && !qualifier2.isEmpty()) { + return GREATER_THAN; } - - private static String getQualifierPart(String[] versionParts) { - return versionParts.length > 1 ? versionParts[1] : EMPTY; + if (!qualifier1.isEmpty() && qualifier2.isEmpty()) { + return LESS_THAN; } - - private static String[] createMainAndQualifierArray(String version) { - return StringUtils.defaultIfBlank(version, EMPTY).split(DASH_SEPARATOR, 2); + return compareQualifier(qualifier1, qualifier2); + } + + private static String stripLeadingChars(String version) { + Pattern pattern = Pattern.compile(CommonConstants.DIGIT_REGEX); + Matcher matcher = pattern.matcher(version); + if (matcher.find()) { + return matcher.group(1); } + return version; + } + + private static int compareMainVersion(String mainVersion, String otherMainVersion) { + String[] parts1 = mainVersion.split(MAIN_VERSION_REGEX); + String[] parts2 = otherMainVersion.split(MAIN_VERSION_REGEX); + + int length = Math.max(parts1.length, parts2.length); + for (int i = 0; i < length; i++) { + int num1 = parseToNumber(parts1, i); + int num2 = parseToNumber(parts2, i); + if (num1 != num2) { + return num1 - num2; + } + } + return EQUAL; + } + + private static String getQualifierPart(String[] versionParts) { + return versionParts.length > 1 ? versionParts[1] : EMPTY; + } - private static int parseToNumber(String[] versionParts, int index) { - if (index < versionParts.length && NumberUtils.isDigits(versionParts[index])) { - return NumberUtils.toInt(versionParts[index]); - } - return 0; + private static String[] createMainAndQualifierArray(String version) { + return StringUtils.defaultIfBlank(version, EMPTY).split(DASH_SEPARATOR, 2); + } + + private static int parseToNumber(String[] versionParts, int index) { + if (index < versionParts.length && NumberUtils.isDigits(versionParts[index])) { + return NumberUtils.toInt(versionParts[index]); } + return 0; + } - private static int compareQualifier(String qualifier1, String qualifier2) { - if (SNAPSHOT_VERSION.equals(qualifier1) && !SNAPSHOT_VERSION.equals(qualifier2)) { - return LESS_THAN; - } - if (!SNAPSHOT_VERSION.equals(qualifier1) && SNAPSHOT_VERSION.equals(qualifier2)) { - return GREATER_THAN; - } - return qualifier1.compareTo(qualifier2); + private static int compareQualifier(String qualifier1, String qualifier2) { + if (SNAPSHOT_VERSION.equals(qualifier1) && !SNAPSHOT_VERSION.equals(qualifier2)) { + return LESS_THAN; + } + if (!SNAPSHOT_VERSION.equals(qualifier1) && SNAPSHOT_VERSION.equals(qualifier2)) { + return GREATER_THAN; } + return qualifier1.compareTo(qualifier2); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/AsyncConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/AsyncConfig.java index 43dcfcdd0..298cb3047 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/AsyncConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/AsyncConfig.java @@ -9,16 +9,16 @@ @Configuration public class AsyncConfig implements AsyncConfigurer { - private static final String THREAD_NAME_PREFIX = "AC-Thread-"; + private static final String THREAD_NAME_PREFIX = "AC-Thread-"; - @Override - public Executor getAsyncExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(5); - executor.setMaxPoolSize(10); - executor.setQueueCapacity(25); - executor.setThreadNamePrefix(THREAD_NAME_PREFIX); - executor.initialize(); - return executor; - } + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(25); + executor.setThreadNamePrefix(THREAD_NAME_PREFIX); + executor.initialize(); + return executor; + } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java index 67e0dfbed..f9134cc1b 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java @@ -24,17 +24,18 @@ public class MarketApiDocumentConfig { @Bean public GroupedOpenApi buildMarketCustomHeader() { return GroupedOpenApi.builder().group(DEFAULT_DOC_GROUP) - .addOpenApiCustomizer(customMarketHeaders()) - .pathsToMatch(PATH_PATTERN).build(); + .addOpenApiCustomizer(customMarketHeaders()) + .pathsToMatch(PATH_PATTERN).build(); } private OpenApiCustomizer customMarketHeaders() { return openApi -> openApi.getPaths().values().forEach((PathItem pathItem) -> { - List operations = Arrays.asList(pathItem.getPut(), pathItem.getPost(), pathItem.getPatch(), pathItem.getDelete()); + List operations = Arrays.asList(pathItem.getPut(), pathItem.getPost(), pathItem.getPatch(), + pathItem.getDelete()); for (Operation operation : operations) { if (operation != null) { Parameter headerParameter = new Parameter().in(HEADER_PARAM).schema(new StringSchema()) - .name(REQUESTED_BY).description(DEFAULT_PARAM).required(true); + .name(REQUESTED_BY).description(DEFAULT_PARAM).required(true); operation.addParametersItem(headerParameter); } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java index f47d860da..9afeff698 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java @@ -14,12 +14,12 @@ public class MarketHeaderInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { + throws Exception { if (HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) { return true; } if (!HttpMethod.GET.name().equalsIgnoreCase(request.getMethod()) - && StringUtils.isBlank(request.getHeader(CommonConstants.REQUESTED_BY))) { + && StringUtils.isBlank(request.getHeader(CommonConstants.REQUESTED_BY))) { throw new MissingHeaderException(); } return true; diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java index 71df8b67b..becc7dafb 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java @@ -1,5 +1,9 @@ package com.axonivy.market.config; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -13,11 +17,6 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; -import com.mongodb.ConnectionString; -import com.mongodb.MongoClientSettings; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; - @Configuration @EnableMongoRepositories(basePackages = "com.axonivy.market.repository") @EnableMongoAuditing diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/SchedulingConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/SchedulingConfig.java index 275717f54..c9006737b 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/SchedulingConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/SchedulingConfig.java @@ -7,14 +7,14 @@ @Configuration public class SchedulingConfig { - private static final String THREAD_NAME_PREFIX = "SC-Thread-"; + private static final String THREAD_NAME_PREFIX = "SC-Thread-"; - @Bean - public ThreadPoolTaskScheduler taskScheduler() { - ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); - taskScheduler.setPoolSize(10); - taskScheduler.setThreadNamePrefix(THREAD_NAME_PREFIX); - taskScheduler.initialize(); - return taskScheduler; - } + @Bean + public ThreadPoolTaskScheduler taskScheduler() { + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.setPoolSize(10); + taskScheduler.setThreadNamePrefix(THREAD_NAME_PREFIX); + taskScheduler.initialize(); + return taskScheduler; + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java index d50fe342c..619dd7225 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java @@ -10,11 +10,11 @@ public class WebConfig implements WebMvcConfigurer { private static final String ALL_MAPPINGS = "/**"; - private static final String[] EXCLUDE_PATHS = { "/", "/swagger-ui/**", "/api-docs/**", "/api/product-details/**/json", - "/api/image/**" }; - private static final String[] ALLOWED_HEADERS = { "Accept-Language", "Content-Type", "Authorization", - "X-Requested-By", "x-requested-with", "X-Forwarded-Host", "x-xsrf-token", "x-authorization" }; - private static final String[] ALLOWED_METHODS = { "GET", "POST", "PUT", "DELETE", "OPTIONS" }; + private static final String[] EXCLUDE_PATHS = {"/", "/swagger-ui/**", "/api-docs/**", "/api/product-details/**/json", + "/api/image/**"}; + private static final String[] ALLOWED_HEADERS = {"Accept-Language", "Content-Type", "Authorization", + "X-Requested-By", "x-requested-with", "X-Forwarded-Host", "x-xsrf-token", "x-authorization"}; + private static final String[] ALLOWED_METHODS = {"GET", "POST", "PUT", "DELETE", "OPTIONS"}; private final MarketHeaderInterceptor headerInterceptor; diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java index 68a5f236a..2966383f4 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java @@ -14,6 +14,7 @@ public class CommonConstants { public static final String BEARER = "Bearer"; public static final String DIGIT_REGEX = "([0-9]+.*)"; public static final String IMAGE_ID_PREFIX = "imageId-"; + public static final String IMAGE_EXTENSION = "(.*?).(jpeg|jpg|png|gif)"; public static final String ID_WITH_NUMBER_PATTERN = "%s-%s"; public static final String ERROR = "error"; public static final String MESSAGE = "message"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/DirectoryConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/DirectoryConstants.java new file mode 100644 index 000000000..6edc835b9 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/DirectoryConstants.java @@ -0,0 +1,11 @@ +package com.axonivy.market.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class DirectoryConstants { + public static final String DATA_DIR = "data"; + public static final String WORK_DIR = "work"; + +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java index 2d0fee374..4e53b3627 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java @@ -8,11 +8,13 @@ public class EntityConstants { public static final String USER = "User"; public static final String PRODUCT = "Product"; public static final String PRODUCT_DESIGNER_INSTALLATION = "ProductDesignerInstallation"; - public static final String MAVEN_ARTIFACT_VERSION = "MavenArtifactVersion"; + public static final String MAVEN_METADATA_SYNC = "MavenMetadataSync"; + public static final String METADATA = "Metadata"; public static final String GH_REPO_META = "GitHubRepoMeta"; public static final String FEEDBACK = "Feedback"; public static final String PRODUCT_CUSTOM_SORT = "ProductCustomSort"; public static final String PRODUCT_JSON_CONTENT = "ProductJsonContent"; public static final String PRODUCT_MODULE_CONTENT = "ProductModuleContent"; public static final String IMAGE = "Image"; + public static final String MAVEN_ARTIFACT_VERSION = "MavenArtifactVersion"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java index 8b4b79b03..a86ebc8c8 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java @@ -8,7 +8,6 @@ public class GitHubConstants { public static final String AXONIVY_MARKET_ORGANIZATION_NAME = "axonivy-market"; public static final String AXONIVY_MARKETPLACE_REPO_NAME = "market"; public static final String AXONIVY_MARKETPLACE_PATH = "market"; - public static final String PRODUCT_JSON_FILE_PATH_FORMAT = "%s/product.json"; public static final String GITHUB_PROVIDER_NAME = "GitHub"; public static final String GITHUB_GET_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; public static final String README_FILE_LOCALE_REGEX = "_(..)"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java index 67ec50e5a..de5735ddd 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java @@ -1,16 +1,26 @@ package com.axonivy.market.constants; public class MavenConstants { - private MavenConstants() { - } - public static final String SNAPSHOT_VERSION = "SNAPSHOT"; public static final String SNAPSHOT_RELEASE_POSTFIX = "-" + SNAPSHOT_VERSION; public static final String SPRINT_RELEASE_POSTFIX = "-m"; public static final String PRODUCT_ARTIFACT_POSTFIX = "-product"; - public static final String METADATA_URL_FORMAT = "%s/%s/%s/maven-metadata.xml"; public static final String DEFAULT_IVY_MAVEN_BASE_URL = "https://maven.axonivy.com"; - public static final String ARTIFACT_DOWNLOAD_URL_FORMAT = "%s/%s/%s/%s/%s-%s.%s"; + public static final String ARTIFACT_FILE_NAME_FORMAT = "%s-%s.%s"; public static final String ARTIFACT_NAME_FORMAT = "%s (%s)"; - public static final String VERSION_EXTRACT_FORMAT_FROM_METADATA_FILE = "//versions/version/text()"; + public static final String MAIN_VERSION_REGEX = "\\."; + public static final String LATEST_VERSION_TAG = "latest"; + public static final String LATEST_RELEASE_TAG = "release"; + public static final String DATE_TIME_FORMAT = "yyyyMMddHHmmss"; + public static final String VERSION_TAG = "version"; + public static final String LAST_UPDATED_TAG = "lastUpdated"; + public static final String METADATA_URL_POSTFIX = "maven-metadata.xml"; + public static final String SNAPSHOT_LAST_UPDATED_TAG = "timestamp"; + public static final String SNAPSHOT_LAST_UPDATED_DATE_TIME_FORMAT = "yyyyMMdd.HHmmss"; + public static final String VALUE_TAG = "value"; + public static final String DEFAULT_PRODUCT_FOLDER_TYPE = "zip"; + + + private MavenConstants() { + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/MetaConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/MetaConstants.java index 3b088f957..b8b7443bf 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/MetaConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/MetaConstants.java @@ -8,4 +8,5 @@ public class MetaConstants { public static final String META_FILE = "meta.json"; public static final String DEFAULT_VENDOR_NAME = "Axon Ivy AG"; public static final String DEFAULT_VENDOR_URL = "https://www.axonivy.com"; + public static final String DEFAULT_COST_VALUE = "Free"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/MongoDBConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/MongoDBConstants.java index 917709dd9..0febcf61c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/MongoDBConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/MongoDBConstants.java @@ -2,15 +2,18 @@ public class MongoDBConstants { - private MongoDBConstants() { - } - - public static final String ID ="_id"; - public static final String PRODUCT_COLLECTION ="Product"; - public static final String INSTALLATION_COUNT = "InstallationCount"; - public static final String SYNCHRONIZED_INSTALLATION_COUNT ="SynchronizedInstallationCount"; - public static final String PRODUCT_ID = "productId"; - public static final String DESIGNER_VERSION = "designerVersion"; - public static final String TAG = "tag"; + public static final String ID = "_id"; + public static final String PRODUCT_COLLECTION = "Product"; + public static final String INSTALLATION_COUNT = "InstallationCount"; + public static final String SYNCHRONIZED_INSTALLATION_COUNT = "SynchronizedInstallationCount"; + public static final String PRODUCT_ID = "productId"; + public static final String DESIGNER_VERSION = "designerVersion"; + public static final String TAG = "tag"; + public static final String PROJECT_KEY = "$project"; + public static final String RELEASED_VERSIONS = "releasedVersions"; + public static final String ARTIFACTS = "artifacts"; + public static final String MAVEN_VERSIONS = "mavenVersions"; + private MongoDBConstants() { + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java index fd63733dd..e0bcaaf1b 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java @@ -20,6 +20,7 @@ public class ProductJsonConstants { public static final String CUSTOM_ORDER = "customOrder"; public static final String EN_LANGUAGE = "en"; public static final String NAME = "name"; + public static final String DEFAULT_PRODUCT_TYPE = "iar"; private ProductJsonConstants() { } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java index 8d3124988..c8049e839 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java @@ -7,11 +7,13 @@ public class RequestMappingConstants { public static final String ROOT = "/"; public static final String API = ROOT + "api"; - public static final String SYNC = "sync"; public static final String PRODUCT = API + "/product"; public static final String PRODUCT_DETAILS = API + "/product-details"; public static final String PRODUCT_DESIGNER_INSTALLATION = API + "/product-designer-installation"; public static final String FEEDBACK = API + "/feedback"; + public static final String IMAGE = API + "/image"; + public static final String SYNC = "sync"; + public static final String SYNC_PRODUCT_VERSION = SYNC + "/product-version"; public static final String SWAGGER_URL = "/swagger-ui/index.html"; public static final String GIT_HUB_LOGIN = "/github/login"; public static final String AUTH = "/auth"; @@ -24,8 +26,6 @@ public class RequestMappingConstants { public static final String INSTALLATION_COUNT_BY_ID = "/installationcount/{id}"; public static final String PRODUCT_JSON_CONTENT_BY_PRODUCT_ID_AND_VERSION = "/{id}/{version}/json"; public static final String VERSIONS_IN_DESIGNER = "/{id}/designerversions"; - public static final String DESIGNER_INSTALLATION_BY_PRODUCT_ID_AND_DESIGNER_VERSION = "/installation/{productId}/designer/{designerVersion}"; public static final String DESIGNER_INSTALLATION_BY_ID = "/installation/{id}/designer"; public static final String CUSTOM_SORT = "custom-sort"; - public static final String IMAGE = API + "/image"; } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java index 79668a021..1b60e3f0c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java @@ -40,13 +40,8 @@ import java.net.URI; import java.util.List; -import static com.axonivy.market.constants.RequestMappingConstants.BY_ID; -import static com.axonivy.market.constants.RequestMappingConstants.FEEDBACK; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_BY_ID; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_RATING_BY_ID; -import static com.axonivy.market.constants.RequestParamConstants.ID; -import static com.axonivy.market.constants.RequestParamConstants.USER_ID; -import static com.axonivy.market.constants.RequestParamConstants.X_AUTHORIZATION; +import static com.axonivy.market.constants.RequestMappingConstants.*; +import static com.axonivy.market.constants.RequestParamConstants.*; @RestController @RequestMapping(FEEDBACK) @@ -68,12 +63,19 @@ public FeedbackController(FeedbackService feedbackService, JwtService jwtService } @GetMapping(PRODUCT_BY_ID) - @Operation(summary = "Find feedbacks by product id with lazy loading", description = "Get all user feedback by product id (from meta.json) with lazy loading", parameters = { - @Parameter(name = "page", description = "Page number to retrieve", in = ParameterIn.QUERY, example = "0", required = true), - @Parameter(name = "size", description = "Number of items per page", in = ParameterIn.QUERY, example = "20", required = true), - @Parameter(name = "sort", description = "Sorting criteria in the format: Sorting criteria(popularity|alphabetically|recent), Sorting order(asc|desc)", in = ParameterIn.QUERY, example = "[\"popularity\",\"asc\"]", required = true) }) + @Operation(summary = "Find feedbacks by product id with lazy loading", + description = "Get all user feedback by product id (from meta.json) with lazy loading", parameters = { + @Parameter(name = "page", description = "Page number to retrieve", in = ParameterIn.QUERY, example = "0", + required = true), + @Parameter(name = "size", description = "Number of items per page", in = ParameterIn.QUERY, example = "20", + required = true), + @Parameter(name = "sort", + description = "Sorting criteria in the format: Sorting criteria(popularity|alphabetically|recent), Sorting " + + "order(asc|desc)", + in = ParameterIn.QUERY, example = "[\"popularity\",\"asc\"]", required = true)}) public ResponseEntity> findFeedbacks( - @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "portal", in = ParameterIn.PATH) String productId, + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "portal", + in = ParameterIn.PATH) String productId, @ParameterObject Pageable pageable) { Page results = feedbackService.findFeedbacks(productId, pageable); if (results.isEmpty()) { @@ -85,29 +87,38 @@ public ResponseEntity> findFeedbacks( } @GetMapping(BY_ID) - @Operation(summary = "Find all feedbacks by product id", description = "Get all feedbacks by product id(from meta.json) which is used in mobile viewport.") + @Operation(summary = "Find all feedbacks by product id", + description = "Get all feedbacks by product id(from meta.json) which is used in mobile viewport.") public ResponseEntity findFeedback( - @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "portal", in = ParameterIn.PATH) String id) { + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "portal", + in = ParameterIn.PATH) String id) { Feedback feedback = feedbackService.findFeedback(id); return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback)); } @GetMapping() - @Operation(summary = "Find all feedbacks by user id and product id", description = "Get current user feedback on target product.") + @Operation(summary = "Find all feedbacks by user id and product id", + description = "Get current user feedback on target product.") public ResponseEntity findFeedbackByUserIdAndProductId( - @RequestParam(USER_ID) @Parameter(description = "Id of current user from DB", example = "1234", in = ParameterIn.QUERY) String userId, - @RequestParam("productId") @Parameter(description = "Product id (from meta.json)", example = "portal", in = ParameterIn.QUERY) String productId) { + @RequestParam(USER_ID) @Parameter(description = "Id of current user from DB", example = "1234", + in = ParameterIn.QUERY) String userId, + @RequestParam("productId") @Parameter(description = "Product id (from meta.json)", example = "portal", + in = ParameterIn.QUERY) String productId) { Feedback feedback = feedbackService.findFeedbackByUserIdAndProductId(userId, productId); return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback)); } @PostMapping - @Operation(summary = "Create user feedback", description = "Save user feedback of product with their token from Github account.") - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Example request body for feedback", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = FeedbackModelRequest.class))) - @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Successfully created user feedback"), - @ApiResponse(responseCode = "401", description = "Unauthorized request") }) + @Operation(summary = "Create user feedback", + description = "Save user feedback of product with their token from Github account.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Example request body for feedback", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = FeedbackModelRequest.class))) + @ApiResponses(value = {@ApiResponse(responseCode = "201", description = "Successfully created user feedback"), + @ApiResponse(responseCode = "401", description = "Unauthorized request")}) public ResponseEntity createFeedback(@RequestBody @Valid FeedbackModelRequest feedbackRequest, - @RequestHeader(value = X_AUTHORIZATION) @Parameter(description = "JWT Bearer token", example = "Bearer 123456", in = ParameterIn.HEADER) String bearerToken) { + @RequestHeader(value = X_AUTHORIZATION) @Parameter(description = "JWT Bearer token", example = "Bearer 123456", + in = ParameterIn.HEADER) String bearerToken) { String token = null; if (bearerToken != null && bearerToken.startsWith(CommonConstants.BEARER)) { token = bearerToken.substring(CommonConstants.BEARER.length()).trim(); // Remove "Bearer " prefix @@ -127,10 +138,12 @@ public ResponseEntity createFeedback(@RequestBody @Valid FeedbackModelRequ return ResponseEntity.created(location).build(); } - @Operation(summary = "Find rating information of product by its id.", description = "Get overall rating of product by its id.") + @Operation(summary = "Find rating information of product by its id.", + description = "Get overall rating of product by its id.") @GetMapping(PRODUCT_RATING_BY_ID) public ResponseEntity> getProductRating( - @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "portal", in = ParameterIn.PATH) String productId) { + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "portal", + in = ParameterIn.PATH) String productId) { return ResponseEntity.ok(feedbackService.getProductRatingById(productId)); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ImageController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ImageController.java index 5264d7e40..a3df81a39 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ImageController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ImageController.java @@ -33,12 +33,15 @@ public ImageController(ImageService imageService) { } @GetMapping(BY_ID) - @Operation(summary = "Get the image content by id", description = "Collect the byte[] of image with contentType in header is PNG") - @ApiResponse(responseCode = "200", description = "Image found and returned", content = @Content(mediaType = MediaType.IMAGE_PNG_VALUE, schema = @Schema(implementation = Image.class))) + @Operation(summary = "Get the image content by id", + description = "Collect the byte[] of image with contentType in header is PNG") + @ApiResponse(responseCode = "200", description = "Image found and returned", + content = @Content(mediaType = MediaType.IMAGE_PNG_VALUE, schema = @Schema(implementation = Image.class))) @ApiResponse(responseCode = "404", description = "Image not found") @ApiResponse(responseCode = "204", description = "No content (image empty)") public ResponseEntity findImageById( - @PathVariable(ID) @Parameter(description = "The image id", example = "66e7efc8a24f36158df06fc7", in = ParameterIn.PATH) String id) { + @PathVariable(ID) @Parameter(description = "The image id", example = "66e7efc8a24f36158df06fc7", + in = ParameterIn.PATH) String id) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.IMAGE_PNG); byte[] imageData = imageService.readImage(id); diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java index afc4aba6d..2f6a4a3ca 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java @@ -1,14 +1,14 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.AUTH; -import static com.axonivy.market.constants.RequestMappingConstants.GIT_HUB_LOGIN; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.User; import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.github.model.GitHubAccessTokenResponse; +import com.axonivy.market.github.model.GitHubProperty; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.Oauth2AuthorizationCode; +import com.axonivy.market.service.JwtService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -22,13 +22,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.entity.User; -import com.axonivy.market.github.model.GitHubAccessTokenResponse; -import com.axonivy.market.github.model.GitHubProperty; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.model.Oauth2AuthorizationCode; -import com.axonivy.market.service.JwtService; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static com.axonivy.market.constants.RequestMappingConstants.AUTH; +import static com.axonivy.market.constants.RequestMappingConstants.GIT_HUB_LOGIN; @RestController @RequestMapping(AUTH) @@ -49,13 +48,16 @@ public OAuth2Controller(GitHubService gitHubService, JwtService jwtService, GitH @PostMapping(GIT_HUB_LOGIN) @Operation(description = "Get rating authentication token") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully login to GitHub provider", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "200", description = "Successfully login to GitHub provider", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), @ApiResponse(responseCode = "400", description = "Bad Request")}) - @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = Oauth2AuthorizationCode.class))) + @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = Oauth2AuthorizationCode.class))) public ResponseEntity> gitHubLogin(@RequestBody Oauth2AuthorizationCode oauth2AuthorizationCode) { String accessToken; try { - GitHubAccessTokenResponse tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), gitHubProperty); + GitHubAccessTokenResponse tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), + gitHubProperty); accessToken = tokenResponse.getAccessToken(); User user = gitHubService.getAndUpdateUser(accessToken); String jwtToken = jwtService.generateToken(user); diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java index 483f97b41..0bced3833 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java @@ -1,9 +1,5 @@ package com.axonivy.market.controller; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; import com.axonivy.market.assembler.ProductModelAssembler; import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; @@ -13,10 +9,15 @@ import com.axonivy.market.model.Message; import com.axonivy.market.model.ProductCustomSortRequest; import com.axonivy.market.model.ProductModel; +import com.axonivy.market.service.MetadataService; import com.axonivy.market.service.ProductService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; - +import lombok.AllArgsConstructor; import org.apache.commons.lang3.time.StopWatch; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; @@ -35,44 +36,44 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import static com.axonivy.market.constants.RequestMappingConstants.CUSTOM_SORT; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; -import static com.axonivy.market.constants.RequestMappingConstants.SYNC; -import static com.axonivy.market.constants.RequestParamConstants.KEYWORD; -import static com.axonivy.market.constants.RequestParamConstants.LANGUAGE; -import static com.axonivy.market.constants.RequestParamConstants.RESET_SYNC; -import static com.axonivy.market.constants.RequestParamConstants.TYPE; -import static com.axonivy.market.constants.RequestParamConstants.IS_REST_CLIENT; +import static com.axonivy.market.constants.RequestMappingConstants.*; +import static com.axonivy.market.constants.RequestParamConstants.*; import static org.springframework.http.HttpHeaders.AUTHORIZATION; @RestController @RequestMapping(PRODUCT) +@AllArgsConstructor @Tag(name = "Product Controller", description = "API collection to get and search products") public class ProductController { private final ProductService productService; private final GitHubService gitHubService; private final ProductModelAssembler assembler; private final PagedResourcesAssembler pagedResourcesAssembler; - - public ProductController(ProductService productService, GitHubService gitHubService, ProductModelAssembler assembler, - PagedResourcesAssembler pagedResourcesAssembler) { - this.productService = productService; - this.gitHubService = gitHubService; - this.assembler = assembler; - this.pagedResourcesAssembler = pagedResourcesAssembler; - } + private final MetadataService metadataService; @GetMapping() - @Operation(summary = "Retrieve a paginated list of all products, optionally filtered by type, keyword, and language", description = "By default, the system finds products with type 'all'", parameters = { - @Parameter(name = "page", description = "Page number to retrieve", in = ParameterIn.QUERY, example = "0", required = true), - @Parameter(name = "size", description = "Number of items per page", in = ParameterIn.QUERY, example = "20", required = true), - @Parameter(name = "sort", description = "Sorting criteria in the format: Sorting criteria(popularity|alphabetically|recent), Sorting order(asc|desc)", + @Operation(summary = "Retrieve a paginated list of all products, optionally filtered by type, keyword, and language", + description = "By default, the system finds products with type 'all'", parameters = { + @Parameter(name = "page", description = "Page number to retrieve", in = ParameterIn.QUERY, example = "0", + required = true), + @Parameter(name = "size", description = "Number of items per page", in = ParameterIn.QUERY, example = "20", + required = true), + @Parameter(name = "sort", + description = "Sorting criteria in the format: Sorting criteria(popularity|alphabetically|recent), Sorting " + + "order(asc|desc)", in = ParameterIn.QUERY, example = "[\"popularity\",\"asc\"]", required = true)}) public ResponseEntity> findProducts( - @RequestParam(name = TYPE) @Parameter(description = "Type of product.", in = ParameterIn.QUERY, schema = @Schema(type = "string", allowableValues = {"all", "connectors", "utilities", "solutions", "demos"})) String type, - @RequestParam(required = false, name = KEYWORD) @Parameter(description = "Keyword that exist in product's name or short description", example = "connector", in = ParameterIn.QUERY) String keyword, - @RequestParam(name = LANGUAGE) @Parameter(description = "Language of product short description", in = ParameterIn.QUERY, schema = @Schema(allowableValues = {"en", "de"})) String language, - @RequestParam(name = IS_REST_CLIENT) @Parameter(description = "Option to render the website in the REST Client Editor of Designer", in = ParameterIn.QUERY) Boolean isRESTClient, + @RequestParam(name = TYPE) @Parameter(description = "Type of product.", in = ParameterIn.QUERY, + schema = @Schema(type = "string", + allowableValues = {"all", "connectors", "utilities", "solutions", "demos"})) String type, + @RequestParam(required = false, name = KEYWORD) @Parameter( + description = "Keyword that exist in product's name or short description", example = "connector", + in = ParameterIn.QUERY) String keyword, + @RequestParam(name = LANGUAGE) @Parameter(description = "Language of product short description", + in = ParameterIn.QUERY, schema = @Schema(allowableValues = {"en", "de"})) String language, + @RequestParam(name = IS_REST_CLIENT) @Parameter( + description = "Option to render the website in the REST Client Editor of Designer", + in = ParameterIn.QUERY) Boolean isRESTClient, @ParameterObject Pageable pageable) { Page results = productService.findProducts(type, keyword, language, isRESTClient, pageable); if (results.isEmpty()) { @@ -108,6 +109,25 @@ public ResponseEntity syncProducts(@RequestHeader(value = AUTHORIZATION return new ResponseEntity<>(message, HttpStatus.OK); } + @PutMapping(SYNC_PRODUCT_VERSION) + @Operation(hidden = true) + public ResponseEntity syncProductVersions(@RequestHeader(value = AUTHORIZATION) String authorizationHeader) { + String token = getBearerToken(authorizationHeader); + gitHubService.validateUserOrganization(token, GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); + int nonSyncResult = metadataService.syncAllProductsMetadata(); + var message = new Message(); + HttpStatus statusCode = HttpStatus.OK; + if(nonSyncResult == 1) { + message.setHelpCode(ErrorCode.SUCCESSFUL.getCode()); + message.setHelpText(ErrorCode.SUCCESSFUL.getHelpText()); + } else { + statusCode = HttpStatus.INTERNAL_SERVER_ERROR; + message.setHelpCode(ErrorCode.MAVEN_VERSION_SYNC_FAILED.getCode()); + message.setMessageDetails(ErrorCode.MAVEN_VERSION_SYNC_FAILED.getHelpText()); + } + return new ResponseEntity<>(message, statusCode); + } + @PostMapping(CUSTOM_SORT) @Operation(hidden = true) public ResponseEntity createCustomSortProducts( @@ -123,7 +143,8 @@ public ResponseEntity createCustomSortProducts( @SuppressWarnings("unchecked") private ResponseEntity> generateEmptyPagedModel() { - var emptyPagedModel = (PagedModel) pagedResourcesAssembler.toEmptyModel(Page.empty(), ProductModel.class); + var emptyPagedModel = (PagedModel) pagedResourcesAssembler.toEmptyModel(Page.empty(), + ProductModel.class); return new ResponseEntity<>(emptyPagedModel, HttpStatus.OK); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDesignerInstallationController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDesignerInstallationController.java index 301cb7439..d2d2f00ed 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDesignerInstallationController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDesignerInstallationController.java @@ -21,18 +21,22 @@ @RestController @RequestMapping(PRODUCT_DESIGNER_INSTALLATION) -@Tag(name = "Product Designer Installation Controllers", description = "API collection to get designer installation count.") +@Tag(name = "Product Designer Installation Controllers", + description = "API collection to get designer installation count.") public class ProductDesignerInstallationController { - private final ProductDesignerInstallationService productDesignerInstallationService; + private final ProductDesignerInstallationService productDesignerInstallationService; - public ProductDesignerInstallationController(ProductDesignerInstallationService productDesignerInstallationService) { - this.productDesignerInstallationService = productDesignerInstallationService; - } + public ProductDesignerInstallationController(ProductDesignerInstallationService productDesignerInstallationService) { + this.productDesignerInstallationService = productDesignerInstallationService; + } - @GetMapping(DESIGNER_INSTALLATION_BY_ID) - @Operation(summary = "Get designer installation count by product id.", description = "get designer installation count by product id") - public ResponseEntity> getProductDesignerInstallationByProductId(@PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", in = ParameterIn.PATH) String productId) { - List models = productDesignerInstallationService.findByProductId(productId); - return new ResponseEntity<>(models, HttpStatus.OK); - } + @GetMapping(DESIGNER_INSTALLATION_BY_ID) + @Operation(summary = "Get designer installation count by product id.", + description = "get designer installation count by product id") + public ResponseEntity> getProductDesignerInstallationByProductId( + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", + in = ParameterIn.PATH) String productId) { + List models = productDesignerInstallationService.findByProductId(productId); + return new ResponseEntity<>(models, HttpStatus.OK); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java index 5b68520bd..155acca0f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java @@ -1,21 +1,12 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestParamConstants.DESIGNER_VERSION; -import static com.axonivy.market.constants.RequestParamConstants.ID; -import static com.axonivy.market.constants.RequestParamConstants.SHOW_DEV_VERSION; -import static com.axonivy.market.constants.RequestParamConstants.VERSION; -import static com.axonivy.market.constants.RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION; -import static com.axonivy.market.constants.RequestMappingConstants.BY_ID; -import static com.axonivy.market.constants.RequestMappingConstants.BY_ID_AND_VERSION; -import static com.axonivy.market.constants.RequestMappingConstants.INSTALLATION_COUNT_BY_ID; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_JSON_CONTENT_BY_PRODUCT_ID_AND_VERSION; -import static com.axonivy.market.constants.RequestMappingConstants.VERSIONS_BY_ID; -import static com.axonivy.market.constants.RequestMappingConstants.VERSIONS_IN_DESIGNER; -import java.util.List; -import java.util.Map; - +import com.axonivy.market.assembler.ProductDetailModelAssembler; +import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.model.ProductDetailModel; import com.axonivy.market.model.VersionAndUrlModel; +import com.axonivy.market.service.ProductService; +import com.axonivy.market.service.VersionService; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.tags.Tag; @@ -28,13 +19,11 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.axonivy.market.assembler.ProductDetailModelAssembler; -import com.axonivy.market.model.MavenArtifactVersionModel; -import com.axonivy.market.model.ProductDetailModel; -import com.axonivy.market.service.ProductService; -import com.axonivy.market.service.VersionService; +import java.util.List; +import java.util.Map; -import io.swagger.v3.oas.annotations.Operation; +import static com.axonivy.market.constants.RequestMappingConstants.*; +import static com.axonivy.market.constants.RequestParamConstants.*; @RestController @RequestMapping(PRODUCT_DETAILS) @@ -52,28 +41,44 @@ public ProductDetailsController(VersionService versionService, ProductService pr } @GetMapping(BY_ID_AND_VERSION) - @Operation(summary = "Find product detail by product id and release version.", description = "get product detail by it product id and release version") + @Operation(summary = "Find product detail by product id and release version.", + description = "get product detail by it product id and release version") public ResponseEntity findProductDetailsByVersion( - @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", in = ParameterIn.PATH) String id, - @PathVariable(VERSION) @Parameter(description = "Release version (from maven metadata.xml)", example = "10.0.20", in = ParameterIn.PATH) String version) { + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", + in = ParameterIn.PATH) String id, + @PathVariable(VERSION) @Parameter(description = "Release version (from maven metadata.xml)", example = "10.0.20", + in = ParameterIn.PATH) String version) { var productDetail = productService.fetchProductDetailByIdAndVersion(id, version); + if (productDetail == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, version, BY_ID_AND_VERSION), HttpStatus.OK); } @GetMapping(BEST_MATCH_BY_ID_AND_VERSION) - @Operation(summary = "Find best match product detail by product id and version.", description = "get product detail by it product id and version") + @Operation(summary = "Find best match product detail by product id and version.", + description = "get product detail by it product id and version") public ResponseEntity findBestMatchProductDetailsByVersion( - @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", in = ParameterIn.PATH) String id, - @PathVariable(VERSION) @Parameter(description = "Version", example = "10.0.20", in = ParameterIn.PATH) String version) { - var productDetail = productService.fetchBestMatchProductDetail(id,version); - return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, version, BEST_MATCH_BY_ID_AND_VERSION), HttpStatus.OK); + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", + in = ParameterIn.PATH) String id, + @PathVariable(VERSION) @Parameter(description = "Version", example = "10.0.20", + in = ParameterIn.PATH) String version) { + var productDetail = productService.fetchBestMatchProductDetail(id, version); + if (productDetail == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, version, BEST_MATCH_BY_ID_AND_VERSION), + HttpStatus.OK); } @PutMapping(INSTALLATION_COUNT_BY_ID) - @Operation(summary = "Update installation count of product", description = "By default, increase installation count when click download product files by users") + @Operation(summary = "Update installation count of product", + description = "By default, increase installation count when click download product files by users") public ResponseEntity syncInstallationCount( - @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", in = ParameterIn.PATH) String productId, - @RequestParam(name = DESIGNER_VERSION, required = false) @Parameter(in = ParameterIn.QUERY, example = "v10.0.20") String designerVersion) { + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", + in = ParameterIn.PATH) String productId, + @RequestParam(name = DESIGNER_VERSION, required = false) @Parameter(in = ParameterIn.QUERY, + example = "v10.0.20") String designerVersion) { int result = productService.updateInstallationCountForProduct(productId, designerVersion); return new ResponseEntity<>(result, HttpStatus.OK); } @@ -81,31 +86,41 @@ public ResponseEntity syncInstallationCount( @GetMapping(BY_ID) @Operation(summary = "get product detail by ID", description = "Return product detail by product id (from meta.json)") public ResponseEntity findProductDetails( - @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", in = ParameterIn.PATH) String id) { + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", + in = ParameterIn.PATH) String id) { var productDetail = productService.fetchProductDetail(id); + if (productDetail == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, BY_ID), HttpStatus.OK); } @GetMapping(VERSIONS_BY_ID) public ResponseEntity> findProductVersionsById( - @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", in = ParameterIn.PATH) String id, - @RequestParam(SHOW_DEV_VERSION) @Parameter(description = "Option to get Dev Version (Snapshot/ sprint release)", in = ParameterIn.QUERY) boolean isShowDevVersion, - @RequestParam(name = DESIGNER_VERSION, required = false) @Parameter(in = ParameterIn.QUERY, example = "v10.0.20") String designerVersion) { + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", + in = ParameterIn.PATH) String id, + @RequestParam(SHOW_DEV_VERSION) @Parameter(description = "Option to get Dev Version (Snapshot/ sprint release)", + in = ParameterIn.QUERY) boolean isShowDevVersion, + @RequestParam(name = DESIGNER_VERSION, required = false) @Parameter(in = ParameterIn.QUERY, + example = "v10.0.20") String designerVersion) { List models = versionService.getArtifactsAndVersionToDisplay(id, isShowDevVersion, designerVersion); return new ResponseEntity<>(models, HttpStatus.OK); } @GetMapping(PRODUCT_JSON_CONTENT_BY_PRODUCT_ID_AND_VERSION) - @Operation(summary = "Get product json content for designer to install", description = "When we click install in designer, this API will send content of product json for installing in Ivy designer") + @Operation(summary = "Get product json content for designer to install", + description = "When we click install in designer, this API will send content of product json for installing in " + + "Ivy designer") public ResponseEntity> findProductJsonContent(@PathVariable(ID) String productId, @PathVariable(VERSION) String version) { - Map productJsonContent = versionService.getProductJsonContentByIdAndVersion(productId, version); + Map productJsonContent = versionService.getProductJsonContentByIdAndTag(productId, version); return new ResponseEntity<>(productJsonContent, HttpStatus.OK); } @GetMapping(VERSIONS_IN_DESIGNER) - @Operation(summary = "Get the list of released version in product", description = "Collect the released versions in product for ivy designer") + @Operation(summary = "Get the list of released version in product", + description = "Collect the released versions in product for ivy designer") public ResponseEntity> findVersionsForDesigner(@PathVariable(ID) String id) { List versionList = versionService.getVersionsForDesigner(id); return new ResponseEntity<>(versionList, HttpStatus.OK); diff --git a/marketplace-service/src/main/java/com/axonivy/market/criteria/ProductSearchCriteria.java b/marketplace-service/src/main/java/com/axonivy/market/criteria/ProductSearchCriteria.java index 60ed02595..d20b243c8 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/criteria/ProductSearchCriteria.java +++ b/marketplace-service/src/main/java/com/axonivy/market/criteria/ProductSearchCriteria.java @@ -1,18 +1,17 @@ package com.axonivy.market.criteria; -import static com.axonivy.market.enums.DocumentField.NAMES; -import static com.axonivy.market.enums.DocumentField.SHORT_DESCRIPTIONS; - -import java.util.List; - import com.axonivy.market.enums.DocumentField; import com.axonivy.market.enums.Language; import com.axonivy.market.enums.TypeOption; - import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; + +import static com.axonivy.market.enums.DocumentField.NAMES; +import static com.axonivy.market.enums.DocumentField.SHORT_DESCRIPTIONS; + @Data @NoArgsConstructor @AllArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Image.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Image.java index 85e6842ef..0a2075581 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/Image.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Image.java @@ -23,9 +23,11 @@ public class Image { private String id; @Schema(description = "Product id", example = "jira-connector") private String productId; - @Schema(description = "The download url from github", example = "https://raw.githubusercontent.comamazon-comprehend/logo.png") + @Schema(description = "The download url from github", + example = "https://raw.githubusercontent.comamazon-comprehend/logo.png") private String imageUrl; - @Schema(description = "The image content as binary type", example = "Binary(Buffer.from(\"89504e470d0a1a0a0000000d\", \"hex\"), 0)") + @Schema(description = "The image content as binary type", + example = "Binary(Buffer.from(\"89504e470d0a1a0a0000000d\", \"hex\"), 0)") private Binary imageData; @Schema(description = "The SHA from github", example = "93b1e2f1595d3a85e51b01") private String sha; diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java deleted file mode 100644 index e1bb99383..000000000 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.axonivy.market.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.springframework.data.annotation.Transient; - -import java.io.Serial; -import java.io.Serializable; -import java.util.Objects; - -@AllArgsConstructor -@NoArgsConstructor -@Setter -@Getter -public class MavenArtifactModel implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - @Schema(description = "Display name and type of artifact", example = "Adobe Acrobat Sign Connector (.iar)") - private String name; - @Schema(description = "Artifact download url", example = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/10.0.25/adobe-acrobat-sign-connector-10.0.25.iar") - private String downloadUrl; - @Transient - private Boolean isProductArtifact; - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) { - return false; - } - MavenArtifactModel reference = (MavenArtifactModel) object; - return Objects.equals(name, reference.getName()) && Objects.equals(downloadUrl, reference.getDownloadUrl()); - } - - @Override - public int hashCode() { - return Objects.hash(name, downloadUrl); - } -} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java index e7a6a2e92..c4ecedfa6 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactVersion.java @@ -1,7 +1,8 @@ package com.axonivy.market.entity; +import com.axonivy.market.model.MavenArtifactModel; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; import lombok.NoArgsConstructor; import lombok.Setter; import org.springframework.data.annotation.Id; @@ -9,28 +10,42 @@ import java.io.Serial; import java.io.Serializable; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import static com.axonivy.market.constants.EntityConstants.MAVEN_ARTIFACT_VERSION; - -@Getter @Setter @AllArgsConstructor @NoArgsConstructor +@Builder @Document(MAVEN_ARTIFACT_VERSION) public class MavenArtifactVersion implements Serializable { @Serial private static final long serialVersionUID = -6492612804634492078L; - @Id private String productId; - private List versions = new ArrayList<>(); - private Map> productArtifactWithVersionReleased = new HashMap<>(); + private Map> productArtifactsByVersion; + private Map> additionalArtifactsByVersion; + + public String getProductId() { + return productId; + } - public MavenArtifactVersion(String productId) { - this.productId = productId; + public Map> getProductArtifactsByVersion() { + if (Objects.isNull(productArtifactsByVersion)) { + this.productArtifactsByVersion = new HashMap<>(); + } + return this.productArtifactsByVersion; } + + public Map> getAdditionalArtifactsByVersion() { + if (Objects.isNull(this.additionalArtifactsByVersion)) { + this.additionalArtifactsByVersion = new HashMap<>(); + } + return this.additionalArtifactsByVersion; + } + + } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Metadata.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Metadata.java new file mode 100644 index 000000000..670d013d7 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Metadata.java @@ -0,0 +1,60 @@ +package com.axonivy.market.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.Set; + +import static com.axonivy.market.constants.EntityConstants.METADATA; + + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Document(METADATA) +public class Metadata implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + @Id + private String url; + private String productId; + private LocalDateTime lastUpdated; + private String artifactId; + private String groupId; + private String latest; + private String release; + private Set versions; + private String repoUrl; + private String type; + private String name; + private boolean isProductArtifact; + private String snapshotVersionValue; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Metadata that = (Metadata) o; + return Objects.equals(url, that.url); + } + + @Override + public int hashCode() { + return Objects.hashCode(url); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/MetadataSync.java b/marketplace-service/src/main/java/com/axonivy/market/entity/MetadataSync.java new file mode 100644 index 000000000..1b774d787 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/MetadataSync.java @@ -0,0 +1,29 @@ +package com.axonivy.market.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.Date; +import java.util.Set; + +import static com.axonivy.market.constants.EntityConstants.MAVEN_METADATA_SYNC; + +@NoArgsConstructor +@AllArgsConstructor +@Setter +@Getter +@Builder +@Document(MAVEN_METADATA_SYNC) +public class MetadataSync { + @Id + private String productId; + private Set syncedVersions; + @LastModifiedDate + private Date lastSync; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java index 00db4d4e4..690a9c481 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java @@ -1,7 +1,6 @@ package com.axonivy.market.entity; -import static com.axonivy.market.constants.EntityConstants.PRODUCT; -import com.axonivy.market.github.model.MavenArtifact; +import com.axonivy.market.bo.Artifact; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; @@ -21,6 +20,8 @@ import java.util.List; import java.util.Map; +import static com.axonivy.market.constants.EntityConstants.PRODUCT; + @Getter @Setter @AllArgsConstructor @@ -59,7 +60,7 @@ public class Product implements Serializable { private String newestReleaseVersion; @Transient private ProductModuleContent productModuleContent; - private List artifacts; + private List artifacts; private Boolean synchronizedInstallationCount; private Integer customOrder; private List releasedVersions; @@ -68,6 +69,8 @@ public class Product implements Serializable { private String logoId; @LastModifiedDate private Date updatedAt; + @Transient + private String bestMatchVersion; @Override public int hashCode() { diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductDesignerInstallation.java b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductDesignerInstallation.java index 127a92ead..e558f0623 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductDesignerInstallation.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductDesignerInstallation.java @@ -1,6 +1,10 @@ package com.axonivy.market.entity; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.springframework.data.annotation.Id; @@ -18,24 +22,24 @@ @Builder @Document(PRODUCT_DESIGNER_INSTALLATION) public class ProductDesignerInstallation implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - @Id - private String id; - private String productId; - private String designerVersion; - private int installationCount; + @Serial + private static final long serialVersionUID = 1L; + @Id + private String id; + private String productId; + private String designerVersion; + private int installationCount; - @Override - public int hashCode() { - return new HashCodeBuilder().append(productId).hashCode(); - } + @Override + public int hashCode() { + return new HashCodeBuilder().append(productId).hashCode(); + } - @Override - public boolean equals(Object obj) { - if (obj == null || this.getClass() != obj.getClass()) { - return false; - } - return new EqualsBuilder().append(productId, ((ProductDesignerInstallation) obj).getProductId()).isEquals(); + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; } + return new EqualsBuilder().append(productId, ((ProductDesignerInstallation) obj).getProductId()).isEquals(); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductJsonContent.java b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductJsonContent.java index a6a48fb6f..181babb66 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductJsonContent.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductJsonContent.java @@ -25,6 +25,7 @@ public class ProductJsonContent { @JsonIgnore private String id; private String version; + private String relatedTag; private String productId; private String name; private String content; diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java index 08762f4ed..c2542768a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java @@ -14,6 +14,7 @@ import java.io.Serializable; import java.util.Date; import java.util.Map; +import java.util.Set; import static com.axonivy.market.constants.EntityConstants.PRODUCT_MODULE_CONTENT; @@ -32,7 +33,10 @@ public class ProductModuleContent implements Serializable { private String productId; @Schema(description = "Target release tag", example = "v10.0.25") private String tag; - @Schema(description = "Product detail description content ", example = "{ \"de\": \"E-Sign-Konnektor\", \"en\": \"E-sign connector\" }") + @Schema(description = "Versions in maven", example = "10.0.25-SNAPSHOT") + private Set mavenVersions; + @Schema(description = "Product detail description content ", + example = "{ \"de\": \"E-Sign-Konnektor\", \"en\": \"E-sign connector\" }") private Map description; @Schema(description = "Setup tab content", example = "{ \"de\": \"Setup\", \"en\": \"Setup\" ") private Map setup; diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java index fa939c58c..89e5a5389 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java @@ -18,8 +18,9 @@ public enum ErrorCode { GH_FILE_STATUS_INVALID("0201", "GIT_HUB_FILE_STATUS_INVALID"), GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"), USER_NOT_FOUND("2103", "USER_NOT_FOUND"), GITHUB_USER_NOT_FOUND("2204", "GITHUB_USER_NOT_FOUND"), GITHUB_USER_UNAUTHORIZED("2205", "GITHUB_USER_UNAUTHORIZED"), - FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), NO_FEEDBACK_OF_USER_FOR_PRODUCT("3103", "NO_FEEDBACK_OF_USER_FOR_PRODUCT"), - ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST"); + FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), NO_FEEDBACK_OF_USER_FOR_PRODUCT("3103", + "NO_FEEDBACK_OF_USER_FOR_PRODUCT"), ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST"), + MAVEN_VERSION_SYNC_FAILED("1104","PRODUCT_MAVEN_SYNCED_FAILED"); String code; String helpText; diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java b/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java index 0535ce237..68b1f72cf 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java @@ -2,6 +2,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; import java.util.Arrays; import java.util.Map; @@ -15,18 +16,28 @@ public enum NonStandardProduct { PORTAL("portal", true, COMMON_IMAGES_FOLDER_NAME, "AxonIvyPortal/portal-product"), MICROSOFT_REPO_NAME("msgraph-connector", false, COMMON_IMAGES_FOLDER_NAME, ""), - MICROSOFT_365("msgraph", false, COMMON_IMAGES_FOLDER_NAME, "msgraph-connector-product/products/msgraph-connector"), // No meta.json - MICROSOFT_CALENDAR("msgraph-calendar", false, COMMON_IMAGES_FOLDER_NAME, "msgraph-connector-product/products/msgraph-calendar"), // no fix product json - MICROSOFT_MAIL("msgraph-mail", false, COMMON_IMAGES_FOLDER_NAME, "msgraph-connector-product/products/msgraph-mail"),// no fix product json - MICROSOFT_TEAMS("msgraph-chat", false, COMMON_IMAGES_FOLDER_NAME, "msgraph-connector-product/products/msgraph-chat"),// no fix product json - MICROSOFT_TODO("msgraph-todo", false, COMMON_IMAGES_FOLDER_NAME, "msgraph-connector-product/products/msgraph-todo"),// no fix product json - CONNECTIVITY_FEATURE("connectivity-demo", false, COMMON_IMAGES_FOLDER_NAME, "connectivity/connectivity-demos-product"), + MICROSOFT_365("msgraph", false, COMMON_IMAGES_FOLDER_NAME, + "msgraph-connector-product/products/msgraph-connector"), // No meta.json + MICROSOFT_CALENDAR("msgraph-calendar", false, COMMON_IMAGES_FOLDER_NAME, + "msgraph-connector-product/products/msgraph-calendar"), // no fix product json + MICROSOFT_MAIL("msgraph-mail", false, COMMON_IMAGES_FOLDER_NAME, + "msgraph-connector-product/products/msgraph-mail"),// no fix product json + MICROSOFT_TEAMS("msgraph-chat", false, COMMON_IMAGES_FOLDER_NAME, + "msgraph-connector-product/products/msgraph-chat"),// no fix product json + MICROSOFT_TODO("msgraph-todo", false, COMMON_IMAGES_FOLDER_NAME, + "msgraph-connector-product/products/msgraph-todo"),// no fix product json + CONNECTIVITY_FEATURE("connectivity-demo", false, COMMON_IMAGES_FOLDER_NAME, + "connectivity/connectivity-demos-product"), EMPLOYEE_ONBOARDING("employee-onboarding", false, COMMON_IMAGES_FOLDER_NAME, ""), // Invalid meta.json - ERROR_HANDLING("error-handling-demo", false, COMMON_IMAGES_FOLDER_NAME, "error-handling/error-handling-demos-product"), + ERROR_HANDLING("error-handling-demo", false, COMMON_IMAGES_FOLDER_NAME, + "error-handling/error-handling-demos-product"), RULE_ENGINE_DEMOS("rule-engine-demo", false, COMMON_IMAGES_FOLDER_NAME, "rule-engine/rule-engine-demos-product"), WORKFLOW_DEMO("workflow-demo", false, COMMON_IMAGES_FOLDER_NAME, "workflow/workflow-demos-product"), HTML_DIALOG_DEMO("html-dialog-demo", false, COMMON_IMAGES_FOLDER_NAME, "html-dialog/html-dialog-demos-product"), PROCESSING_VALVE_DEMO("processing-valve-demo", false, COMMON_IMAGES_FOLDER_NAME, ""),// no product json + ASPOSE_BARCODE("aspose-barcode-demo", false, "", "aspose-barcode-demo-product"), + DOC_FACTORY("doc-factory", false, "", "doc-factory-product"), + ASPOSE_EMAIL("aspose-email-demo", false, "", "aspose-email-demo-product"), OPENAI_CONNECTOR("openai-connector", false, COMMON_IMAGES_FOLDER_NAME, "openai-connector-product"), OPENAI_ASSISTANT("openai-assistant", false, "docs", "openai-assistant-product"), // Non standard image folder name @@ -43,10 +54,16 @@ public enum NonStandardProduct { private static final Map NON_STANDARD_PRODUCT_MAP; static { - NON_STANDARD_PRODUCT_MAP = Arrays.stream(NonStandardProduct.values()).collect(Collectors.toMap(NonStandardProduct::getId, Function.identity())); + NON_STANDARD_PRODUCT_MAP = Arrays.stream(NonStandardProduct.values()).collect( + Collectors.toMap(NonStandardProduct::getId, Function.identity())); } public static NonStandardProduct findById(String id) { - return NON_STANDARD_PRODUCT_MAP.getOrDefault(id,DEFAULT); + return NON_STANDARD_PRODUCT_MAP.getOrDefault(id, DEFAULT); } -} + + public static String findById(String id, String currentPath) { + String nonStandardPath = findById(id).pathToProductFolder; + return StringUtils.isNotBlank(nonStandardPath) ? nonStandardPath : currentPath; + } +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java index 24738b39a..900134acc 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java @@ -84,6 +84,7 @@ public ResponseEntity handleOauth2ExchangeCodeException( errorMessage.setMessageDetails(oauth2ExchangeCodeException.getErrorDescription()); return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); } + @ExceptionHandler(UnauthorizedException.class) public ResponseEntity handleUnauthorizedException( UnauthorizedException unauthorizedException) { diff --git a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java index a1ee2fdce..b4f6dc31f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java +++ b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java @@ -1,11 +1,8 @@ package com.axonivy.market.factory; -import static com.axonivy.market.constants.CommonConstants.SLASH; -import static com.axonivy.market.constants.MetaConstants.DEFAULT_VENDOR_NAME; -import static com.axonivy.market.constants.MetaConstants.DEFAULT_VENDOR_URL; -import static com.axonivy.market.constants.MetaConstants.META_FILE; -import static org.apache.commons.lang3.StringUtils.EMPTY; +import com.axonivy.market.bo.Artifact; import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.MetaConstants; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductJsonContent; import com.axonivy.market.entity.ProductModuleContent; @@ -19,12 +16,16 @@ import org.apache.commons.lang3.StringUtils; import org.kohsuke.github.GHContent; import org.springframework.util.CollectionUtils; + import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import static com.axonivy.market.constants.CommonConstants.SLASH; +import static org.apache.commons.lang3.StringUtils.EMPTY; + @Log4j2 @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ProductFactory { @@ -36,7 +37,7 @@ public static Product mappingByGHContent(Product product, GHContent content) { } var contentName = content.getName(); - if (StringUtils.endsWith(contentName, META_FILE)) { + if (StringUtils.endsWith(contentName, MetaConstants.META_FILE)) { mappingByMetaJSONFile(product, content); } return product; @@ -59,17 +60,22 @@ public static Product mappingByMetaJSONFile(Product product, GHContent ghContent product.setTags(meta.getTags()); product.setVersion(meta.getVersion()); product.setShortDescriptions(mappingMultilingualismValueByMetaJSONFile(meta.getDescriptions())); - product.setVendor(StringUtils.isBlank(meta.getVendor()) ? DEFAULT_VENDOR_NAME : meta.getVendor()); - product.setVendorUrl(StringUtils.isBlank(meta.getVendorUrl()) ? DEFAULT_VENDOR_URL : meta.getVendorUrl()); + product.setVendor(StringUtils.defaultIfEmpty(meta.getVendor(), MetaConstants.DEFAULT_VENDOR_NAME)); + product.setVendorUrl(StringUtils.defaultIfEmpty(meta.getVendorUrl(), MetaConstants.DEFAULT_VENDOR_URL)); product.setPlatformReview(meta.getPlatformReview()); product.setStatusBadgeUrl(meta.getStatusBadgeUrl()); product.setLanguage(meta.getLanguage()); product.setIndustry(meta.getIndustry()); product.setContactUs(BooleanUtils.isTrue(meta.getContactUs())); - product.setCost(StringUtils.isBlank(meta.getCost()) ? "Free" : StringUtils.capitalize(meta.getCost())); + product.setCost( + StringUtils.capitalize(StringUtils.defaultIfEmpty(meta.getCost(), MetaConstants.DEFAULT_COST_VALUE))); product.setCompatibility(meta.getCompatibility()); extractSourceUrl(product, meta); - product.setArtifacts(meta.getMavenArtifacts()); + List artifacts = CollectionUtils.isEmpty( + meta.getMavenArtifacts()) ? new ArrayList<>() : meta.getMavenArtifacts(); + artifacts.stream().forEach( + artifact -> artifact.setInvalidArtifact(!artifact.getArtifactId().contains(meta.getId()))); + product.setArtifacts(artifacts); product.setReleasedVersions(new ArrayList<>()); return product; } @@ -89,7 +95,6 @@ private static Map mappingMultilingualismValueByMetaJSONFile(Lis value.put(name.getLocale(), name.getValue()); } } - return value; } @@ -118,14 +123,17 @@ private static Meta jsonDecode(GHContent ghContent) throws IOException { } public static void mappingIdForProductModuleContent(ProductModuleContent content) { - if (StringUtils.isNotBlank(content.getProductId()) && StringUtils.isNotBlank(content.getTag())) { - content.setId(String.format(CommonConstants.ID_WITH_NUMBER_PATTERN, content.getProductId(), content.getTag())); + if (StringUtils.isNotBlank(content.getProductId())) { + String version = StringUtils.isNotBlank( + content.getTag()) ? content.getTag() : content.getMavenVersions().stream().findAny().orElse(null); + content.setId(String.format(CommonConstants.ID_WITH_NUMBER_PATTERN, content.getProductId(), version)); } } public static void mappingIdForProductJsonContent(ProductJsonContent content) { if (StringUtils.isNotBlank(content.getProductId()) && StringUtils.isNotBlank(content.getVersion())) { - content.setId(String.format(CommonConstants.ID_WITH_NUMBER_PATTERN, content.getProductId(), content.getVersion())); + content.setId( + String.format(CommonConstants.ID_WITH_NUMBER_PATTERN, content.getProductId(), content.getVersion())); } } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubProperty.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubProperty.java index 3c9c52c59..d450eac61 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubProperty.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubProperty.java @@ -1,12 +1,11 @@ package com.axonivy.market.github.model; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; @Getter @Setter diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java index 918b90378..89171a989 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java @@ -1,5 +1,6 @@ package com.axonivy.market.github.model; +import com.axonivy.market.bo.Artifact; import com.axonivy.market.model.DisplayValue; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; @@ -32,7 +33,7 @@ public class Meta { private String vendor; private String vendorUrl; private List tags; - private List mavenArtifacts; + private List mavenArtifacts; private String compatibility; private Boolean contactUs; private String cost; diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java index debe27c42..b4b8821c1 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java @@ -2,7 +2,6 @@ import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductModuleContent; -import com.axonivy.market.github.model.MavenArtifact; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHTag; @@ -18,7 +17,6 @@ public interface GHAxonIvyProductRepoService { ProductModuleContent getReadmeAndProductContentsFromTag(Product product, GHRepository ghRepository, String tag); - List convertProductJsonToMavenProductInfo(GHContent content) throws IOException; - - void extractReadMeFileFromContents(Product product, List contents, ProductModuleContent productModuleContent); + void extractReadMeFileFromContents(Product product, List contents, + ProductModuleContent productModuleContent); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java index a5996d18b..a31309e4a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java @@ -1,20 +1,19 @@ package com.axonivy.market.github.service; -import java.io.IOException; -import java.util.List; - -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GitHub; -import org.kohsuke.github.GHTag; - import com.axonivy.market.entity.User; import com.axonivy.market.exceptions.model.MissingHeaderException; import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; import com.axonivy.market.exceptions.model.UnauthorizedException; import com.axonivy.market.github.model.GitHubAccessTokenResponse; import com.axonivy.market.github.model.GitHubProperty; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTag; +import org.kohsuke.github.GitHub; + +import java.io.IOException; +import java.util.List; public interface GitHubService { diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java index db2053337..e59833d5e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java @@ -29,10 +29,9 @@ @Service public class GHAxonIvyMarketRepoServiceImpl implements GHAxonIvyMarketRepoService { private static final LocalDateTime INITIAL_COMMIT_DATE = LocalDateTime.of(2020, 10, 30, 0, 0); + private final GitHubService gitHubService; private GHOrganization organization; private GHRepository repository; - - private final GitHubService gitHubService; @Value("${market.github.market.branch}") private String marketRepoBranch; diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java index 2d0e673f0..45d681b8e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java @@ -1,31 +1,23 @@ package com.axonivy.market.github.service.impl; +import com.axonivy.market.bo.Artifact; import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.constants.MavenConstants; import com.axonivy.market.constants.ProductJsonConstants; import com.axonivy.market.constants.ReadmeConstants; -import com.axonivy.market.entity.Image; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductModuleContent; -import com.axonivy.market.entity.ProductJsonContent; -import com.axonivy.market.enums.Language; import com.axonivy.market.enums.NonStandardProduct; -import com.axonivy.market.factory.ProductFactory; -import com.axonivy.market.github.model.MavenArtifact; import com.axonivy.market.github.service.GHAxonIvyProductRepoService; import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.github.util.GitHubUtils; -import com.axonivy.market.repository.ProductJsonContentRepository; import com.axonivy.market.service.ImageService; +import com.axonivy.market.service.ProductJsonContentService; +import com.axonivy.market.util.ProductContentUtils; import com.axonivy.market.util.VersionUtils; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.log4j.Log4j2; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.util.Strings; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; @@ -33,116 +25,38 @@ import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; -import javax.swing.text.html.Option; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import static com.axonivy.market.constants.CommonConstants.IMAGE_ID_PREFIX; -import static com.axonivy.market.constants.ProductJsonConstants.EN_LANGUAGE; -import static com.axonivy.market.constants.ProductJsonConstants.VERSION_VALUE; @Log4j2 @Service public class GHAxonIvyProductRepoServiceImpl implements GHAxonIvyProductRepoService { - private GHOrganization organization; + public static final String IMAGE_EXTENSION = "(.*?).(jpeg|jpg|png|gif)"; private final GitHubService gitHubService; private final ImageService imageService; - private final ProductJsonContentRepository productJsonContentRepository; - private String repoUrl; - private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final String HASH = "#"; - public static final String DEMO_SETUP_TITLE = "(?i)## Demo|## Setup"; - public static final String IMAGE_EXTENSION = "(.*?).(jpeg|jpg|png|gif)"; - public static final String README_IMAGE_FORMAT = "\\(([^)]*?%s[^)]*?)\\)"; - public static final String IMAGE_DOWNLOAD_URL_FORMAT = "(%s)"; - public static final String DESCRIPTION = "description"; - public static final String DEMO = "demo"; - public static final String SETUP = "setup"; + private GHOrganization organization; + private final ProductJsonContentService productJsonContentService; public GHAxonIvyProductRepoServiceImpl(GitHubService gitHubService, ImageService imageService, - ProductJsonContentRepository productJsonContentRepository) { + ProductJsonContentService productJsonContentService) { this.gitHubService = gitHubService; this.imageService = imageService; - this.productJsonContentRepository = productJsonContentRepository; - } - - @Override - public List convertProductJsonToMavenProductInfo(GHContent content) throws IOException { - List artifacts = new ArrayList<>(); - InputStream contentStream = extractedContentStream(content); - if (Objects.isNull(contentStream)) { - return artifacts; - } - - JsonNode rootNode = objectMapper.readTree(contentStream); - JsonNode installersNode = rootNode.path(ProductJsonConstants.INSTALLERS); - - for (JsonNode mavenNode : installersNode) { - JsonNode dataNode = mavenNode.path(ProductJsonConstants.DATA); - - // Not convert to artifact if id of node is not maven-import or maven-dependency - List installerIdsToDisplay = List.of(ProductJsonConstants.MAVEN_DEPENDENCY_INSTALLER_ID, - ProductJsonConstants.MAVEN_IMPORT_INSTALLER_ID); - if (!installerIdsToDisplay.contains(mavenNode.path(ProductJsonConstants.ID).asText())) { - continue; - } - - // Extract repository URL - JsonNode repositoriesNode = dataNode.path(ProductJsonConstants.REPOSITORIES); - repoUrl = repositoriesNode.get(0).path(ProductJsonConstants.URL).asText(); - - // Process projects - if (dataNode.has(ProductJsonConstants.PROJECTS)) { - extractMavenArtifactFromJsonNode(dataNode, false, artifacts); - } - - // Process dependencies - if (dataNode.has(ProductJsonConstants.DEPENDENCIES)) { - extractMavenArtifactFromJsonNode(dataNode, true, artifacts); - } - } - return artifacts; + this.productJsonContentService = productJsonContentService; } - public InputStream extractedContentStream(GHContent content) { - try { - return content.read(); - } catch (IOException | NullPointerException e) { - log.warn("Can not read the current content: {}", e.getMessage()); - return null; - } - } - - public void extractMavenArtifactFromJsonNode(JsonNode dataNode, boolean isDependency, List artifacts) { - String nodeName = ProductJsonConstants.PROJECTS; - if (isDependency) { - nodeName = ProductJsonConstants.DEPENDENCIES; - } - JsonNode dependenciesNode = dataNode.path(nodeName); - for (JsonNode dependencyNode : dependenciesNode) { - MavenArtifact artifact = createArtifactFromJsonNode(dependencyNode, repoUrl, isDependency); - artifacts.add(artifact); - } - } - - public MavenArtifact createArtifactFromJsonNode(JsonNode node, String repoUrl, boolean isDependency) { - MavenArtifact artifact = new MavenArtifact(); - artifact.setRepoUrl(repoUrl); - artifact.setIsDependency(isDependency); - artifact.setGroupId(node.path(ProductJsonConstants.GROUP_ID).asText()); - artifact.setArtifactId(node.path(ProductJsonConstants.ARTIFACT_ID).asText()); - artifact.setType(node.path(ProductJsonConstants.TYPE).asText()); - artifact.setIsProductArtifact(true); - return artifact; + private static GHContent getProductJsonFile(List contents) { + return contents.stream().filter(GHContent::isFile) + .filter(content -> ProductJsonConstants.PRODUCT_JSON_FILE.equals(content.getName())).findFirst().orElse(null); } @Override @@ -170,98 +84,57 @@ public List getAllTagsFromRepoName(String repoName) throws IOException { @Override public ProductModuleContent getReadmeAndProductContentsFromTag(Product product, GHRepository ghRepository, String tag) { - ProductModuleContent productModuleContent = new ProductModuleContent(); + ProductModuleContent productModuleContent = ProductContentUtils.initProductModuleContent(product, tag, + new HashSet<>()); try { List contents = getProductFolderContents(product, ghRepository, tag); - productModuleContent.setProductId(product.getId()); - productModuleContent.setTag(tag); - ProductFactory.mappingIdForProductModuleContent(productModuleContent); - updateDependencyContentsFromProductJson(productModuleContent, contents , product); + updateDependencyContentsFromProductJson(productModuleContent, contents, product); extractReadMeFileFromContents(product, contents, productModuleContent); } catch (Exception e) { - log.error("Cannot get product.json content {}", e.getMessage()); + log.error("Cannot get product.json content in {} - {}", ghRepository.getName(), e.getMessage()); return null; } return productModuleContent; } - public void extractReadMeFileFromContents(Product product, List contents, ProductModuleContent productModuleContent) { + public void extractReadMeFileFromContents(Product product, List contents, + ProductModuleContent productModuleContent) { try { List readmeFiles = contents.stream().filter(GHContent::isFile) .filter(content -> content.getName().startsWith(ReadmeConstants.README_FILE_NAME)).toList(); Map> moduleContents = new HashMap<>(); + if (!CollectionUtils.isEmpty(readmeFiles)) { for (GHContent readmeFile : readmeFiles) { String readmeContents = new String(readmeFile.read().readAllBytes()); - if (hasImageDirectives(readmeContents)) { + if (ProductContentUtils.hasImageDirectives(readmeContents)) { readmeContents = updateImagesWithDownloadUrl(product, contents, readmeContents); } - String locale = getReadmeFileLocale(readmeFile.getName()); - getExtractedPartsOfReadme(moduleContents, readmeContents, locale); + ProductContentUtils.getExtractedPartsOfReadme(moduleContents, readmeContents, readmeFile.getName()); } - productModuleContent.setDescription(replaceEmptyContentsWithEnContent(moduleContents.get(DESCRIPTION))); - productModuleContent.setDemo(replaceEmptyContentsWithEnContent(moduleContents.get(DEMO))); - productModuleContent.setSetup(replaceEmptyContentsWithEnContent(moduleContents.get(SETUP))); + ProductContentUtils.updateProductModuleTabContents(productModuleContent, moduleContents); } } catch (Exception e) { log.error("Cannot get README file's content {}", e.getMessage()); } } - /** - * MARP-810: Sabine requires that content in other languages, which has not been translated, be left empty and replaced with English content. - */ - public Map replaceEmptyContentsWithEnContent(Map map) { - String enValue = map.get(Language.EN.getValue()); - for (Map.Entry entry : map.entrySet()) { - if (StringUtils.isBlank(entry.getValue())) { - map.put(entry.getKey(), enValue); - } - } - return map; - } - - private String getReadmeFileLocale(String readmeFile) { - String result = StringUtils.EMPTY; - Pattern pattern = Pattern.compile(GitHubConstants.README_FILE_LOCALE_REGEX); - Matcher matcher = pattern.matcher(readmeFile); - if (matcher.find()) { - result = matcher.group(1); - } - return result; - } - private void updateDependencyContentsFromProductJson(ProductModuleContent productModuleContent, List contents, Product product) throws IOException { GHContent productJsonFile = getProductJsonFile(contents); if (Objects.nonNull(productJsonFile)) { - List artifacts = convertProductJsonToMavenProductInfo(productJsonFile); - MavenArtifact artifact = artifacts.stream().filter(MavenArtifact::getIsDependency).findFirst().orElse(null); - - if (Objects.nonNull(artifact)) { - productModuleContent.setIsDependency(Boolean.TRUE); - productModuleContent.setGroupId(artifact.getGroupId()); - productModuleContent.setArtifactId(artifact.getArtifactId()); - productModuleContent.setType(artifact.getType()); - productModuleContent.setName(artifact.getName()); - } + List artifacts = GitHubUtils.convertProductJsonToMavenProductInfo(productJsonFile); + ProductContentUtils.updateProductModule(productModuleContent, artifacts); String currentVersion = VersionUtils.convertTagToVersion(productModuleContent.getTag()); String content = extractProductJsonContent(productJsonFile, productModuleContent.getTag()); - if (ObjectUtils.isNotEmpty(content)) { - ProductJsonContent jsonContent = new ProductJsonContent(); - jsonContent.setVersion(currentVersion); - jsonContent.setProductId(product.getId()); - ProductFactory.mappingIdForProductJsonContent(jsonContent); - jsonContent.setName(product.getNames().get(EN_LANGUAGE)); - jsonContent.setContent(content.replace(VERSION_VALUE, currentVersion)); - productJsonContentRepository.save(jsonContent); - } + productJsonContentService.updateProductJsonContent(content, productModuleContent.getTag(), currentVersion, + ProductJsonConstants.VERSION_VALUE, product); } } public String extractProductJsonContent(GHContent ghContent, String tag) { try { - InputStream contentStream = extractedContentStream(ghContent); + InputStream contentStream = GitHubUtils.extractedContentStream(ghContent); return IOUtils.toString(contentStream, StandardCharsets.UTF_8); } catch (Exception exception) { log.error("Cannot paste content of product.json {} at tag: {}", ghContent.getPath(), tag); @@ -269,121 +142,28 @@ public String extractProductJsonContent(GHContent ghContent, String tag) { } } - private static GHContent getProductJsonFile(List contents) { - return contents.stream().filter(GHContent::isFile) - .filter(content -> ProductJsonConstants.PRODUCT_JSON_FILE.equals(content.getName())).findFirst().orElse(null); - } - public String updateImagesWithDownloadUrl(Product product, List contents, String readmeContents) { - - List imagesAtRootFolder = contents.stream().filter(GHContent::isFile) - .filter(content -> content.getName().toLowerCase().matches(IMAGE_EXTENSION)).toList(); - - List allContentOfImages = ObjectUtils.isNotEmpty(imagesAtRootFolder) - ? imagesAtRootFolder - : getImagesFromImageFolder(product, contents); - + List allContentOfImages = getAllImagesFromProductFolder(contents); Map imageUrls = new HashMap<>(); allContentOfImages.forEach(content -> Optional.of(imageService.mappingImageFromGHContent(product, content, false)) .ifPresent(image -> imageUrls.put(content.getName(), IMAGE_ID_PREFIX.concat(image.getId())))); - - for (Map.Entry entry : imageUrls.entrySet()) { - String imageUrlPattern = String.format(README_IMAGE_FORMAT, Pattern.quote(entry.getKey())); - readmeContents = readmeContents.replaceAll(imageUrlPattern, - String.format(IMAGE_DOWNLOAD_URL_FORMAT, entry.getValue())); - } - - return readmeContents; + return ProductContentUtils.replaceImageDirWithImageCustomId(imageUrls, readmeContents); } - private List getImagesFromImageFolder(Product product, List contents) { + private List getAllImagesFromProductFolder(List productFolderContents) { List images = new ArrayList<>(); - String imageFolderPath = GitHubUtils.getNonStandardImageFolder(product.getId()); - contents.stream().filter(GHContent::isDirectory) - .filter(content -> imageFolderPath.equals(content.getName())) - .findFirst().ifPresent(imageFolder -> { - try { - images.addAll(imageFolder.listDirectoryContent().toList()); - } catch (IOException e) { - log.error(e.getMessage()); - } - }); + GitHubUtils.findImages(productFolderContents, images); return images; } - // Cover some cases including when demo and setup parts switch positions or - // missing one of them - public void getExtractedPartsOfReadme(Map> moduleContents, String readmeContents, - String locale) { - String[] parts = readmeContents.split(DEMO_SETUP_TITLE); - int demoIndex = readmeContents.indexOf(ReadmeConstants.DEMO_PART); - int setupIndex = readmeContents.indexOf(ReadmeConstants.SETUP_PART); - String description = Strings.EMPTY; - String setup = Strings.EMPTY; - String demo = Strings.EMPTY; - - if (parts.length > 0) { - description = removeFirstLine(parts[0]); - } - - if (demoIndex != -1 && setupIndex != -1) { - if (demoIndex < setupIndex) { - demo = parts[1]; - setup = parts[2]; - } else { - setup = parts[1]; - demo = parts[2]; - } - } else if (demoIndex != -1) { - demo = parts[1]; - } else if (setupIndex != -1) { - setup = parts[1]; - } - locale = StringUtils.isEmpty(locale) ? Language.EN.getValue() : locale.toLowerCase(); - addLocaleContent(moduleContents, DESCRIPTION, description.trim(), locale); - addLocaleContent(moduleContents, DEMO, demo.trim(), locale); - addLocaleContent(moduleContents, SETUP, setup.trim(), locale); - } - - private void addLocaleContent(Map> moduleContents, String type, String content, String locale) { - moduleContents.computeIfAbsent(type, key -> new HashMap<>()).put(locale, content); - } - private List getProductFolderContents(Product product, GHRepository ghRepository, String tag) throws IOException { String productFolderPath = ghRepository.getDirectoryContent(CommonConstants.SLASH, tag).stream() .filter(GHContent::isDirectory).map(GHContent::getName) .filter(content -> content.endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)).findFirst().orElse(null); - if (StringUtils.isBlank(productFolderPath) || hasChildConnector(ghRepository)) { - productFolderPath = GitHubUtils.getNonStandardProductFilePath(product.getId()); - } + productFolderPath = NonStandardProduct.findById(product.getId(), productFolderPath); return ghRepository.getDirectoryContent(productFolderPath, tag); } - - private boolean hasChildConnector(GHRepository ghRepository) { - return NonStandardProduct.MICROSOFT_REPO_NAME.getId().equals(ghRepository.getName()) - || NonStandardProduct.OPENAI_CONNECTOR.getId().equals(ghRepository.getName()); - } - - private boolean hasImageDirectives(String readmeContents) { - Pattern pattern = Pattern.compile(IMAGE_EXTENSION); - Matcher matcher = pattern.matcher(readmeContents); - return matcher.find(); - } - - private String removeFirstLine(String text) { - String result; - if (text.isBlank()) { - result = Strings.EMPTY; - } else if (text.startsWith(HASH)) { - int index = text.indexOf(StringUtils.LF); - result = index != StringUtils.INDEX_NOT_FOUND ? text.substring(index + 1).trim() : Strings.EMPTY; - } else { - result = text; - } - - return result; - } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java index 6d72dab73..31326fc4c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java @@ -1,7 +1,9 @@ package com.axonivy.market.github.util; +import com.axonivy.market.bo.Artifact; import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.enums.NonStandardProduct; +import com.axonivy.market.util.MavenUtils; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -11,8 +13,11 @@ import org.kohsuke.github.PagedIterable; import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import static com.axonivy.market.constants.MetaConstants.META_FILE; @@ -100,4 +105,40 @@ public static int sortMetaJsonFirst(String fileName1, String fileName2) { return 1; return fileName1.compareTo(fileName2); } + + public static void findImages(List files, List images) { + for (GHContent file : files) { + if (file.isDirectory()) { + findImagesInDirectory(file, images); + } else if (file.getName().toLowerCase().matches(CommonConstants.IMAGE_EXTENSION)) { + images.add(file); + } + } + } + + private static void findImagesInDirectory(GHContent file, List images) { + try { + List childrenFiles = file.listDirectoryContent().toList(); + findImages(childrenFiles, images); + } catch (IOException e) { + log.error(e.getMessage()); + } + } + + public static List convertProductJsonToMavenProductInfo(GHContent content) throws IOException { + InputStream contentStream = extractedContentStream(content); + if (Objects.isNull(contentStream)) { + return new ArrayList<>(); + } + return MavenUtils.extractMavenArtifactsFromContentStream(contentStream); + } + + public static InputStream extractedContentStream(GHContent content) { + try { + return content.read(); + } catch (IOException | NullPointerException e) { + log.warn("Can not read the current content: {}", e.getMessage()); + return null; + } + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactModel.java new file mode 100644 index 000000000..3ee59d055 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactModel.java @@ -0,0 +1,32 @@ +package com.axonivy.market.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Id; + +import java.io.Serial; +import java.io.Serializable; + +@AllArgsConstructor +@NoArgsConstructor +@Setter +@Getter +@Builder +public class MavenArtifactModel implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + @Id + @Schema(description = "Display name and type of artifact", example = "Adobe Acrobat Sign Connector (.iar)") + private String name; + @Schema(description = "Artifact download url", + example = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/10.0" + + ".25/adobe-acrobat-sign-connector-10.0.25.iar") + private String downloadUrl; + @JsonIgnore + private boolean isInvalidArtifact; +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java index 32ef52dcd..59caa8074 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java @@ -1,6 +1,5 @@ package com.axonivy.market.model; -import com.axonivy.market.entity.MavenArtifactModel; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java index 061e1fa63..5e55e4574 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java @@ -20,9 +20,11 @@ public class ProductDetailModel extends ProductModel { private String newestReleaseVersion; @Schema(description = "Product cost", example = "Free") private String cost; - @Schema(description = "Source repository url", example = "https://github.com/axonivy-market/adobe-acrobat-sign-connector") + @Schema(description = "Source repository url", + example = "https://github.com/axonivy-market/adobe-acrobat-sign-connector") private String sourceUrl; - @Schema(description = "Status badge url", example = "https://github.com/axonivy-market/adobe-acrobat-sign-connector/actions/workflows/ci.yml/badge.svg") + @Schema(description = "Status badge url", + example = "https://github.com/axonivy-market/adobe-acrobat-sign-connector/actions/workflows/ci.yml/badge.svg") private String statusBadgeUrl; @Schema(description = "Default language", example = "English") private String language; diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java index 74fbe9471..a531ac080 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java @@ -22,11 +22,17 @@ public class ProductModel extends RepresentationModel { @Schema(description = "Product id", example = "jira-connector") private String id; - @Schema(description = "Product name by locale", example = "{ \"de\": \"Atlassian Jira\", \"en\": \"Atlassian Jira\" }") + @Schema(description = "Product name by locale", + example = "{ \"de\": \"Atlassian Jira\", \"en\": \"Atlassian Jira\" }") private Map names; - @Schema(description = "Product's short descriptions by locale", example = "{ \"de\": \"Nutze den Jira Connector von Atlassian, um Jira-Tickets direkt von der Axon Ivy Plattform aus zu verfolgen.\", \"en\": \"Atlassian's Jira connector lets you track issues directly from the Axon Ivy platform\" }") + @Schema(description = "Product's short descriptions by locale", + example = "{ \"de\": \"Nutze den Jira Connector von Atlassian, um Jira-Tickets direkt von der Axon Ivy " + + "Plattform aus zu verfolgen.\", \"en\": \"Atlassian's Jira connector lets you track issues directly " + + "from the Axon Ivy platform\" }") private Map shortDescriptions; - @Schema(description = "Product's logo url", example = "https://raw.githubusercontent.com/axonivy-market/market/feature/MARP-463-Multilingualism-for-Website/market/connector/jira/logo.png") + @Schema(description = "Product's logo url", + example = "https://raw.githubusercontent.com/axonivy-market/market/feature/MARP-463-Multilingualism-for-Website" + + "/market/connector/jira/logo.png") private String logoUrl; @Schema(description = "Type of product", example = "connector") private String type; diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductRepository.java index 3fc59e926..acf8852e2 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductRepository.java @@ -1,11 +1,12 @@ package com.axonivy.market.repository; import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; import java.util.List; public interface CustomProductRepository { - Product getProductByIdAndTag(String id, String tag); + Product getProductByIdWithTagOrVersion(String id, String tag); Product getProductById(String id); @@ -16,4 +17,8 @@ public interface CustomProductRepository { int increaseInstallationCount(String productId); void increaseInstallationCountForProductByDesignerVersion(String productId, String designerVersion); + + List getAllProductsWithIdAndReleaseTagAndArtifact(); + + ProductModuleContent findByProductIdAndTagOrMavenVersion(String productId, String tag); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ImageRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ImageRepository.java index 4949a1d2c..7457d3296 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ImageRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ImageRepository.java @@ -4,9 +4,15 @@ import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface ImageRepository extends MongoRepository { Image findByProductIdAndSha(String productId, String sha); + List findByImageUrlEndsWithIgnoreCase(String fileName); + + List findByProductId(String productId); + void deleteAllByProductId(String productId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataRepository.java new file mode 100644 index 000000000..ebd264657 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataRepository.java @@ -0,0 +1,10 @@ +package com.axonivy.market.repository; + +import com.axonivy.market.entity.Metadata; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +public interface MetadataRepository extends MongoRepository { + List findByProductId(String productId); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataSyncRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataSyncRepository.java new file mode 100644 index 000000000..b4e7317aa --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/MetadataSyncRepository.java @@ -0,0 +1,7 @@ +package com.axonivy.market.repository; + +import com.axonivy.market.entity.MetadataSync; +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface MetadataSyncRepository extends MongoRepository { +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductDesignerInstallationRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductDesignerInstallationRepository.java index 2b5789521..7d007f64f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductDesignerInstallationRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductDesignerInstallationRepository.java @@ -10,5 +10,5 @@ @Repository public interface ProductDesignerInstallationRepository extends MongoRepository { - List findByProductId(String productId, Sort sort); + List findByProductId(String productId, Sort sort); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductJsonContentRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductJsonContentRepository.java index 9d8dbfc7a..7b616da5f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductJsonContentRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductJsonContentRepository.java @@ -4,8 +4,10 @@ import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface ProductJsonContentRepository extends MongoRepository { - ProductJsonContent findByProductIdAndVersion(String productId , String version); + List findByProductIdAndVersion(String productId, String version); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductModuleContentRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductModuleContentRepository.java index 123a30c1c..1436e6d90 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductModuleContentRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductModuleContentRepository.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Repository; @Repository -public interface ProductModuleContentRepository extends MongoRepository, CustomProductModuleContentRepository { +public interface ProductModuleContentRepository extends MongoRepository, + CustomProductModuleContentRepository { ProductModuleContent findByTagAndProductId(String tag, String productId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java index 946a87bc0..954469887 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java @@ -1,15 +1,13 @@ package com.axonivy.market.repository; +import com.axonivy.market.entity.Product; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.stereotype.Repository; -import com.axonivy.market.entity.Product; +import java.util.List; @Repository public interface ProductRepository extends MongoRepository, ProductSearchRepository, CustomProductRepository { - - Product findByLogoUrl(String logoUrl); - - Product findByLogoId(String logoId); + List findByMarketDirectory(String marketDirectory); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductSearchRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductSearchRepository.java index 976a8b64f..8f4b238f0 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductSearchRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductSearchRepository.java @@ -2,7 +2,6 @@ import com.axonivy.market.criteria.ProductSearchCriteria; import com.axonivy.market.entity.Product; - import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java index a91d50200..785713633 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java @@ -3,8 +3,8 @@ import com.axonivy.market.constants.EntityConstants; import com.axonivy.market.constants.MongoDBConstants; import com.axonivy.market.entity.Product; -import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.entity.ProductDesignerInstallation; +import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.repository.CustomProductRepository; import com.axonivy.market.repository.CustomRepository; import com.axonivy.market.repository.ProductModuleContentRepository; @@ -29,7 +29,7 @@ public class CustomProductRepositoryImpl extends CustomRepository implements Cus public CustomProductRepositoryImpl(MongoTemplate mongoTemplate, ProductModuleContentRepository contentRepository) { this.mongoTemplate = mongoTemplate; - this.contentRepository = contentRepository; + this.contentRepository = contentRepository; } @@ -38,16 +38,32 @@ public Product queryProductByAggregation(Aggregation aggregation) { .map(AggregationResults::getUniqueMappedResult).orElse(null); } + public List queryProductsByAggregation(Aggregation aggregation) { + return Optional.of(mongoTemplate.aggregate(aggregation, EntityConstants.PRODUCT, Product.class)) + .map(AggregationResults::getMappedResults).orElse(Collections.emptyList()); + } + @Override - public Product getProductByIdAndTag(String id, String tag) { + public Product getProductByIdWithTagOrVersion(String id, String tag) { Product result = findProductById(id); if (!Objects.isNull(result)) { - ProductModuleContent content = contentRepository.findByTagAndProductId(tag,id); + ProductModuleContent content = findByProductIdAndTagOrMavenVersion(id, tag); result.setProductModuleContent(content); } return result; } + @Override + public ProductModuleContent findByProductIdAndTagOrMavenVersion(String productId, String tag) { + Criteria productIdCriteria = Criteria.where(MongoDBConstants.PRODUCT_ID).is(productId); + Criteria orCriteria = new Criteria().orOperator( + Criteria.where(MongoDBConstants.TAG).is(tag), + Criteria.where(MongoDBConstants.MAVEN_VERSIONS).in(tag) + ); + Query query = new Query(new Criteria().andOperator(productIdCriteria, orCriteria)); + return mongoTemplate.findOne(query, ProductModuleContent.class); + } + private Product findProductById(String id) { Aggregation aggregation = Aggregation.newAggregation(createIdMatchOperation(id)); return queryProductByAggregation(aggregation); @@ -75,7 +91,8 @@ public List getReleasedVersionsById(String id) { } public int updateInitialCount(String productId, int initialCount) { - Update update = new Update().inc(MongoDBConstants.INSTALLATION_COUNT, initialCount).set(MongoDBConstants.SYNCHRONIZED_INSTALLATION_COUNT, true); + Update update = new Update().inc(MongoDBConstants.INSTALLATION_COUNT, initialCount).set( + MongoDBConstants.SYNCHRONIZED_INSTALLATION_COUNT, true); mongoTemplate.updateFirst(createQueryById(productId), update, Product.class); return Optional.ofNullable(getProductById(productId)).map(Product::getInstallationCount).orElse(0); } @@ -93,11 +110,23 @@ public int increaseInstallationCount(String productId) { public void increaseInstallationCountForProductByDesignerVersion(String productId, String designerVersion) { Update update = new Update().inc(MongoDBConstants.INSTALLATION_COUNT, 1); mongoTemplate.upsert(createQueryByProductIdAndDesignerVersion(productId, designerVersion), - update, ProductDesignerInstallation.class); + update, ProductDesignerInstallation.class); + } + + @Override + public List getAllProductsWithIdAndReleaseTagAndArtifact() { + return queryProductsByAggregation( + createProjectIdAndReleasedVersionsAndArtifactsAggregation()); } private Query createQueryByProductIdAndDesignerVersion(String productId, String designerVersion) { return new Query(Criteria.where(MongoDBConstants.PRODUCT_ID).is(productId) - .andOperator(Criteria.where(MongoDBConstants.DESIGNER_VERSION).is(designerVersion))); + .andOperator(Criteria.where(MongoDBConstants.DESIGNER_VERSION).is(designerVersion))); + } + + protected Aggregation createProjectIdAndReleasedVersionsAndArtifactsAggregation() { + return Aggregation.newAggregation( + Aggregation.project(MongoDBConstants.ID, MongoDBConstants.ARTIFACTS, MongoDBConstants.RELEASED_VERSIONS) + ); } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImpl.java b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImpl.java index 68a86856f..627cb2ed5 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImpl.java @@ -1,11 +1,11 @@ package com.axonivy.market.repository.impl; -import static com.axonivy.market.enums.DocumentField.LISTED; -import static com.axonivy.market.enums.DocumentField.TYPE; - -import java.util.ArrayList; -import java.util.List; - +import com.axonivy.market.criteria.ProductSearchCriteria; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.DocumentField; +import com.axonivy.market.enums.Language; +import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.repository.ProductSearchRepository; import org.apache.commons.lang3.StringUtils; import org.bson.BsonRegularExpression; import org.springframework.data.domain.Page; @@ -16,12 +16,11 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.util.CollectionUtils; -import com.axonivy.market.criteria.ProductSearchCriteria; -import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.DocumentField; -import com.axonivy.market.enums.Language; -import com.axonivy.market.enums.TypeOption; -import com.axonivy.market.repository.ProductSearchRepository; +import java.util.ArrayList; +import java.util.List; + +import static com.axonivy.market.enums.DocumentField.LISTED; +import static com.axonivy.market.enums.DocumentField.TYPE; public class ProductSearchRepositoryImpl implements ProductSearchRepository { @@ -103,7 +102,8 @@ private Criteria createQueryByKeywordRegex(ProductSearchCriteria searchCriteria) for (var property : filterProperties) { Criteria filterByKeywordCriteria; if (property.isLocalizedSupport()) { - filterByKeywordCriteria = Criteria.where(LOCALIZE_SEARCH_PATTERN.formatted(property.getFieldName(), language.getValue())); + filterByKeywordCriteria = Criteria.where( + LOCALIZE_SEARCH_PATTERN.formatted(property.getFieldName(), language.getValue())); } else { filterByKeywordCriteria = Criteria.where(property.getFieldName()); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java b/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java index 9e21e7453..ee2290b3a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java +++ b/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java @@ -1,21 +1,23 @@ package com.axonivy.market.schedulingtask; +import com.axonivy.market.service.MetadataService; import com.axonivy.market.service.ProductService; +import lombok.AllArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Log4j2 @Component +@AllArgsConstructor public class ScheduledTasks { private static final String SCHEDULING_TASK_PRODUCTS_CRON = "0 0 0/1 ? * *"; + // Maven version sync will start at 00:20 in order to prevent running at the same time with product repo sync + private static final String SCHEDULING_TASK_MAVEN_VERSION_CRON = "0 0 20 * * *"; private final ProductService productService; - - public ScheduledTasks(ProductService productService) { - this.productService = productService; - } + private final MetadataService metadataService; @Scheduled(cron = SCHEDULING_TASK_PRODUCTS_CRON) public void syncDataForProductFromGitHubRepo() { @@ -23,4 +25,9 @@ public void syncDataForProductFromGitHubRepo() { productService.syncLatestDataFromMarketRepo(); } + @Scheduled(cron = SCHEDULING_TASK_MAVEN_VERSION_CRON) + public void syncDataForMavenMetadata() { + log.warn("Started sync data for Maven metadata"); + metadataService.syncAllProductsMetadata(); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/FileDownloadService.java b/marketplace-service/src/main/java/com/axonivy/market/service/FileDownloadService.java new file mode 100644 index 000000000..d46082378 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/FileDownloadService.java @@ -0,0 +1,18 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.Metadata; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import static com.axonivy.market.constants.DirectoryConstants.DATA_DIR; +import static com.axonivy.market.constants.DirectoryConstants.WORK_DIR; + +public interface FileDownloadService { + String ROOT_STORAGE_FOR_PRODUCT_CONTENT = String.join(File.separator, DATA_DIR, WORK_DIR); + + String downloadAndUnzipProductContentFile(String url, Metadata snapShotMetadata) throws IOException; + + void deleteDirectory(Path path); +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java index f87dbf830..c30809751 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java @@ -1,15 +1,18 @@ package com.axonivy.market.service; +import com.axonivy.market.entity.Image; +import com.axonivy.market.entity.Product; import org.bson.types.Binary; import org.kohsuke.github.GHContent; -import com.axonivy.market.entity.Image; -import com.axonivy.market.entity.Product; +import java.nio.file.Path; public interface ImageService { Binary getImageBinary(GHContent ghContent); Image mappingImageFromGHContent(Product product, GHContent ghContent, boolean isLogo); + Image mappingImageFromDownloadedFolder(Product product, Path imagePath); + byte[] readImage(String id); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/MetadataService.java b/marketplace-service/src/main/java/com/axonivy/market/service/MetadataService.java new file mode 100644 index 000000000..b0e134240 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/MetadataService.java @@ -0,0 +1,5 @@ +package com.axonivy.market.service; + +public interface MetadataService { + int syncAllProductsMetadata(); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ProductJsonContentService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ProductJsonContentService.java new file mode 100644 index 000000000..a904c9173 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ProductJsonContentService.java @@ -0,0 +1,8 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.Product; + +public interface ProductJsonContentService { + void updateProductJsonContent(String jsonContent, String relatedTag, String currentVersion, String replaceVersion, + Product product); +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java b/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java index e4800c5a9..3e913fed0 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java @@ -2,19 +2,17 @@ import com.axonivy.market.model.MavenArtifactVersionModel; import com.axonivy.market.model.VersionAndUrlModel; + import java.util.List; import java.util.Map; public interface VersionService { - List getVersionsFromArtifactDetails(String repoUrl, String groupId, String artifactId); - - String buildMavenMetadataUrlFromArtifact(String repoUrl, String groupId, String artifactId); - List getArtifactsAndVersionToDisplay(String productId, Boolean isShowDevVersion, String designerVersion); - Map getProductJsonContentByIdAndVersion(String name , String version); + Map getProductJsonContentByIdAndTag(String name, String version); List getVersionsForDesigner(String productId); + } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java index 3d648c144..3ae807b26 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java @@ -47,7 +47,8 @@ public Feedback findFeedback(String id) throws NotFoundException { } @Override - public Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException, NoContentException { + public Feedback findFeedbackByUserIdAndProductId(String userId, + String productId) throws NotFoundException, NoContentException { if (StringUtils.isNotBlank(userId)) { validateUserExists(userId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FileDownloadServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FileDownloadServiceImpl.java new file mode 100644 index 000000000..32688ace1 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FileDownloadServiceImpl.java @@ -0,0 +1,151 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.entity.Metadata; +import com.axonivy.market.service.FileDownloadService; +import org.springframework.stereotype.Service; + +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.SystemUtils; +import org.springframework.web.client.RestTemplate; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +@Service +@Log4j2 +public class FileDownloadServiceImpl implements FileDownloadService { + private static final String ZIP_EXTENSION = ".zip"; + private static final Set PERMS = EnumSet.allOf(PosixFilePermission.class); + private static final int THRESHOLD_ENTRIES = 10000; + private static final int THRESHOLD_SIZE = 1000000000; + private static final double THRESHOLD_RATIO = 10; + + private byte[] downloadFileByRestTemplate(String url) { + return new RestTemplate().getForObject(url, byte[].class); + } + + @Override + public String downloadAndUnzipProductContentFile(String url, Metadata snapShotMetadata) throws IOException { + String unzippedFilePath = String.join(File.separator, ROOT_STORAGE_FOR_PRODUCT_CONTENT, + snapShotMetadata.getArtifactId()); + createFolder(unzippedFilePath); + + Path tempZipPath = createTempFile(); + Files.write(tempZipPath, downloadFileByRestTemplate(url)); + + unzipFile(tempZipPath.toString(), unzippedFilePath); + + Files.delete(tempZipPath); + return unzippedFilePath; + } + + private Path createTempFile() throws IOException { + Path tempZipPath; + var tempFileName = UUID.randomUUID().toString(); + if (SystemUtils.IS_OS_UNIX) { + FileAttribute> attr = PosixFilePermissions.asFileAttribute(PERMS); + tempZipPath = Files.createTempFile(tempFileName, ZIP_EXTENSION, attr); + } else { + File tempFile = Files.createTempFile(tempFileName, ZIP_EXTENSION).toFile(); + tempZipPath = tempFile.toPath(); + } + return tempZipPath; + } + + public int unzipFile(String zipFilePath, String location) throws IOException { + int totalSizeArchive = 0; + Path destDirPath = Paths.get(location).toAbsolutePath().normalize(); + try (ZipFile zipFile = new ZipFile(new File(zipFilePath))) { + Enumeration entries = zipFile.entries(); + int totalEntryArchive = 0; + + while (entries.hasMoreElements()) { + ZipEntry zipEntry = entries.nextElement(); + Path entryPath = destDirPath.resolve(zipEntry.getName()).normalize(); + if (!entryPath.startsWith(destDirPath)) { + throw new IOException("Bad zip zipEntry: " + zipEntry.getName()); + } + if (zipEntry.isDirectory()) { + createFolder(entryPath.toString()); + } else { + totalEntryArchive++; + totalSizeArchive = extractFile(zipFile, zipEntry, entryPath.toString(), totalSizeArchive); + } + if (totalSizeArchive > THRESHOLD_SIZE || totalEntryArchive > THRESHOLD_ENTRIES) { + log.warn("Unzip is skipped due to threshold issue {} {}", totalSizeArchive, totalEntryArchive); + break; + } + } + } + return totalSizeArchive; + } + + public int extractFile(ZipFile zipFile, ZipEntry zipEntry, String filePath, + int totalSizeArchive) { + try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) { + InputStream stream = new BufferedInputStream(zipFile.getInputStream(zipEntry)); + byte[] bytesIn = new byte[4096]; + int totalSizeEntry = 0; + int read; + while ((read = stream.read(bytesIn)) != -1) { + bos.write(bytesIn, 0, read); + totalSizeEntry += read; + totalSizeArchive += read; + + var compressionRatio = totalSizeEntry / zipEntry.getCompressedSize(); + if (compressionRatio > THRESHOLD_RATIO) { + log.warn("Extract file is skipped due to threshold issue {}", compressionRatio); + break; + } + } + stream.close(); + } catch (IOException e) { + log.error("Cannot extract file", e); + } + return totalSizeArchive; + } + + public Path createFolder(String location) { + Path folderPath = Paths.get(location); + try { + Files.createDirectories(folderPath); + } catch (IOException e) { + log.error("An error occurred while creating the folder: ", e); + } + return folderPath; + } + + @Override + public void deleteDirectory(Path path) { + try (Stream paths = Files.walk(path)) { + paths.sorted(Comparator.reverseOrder()) + .forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + log.error("Failed to delete files in {} - {}", p, e.getMessage()); + } + }); + } catch (IOException e) { + log.error("Failed to walk directory {} - {}", path, e.getMessage()); + } + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java index 1b589872f..03c273937 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java @@ -5,6 +5,7 @@ import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.repository.ImageRepository; import com.axonivy.market.service.ImageService; +import com.axonivy.market.util.MavenUtils; import lombok.extern.log4j.Log4j2; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.ObjectUtils; @@ -12,7 +13,12 @@ import org.kohsuke.github.GHContent; import org.springframework.stereotype.Service; +import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; @Service @Log4j2 @@ -39,7 +45,7 @@ public Binary getImageBinary(GHContent ghContent) { @Override public Image mappingImageFromGHContent(Product product, GHContent ghContent, boolean isLogo) { if (ObjectUtils.isEmpty(ghContent)) { - log.info("There is missing for image content for product {}" , product.getId()); + log.info("There is missing for image content for product {}", product.getId()); return null; } @@ -58,6 +64,31 @@ public Image mappingImageFromGHContent(Product product, GHContent ghContent, boo return imageRepository.save(image); } + @Override + public Image mappingImageFromDownloadedFolder(Product product, Path imagePath) { + List existingImages = imageRepository.findByProductId(product.getId()); + try { + InputStream contentStream = MavenUtils.extractedContentStream(imagePath); + byte[] sourceBytes = IOUtils.toByteArray(contentStream); + + Image existedImage = existingImages.stream().filter(image -> { + byte[] imageData = Optional.of(image).map(Image::getImageData).map(Binary::getData).orElse(null); + return ObjectUtils.isNotEmpty(imageData) && Arrays.equals(imageData, sourceBytes); + }).findAny().orElse(null); + + if (ObjectUtils.isEmpty(existedImage)) { + Image image = new Image(); + image.setImageData(new Binary(sourceBytes)); + image.setProductId(product.getId()); + return imageRepository.save(image); + } + return existedImage; + } catch (IOException | NullPointerException e) { + log.error("Cannot get image from downloaded folder {}", e.getMessage()); + return null; + } + } + @Override public byte[] readImage(String id) { return imageRepository.findById(id).map(Image::getImageData).map(Binary::getData).orElse(null); diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/MetadataServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/MetadataServiceImpl.java new file mode 100644 index 000000000..8c736f0dc --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/MetadataServiceImpl.java @@ -0,0 +1,363 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.bo.Artifact; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.MavenConstants; +import com.axonivy.market.constants.ProductJsonConstants; +import com.axonivy.market.constants.ReadmeConstants; +import com.axonivy.market.entity.MavenArtifactVersion; +import com.axonivy.market.entity.Metadata; +import com.axonivy.market.entity.MetadataSync; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductJsonContent; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.enums.NonStandardProduct; +import com.axonivy.market.model.MavenArtifactModel; +import com.axonivy.market.repository.MavenArtifactVersionRepository; +import com.axonivy.market.repository.MetadataRepository; +import com.axonivy.market.repository.MetadataSyncRepository; +import com.axonivy.market.repository.ProductJsonContentRepository; +import com.axonivy.market.repository.ProductModuleContentRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.service.FileDownloadService; +import com.axonivy.market.service.ImageService; +import com.axonivy.market.service.MetadataService; +import com.axonivy.market.service.ProductJsonContentService; +import com.axonivy.market.util.MavenUtils; +import com.axonivy.market.util.MetadataReaderUtils; +import com.axonivy.market.util.ProductContentUtils; +import com.axonivy.market.util.VersionUtils; +import lombok.AllArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +@Service +@AllArgsConstructor +@Log4j2 +public class MetadataServiceImpl implements MetadataService { + private final ProductRepository productRepo; + private final MetadataSyncRepository metadataSyncRepo; + private final ProductJsonContentRepository productJsonRepo; + private final MavenArtifactVersionRepository mavenArtifactVersionRepo; + private final MetadataRepository metadataRepo; + private final ImageService imageService; + private final ProductJsonContentService productJsonContentService; + private final FileDownloadService fileDownloadService; + private final ProductModuleContentRepository productContentRepo; + + + public void updateMavenArtifactVersionCacheWithModel(MavenArtifactVersion artifactVersionCache, + String version, Metadata metadata) { + List artifactModelsInVersion = + artifactVersionCache.getProductArtifactsByVersion().computeIfAbsent( + version, k -> new ArrayList<>()); + if (metadata.isProductArtifact()) { + if (artifactModelsInVersion.stream().anyMatch(artifact -> StringUtils.equals(metadata.getName(), + artifact.getName()))) { + return; + } + artifactModelsInVersion.add( + MavenUtils.buildMavenArtifactModelFromMetadata(version, metadata)); + } else { + artifactVersionCache.getAdditionalArtifactsByVersion().computeIfAbsent(version, k -> new ArrayList<>()).add( + MavenUtils.buildMavenArtifactModelFromMetadata(version, metadata)); + } + } + + public void updateMavenArtifactVersionData(String productId, List releasedVersions, + Set metadataSet, MavenArtifactVersion artifactVersionCache) { + for (Metadata metadata : metadataSet) { + String metadataContent = MavenUtils.getMetadataContentFromUrl(metadata.getUrl()); + if (StringUtils.isBlank(metadataContent)) { + continue; + } + Metadata metadataWithVersions = MetadataReaderUtils.updateMetadataFromMavenXML(metadataContent, metadata, false); + updateMavenArtifactVersionFromMetadata(artifactVersionCache, metadataWithVersions); + updateContentsFromNonMatchVersions(productId, releasedVersions, metadataWithVersions); + } + } + + public int syncAllProductsMetadata() { + List products = productRepo.getAllProductsWithIdAndReleaseTagAndArtifact(); + log.warn("**MetadataService: Start to sync version for {} product(s)", products.size()); + int nonUpdatedSyncCount = 0; + for (Product product : products) { + // Set up cache before sync + String productId = product.getId(); + Set metadataSet = new HashSet<>(metadataRepo.findByProductId(product.getId())); + MavenArtifactVersion artifactVersionCache = mavenArtifactVersionRepo.findById(product.getId()).orElse( + MavenArtifactVersion.builder().productId(productId).build()); + MetadataSync syncCache = metadataSyncRepo.findById(product.getId()).orElse( + MetadataSync.builder().productId(product.getId()).syncedVersions(new HashSet<>()).build()); + Set artifactsFromNewTags = new HashSet<>(); + + // Find artifacts from unhandled tags + List nonSyncedVersionOfTags = VersionUtils.removeSyncedVersionsFromReleasedVersions( + product.getReleasedVersions(), syncCache.getSyncedVersions()); + if (ObjectUtils.isNotEmpty(nonSyncedVersionOfTags)) { + artifactsFromNewTags.addAll(getArtifactsFromNonSyncedVersion(product.getId(), nonSyncedVersionOfTags)); + syncCache.getSyncedVersions().addAll(nonSyncedVersionOfTags); + log.info("**MetadataService: New tags detected: {} in product {}", nonSyncedVersionOfTags.toString(), + productId); + } + + // Sync versions from maven & update artifacts-version table + metadataSet.addAll(MavenUtils.convertArtifactsToMetadataSet(artifactsFromNewTags, productId)); + if (ObjectUtils.isNotEmpty(product.getArtifacts())) { + metadataSet.addAll( + MavenUtils.convertArtifactsToMetadataSet(new HashSet<>(product.getArtifacts()), productId)); + } + if (CollectionUtils.isEmpty(metadataSet)) { + log.info("**MetadataService: No artifact found in product {}", productId); + nonUpdatedSyncCount +=1; + continue; + } + artifactVersionCache.setAdditionalArtifactsByVersion(new HashMap<>()); + updateMavenArtifactVersionData(productId, product.getReleasedVersions(), metadataSet, artifactVersionCache); + + // Persist changed + metadataSyncRepo.save(syncCache); + mavenArtifactVersionRepo.save(artifactVersionCache); + metadataRepo.saveAll(metadataSet); + } + log.warn("**MetadataService: version sync finished"); + return nonUpdatedSyncCount; + } + + public void updateContentsFromNonMatchVersions(String productId, List releasedVersions, + Metadata metadata) { + List productModuleContents = new ArrayList<>(); + Set nonMatchSnapshotVersions = getNonMatchSnapshotVersions(productId, releasedVersions, + metadata.getVersions()); + + for (String nonMatchSnapshotVersion : nonMatchSnapshotVersions) { + if (MavenUtils.isProductArtifactId(metadata.getArtifactId())) { + handleProductArtifact(metadata.getProductId(), nonMatchSnapshotVersion, metadata, productModuleContents); + } + } + if (ObjectUtils.isNotEmpty(productModuleContents)) { + productContentRepo.saveAll(productModuleContents); + } + } + + public void handleProductArtifact(String productId, String nonMatchSnapshotVersion, Metadata productArtifact, + List productModuleContents) { + Metadata snapShotMetadata = MavenUtils.buildSnapShotMetadataFromVersion(productArtifact, nonMatchSnapshotVersion); + MetadataReaderUtils.updateMetadataFromMavenXML( + MavenUtils.getMetadataContentFromUrl(snapShotMetadata.getUrl()), snapShotMetadata, true); + + String url = buildProductFolderDownloadUrl(snapShotMetadata, nonMatchSnapshotVersion); + + Product product = productRepo.findById(productId).orElse(null); + if (StringUtils.isBlank(url) || Objects.isNull(product)) { + return; + } + + try { + addProductContent(product, nonMatchSnapshotVersion, snapShotMetadata, url, productModuleContents); + } catch (Exception e) { + log.error("Cannot download and unzip file {}", e.getMessage()); + } + } + + public String buildProductFolderDownloadUrl(Metadata snapShotMetadata, String nonMatchSnapshotVersion) { + return MavenUtils.buildDownloadUrl( + snapShotMetadata.getArtifactId(), nonMatchSnapshotVersion, + MavenConstants.DEFAULT_PRODUCT_FOLDER_TYPE, + snapShotMetadata.getRepoUrl(), snapShotMetadata.getGroupId(), + snapShotMetadata.getSnapshotVersionValue()); + } + + private void addProductContent(Product product, String nonMatchSnapshotVersion, Metadata snapShotMetadata, String url, + List productModuleContents) { + ProductModuleContent productModuleContent = getReadmeAndProductContentsFromTag(product, nonMatchSnapshotVersion, + snapShotMetadata, url); + if (Objects.nonNull(productModuleContent)) { + productModuleContents.add(productModuleContent); + } + } + + public Set getNonMatchSnapshotVersions(String productId, List releasedVersions, + Set metaVersions) { + Set nonMatchSnapshotVersions = new HashSet<>(); + for (String metaVersion : metaVersions) { + String matchedVersion = VersionUtils.getMavenVersionMatchWithTag(releasedVersions, metaVersion); + + updateProductJsonAndReadmeContents(productId, metaVersion, matchedVersion); + if (matchedVersion == null && VersionUtils.isSnapshotVersion(metaVersion)) { + nonMatchSnapshotVersions.add(metaVersion); + } + } + return nonMatchSnapshotVersions; + } + + private void updateProductJsonAndReadmeContents(String productId, String metaVersion, String matchedVersion) { + if (StringUtils.isNotBlank(matchedVersion)) { + // Clone new record from matchVersion's values + productJsonRepo.findByProductIdAndVersion(productId, + matchedVersion).stream().findAny().ifPresent(json -> + productRepo.findById(productId).ifPresent(product -> + productJsonContentService.updateProductJsonContent(json.getContent(), null, metaVersion, + matchedVersion, product) + ) + ); + + // Note metaVersion that get matchTag's contents to display + ProductModuleContent moduleContent = + productContentRepo.findByTagAndProductId(VersionUtils.convertVersionToTag(productId, matchedVersion), + productId); + if (ObjectUtils.isEmpty(moduleContent)) { + return; + } + Set mavenVersions = moduleContent.getMavenVersions(); + if (CollectionUtils.isEmpty(mavenVersions) || !mavenVersions.contains(metaVersion)) { + mavenVersions.add(metaVersion); + moduleContent.setMavenVersions(mavenVersions); + productContentRepo.save(moduleContent); + } + } + } + + private ProductModuleContent getReadmeAndProductContentsFromTag(Product product, String nonMatchSnapshotVersion, + Metadata snapShotMetadata, String url) { + ProductModuleContent productModuleContent = ProductContentUtils.initProductModuleContent(product, Strings.EMPTY, + Set.of(nonMatchSnapshotVersion)); + String unzippedFolderPath = Strings.EMPTY; + try { + unzippedFolderPath = fileDownloadService.downloadAndUnzipProductContentFile(url, snapShotMetadata); + updateDependencyContentsFromProductJson(productModuleContent, product, unzippedFolderPath); + extractReadMeFileFromContents(product, unzippedFolderPath, productModuleContent); + } catch (Exception e) { + log.error("Cannot get product.json content in {}", e.getMessage()); + return null; + } finally { + if (StringUtils.isNotBlank(unzippedFolderPath)) { + fileDownloadService.deleteDirectory(Path.of(unzippedFolderPath)); + } + } + return productModuleContent; + } + + private void updateDependencyContentsFromProductJson(ProductModuleContent productModuleContent, + Product product, String unzippedFolderPath) throws IOException { + List artifacts = MavenUtils.convertProductJsonToMavenProductInfo( + Paths.get(unzippedFolderPath)); + ProductContentUtils.updateProductModule(productModuleContent, artifacts); + String currentVersion = productModuleContent.getMavenVersions().stream().findAny().orElse(null); + Path productJsonPath = Paths.get(unzippedFolderPath, ProductJsonConstants.PRODUCT_JSON_FILE); + String content = extractProductJsonContent(productJsonPath); + productJsonContentService.updateProductJsonContent(content, null, currentVersion, + ProductJsonConstants.VERSION_VALUE, product); + } + + private void extractReadMeFileFromContents(Product product, String unzippedFolderPath, + ProductModuleContent productModuleContent) { + try { + List readmeFiles; + Map> moduleContents = new HashMap<>(); + try (Stream readmePathStream = Files.walk(Paths.get(unzippedFolderPath))) { + readmeFiles = readmePathStream.filter(Files::isRegularFile).filter( + path -> path.getFileName().toString().startsWith(ReadmeConstants.README_FILE_NAME)).toList(); + } + if (ObjectUtils.isNotEmpty(readmeFiles)) { + for (Path readmeFile : readmeFiles) { + String readmeContents = Files.readString(readmeFile); + if (ProductContentUtils.hasImageDirectives(readmeContents)) { + readmeContents = updateImagesWithDownloadUrl(product, unzippedFolderPath, readmeContents); + } + ProductContentUtils.getExtractedPartsOfReadme(moduleContents, readmeContents, + readmeFile.getFileName().toString()); + } + ProductContentUtils.updateProductModuleTabContents(productModuleContent, moduleContents); + } + } catch (Exception e) { + log.error("Cannot get README file's content from folder {}: {}", unzippedFolderPath, e.getMessage()); + } + } + + private String updateImagesWithDownloadUrl(Product product, String unzippedFolderPath, + String readmeContents) throws IOException { + List allImagePaths; + Map imageUrls = new HashMap<>(); + try (Stream imagePathStream = Files.walk(Paths.get(unzippedFolderPath))) { + allImagePaths = imagePathStream.filter(Files::isRegularFile).filter( + path -> path.getFileName().toString().toLowerCase().matches(CommonConstants.IMAGE_EXTENSION)).toList(); + } + allImagePaths.forEach( + imagePath -> Optional.of(imageService.mappingImageFromDownloadedFolder(product, imagePath)).ifPresent( + image -> imageUrls.put(imagePath.getFileName().toString(), + CommonConstants.IMAGE_ID_PREFIX.concat(image.getId())))); + + return ProductContentUtils.replaceImageDirWithImageCustomId(imageUrls, readmeContents); + } + + private String extractProductJsonContent(Path filePath) { + try { + InputStream contentStream = MavenUtils.extractedContentStream(filePath); + return IOUtils.toString(Objects.requireNonNull(contentStream), StandardCharsets.UTF_8); + } catch (Exception e) { + log.error("Cannot extract product.json file {}", e.getMessage()); + return null; + } + } + + public void updateMavenArtifactVersionFromMetadata(MavenArtifactVersion artifactVersionCache, + Metadata metadata) { + // Skip to add new model for product artifact + if (MavenUtils.isProductArtifactId(metadata.getArtifactId())) { + return; + } + metadata.getVersions().forEach(version -> { + if (VersionUtils.isSnapshotVersion(version)) { + if (VersionUtils.isOfficialVersionOrUnReleasedDevVersion(metadata.getVersions().stream().toList(), version)) { + updateMavenArtifactVersionForNonReleaseDevVersion(artifactVersionCache, metadata, version); + } + } else { + updateMavenArtifactVersionCacheWithModel(artifactVersionCache, version, metadata); + } + }); + } + + public void updateMavenArtifactVersionForNonReleaseDevVersion(MavenArtifactVersion artifactVersionCache, + Metadata metadata, String version) { + Metadata snapShotMetadata = MavenUtils.buildSnapShotMetadataFromVersion(metadata, version); + MetadataReaderUtils.updateMetadataFromMavenXML(MavenUtils.getMetadataContentFromUrl(snapShotMetadata.getUrl()), + snapShotMetadata, true); + updateMavenArtifactVersionCacheWithModel(artifactVersionCache, version, snapShotMetadata); + } + + public Set getArtifactsFromNonSyncedVersion(String productId, List nonSyncedVersions) { + Set artifacts = new HashSet<>(); + if (CollectionUtils.isEmpty(nonSyncedVersions)) { + return artifacts; + } + nonSyncedVersions.forEach(version -> { + ProductJsonContent productJson = + productJsonRepo.findByProductIdAndVersion(productId, version).stream().findAny().orElse(null); + List artifactsInVersion = MavenUtils.getMavenArtifactsFromProductJson(productJson); + artifacts.addAll(artifactsInVersion); + }); + return artifacts; + } +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductDesignerInstallationServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductDesignerInstallationServiceImpl.java index c95241e08..78cd4c70d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductDesignerInstallationServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductDesignerInstallationServiceImpl.java @@ -15,21 +15,24 @@ @Log4j2 @Service public class ProductDesignerInstallationServiceImpl implements ProductDesignerInstallationService { - private final ProductDesignerInstallationRepository productDesignerInstallationRepository; + private final ProductDesignerInstallationRepository productDesignerInstallationRepository; - public ProductDesignerInstallationServiceImpl(ProductDesignerInstallationRepository productDesignerInstallationRepository) { - this.productDesignerInstallationRepository = productDesignerInstallationRepository; - } + public ProductDesignerInstallationServiceImpl( + ProductDesignerInstallationRepository productDesignerInstallationRepository) { + this.productDesignerInstallationRepository = productDesignerInstallationRepository; + } - @Override - public List findByProductId(String productId) { - List designerInstallations = new ArrayList<>(); - List productDesignerInstallations = - productDesignerInstallationRepository.findByProductId(productId, Sort.by(Sort.Direction.DESC, MongoDBConstants.DESIGNER_VERSION)); - for (ProductDesignerInstallation productDesignerInstallation : productDesignerInstallations) { - DesignerInstallation designerInstallation = new DesignerInstallation(productDesignerInstallation.getDesignerVersion(), productDesignerInstallation.getInstallationCount()); - designerInstallations.add(designerInstallation); - } - return designerInstallations; + @Override + public List findByProductId(String productId) { + List designerInstallations = new ArrayList<>(); + List productDesignerInstallations = + productDesignerInstallationRepository.findByProductId(productId, + Sort.by(Sort.Direction.DESC, MongoDBConstants.DESIGNER_VERSION)); + for (ProductDesignerInstallation productDesignerInstallation : productDesignerInstallations) { + DesignerInstallation designerInstallation = new DesignerInstallation( + productDesignerInstallation.getDesignerVersion(), productDesignerInstallation.getInstallationCount()); + designerInstallations.add(designerInstallation); } + return designerInstallations; + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductJsonContentServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductJsonContentServiceImpl.java new file mode 100644 index 000000000..cd0d3ca25 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductJsonContentServiceImpl.java @@ -0,0 +1,33 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductJsonContent; +import com.axonivy.market.factory.ProductFactory; +import com.axonivy.market.repository.ProductJsonContentRepository; +import com.axonivy.market.service.ProductJsonContentService; +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.stereotype.Service; + +import static com.axonivy.market.constants.ProductJsonConstants.EN_LANGUAGE; + +@Service +@AllArgsConstructor +public class ProductJsonContentServiceImpl implements ProductJsonContentService { + private final ProductJsonContentRepository productJsonRepo; + + @Override + public void updateProductJsonContent(String jsonContent, String relatedTag, String currentVersion, + String replaceVersion, Product product) { + if (ObjectUtils.isNotEmpty(jsonContent)) { + ProductJsonContent productJsonContent = new ProductJsonContent(); + productJsonContent.setRelatedTag(relatedTag); + productJsonContent.setVersion(currentVersion); + productJsonContent.setProductId(product.getId()); + ProductFactory.mappingIdForProductJsonContent(productJsonContent); + productJsonContent.setName(product.getNames().get(EN_LANGUAGE)); + productJsonContent.setContent(jsonContent.replace(replaceVersion, currentVersion)); + productJsonRepo.save(productJsonContent); + } + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index 6c2c7ffb9..fc68b7f50 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -6,6 +6,8 @@ import com.axonivy.market.constants.ProductJsonConstants; import com.axonivy.market.criteria.ProductSearchCriteria; import com.axonivy.market.entity.GitHubRepoMeta; +import com.axonivy.market.entity.Image; +import com.axonivy.market.entity.MavenArtifactVersion; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductCustomSort; import com.axonivy.market.entity.ProductModuleContent; @@ -24,11 +26,13 @@ import com.axonivy.market.model.ProductCustomSortRequest; import com.axonivy.market.repository.GitHubRepoMetaRepository; import com.axonivy.market.repository.ImageRepository; +import com.axonivy.market.repository.MavenArtifactVersionRepository; import com.axonivy.market.repository.ProductCustomSortRepository; import com.axonivy.market.repository.ProductModuleContentRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.ImageService; import com.axonivy.market.service.ProductService; +import com.axonivy.market.util.MavenUtils; import com.axonivy.market.util.VersionUtils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -67,16 +71,19 @@ import java.util.Optional; import java.util.function.Predicate; +import static com.axonivy.market.constants.CommonConstants.SLASH; import static com.axonivy.market.constants.ProductJsonConstants.LOGO_FILE; import static com.axonivy.market.enums.DocumentField.MARKET_DIRECTORY; import static com.axonivy.market.enums.DocumentField.SHORT_DESCRIPTIONS; +import static com.axonivy.market.enums.FileStatus.ADDED; +import static com.axonivy.market.enums.FileStatus.MODIFIED; import static java.util.Optional.ofNullable; import static org.apache.commons.lang3.StringUtils.EMPTY; @Log4j2 @Service public class ProductServiceImpl implements ProductService { - + private static final String INITIAL_VERSION = "1.0"; private final ProductRepository productRepository; private final ProductModuleContentRepository productModuleContentRepository; private final GHAxonIvyMarketRepoService axonIvyMarketRepoService; @@ -84,30 +91,25 @@ public class ProductServiceImpl implements ProductService { private final GitHubRepoMetaRepository gitHubRepoMetaRepository; private final GitHubService gitHubService; private final ProductCustomSortRepository productCustomSortRepository; + private final MavenArtifactVersionRepository mavenArtifactVersionRepo; private final ImageRepository imageRepository; - private final ImageService imageService; private final MongoTemplate mongoTemplate; - + private final ObjectMapper mapper = new ObjectMapper(); + private final SecureRandom random = new SecureRandom(); private GHCommit lastGHCommit; private GitHubRepoMeta marketRepoMeta; - private final ObjectMapper mapper = new ObjectMapper(); - @Value("${synchronized.installation.counts.path}") private String installationCountPath; - @Value("${market.github.market.branch}") private String marketRepoBranch; - public static final String NON_NUMERIC_CHAR = "[^0-9.]"; - private static final String INITIAL_VERSION = "1.0"; - private final SecureRandom random = new SecureRandom(); - public ProductServiceImpl(ProductRepository productRepository, ProductModuleContentRepository productModuleContentRepository, GHAxonIvyMarketRepoService axonIvyMarketRepoService, GHAxonIvyProductRepoService axonIvyProductRepoService, GitHubRepoMetaRepository gitHubRepoMetaRepository, GitHubService gitHubService, - ProductCustomSortRepository productCustomSortRepository, ImageRepository imageRepository1, + ProductCustomSortRepository productCustomSortRepository, MavenArtifactVersionRepository mavenArtifactVersionRepo, + ImageRepository imageRepository1, ImageService imageService, MongoTemplate mongoTemplate) { this.productRepository = productRepository; this.productModuleContentRepository = productModuleContentRepository; @@ -116,11 +118,16 @@ public ProductServiceImpl(ProductRepository productRepository, this.gitHubRepoMetaRepository = gitHubRepoMetaRepository; this.gitHubService = gitHubService; this.productCustomSortRepository = productCustomSortRepository; + this.mavenArtifactVersionRepo = mavenArtifactVersionRepo; this.imageRepository = imageRepository1; this.imageService = imageService; this.mongoTemplate = mongoTemplate; } + private static Predicate filterNonPersistGhTagName(List currentTags) { + return tag -> !currentTags.contains(tag.getName()); + } + @Override public Page findProducts(String type, String keyword, String language, Boolean isRESTClient, Pageable pageable) { @@ -155,8 +162,8 @@ public boolean syncLatestDataFromMarketRepo() { @Override public int updateInstallationCountForProduct(String key, String designerVersion) { - Product product= productRepository.getProductById(key); - if (Objects.isNull(product)){ + Product product = productRepository.getProductById(key); + if (Objects.isNull(product)) { return 0; } @@ -217,63 +224,79 @@ private void updateLatestChangeToProductsFromGithubRepo() { String filePath = file.getFileName(); var parentPath = filePath.replace(FileType.META.getFileName(), EMPTY).replace(FileType.LOGO.getFileName(), EMPTY); var files = groupGitHubFiles.getOrDefault(parentPath, new ArrayList<>()); - files.sort((file1, file2) -> GitHubUtils.sortMetaJsonFirst(file1.getFileName(), file2.getFileName())); files.add(file); + files.sort((file1, file2) -> GitHubUtils.sortMetaJsonFirst(file1.getFileName(), file2.getFileName())); groupGitHubFiles.putIfAbsent(parentPath, files); } - groupGitHubFiles.entrySet().forEach(ghFileEntity -> { - for (var file : ghFileEntity.getValue()) { - Product product = new Product(); - GHContent fileContent; - try { - fileContent = gitHubService.getGHContent(axonIvyMarketRepoService.getRepository(), file.getFileName(), - marketRepoBranch); - } catch (IOException e) { - log.error("Get GHContent failed: ", e); - continue; - } - - ProductFactory.mappingByGHContent(product, fileContent); - if (FileType.META == file.getType()) { - transferComputedDataFromDB(product); - modifyProductByMetaContent(file, product); + groupGitHubFiles.forEach((key, value) -> { + for (var file : value) { + if (file.getStatus() == MODIFIED || file.getStatus() == ADDED) { + modifyProductMetaOrLogo(file, key); } else { - modifyProductLogo(ghFileEntity.getKey(), file, product, fileContent); + removeProductAndImage(file); } } }); } - private static Predicate filterNonPersistGhTagName(List currentTags) { - return tag -> !currentTags.contains(tag.getName()); + private void removeProductAndImage(GitHubFile file) { + if (FileType.META == file.getType()) { + String[] splitMetaJsonPath = file.getFileName().split(SLASH); + String extractMarketDirectory = file.getFileName().replace(splitMetaJsonPath[splitMetaJsonPath.length - 1], + EMPTY); + List productList = productRepository.findByMarketDirectory(extractMarketDirectory); + if (ObjectUtils.isNotEmpty(productList)) { + String productId = productList.get(0).getId(); + productRepository.deleteById(productId); + imageRepository.deleteAllByProductId(productId); + } + } else { + List images = imageRepository.findByImageUrlEndsWithIgnoreCase(file.getFileName()); + if (ObjectUtils.isNotEmpty(images)) { + Image currentImage = images.get(0); + productRepository.deleteById(currentImage.getProductId()); + imageRepository.deleteAllByProductId(currentImage.getProductId()); + } + } + } + + private void modifyProductMetaOrLogo(GitHubFile file, String parentPath) { + try { + GHContent fileContent = gitHubService.getGHContent(axonIvyMarketRepoService.getRepository(), file.getFileName(), + marketRepoBranch); + updateProductByMetaJsonAndLogo(fileContent, file, parentPath); + } catch (IOException e) { + log.error("Get GHContent failed: ", e); + } + } + + private void updateProductByMetaJsonAndLogo(GHContent fileContent, GitHubFile file, String parentPath) { + Product product = new Product(); + ProductFactory.mappingByGHContent(product, fileContent); + if (FileType.META == file.getType()) { + transferComputedDataFromDB(product); + productRepository.save(product); + } else { + modifyProductLogo(parentPath, fileContent); + } } - private void modifyProductLogo(String parentPath, GitHubFile file, Product product, GHContent fileContent) { - Product result; - switch (file.getStatus()) { - case MODIFIED, ADDED: - var searchCriteria = new ProductSearchCriteria(); - searchCriteria.setKeyword(parentPath); - searchCriteria.setFields(List.of(MARKET_DIRECTORY)); - result = productRepository.findByCriteria(searchCriteria); - if (result != null) { - Optional.ofNullable(imageService.mappingImageFromGHContent(result, fileContent, true)).ifPresent(image -> { + private void modifyProductLogo(String parentPath, GHContent fileContent) { + var searchCriteria = new ProductSearchCriteria(); + searchCriteria.setKeyword(parentPath); + searchCriteria.setFields(List.of(MARKET_DIRECTORY)); + Product result = productRepository.findByCriteria(searchCriteria); + if (result != null) { + Optional.ofNullable(imageService.mappingImageFromGHContent(result, fileContent, true)).ifPresent(image -> { + if (StringUtils.isNotBlank(result.getLogoId())) { imageRepository.deleteById(result.getLogoId()); - result.setLogoId(image.getId()); - productRepository.save(result); - }); - } - break; - case REMOVED: - result = productRepository.findByLogoId(product.getLogoId()); - if (result != null) { - imageRepository.deleteAllByProductId(result.getId()); - productRepository.deleteById(result.getId()); - } - break; - default: - break; + } + result.setLogoId(image.getId()); + productRepository.save(result); + }); + } else { + log.info("There is no product to update the logo with path {}", parentPath); } } @@ -325,19 +348,6 @@ private boolean isLastGithubCommitCovered() { return isLastCommitCovered; } - private void modifyProductByMetaContent(GitHubFile file, Product product) { - switch (file.getStatus()) { - case MODIFIED, ADDED: - productRepository.save(product); - break; - case REMOVED: - productRepository.deleteById(product.getId()); - break; - default: - break; - } - } - private void updateLatestReleaseTagContentsFromProductRepo() { List products = productRepository.findAll(); if (ObjectUtils.isEmpty(products)) { @@ -352,7 +362,8 @@ private void updateLatestReleaseTagContentsFromProductRepo() { } } - private void updateProductContentForNonStandardProduct(Map.Entry> ghContentEntity, Product product) { + private void updateProductContentForNonStandardProduct(Map.Entry> ghContentEntity, + Product product) { ProductModuleContent initialContent = new ProductModuleContent(); initialContent.setTag(INITIAL_VERSION); initialContent.setProductId(product.getId()); @@ -376,25 +387,25 @@ private void syncProductsFromGitHubRepo() { log.warn("**ProductService: synchronize products from scratch based on the Market repo"); var gitHubContentMap = axonIvyMarketRepoService.fetchAllMarketItems(); for (Map.Entry> ghContentEntity : gitHubContentMap.entrySet()) { - Product product = new Product(); - //update the meta.json first - ghContentEntity.getValue() - .sort((file1, file2) -> GitHubUtils.sortMetaJsonFirst(file1.getName(), file2.getName())); - for (var content : ghContentEntity.getValue()) { - ProductFactory.mappingByGHContent(product, content); - mappingLogoFromGHContent(product, content); - } - if (productRepository.findById(product.getId()).isPresent()) { - continue; - } - if (StringUtils.isNotBlank(product.getRepositoryName())) { - updateProductCompatibility(product); - getProductContents(product); - } else { - updateProductContentForNonStandardProduct(ghContentEntity, product); - } - transferComputedDataFromDB(product); - productRepository.save(product); + Product product = new Product(); + //update the meta.json first + ghContentEntity.getValue() + .sort((file1, file2) -> GitHubUtils.sortMetaJsonFirst(file1.getName(), file2.getName())); + for (var content : ghContentEntity.getValue()) { + ProductFactory.mappingByGHContent(product, content); + mappingLogoFromGHContent(product, content); + } + if (productRepository.findById(product.getId()).isPresent()) { + continue; + } + if (StringUtils.isNotBlank(product.getRepositoryName())) { + updateProductCompatibility(product); + getProductContents(product); + } else { + updateProductContentForNonStandardProduct(ghContentEntity, product); + } + transferComputedDataFromDB(product); + productRepository.save(product); } } @@ -494,12 +505,17 @@ public Product fetchProductDetail(String id) { @Override public Product fetchBestMatchProductDetail(String id, String version) { - List releasedVersions = productRepository.getReleasedVersionsById(id); - String bestMatchVersion = VersionUtils.getBestMatchVersion(releasedVersions, version); - String bestMatchTag = VersionUtils.convertVersionToTag(id,bestMatchVersion); - Product product = StringUtils.isBlank(bestMatchTag) ? productRepository.getProductById(id) : productRepository.getProductByIdAndTag(id, bestMatchTag); + MavenArtifactVersion existingMavenArtifactVersion = mavenArtifactVersionRepo.findById(id).orElse( + MavenArtifactVersion.builder().productId(id).build()); + List versions = MavenUtils.getAllExistingVersions(existingMavenArtifactVersion, true, + null); + String bestMatchVersion = VersionUtils.getBestMatchVersion(versions, version); + String bestMatchTag = VersionUtils.convertVersionToTag(id, bestMatchVersion); + Product product = StringUtils.isBlank(bestMatchTag) ? productRepository.getProductById( + id) : productRepository.getProductByIdWithTagOrVersion(id, bestMatchTag); return Optional.ofNullable(product).map(productItem -> { updateProductInstallationCount(id, productItem); + productItem.setBestMatchVersion(bestMatchVersion); return productItem; }).orElse(null); } @@ -514,7 +530,7 @@ public void updateProductInstallationCount(String id, Product productItem) { @Override public Product fetchProductDetailByIdAndVersion(String id, String version) { - return productRepository.getProductByIdAndTag(id, VersionUtils.convertVersionToTag(id, version)); + return productRepository.getProductByIdWithTagOrVersion(id, version); } @Override diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java index 2f0a81e8d..05bef80ff 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java @@ -1,128 +1,86 @@ package com.axonivy.market.service.impl; -import com.axonivy.market.comparator.ArchivedArtifactsComparator; -import com.axonivy.market.comparator.LatestVersionComparator; -import com.axonivy.market.constants.CommonConstants; -import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.constants.MavenConstants; +import com.axonivy.market.bo.Artifact; import com.axonivy.market.controller.ProductDetailsController; -import com.axonivy.market.entity.MavenArtifactModel; import com.axonivy.market.entity.MavenArtifactVersion; -import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductJsonContent; -import com.axonivy.market.enums.NonStandardProduct; -import com.axonivy.market.github.model.ArchivedArtifact; -import com.axonivy.market.github.model.MavenArtifact; -import com.axonivy.market.github.service.GHAxonIvyProductRepoService; -import com.axonivy.market.github.util.GitHubUtils; +import com.axonivy.market.model.MavenArtifactModel; import com.axonivy.market.model.MavenArtifactVersionModel; import com.axonivy.market.model.VersionAndUrlModel; import com.axonivy.market.repository.MavenArtifactVersionRepository; import com.axonivy.market.repository.ProductJsonContentRepository; +import com.axonivy.market.repository.ProductModuleContentRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.VersionService; +import com.axonivy.market.util.MavenUtils; import com.axonivy.market.util.VersionUtils; -import com.axonivy.market.util.XmlReaderUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.Getter; +import lombok.AllArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.kohsuke.github.GHContent; import org.springframework.hateoas.Link; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; -import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import static com.axonivy.market.constants.ProductJsonConstants.NAME; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + @Log4j2 @Service -@Getter +@AllArgsConstructor public class VersionServiceImpl implements VersionService { - private final GHAxonIvyProductRepoService gitHubService; private final MavenArtifactVersionRepository mavenArtifactVersionRepository; - private final ProductRepository productRepository; - private final ProductJsonContentRepository productJsonContentRepository; - @Getter - private String repoName; - private Map> archivedArtifactsMap; - private List artifactsFromMeta; - private MavenArtifactVersion proceedDataCache; - private MavenArtifact metaProductArtifact; - private final LatestVersionComparator latestVersionComparator = new LatestVersionComparator(); - @Getter - private String productJsonFilePath; - private String productId; + private final ProductRepository productRepo; + private final ProductJsonContentRepository productJsonRepo; + private final ProductModuleContentRepository productContentRepo; private final ObjectMapper mapper = new ObjectMapper(); - public VersionServiceImpl(GHAxonIvyProductRepoService gitHubService, - MavenArtifactVersionRepository mavenArtifactVersionRepository, ProductRepository productRepository, - ProductJsonContentRepository productJsonContentRepository) { - this.gitHubService = gitHubService; - this.mavenArtifactVersionRepository = mavenArtifactVersionRepository; - this.productRepository = productRepository; - - this.productJsonContentRepository = productJsonContentRepository; - } - - private void resetData() { - repoName = null; - archivedArtifactsMap = new HashMap<>(); - artifactsFromMeta = Collections.emptyList(); - proceedDataCache = null; - metaProductArtifact = null; - productJsonFilePath = null; - productId = null; - } - public List getArtifactsAndVersionToDisplay(String productId, Boolean isShowDevVersion, String designerVersion) { + MavenArtifactVersion existingMavenArtifactVersion = mavenArtifactVersionRepository.findById(productId).orElse( + MavenArtifactVersion.builder().productId(productId).build()); List results = new ArrayList<>(); - resetData(); - - this.productId = productId; - artifactsFromMeta = getProductMetaArtifacts(productId); - List versionsToDisplay = VersionUtils.getVersionsToDisplay(getVersionsFromMavenArtifacts(), isShowDevVersion, designerVersion); - proceedDataCache = mavenArtifactVersionRepository.findById(productId).orElse(new MavenArtifactVersion(productId)); - metaProductArtifact = artifactsFromMeta.stream() - .filter(artifact -> artifact.getArtifactId().endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)).findAny() - .orElse(new MavenArtifact()); - - sanitizeMetaArtifactBeforeHandle(); - boolean isNewVersionDetected = handleArtifactForVersionToDisplay(versionsToDisplay, results); - if (isNewVersionDetected) { - mavenArtifactVersionRepository.save(proceedDataCache); + for (String mavenVersion : MavenUtils.getAllExistingVersions(existingMavenArtifactVersion, isShowDevVersion, + designerVersion)) { + List artifactsByVersion = new ArrayList<>(); + artifactsByVersion.addAll( + existingMavenArtifactVersion.getProductArtifactsByVersion().computeIfAbsent(mavenVersion, + k -> new ArrayList<>())); + artifactsByVersion.addAll( + existingMavenArtifactVersion.getAdditionalArtifactsByVersion().computeIfAbsent(mavenVersion, + k -> new ArrayList<>())); + + if (ObjectUtils.isNotEmpty(artifactsByVersion)) { + artifactsByVersion = artifactsByVersion.stream().distinct().toList(); + results.add(new MavenArtifactVersionModel(mavenVersion, artifactsByVersion)); + } } return results; } - @Override - public Map getProductJsonContentByIdAndVersion(String productId, String version){ + public Map getProductJsonContentByIdAndTag(String productId, String tag) { Map result = new HashMap<>(); try { - ProductJsonContent productJsonContent = productJsonContentRepository.findByProductIdAndVersion(productId, version); + ProductJsonContent productJsonContent = + productJsonRepo.findByProductIdAndVersion(productId, tag).stream().findAny().orElse(null); if (ObjectUtils.isEmpty(productJsonContent)) { return new HashMap<>(); } result = mapper.readValue(productJsonContent.getContent(), Map.class); result.computeIfAbsent(NAME, k -> productJsonContent.getName()); - - } catch (JsonProcessingException jsonProcessingException){ + } catch (JsonProcessingException jsonProcessingException) { log.error(jsonProcessingException.getMessage()); } return result; @@ -131,7 +89,10 @@ public Map getProductJsonContentByIdAndVersion(String productId, @Override public List getVersionsForDesigner(String productId) { List versionAndUrlList = new ArrayList<>(); - List versions = productRepository.getReleasedVersionsById(productId); + MavenArtifactVersion existingMavenArtifactVersion = mavenArtifactVersionRepository.findById(productId).orElse( + MavenArtifactVersion.builder().productId(productId).build()); + List versions = MavenUtils.getAllExistingVersions(existingMavenArtifactVersion, true, + null); for (String version : versions) { Link link = linkTo( methodOn(ProductDetailsController.class).findProductJsonContent(productId, version)).withSelfRel(); @@ -141,174 +102,23 @@ public List getVersionsForDesigner(String productId) { return versionAndUrlList; } - public boolean handleArtifactForVersionToDisplay(List versionsToDisplay, - List result) { - boolean isNewVersionDetected = false; - for (String version : versionsToDisplay) { - List artifactsInVersion = convertMavenArtifactsToModels(artifactsFromMeta, version); - List productArtifactModels = proceedDataCache.getProductArtifactWithVersionReleased() - .get(version); - if (productArtifactModels == null) { - isNewVersionDetected = true; - productArtifactModels = updateArtifactsInVersionWithProductArtifact(version); - } - artifactsInVersion.addAll(productArtifactModels); - result.add(new MavenArtifactVersionModel(version, artifactsInVersion.stream().distinct().toList())); - } - return isNewVersionDetected; - } - - public List updateArtifactsInVersionWithProductArtifact(String version) { - List productArtifactModels = convertMavenArtifactsToModels(getProductJsonByVersion(version), - version); - proceedDataCache.getVersions().add(version); - proceedDataCache.getProductArtifactWithVersionReleased().put(version, productArtifactModels); - return productArtifactModels; - } - - public List getProductMetaArtifacts(String productId) { - Product productInfo = productRepository.findById(productId).orElse(new Product()); - String fullRepoName = productInfo.getRepositoryName(); - if (StringUtils.isNotEmpty(fullRepoName)) { - repoName = getRepoNameFromMarketRepo(fullRepoName); - } - return Optional.ofNullable(productInfo.getArtifacts()).orElse(new ArrayList<>()); - } - - public void sanitizeMetaArtifactBeforeHandle() { - artifactsFromMeta.remove(metaProductArtifact); - artifactsFromMeta.forEach(artifact -> { - List archivedArtifacts = new ArrayList<>( - Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()).stream() - .sorted(new ArchivedArtifactsComparator()).toList()); - Collections.reverse(archivedArtifacts); - archivedArtifactsMap.put(artifact.getArtifactId(), archivedArtifacts); - }); - } - - public List getVersionsFromMavenArtifacts() { - Set versions = new HashSet<>(); - for (MavenArtifact artifact : artifactsFromMeta) { - versions.addAll( - getVersionsFromArtifactDetails(artifact.getRepoUrl(), artifact.getGroupId(), artifact.getArtifactId())); - Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()).forEach( - archivedArtifact -> versions.addAll( - getVersionsFromArtifactDetails(artifact.getRepoUrl(), archivedArtifact.getGroupId(), - archivedArtifact.getArtifactId()))); - } - List versionList = new ArrayList<>(versions); - versionList.sort(new LatestVersionComparator()); - return versionList; - } - - @Override - public List getVersionsFromArtifactDetails(String repoUrl, String groupId, String artifactID) { + public List getPersistedVersions(String productId) { + var product = productRepo.findById(productId); List versions = new ArrayList<>(); - String baseUrl = buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactID); - if (StringUtils.isNotBlank(baseUrl)) { - versions.addAll(XmlReaderUtils.readXMLFromUrl(baseUrl)); - } - return versions; - } - - @Override - public String buildMavenMetadataUrlFromArtifact(String repoUrl, String groupId, String artifactID) { - if (StringUtils.isAnyBlank(groupId, artifactID)) { - return StringUtils.EMPTY; - } - repoUrl = Optional.ofNullable(repoUrl).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); - groupId = groupId.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); - return String.format(MavenConstants.METADATA_URL_FORMAT, repoUrl, groupId, artifactID); - } - - public List getProductJsonByVersion(String version) { - List result = new ArrayList<>(); - String versionTag = getVersionTag(version); - productJsonFilePath = buildProductJsonFilePath(); - try { - GHContent productJsonContent = gitHubService.getContentFromGHRepoAndTag(repoName, productJsonFilePath, - versionTag); - if (Objects.isNull(productJsonContent)) { - return result; - } - result = gitHubService.convertProductJsonToMavenProductInfo(productJsonContent); - } catch (IOException e) { - log.warn("Can not get the product.json from repo {} by path in {} version {}", repoName, productJsonFilePath, - versionTag); - } - return result; - } - - public String getVersionTag(String version) { - String versionTag = "v" + version; - if (NonStandardProduct.PORTAL.getId().equals(productId)) { - versionTag = version; - } - return versionTag; - } - - public String buildProductJsonFilePath() { - String pathToProductFolderFromTagContent = metaProductArtifact.getArtifactId(); - GitHubUtils.getNonStandardProductFilePath(productId); - productJsonFilePath = String.format(GitHubConstants.PRODUCT_JSON_FILE_PATH_FORMAT, - pathToProductFolderFromTagContent); - return productJsonFilePath; - } - - public MavenArtifactModel convertMavenArtifactToModel(MavenArtifact artifact, String version) { - String artifactName = artifact.getName(); - if (StringUtils.isBlank(artifactName)) { - artifactName = GitHubUtils.convertArtifactIdToName(artifact.getArtifactId()); + if (product.isPresent()) { + versions.addAll(product.get().getReleasedVersions()); } - artifact.setType(Optional.ofNullable(artifact.getType()).orElse("iar")); - artifactName = String.format(MavenConstants.ARTIFACT_NAME_FORMAT, artifactName, artifact.getType()); - return new MavenArtifactModel(artifactName, buildDownloadUrlFromArtifactAndVersion(artifact, version), - artifact.getIsProductArtifact()); - } - - public List convertMavenArtifactsToModels(List artifacts, String version) { - List results = new ArrayList<>(); - if (!CollectionUtils.isEmpty(artifacts)) { - for (MavenArtifact artifact : artifacts) { - MavenArtifactModel mavenArtifactModel = convertMavenArtifactToModel(artifact, version); - results.add(mavenArtifactModel); - } - } - return results; - } - - public String buildDownloadUrlFromArtifactAndVersion(MavenArtifact artifact, String version) { - String groupIdByVersion = artifact.getGroupId(); - String artifactIdByVersion = artifact.getArtifactId(); - String repoUrl = Optional.ofNullable(artifact.getRepoUrl()).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); - ArchivedArtifact archivedArtifactBestMatchVersion = findArchivedArtifactInfoBestMatchWithVersion( - artifact.getArtifactId(), version); - - if (Objects.nonNull(archivedArtifactBestMatchVersion)) { - groupIdByVersion = archivedArtifactBestMatchVersion.getGroupId(); - artifactIdByVersion = archivedArtifactBestMatchVersion.getArtifactId(); + if (CollectionUtils.isEmpty(versions)) { + versions.addAll(productContentRepo.findTagsByProductId(productId)); + versions = versions.stream().map(VersionUtils::convertTagToVersion).collect(Collectors.toList()); } - groupIdByVersion = groupIdByVersion.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); - return String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, repoUrl, groupIdByVersion, artifactIdByVersion, - version, artifactIdByVersion, version, artifact.getType()); - } - - public ArchivedArtifact findArchivedArtifactInfoBestMatchWithVersion(String artifactId, String version) { - List archivedArtifacts = archivedArtifactsMap.get(artifactId); - - if (CollectionUtils.isEmpty(archivedArtifacts)) { - return null; - } - for (ArchivedArtifact archivedArtifact : archivedArtifacts) { - if (latestVersionComparator.compare(archivedArtifact.getLastVersion(), version) <= 0) { - return archivedArtifact; - } - } - return null; + return versions; } - public String getRepoNameFromMarketRepo(String fullRepoName) { - String[] repoNamePart = fullRepoName.split("/"); - return repoNamePart[repoNamePart.length - 1]; + public List getMavenArtifactsFromProductJsonByTag(String tag, + String productId) { + ProductJsonContent productJson = + productJsonRepo.findByProductIdAndVersion(productId, tag).stream().findAny().orElse(null); + return MavenUtils.getMavenArtifactsFromProductJson(productJson); } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/ImageUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/ImageUtils.java index ddab4a2fe..608649e78 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/util/ImageUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/util/ImageUtils.java @@ -1,8 +1,10 @@ package com.axonivy.market.util; -import static com.axonivy.market.constants.CommonConstants.IMAGE_ID_PREFIX; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; +import com.axonivy.market.controller.ImageController; +import com.axonivy.market.entity.ProductModuleContent; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.logging.log4j.util.Strings; +import org.springframework.hateoas.Link; import java.util.ArrayList; import java.util.List; @@ -10,11 +12,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.axonivy.market.controller.ImageController; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.logging.log4j.util.Strings; -import org.springframework.hateoas.Link; -import com.axonivy.market.entity.ProductModuleContent; +import static com.axonivy.market.constants.CommonConstants.IMAGE_ID_PREFIX; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; public class ImageUtils { public static final String IMAGE_ID_FORMAT_PATTERN = "imageId-\\w+"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/MavenUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/MavenUtils.java new file mode 100644 index 000000000..b3eadceaa --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/util/MavenUtils.java @@ -0,0 +1,308 @@ +package com.axonivy.market.util; + +import com.axonivy.market.bo.ArchivedArtifact; +import com.axonivy.market.bo.Artifact; +import com.axonivy.market.comparator.MavenVersionComparator; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.MavenConstants; +import com.axonivy.market.constants.ProductJsonConstants; +import com.axonivy.market.entity.MavenArtifactVersion; +import com.axonivy.market.entity.Metadata; +import com.axonivy.market.entity.ProductJsonContent; +import com.axonivy.market.github.util.GitHubUtils; +import com.axonivy.market.model.MavenArtifactModel; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +@Log4j2 +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MavenUtils { + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final RestTemplate restTemplate = new RestTemplate(); + + public static List getMavenArtifactsFromProductJson(ProductJsonContent productJson) { + if (Objects.isNull(productJson) || StringUtils.isBlank(productJson.getContent())) { + return new ArrayList<>(); + } + InputStream contentStream = IOUtils.toInputStream(productJson.getContent(), StandardCharsets.UTF_8); + try { + return extractMavenArtifactsFromContentStream(contentStream); + } catch (IOException e) { + log.error("Can not get maven artifacts from Product.json of {}", productJson); + return new ArrayList<>(); + } + } + + public static Artifact createArtifactFromJsonNode(JsonNode node, String repoUrl, boolean isDependency) { + Artifact artifact = new Artifact(); + artifact.setRepoUrl(repoUrl); + artifact.setIsDependency(isDependency); + artifact.setGroupId(getTextValueFromNodeAndPath(node, ProductJsonConstants.GROUP_ID)); + artifact.setArtifactId(getTextValueFromNodeAndPath(node, ProductJsonConstants.ARTIFACT_ID)); + artifact.setType(getTextValueFromNodeAndPath(node, ProductJsonConstants.TYPE)); + artifact.setIsProductArtifact(true); + return artifact; + } + + private static String getTextValueFromNodeAndPath(JsonNode node, String path) { + return Objects.isNull(node.path(path)) ? StringUtils.EMPTY : node.path(path).asText(); + } + + public static void extractMavenArtifactFromJsonNode(JsonNode dataNode, boolean isDependency, List artifacts, + String repoUrl) { + String nodeName = ProductJsonConstants.PROJECTS; + if (isDependency) { + nodeName = ProductJsonConstants.DEPENDENCIES; + } + JsonNode dependenciesNode = dataNode.path(nodeName); + for (JsonNode dependencyNode : dependenciesNode) { + Artifact artifact = createArtifactFromJsonNode(dependencyNode, repoUrl, isDependency); + artifacts.add(artifact); + } + } + + public static List convertProductJsonToMavenProductInfo(Path folderPath) throws IOException { + Path productJsonPath = folderPath.resolve(ProductJsonConstants.PRODUCT_JSON_FILE); + + if (!(Files.exists(productJsonPath) && Files.isRegularFile(productJsonPath))) { + log.warn("product.json file not found in the folder: {}", folderPath); + return new ArrayList<>(); + } + + InputStream contentStream = extractedContentStream(productJsonPath); + if (Objects.isNull(contentStream)) { + return new ArrayList<>(); + } + return extractMavenArtifactsFromContentStream(contentStream); + } + + public static InputStream extractedContentStream(Path filePath) { + try { + return Files.newInputStream(filePath); + } catch (IOException | NullPointerException e) { + log.warn("Cannot read the current file: {}", e.getMessage()); + return null; + } + } + + public static List extractMavenArtifactsFromContentStream(InputStream contentStream) throws IOException { + List artifacts = new ArrayList<>(); + JsonNode rootNode = objectMapper.readTree(contentStream); + JsonNode installersNode = rootNode.path(ProductJsonConstants.INSTALLERS); + + // Not convert to artifact if id of node is not maven-import or maven-dependency + List installerIdsToDisplay = List.of(ProductJsonConstants.MAVEN_DEPENDENCY_INSTALLER_ID, + ProductJsonConstants.MAVEN_IMPORT_INSTALLER_ID, ProductJsonConstants.MAVEN_DROPINS_INSTALLER_ID); + + for (JsonNode mavenNode : installersNode) { + JsonNode dataNode = mavenNode.path(ProductJsonConstants.DATA); + if (!installerIdsToDisplay.contains(mavenNode.path(ProductJsonConstants.ID).asText())) { + continue; + } + + // Extract repository URL + JsonNode repositoriesNode = dataNode.path(ProductJsonConstants.REPOSITORIES); + String repoUrl = repositoriesNode.get(0).path(ProductJsonConstants.URL).asText(); + + // Process projects + if (dataNode.has(ProductJsonConstants.PROJECTS)) { + extractMavenArtifactFromJsonNode(dataNode, false, artifacts, repoUrl); + } + + // Process dependencies + if (dataNode.has(ProductJsonConstants.DEPENDENCIES)) { + extractMavenArtifactFromJsonNode(dataNode, true, artifacts, repoUrl); + } + } + return artifacts; + } + + public static String buildDownloadUrl(Artifact artifact, String version) { + String groupIdByVersion = artifact.getGroupId(); + String artifactIdByVersion = artifact.getArtifactId(); + String repoUrl = StringUtils.defaultIfBlank(artifact.getRepoUrl(), MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + ArchivedArtifact archivedArtifactBestMatchVersion = findArchivedArtifactInfoBestMatchWithVersion(version, + artifact.getArchivedArtifacts()); + + if (Objects.nonNull(archivedArtifactBestMatchVersion)) { + groupIdByVersion = archivedArtifactBestMatchVersion.getGroupId(); + artifactIdByVersion = archivedArtifactBestMatchVersion.getArtifactId(); + } + groupIdByVersion = groupIdByVersion.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); + return buildDownloadUrl(artifactIdByVersion, version, artifact.getType(), repoUrl, groupIdByVersion, + StringUtils.EMPTY); + } + + public static String buildDownloadUrl(Metadata metadata, String version) { + String groupIdByVersion = metadata.getGroupId(); + groupIdByVersion = groupIdByVersion.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); + return buildDownloadUrl(metadata.getArtifactId(), version, metadata.getType(), metadata.getRepoUrl(), + groupIdByVersion, StringUtils.EMPTY); + } + + public static String buildDownloadUrl(String artifactId, String baseVersion, String type, String repoUrl, + String groupId, String version) { + groupId = groupId.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); + if (StringUtils.isBlank(version)) { + version = baseVersion; + } + String artifactFileName = String.format(MavenConstants.ARTIFACT_FILE_NAME_FORMAT, artifactId, version, type); + return String.join(CommonConstants.SLASH, repoUrl, groupId, artifactId, baseVersion, artifactFileName); + } + + + public static ArchivedArtifact findArchivedArtifactInfoBestMatchWithVersion(String version, + List archivedArtifacts) { + if (CollectionUtils.isEmpty(archivedArtifacts)) { + return null; + } + return archivedArtifacts.stream().filter( + archivedArtifact -> MavenVersionComparator.compare(archivedArtifact.getLastVersion(), + version) >= 0).findAny().orElse(null); + } + + public static MavenArtifactModel convertMavenArtifactToModel(Artifact artifact, String version) { + String artifactName = artifact.getName(); + if (StringUtils.isBlank(artifactName)) { + artifactName = GitHubUtils.convertArtifactIdToName(artifact.getArtifactId()); + } + artifact.setType(StringUtils.defaultIfBlank(artifact.getType(), ProductJsonConstants.DEFAULT_PRODUCT_TYPE)); + artifactName = String.format(MavenConstants.ARTIFACT_NAME_FORMAT, artifactName, artifact.getType()); + return MavenArtifactModel.builder().name(artifactName).downloadUrl(buildDownloadUrl(artifact, version)).build(); + } + + public static List convertArtifactsToModels(List artifacts, String version) { + List results = new ArrayList<>(); + if (!CollectionUtils.isEmpty(artifacts)) { + for (Artifact artifact : artifacts) { + MavenArtifactModel mavenArtifactModel = convertMavenArtifactToModel(artifact, version); + results.add(mavenArtifactModel); + } + } + return results; + } + + public static String buildSnapshotMetadataUrlFromArtifactInfo(String repoUrl, String groupId, String artifactId, + String snapshotVersion) { + if (StringUtils.isAnyBlank(groupId, artifactId)) { + return StringUtils.EMPTY; + } + repoUrl = StringUtils.defaultIfEmpty(repoUrl, MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + groupId = groupId.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); + return String.join(CommonConstants.SLASH, repoUrl, groupId, artifactId, snapshotVersion, + MavenConstants.METADATA_URL_POSTFIX); + } + + public static String buildMetadataUrlFromArtifactInfo(String repoUrl, String groupId, String artifactId) { + if (StringUtils.isAnyBlank(groupId, artifactId)) { + return StringUtils.EMPTY; + } + repoUrl = StringUtils.defaultIfEmpty(repoUrl, MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + groupId = groupId.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); + return String.join(CommonConstants.SLASH, repoUrl, groupId, artifactId, MavenConstants.METADATA_URL_POSTFIX); + } + + public static Metadata convertArtifactToMetadata(String productId, Artifact artifact, String metadataUrl) { + String artifactName = artifact.getName(); + if (StringUtils.isBlank(artifactName)) { + artifactName = GitHubUtils.convertArtifactIdToName(artifact.getArtifactId()); + } + String type = StringUtils.defaultIfBlank(artifact.getType(), ProductJsonConstants.DEFAULT_PRODUCT_TYPE); + artifactName = String.format(MavenConstants.ARTIFACT_NAME_FORMAT, artifactName, type); + return Metadata.builder().groupId(artifact.getGroupId()).versions(new HashSet<>()).productId(productId).artifactId( + artifact.getArtifactId()).url(metadataUrl).repoUrl( + StringUtils.defaultIfEmpty(artifact.getRepoUrl(), MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL)).type(type).name( + artifactName).isProductArtifact(BooleanUtils.isTrue(artifact.getIsProductArtifact())).build(); + } + + public static Metadata buildSnapShotMetadataFromVersion(Metadata metadata, String version) { + String snapshotMetadataUrl = buildSnapshotMetadataUrlFromArtifactInfo(metadata.getRepoUrl(), metadata.getGroupId(), + metadata.getArtifactId(), version); + return Metadata.builder().url(snapshotMetadataUrl).repoUrl(metadata.getRepoUrl()).groupId( + metadata.getGroupId()).artifactId(metadata.getArtifactId()).type(metadata.getType()).productId( + metadata.getProductId()).name(metadata.getName()).isProductArtifact(metadata.isProductArtifact()).build(); + } + + public static MavenArtifactModel buildMavenArtifactModelFromMetadata(String version, Metadata metadata) { + return new MavenArtifactModel(metadata.getName(), + buildDownloadUrl(metadata.getArtifactId(), version, metadata.getType(), + metadata.getRepoUrl(), metadata.getGroupId(), metadata.getSnapshotVersionValue()), + metadata.getArtifactId().contains(metadata.getGroupId())); + } + + public static String getMetadataContentFromUrl(String metadataUrl) { + try { + return restTemplate.getForObject(metadataUrl, String.class); + } catch (Exception e) { + log.error("**MetadataService: Failed to fetch metadata from url {}", metadataUrl); + return StringUtils.EMPTY; + } + } + + public static boolean isProductArtifactId(String artifactId) { + return StringUtils.endsWith(artifactId, MavenConstants.PRODUCT_ARTIFACT_POSTFIX); + } + + public static Set convertArtifactsToMetadataSet(Set artifacts, String productId) { + Set results = new HashSet<>(); + if (!CollectionUtils.isEmpty(artifacts)) { + artifacts.forEach(artifact -> { + String metadataUrl = buildMetadataUrlFromArtifactInfo(artifact.getRepoUrl(), artifact.getGroupId(), + artifact.getArtifactId()); + results.add(convertArtifactToMetadata(productId, artifact, metadataUrl)); + results.addAll(extractMetaDataFromArchivedArtifacts(productId, artifact)); + }); + } + return results; + } + + public static Set extractMetaDataFromArchivedArtifacts(String productId, Artifact artifact) { + Set results = new HashSet<>(); + if (!CollectionUtils.isEmpty(artifact.getArchivedArtifacts())) { + artifact.getArchivedArtifacts().forEach(archivedArtifact -> { + String archivedMetadataUrl = buildMetadataUrlFromArtifactInfo(artifact.getRepoUrl(), + archivedArtifact.getGroupId(), archivedArtifact.getArtifactId()); + results.add(convertArtifactToMetadata(productId, artifact, archivedMetadataUrl)); + }); + } + return results; + } + + public static List filterNonProductArtifactFromList(List artifactsFromMeta) { + if (CollectionUtils.isEmpty(artifactsFromMeta)) { + return artifactsFromMeta; + } + return artifactsFromMeta.stream().filter( + artifact -> !artifact.getArtifactId().endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)).toList(); + } + + public static List getAllExistingVersions(MavenArtifactVersion existingMavenArtifactVersion, + boolean isShowDevVersion, String designerVersion) { + Set existingProductsArtifactByVersion = + new HashSet<>(existingMavenArtifactVersion.getProductArtifactsByVersion().keySet()); + Set existingAdditionalArtifactByVersion = + existingMavenArtifactVersion.getProductArtifactsByVersion().keySet(); + existingProductsArtifactByVersion.addAll(existingAdditionalArtifactByVersion); + return VersionUtils.getVersionsToDisplay(new ArrayList<>(existingProductsArtifactByVersion), isShowDevVersion, + designerVersion); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/MetadataReaderUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/MetadataReaderUtils.java new file mode 100644 index 000000000..dfcbe0c5c --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/util/MetadataReaderUtils.java @@ -0,0 +1,72 @@ +package com.axonivy.market.util; + +import com.axonivy.market.constants.MavenConstants; +import com.axonivy.market.entity.Metadata; +import lombok.extern.log4j.Log4j2; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.StringReader; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +@Log4j2 +public class MetadataReaderUtils { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern( + MavenConstants.DATE_TIME_FORMAT); + private static final DateTimeFormatter SNAPSHOT_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern( + MavenConstants.SNAPSHOT_LAST_UPDATED_DATE_TIME_FORMAT); + private MetadataReaderUtils() { + } + + public static Metadata updateMetadataFromMavenXML(String xmlData, Metadata metadata, boolean isSnapShot) { + try { + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document document = builder.parse(new InputSource(new StringReader(xmlData))); + document.getDocumentElement().normalize(); + LocalDateTime lastUpdated = getLastUpdatedTimeFromDocument(document, isSnapShot); + if (lastUpdated.equals(metadata.getLastUpdated())) { + return metadata; + } + metadata.setLastUpdated(lastUpdated); + updateMetadataVersions(metadata, document, isSnapShot); + } catch (Exception e) { + log.error("Metadata Reader: can not read the metadata of {} with error", xmlData, e); + } + return metadata; + } + + private static void updateMetadataVersions(Metadata metadata, Document document, boolean isSnapShot) { + if (isSnapShot) { + String value = document.getElementsByTagName(MavenConstants.VALUE_TAG).item(0).getTextContent(); + metadata.setSnapshotVersionValue(value); + return; + } + metadata.setLatest(getElementValue(document, MavenConstants.LATEST_VERSION_TAG)); + metadata.setRelease(getElementValue(document, MavenConstants.LATEST_RELEASE_TAG)); + NodeList versionNodes = document.getElementsByTagName(MavenConstants.VERSION_TAG); + for (int i = 0; i < versionNodes.getLength(); i++) { + metadata.getVersions().add(versionNodes.item(i).getTextContent()); + } + } + + private static LocalDateTime getLastUpdatedTimeFromDocument(Document document, boolean isSnapShot) { + String textValue = isSnapShot ? Objects.requireNonNull(getElementValue(document, + MavenConstants.SNAPSHOT_LAST_UPDATED_TAG)) : Objects.requireNonNull(getElementValue(document, + MavenConstants.LAST_UPDATED_TAG)); + DateTimeFormatter lastUpdatedFormatter = isSnapShot ? SNAPSHOT_DATE_TIME_FORMATTER : DATE_TIME_FORMATTER; + return LocalDateTime.parse(textValue, lastUpdatedFormatter); + } + + private static String getElementValue(Document doc, String tagName) { + NodeList nodeList = doc.getElementsByTagName(tagName); + if (nodeList.getLength() > 0) { + return nodeList.item(0).getTextContent(); + } + return null; + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/ProductContentUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/ProductContentUtils.java new file mode 100644 index 000000000..f8ba2d5e2 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/util/ProductContentUtils.java @@ -0,0 +1,155 @@ +package com.axonivy.market.util; + +import com.axonivy.market.bo.Artifact; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.constants.ReadmeConstants; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.enums.Language; +import com.axonivy.market.factory.ProductFactory; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ProductContentUtils { + public static final String DEMO_SETUP_TITLE = "(?i)## Demo|## Setup"; + private static final String HASH = "#"; + public static final String DESCRIPTION = "description"; + public static final String DEMO = "demo"; + public static final String SETUP = "setup"; + public static final String README_IMAGE_FORMAT = "\\(([^)]*?%s[^)]*?)\\)"; + public static final String IMAGE_DOWNLOAD_URL_FORMAT = "(%s)"; + + private ProductContentUtils() { + } + + /** + * MARP-810: Sabine requires that content in other languages, which has not been translated, be left empty and + * replaced with English content. + */ + public static Map replaceEmptyContentsWithEnContent(Map map) { + String enValue = map.get(Language.EN.getValue()); + for (Map.Entry entry : map.entrySet()) { + if (StringUtils.isBlank(entry.getValue())) { + map.put(entry.getKey(), enValue); + } + } + return map; + } + + public static String getReadmeFileLocale(String readmeFile) { + String result = StringUtils.EMPTY; + Pattern pattern = Pattern.compile(GitHubConstants.README_FILE_LOCALE_REGEX); + Matcher matcher = pattern.matcher(readmeFile); + if (matcher.find()) { + result = matcher.group(1); + } + return result; + } + + // Cover some cases including when demo and setup parts switch positions or + // missing one of them + public static void getExtractedPartsOfReadme(Map> moduleContents, String readmeContents, + String readmeFileName) { + String locale = getReadmeFileLocale(readmeFileName); + String[] parts = readmeContents.split(DEMO_SETUP_TITLE); + int demoIndex = readmeContents.indexOf(ReadmeConstants.DEMO_PART); + int setupIndex = readmeContents.indexOf(ReadmeConstants.SETUP_PART); + String description = Strings.EMPTY; + String setup = Strings.EMPTY; + String demo = Strings.EMPTY; + + if (parts.length > 0) { + description = removeFirstLine(parts[0]); + } + + if (demoIndex != -1 && setupIndex != -1) { + if (demoIndex < setupIndex) { + demo = parts[1]; + setup = parts[2]; + } else { + setup = parts[1]; + demo = parts[2]; + } + } else if (demoIndex != -1) { + demo = parts[1]; + } else if (setupIndex != -1) { + setup = parts[1]; + } + locale = StringUtils.isEmpty(locale) ? Language.EN.getValue() : locale.toLowerCase(); + addLocaleContent(moduleContents, DESCRIPTION, description.trim(), locale); + addLocaleContent(moduleContents, DEMO, demo.trim(), locale); + addLocaleContent(moduleContents, SETUP, setup.trim(), locale); + } + + private static void addLocaleContent(Map> moduleContents, String type, String content, + String locale) { + moduleContents.computeIfAbsent(type, key -> new HashMap<>()).put(locale, content); + } + + public static boolean hasImageDirectives(String readmeContents) { + Pattern pattern = Pattern.compile(CommonConstants.IMAGE_EXTENSION); + Matcher matcher = pattern.matcher(readmeContents); + return matcher.find(); + } + + public static String removeFirstLine(String text) { + String result; + if (StringUtils.isBlank(text)) { + result = Strings.EMPTY; + } else if (text.startsWith(HASH)) { + int index = text.indexOf(StringUtils.LF); + result = index != StringUtils.INDEX_NOT_FOUND ? text.substring(index + 1).trim() : Strings.EMPTY; + } else { + result = text; + } + + return result; + } + + public static ProductModuleContent initProductModuleContent(Product product, String tag, Set mavenVersions) { + ProductModuleContent productModuleContent = new ProductModuleContent(); + productModuleContent.setProductId(product.getId()); + productModuleContent.setTag(tag); + productModuleContent.setMavenVersions(mavenVersions); + ProductFactory.mappingIdForProductModuleContent(productModuleContent); + return productModuleContent; + } + + public static void updateProductModule(ProductModuleContent productModuleContent, List artifacts) { + Artifact artifact = artifacts.stream().filter(Artifact::getIsDependency).findFirst().orElse(null); + if (Objects.nonNull(artifact)) { + productModuleContent.setIsDependency(Boolean.TRUE); + productModuleContent.setGroupId(artifact.getGroupId()); + productModuleContent.setArtifactId(artifact.getArtifactId()); + productModuleContent.setType(artifact.getType()); + productModuleContent.setName(artifact.getName()); + } + } + + public static void updateProductModuleTabContents(ProductModuleContent productModuleContent, + Map> moduleContents) { + productModuleContent.setDescription( + replaceEmptyContentsWithEnContent(moduleContents.get(DESCRIPTION))); + productModuleContent.setDemo(replaceEmptyContentsWithEnContent(moduleContents.get(DEMO))); + productModuleContent.setSetup(replaceEmptyContentsWithEnContent(moduleContents.get + (SETUP))); + } + + public static String replaceImageDirWithImageCustomId(Map imageUrls, String readmeContents) { + for (Map.Entry entry : imageUrls.entrySet()) { + String imagePattern = String.format(README_IMAGE_FORMAT, Pattern.quote(entry.getKey())); + readmeContents = readmeContents.replaceAll(imagePattern, + String.format(IMAGE_DOWNLOAD_URL_FORMAT, entry.getValue())); + } + return readmeContents; + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java index ea4bb7963..14672bbd9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java @@ -1,12 +1,15 @@ package com.axonivy.market.util; import com.axonivy.market.comparator.LatestVersionComparator; +import com.axonivy.market.comparator.MavenVersionComparator; import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.constants.MavenConstants; import com.axonivy.market.entity.Product; import com.axonivy.market.enums.NonStandardProduct; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.util.Strings; import org.kohsuke.github.GHTag; @@ -15,119 +18,158 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; +@Log4j2 public class VersionUtils { - public static final String NON_NUMERIC_CHAR = "[^0-9.]"; + public static final String NON_NUMERIC_CHAR = "[^0-9.]"; - private VersionUtils() { + private VersionUtils() { + } + + public static List getVersionsToDisplay(List versions, Boolean isShowDevVersion, + String designerVersion) { + Stream versionStream = versions.stream(); + if (StringUtils.isNotBlank(designerVersion)) { + return versionStream.filter(version -> isMatchWithDesignerVersion(version, designerVersion)).sorted( + new LatestVersionComparator()).toList(); } - public static List getVersionsToDisplay(List versions, Boolean isShowDevVersion, String designerVersion) { - Stream versionStream = versions.stream(); - if (StringUtils.isNotBlank(designerVersion)) { - return versionStream.filter(version -> isMatchWithDesignerVersion(version, designerVersion)).toList(); - } - if (BooleanUtils.isTrue(isShowDevVersion)) { - return versionStream.filter(version -> isOfficialVersionOrUnReleasedDevVersion(versions, version)) - .sorted(new LatestVersionComparator()).toList(); - } - return versions.stream().filter(VersionUtils::isReleasedVersion).sorted(new LatestVersionComparator()).toList(); + if (BooleanUtils.isTrue(isShowDevVersion)) { + return versionStream.filter(version -> isOfficialVersionOrUnReleasedDevVersion(versions, version)) + .sorted(new LatestVersionComparator()).toList(); } - - public static String getBestMatchVersion(List versions, String designerVersion) { - String bestMatchVersion = versions.stream().filter(version -> StringUtils.equals(version, designerVersion)).findAny().orElse(null); - if(StringUtils.isBlank(bestMatchVersion)){ - LatestVersionComparator comparator = new LatestVersionComparator(); - bestMatchVersion = versions.stream().filter(version -> comparator.compare(version, designerVersion) > 0 && isReleasedVersion(version)).findAny().orElse(null); - } - if (StringUtils.isBlank(bestMatchVersion)) { - bestMatchVersion = versions.stream().filter(VersionUtils::isReleasedVersion).findAny().orElse(CollectionUtils.firstElement(versions)); - } - return bestMatchVersion; + return versions.stream().filter(VersionUtils::isReleasedVersion).sorted(new LatestVersionComparator()).toList(); + } + + public static String getMavenVersionMatchWithTag(List releasedVersions, String mavenVersion) { + for (String version : releasedVersions) { + if (mavenVersion.equals(version)) { + return mavenVersion; + } } - - public static boolean isOfficialVersionOrUnReleasedDevVersion(List versions, String version) { - if (isReleasedVersion(version)) { - return true; - } - String bugfixVersion; - if (isSnapshotVersion(version)) { - bugfixVersion = getBugfixVersion(version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY)); - } else { - bugfixVersion = getBugfixVersion(version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]); - } - return versions.stream().noneMatch( - currentVersion -> !currentVersion.equals(version) && isReleasedVersion(currentVersion) && getBugfixVersion( - currentVersion).equals(bugfixVersion)); + return getAlternativeVersion(releasedVersions, mavenVersion); + } + + public static String getAlternativeVersion(List releaseVersions, String version) { + return Optional.ofNullable(releaseVersions).orElse(List.of()).stream().filter( + version::startsWith).sorted().findAny().orElse(null); + } + + public static String getBestMatchVersion(List versions, String designerVersion) { + String bestMatchVersion = versions.stream().filter( + version -> StringUtils.equals(version, designerVersion)).findAny().orElse(null); + if (StringUtils.isBlank(bestMatchVersion)) { + bestMatchVersion = versions.stream().filter( + version -> MavenVersionComparator.compare(version, designerVersion) < 0 && isReleasedVersion( + version)).findAny().orElse(null); } - - public static boolean isSnapshotVersion(String version) { - return version.endsWith(MavenConstants.SNAPSHOT_RELEASE_POSTFIX); + if (StringUtils.isBlank(bestMatchVersion)) { + bestMatchVersion = versions.stream().filter(VersionUtils::isReleasedVersion).findAny().orElse( + CollectionUtils.firstElement(versions)); } + return bestMatchVersion; + } - public static boolean isSprintVersion(String version) { - return version.contains(MavenConstants.SPRINT_RELEASE_POSTFIX); + public static boolean isOfficialVersionOrUnReleasedDevVersion(List versions, String version) { + if (isReleasedVersion(version)) { + return true; } - - public static boolean isReleasedVersion(String version) { - return !(isSprintVersion(version) || isSnapshotVersion(version)); + String bugfixVersion; + if (!isValidFormatReleasedVersion(version)) { + return false; + } else if (isSnapshotVersion(version)) { + bugfixVersion = getBugfixVersion(version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY)); + } else { + bugfixVersion = getBugfixVersion(version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]); } + return versions.stream().noneMatch( + currentVersion -> !currentVersion.equals(version) && isReleasedVersion(currentVersion) && getBugfixVersion( + currentVersion).equals(bugfixVersion)); + } - public static boolean isMatchWithDesignerVersion(String version, String designerVersion) { - return isReleasedVersion(version) && version.startsWith(designerVersion); - } + public static boolean isSnapshotVersion(String version) { + return version.endsWith(MavenConstants.SNAPSHOT_RELEASE_POSTFIX); + } - public static String getBugfixVersion(String version) { - - if (isSnapshotVersion(version)) { - version = version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY); - } else if (isSprintVersion(version)) { - version = version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]; - } - String[] segments = version.split("\\."); - if (segments.length >= 3) { - segments[2] = segments[2].split(CommonConstants.DASH_SEPARATOR)[0]; - return segments[0] + CommonConstants.DOT_SEPARATOR + segments[1] + CommonConstants.DOT_SEPARATOR + segments[2]; - } - return version; - } + public static boolean isSprintVersion(String version) { + return version.contains(MavenConstants.SPRINT_RELEASE_POSTFIX); + } + + public static boolean isValidFormatReleasedVersion(String version) { + return StringUtils.isNumeric(version.split(MavenConstants.MAIN_VERSION_REGEX)[0]); + } - public static String convertTagToVersion (String tag){ - if(StringUtils.isBlank(tag) || !StringUtils.startsWith(tag, GitHubConstants.STANDARD_TAG_PREFIX)){ - return tag; - } - return tag.substring(1); + public static boolean isReleasedVersion(String version) { + return !(isSprintVersion(version) || isSnapshotVersion(version)) && isValidFormatReleasedVersion(version); + } + + public static boolean isMatchWithDesignerVersion(String version, String designerVersion) { + return isReleasedVersion(version) && version.startsWith(designerVersion); + } + + public static String getBugfixVersion(String version) { + + if (isSnapshotVersion(version)) { + version = version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY); + } else if (isSprintVersion(version)) { + version = version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]; + } + String[] segments = version.split("\\."); + if (segments.length >= 3) { + segments[2] = segments[2].split(CommonConstants.DASH_SEPARATOR)[0]; + return segments[0] + CommonConstants.DOT_SEPARATOR + segments[1] + CommonConstants.DOT_SEPARATOR + segments[2]; } + return version; + } - public static List convertTagsToVersions (List tags){ - Objects.requireNonNull(tags); - return tags.stream().map(VersionUtils::convertTagToVersion).toList(); + public static String convertTagToVersion(String tag) { + if (StringUtils.isBlank(tag) || !StringUtils.startsWith(tag, GitHubConstants.STANDARD_TAG_PREFIX)) { + return tag; } + return tag.substring(1); + } - public static String convertVersionToTag(String productId, String version) { - if (StringUtils.isBlank(version)) { - return version; - } - NonStandardProduct product = NonStandardProduct.findById(productId); - if (product.isVersionTagNumberOnly()) { - return version; - } - return GitHubConstants.STANDARD_TAG_PREFIX.concat(version); + public static List convertTagsToVersions(List tags) { + Objects.requireNonNull(tags); + return tags.stream().map(VersionUtils::convertTagToVersion).toList(); + } + + public static String convertVersionToTag(String productId, String version) { + if (StringUtils.isBlank(version)) { + return version; + } + NonStandardProduct product = NonStandardProduct.findById(productId); + if (product.isVersionTagNumberOnly()) { + return version; + } + return GitHubConstants.STANDARD_TAG_PREFIX.concat(version); + } + + public static String getOldestVersion(List tags) { + String result = StringUtils.EMPTY; + if (!CollectionUtils.isEmpty(tags)) { + List releasedTags = tags.stream().map(tag -> tag.getName().replaceAll(NON_NUMERIC_CHAR, Strings.EMPTY)) + .distinct().sorted(new LatestVersionComparator()).toList(); + return CollectionUtils.lastElement(releasedTags); } + return result; + } - public static String getOldestVersion(List tags) { - String result = StringUtils.EMPTY; - if (!CollectionUtils.isEmpty(tags)) { - List releasedTags = tags.stream().map(tag -> tag.getName().replaceAll(NON_NUMERIC_CHAR, Strings.EMPTY)) - .distinct().sorted(new LatestVersionComparator()).toList(); - return CollectionUtils.lastElement(releasedTags); - } - return result; + public static List getReleaseTagsFromProduct(Product product) { + if (Objects.isNull(product) || CollectionUtils.isEmpty(product.getReleasedVersions())) { + return new ArrayList<>(); } - public static List getReleaseTagsFromProduct(Product product) { - if (Objects.isNull(product) || CollectionUtils.isEmpty(product.getReleasedVersions())) { - return new ArrayList<>(); - } - return product.getReleasedVersions().stream().map(version -> convertVersionToTag(product.getId(), version)).toList(); + return product.getReleasedVersions().stream().map( + version -> convertVersionToTag(product.getId(), version)).toList(); + } + + public static List removeSyncedVersionsFromReleasedVersions(List releasedVersion, + Set syncTags) { + if (ObjectUtils.isNotEmpty(syncTags)) { + releasedVersion.removeAll(syncTags); } + return releasedVersion; + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java deleted file mode 100644 index 33fe8e20e..000000000 --- a/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.axonivy.market.util; - -import com.axonivy.market.constants.MavenConstants; -import lombok.extern.log4j.Log4j2; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpression; -import javax.xml.xpath.XPathFactory; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -@Log4j2 -public class XmlReaderUtils { - private static final RestTemplate restTemplate = new RestTemplate(); - - private XmlReaderUtils() { - } - - public static List readXMLFromUrl(String url) { - List versions = new ArrayList<>(); - try { - String xmlData = restTemplate.getForObject(url, String.class); - extractVersions(xmlData, versions); - } catch (HttpClientErrorException e) { - log.error(e.getMessage()); - } - return versions; - } - - public static void extractVersions(String xmlData, List versions) { - try { - DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document document = builder.parse(new InputSource(new StringReader(xmlData))); - - XPath xpath = XPathFactory.newInstance().newXPath(); - XPathExpression expr = xpath.compile(MavenConstants.VERSION_EXTRACT_FORMAT_FROM_METADATA_FILE); - - Object result = expr.evaluate(document, XPathConstants.NODESET); - NodeList versionNodes = (NodeList) result; - - for (int i = 0; i < versionNodes.getLength(); i++) { - versions.add(Optional.ofNullable(versionNodes.item(i)).map(Node::getTextContent).orElse(null)); - } - } catch (Exception e) { - log.error(e.getMessage()); - } - } -} diff --git a/marketplace-service/src/test/java/com/axonivy/market/BaseSetup.java b/marketplace-service/src/test/java/com/axonivy/market/BaseSetup.java index 35ca031e0..4413a0a02 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/BaseSetup.java +++ b/marketplace-service/src/test/java/com/axonivy/market/BaseSetup.java @@ -1,20 +1,19 @@ package com.axonivy.market; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductDesignerInstallation; +import com.axonivy.market.enums.Language; +import com.axonivy.market.enums.SortOption; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.Language; -import com.axonivy.market.enums.SortOption; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class BaseSetup { protected static final String SAMPLE_PRODUCT_ID = "amazon-comprehend"; diff --git a/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java b/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailMavenArtifactModelAssemblerTest.java similarity index 84% rename from marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java rename to marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailMavenArtifactModelAssemblerTest.java index 75c2566b5..41418b7ed 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailMavenArtifactModelAssemblerTest.java @@ -46,14 +46,18 @@ void testToModelWithRequestPath() { @Test void testToModelWithRequestPathAndVersion() { - ProductDetailModel model = productDetailModelAssembler.toModel(mockProduct, VERSION, RequestMappingConstants.BY_ID_AND_VERSION); + ProductDetailModel model = productDetailModelAssembler.toModel(mockProduct, VERSION, + RequestMappingConstants.BY_ID_AND_VERSION); Assertions.assertTrue(model.getLink(SELF_RELATION).get().getHref().endsWith("/api/product-details/portal/10.0.19")); } @Test void testToModelWithRequestPathAndBestMatchVersion() { - ProductDetailModel model = productDetailModelAssembler.toModel(mockProduct, VERSION, RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION); - Assertions.assertTrue(model.getLink(SELF_RELATION).get().getHref().endsWith("/api/product-details/portal/10.0.19/bestmatch")); - Assertions.assertTrue(model.getMetaProductJsonUrl().endsWith("/api/product-details/portal/10.0.8/json")); + mockProduct.setBestMatchVersion("10.0.19"); + ProductDetailModel model = productDetailModelAssembler.toModel(mockProduct, VERSION, + RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION); + Assertions.assertTrue( + model.getLink(SELF_RELATION).get().getHref().endsWith("/api/product-details/portal/10.0.19/bestmatch")); + Assertions.assertTrue(model.getMetaProductJsonUrl().endsWith("/api/product-details/portal/10.0.19/json")); } } \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/bo/ArtifactTest.java b/marketplace-service/src/test/java/com/axonivy/market/bo/ArtifactTest.java new file mode 100644 index 000000000..9bab449c2 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/bo/ArtifactTest.java @@ -0,0 +1,23 @@ +package com.axonivy.market.bo; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ArtifactTest { + @Test + void testEqual() { + Artifact artifact = new Artifact(); + artifact.setGroupId("com.axonivy.com"); + artifact.setArtifactId("octopus-demo"); + Assertions.assertNotEquals(null, artifact); + Assertions.assertNotEquals(new Object(), artifact); + Assertions.assertEquals(artifact, artifact); + + Assertions.assertNotEquals(artifact, new Object()); + + Artifact sameArtifact = new Artifact(); + sameArtifact.setGroupId("com.axonivy.com"); + sameArtifact.setArtifactId("octopus-demo"); + Assertions.assertEquals(sameArtifact, artifact); + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/bo/MetadataTest.java b/marketplace-service/src/test/java/com/axonivy/market/bo/MetadataTest.java new file mode 100644 index 000000000..01044106d --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/bo/MetadataTest.java @@ -0,0 +1,21 @@ +package com.axonivy.market.bo; + +import com.axonivy.market.entity.Metadata; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class MetadataTest { + @Test + void testEqual() { + Metadata meta = + Metadata.builder().url("https://maven.axonivy.com/com/axonivy/utils/octopus/maven-metadata.xml").build(); + + Assertions.assertNotEquals(null, meta); + Assertions.assertNotEquals(new Object(), meta); + Assertions.assertEquals(meta, meta); + + Metadata sameMeta = + Metadata.builder().url("https://maven.axonivy.com/com/axonivy/utils/octopus/maven-metadata.xml").build(); + Assertions.assertEquals(sameMeta, meta); + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java index 0c3e8ce39..bfdc8ddec 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java @@ -9,6 +9,7 @@ import com.axonivy.market.exceptions.model.UnauthorizedException; import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.model.ProductCustomSortRequest; +import com.axonivy.market.service.MetadataService; import com.axonivy.market.service.ProductService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,10 +33,7 @@ import java.util.Map; import java.util.Objects; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @@ -44,8 +42,10 @@ class ProductControllerTest { private static final String PRODUCT_NAME_SAMPLE = "Amazon Comprehend"; private static final String PRODUCT_NAME_DE_SAMPLE = "Amazon Comprehend DE"; - private static final String PRODUCT_DESC_SAMPLE = "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data."; - private static final String PRODUCT_DESC_DE_SAMPLE = "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data. DE"; + private static final String PRODUCT_DESC_SAMPLE = "Amazon Comprehend is a AI service that uses machine learning to " + + "uncover information in unstructured data."; + private static final String PRODUCT_DESC_DE_SAMPLE = "Amazon Comprehend is a AI service that uses machine learning " + + "to uncover information in unstructured data. DE"; private static final String AUTHORIZATION_HEADER = "Bearer valid_token"; private static final String INVALID_AUTHORIZATION_HEADER = "Bearer invalid_token"; @@ -64,6 +64,9 @@ class ProductControllerTest { @InjectMocks private ProductController productController; + @Mock + private MetadataService metadataService; + @BeforeEach void setup() { assembler = new ProductModelAssembler(); @@ -73,7 +76,7 @@ void setup() { void testFindProductsAsEmpty() { PageRequest pageable = PageRequest.of(0, 20); Page mockProducts = new PageImpl<>(List.of(), pageable, 0); - when(service.findProducts(any(), any(), any(), any() , any())).thenReturn(mockProducts); + when(service.findProducts(any(), any(), any(), any(), any())).thenReturn(mockProducts); when(pagedResourcesAssembler.toEmptyModel(any(), any())).thenReturn(PagedModel.empty()); var result = productController.findProducts(TypeOption.ALL.getOption(), null, "en", false, pageable); assertEquals(HttpStatus.OK, result.getStatusCode()); @@ -138,6 +141,33 @@ void testSyncProductsInvalidToken() { assertEquals(ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText(), exception.getMessage()); } + @Test + void testSyncMavenVersionSuccess() { + var response = productController.syncProductVersions(AUTHORIZATION_HEADER); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertTrue(response.hasBody()); + assertEquals(ErrorCode.MAVEN_VERSION_SYNC_FAILED.getCode(), Objects.requireNonNull(response.getBody()).getHelpCode()); + when(metadataService.syncAllProductsMetadata()).thenReturn(1); + response = productController.syncProductVersions(AUTHORIZATION_HEADER); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + assertEquals(ErrorCode.SUCCESSFUL.getCode(), Objects.requireNonNull(response.getBody()).getHelpCode()); + } + + + @Test + void testSyncMavenVersionWithInvalidToken() { + doThrow(new UnauthorizedException(ErrorCode.GITHUB_USER_UNAUTHORIZED.getCode(), + ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText())).when(gitHubService) + .validateUserOrganization(any(String.class), any(String.class)); + + UnauthorizedException exception = assertThrows(UnauthorizedException.class, + () -> productController.syncProductVersions(INVALID_AUTHORIZATION_HEADER)); + + assertEquals(ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText(), exception.getMessage()); + } + + @Test void testCreateCustomSortProductsSuccess() { ProductCustomSortRequest mockProductCustomSortRequest = createProductCustomSortRequestMock(); diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDesignerInstallationControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDesignerInstallationControllerTest.java index b8fd3f553..5b85137d7 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDesignerInstallationControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDesignerInstallationControllerTest.java @@ -17,23 +17,25 @@ @ExtendWith(MockitoExtension.class) class ProductDesignerInstallationControllerTest { - public static final String DESIGNER_VERSION = "11.4.0"; + public static final String DESIGNER_VERSION = "11.4.0"; - @Mock - ProductDesignerInstallationService productDesignerInstallationService; + @Mock + ProductDesignerInstallationService productDesignerInstallationService; - @InjectMocks - private ProductDesignerInstallationController productDesignerInstallationController; + @InjectMocks + private ProductDesignerInstallationController productDesignerInstallationController; - @Test - void testGetProductDesignerInstallationByProductId() { - List models = List.of(new DesignerInstallation(DESIGNER_VERSION, 5)); - Mockito.when(productDesignerInstallationService.findByProductId(Mockito.anyString())).thenReturn(models); - ResponseEntity> result = productDesignerInstallationController.getProductDesignerInstallationByProductId("portal"); - Assertions.assertEquals(HttpStatus.OK, result.getStatusCode()); - Assertions.assertEquals(1, Objects.requireNonNull(result.getBody()).size()); - Assertions.assertEquals(DESIGNER_VERSION, result.getBody().get(0).getDesignerVersion()); - Assertions.assertEquals(5, result.getBody().get(0).getNumberOfDownloads()); - Assertions.assertEquals(models, result.getBody()); - } + @Test + void testGetProductDesignerInstallationByProductId() { + List models = List.of(new DesignerInstallation(DESIGNER_VERSION, 5)); + Mockito.when(productDesignerInstallationService.findByProductId(Mockito.anyString())).thenReturn(models); + ResponseEntity> result = + productDesignerInstallationController.getProductDesignerInstallationByProductId( + "portal"); + Assertions.assertEquals(HttpStatus.OK, result.getStatusCode()); + Assertions.assertEquals(1, Objects.requireNonNull(result.getBody()).size()); + Assertions.assertEquals(DESIGNER_VERSION, result.getBody().get(0).getDesignerVersion()); + Assertions.assertEquals(5, result.getBody().get(0).getNumberOfDownloads()); + Assertions.assertEquals(models, result.getBody()); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java index 42ae3db76..db0633538 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java @@ -1,21 +1,17 @@ package com.axonivy.market.controller; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.util.List; -import java.util.Objects; -import java.util.HashMap; -import java.util.Map; +import com.axonivy.market.assembler.ProductDetailModelAssembler; import com.axonivy.market.constants.RequestMappingConstants; +import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductJsonContent; +import com.axonivy.market.enums.Language; +import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.model.ProductDetailModel; import com.axonivy.market.model.VersionAndUrlModel; import com.axonivy.market.service.ImageService; import com.axonivy.market.service.ProductDesignerInstallationService; +import com.axonivy.market.service.ProductService; +import com.axonivy.market.service.VersionService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -26,13 +22,16 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import com.axonivy.market.assembler.ProductDetailModelAssembler; -import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.Language; -import com.axonivy.market.model.MavenArtifactVersionModel; -import com.axonivy.market.model.ProductDetailModel; -import com.axonivy.market.service.ProductService; -import com.axonivy.market.service.VersionService; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ProductDetailsControllerTest { @@ -57,11 +56,13 @@ class ProductDetailsControllerTest { private static final String PRODUCT_NAME_SAMPLE = "Docker"; private static final String PRODUCT_NAME_DE_SAMPLE = "Docker DE"; public static final String DOCKER_CONNECTOR_ID = "docker-connector"; + public static final String WRONG_PRODUCT_ID = "wrong-product-id"; @Test void testProductDetails() { Mockito.when(productService.fetchProductDetail(Mockito.anyString())).thenReturn(mockProduct()); - Mockito.when(detailModelAssembler.toModel(mockProduct(), RequestMappingConstants.BY_ID)).thenReturn(createProductMockWithDetails()); + Mockito.when(detailModelAssembler.toModel(mockProduct(), RequestMappingConstants.BY_ID)).thenReturn( + createProductMockWithDetails()); ResponseEntity mockExpectedResult = new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); @@ -79,24 +80,31 @@ void testProductDetails() { @Test void testFindBestMatchProductDetailsByVersion() { - Mockito.when(productService.fetchBestMatchProductDetail(Mockito.anyString(), Mockito.anyString())).thenReturn(mockProduct()); - Mockito.when(detailModelAssembler.toModel(mockProduct(), TAG, RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION)).thenReturn(createProductMockWithDetails()); + Mockito.when(productService.fetchBestMatchProductDetail(Mockito.anyString(), Mockito.anyString())).thenReturn( + mockProduct()); + Mockito.when(detailModelAssembler.toModel(mockProduct(), TAG, + RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION)).thenReturn(createProductMockWithDetails()); ResponseEntity mockExpectedResult = new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); - ResponseEntity result = productDetailsController.findBestMatchProductDetailsByVersion(DOCKER_CONNECTOR_ID, TAG); + ResponseEntity result = productDetailsController.findBestMatchProductDetailsByVersion( + DOCKER_CONNECTOR_ID, TAG); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(result, mockExpectedResult); verify(productService, times(1)).fetchBestMatchProductDetail(DOCKER_CONNECTOR_ID, TAG); - verify(detailModelAssembler, times(1)).toModel(mockProduct(), TAG, RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION); + verify(detailModelAssembler, times(1)).toModel(mockProduct(), TAG, + RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION); } @Test void testProductDetailsWithVersion() { - Mockito.when(productService.fetchProductDetailByIdAndVersion(Mockito.anyString(), Mockito.anyString())).thenReturn(mockProduct()); - Mockito.when(detailModelAssembler.toModel(mockProduct(), TAG, RequestMappingConstants.BY_ID_AND_VERSION)).thenReturn(createProductMockWithDetails()); + Mockito.when(productService.fetchProductDetailByIdAndVersion(Mockito.anyString(), Mockito.anyString())).thenReturn( + mockProduct()); + Mockito.when( + detailModelAssembler.toModel(mockProduct(), TAG, RequestMappingConstants.BY_ID_AND_VERSION)).thenReturn( + createProductMockWithDetails()); ResponseEntity mockExpectedResult = new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); @@ -109,11 +117,51 @@ void testProductDetailsWithVersion() { verify(productService, times(1)).fetchProductDetailByIdAndVersion(DOCKER_CONNECTOR_ID, TAG); } + @Test + void testProductDetailsWithVersionWithWrongProductId() { + Mockito.when(productService.fetchProductDetailByIdAndVersion(Mockito.anyString(), Mockito.anyString())).thenReturn( + null); + + ResponseEntity result = productDetailsController.findProductDetailsByVersion( + WRONG_PRODUCT_ID, TAG); + + assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode()); + + verify(productService, times(1)).fetchProductDetailByIdAndVersion(WRONG_PRODUCT_ID, TAG); + } + + @Test + void testBestMatchProductDetailsWithVersionWithWrongProductId() { + Mockito.when(productService.fetchBestMatchProductDetail(Mockito.anyString(), Mockito.anyString())).thenReturn( + null); + + ResponseEntity result = productDetailsController.findBestMatchProductDetailsByVersion( + WRONG_PRODUCT_ID, TAG); + + assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode()); + + verify(productService, times(1)).fetchBestMatchProductDetail(WRONG_PRODUCT_ID, TAG); + } + + @Test + void testProductDetailsWithWrongProductId() { + Mockito.when(productService.fetchProductDetail(Mockito.anyString())).thenReturn( + null); + + ResponseEntity result = productDetailsController.findProductDetails( + WRONG_PRODUCT_ID); + + assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode()); + + verify(productService, times(1)).fetchProductDetail(WRONG_PRODUCT_ID); + } + @Test void testFindProductVersionsById() { List models = List.of(new MavenArtifactVersionModel()); Mockito.when( - versionService.getArtifactsAndVersionToDisplay(Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyString())) + versionService.getArtifactsAndVersionToDisplay(Mockito.anyString(), Mockito.anyBoolean(), + Mockito.anyString())) .thenReturn(models); ResponseEntity> result = productDetailsController.findProductVersionsById("portal", true, "10.0.1"); @@ -146,7 +194,7 @@ void findProductVersionsById() { Objects.requireNonNull(result.getBody()).get(1).getUrl()); } - private List mockVersionAndUrlModels(){ + private List mockVersionAndUrlModels() { VersionAndUrlModel versionAndUrlModel = VersionAndUrlModel.builder() .version("10.0.21") .url("/api/product-details/productjsoncontent/portal/10.0.21") @@ -157,14 +205,14 @@ private List mockVersionAndUrlModels(){ .url("/api/product-details/productjsoncontent/portal/10.0.22") .build(); - return List.of(versionAndUrlModel,versionAndUrlModel2); + return List.of(versionAndUrlModel, versionAndUrlModel2); } @Test void findProductJsonContentByIdAndTag() throws IOException { ProductJsonContent productJsonContent = mockProductJsonContent(); - Map map = new ObjectMapper().readValue(productJsonContent.getContent(), Map.class); - when(versionService.getProductJsonContentByIdAndVersion("bpmnstatistic", "10.0.21")).thenReturn( + Map map = new ObjectMapper().readValue(productJsonContent.getContent(), Map.class); + when(versionService.getProductJsonContentByIdAndTag("bpmnstatistic", "10.0.21")).thenReturn( map); var result = productDetailsController.findProductJsonContent("bpmnstatistic", "10.0.21"); diff --git a/marketplace-service/src/test/java/com/axonivy/market/entity/MavenArtifactVersionTest.java b/marketplace-service/src/test/java/com/axonivy/market/entity/MavenArtifactVersionTest.java new file mode 100644 index 000000000..6d0c62bcb --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/entity/MavenArtifactVersionTest.java @@ -0,0 +1,16 @@ +package com.axonivy.market.entity; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MavenArtifactVersionTest { + @Test + void testSetterShouldInitNewMapIfNull() { + MavenArtifactVersion mockMavenArtifactVersion = new MavenArtifactVersion(); + Assertions.assertNotNull(mockMavenArtifactVersion.getAdditionalArtifactsByVersion()); + Assertions.assertNotNull(mockMavenArtifactVersion.getProductArtifactsByVersion()); + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java index 24aa148c7..a85e1e6f5 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java @@ -2,12 +2,14 @@ import com.axonivy.market.constants.ProductJsonConstants; import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.Language; import com.axonivy.market.github.model.Meta; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHContent; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.util.CollectionUtils; import java.io.IOException; import java.io.InputStream; @@ -20,11 +22,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.axonivy.market.enums.Language; - @ExtendWith(MockitoExtension.class) class ProductFactoryTest { - private static final String DUMMY_LOGO_URL = "https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/amazon-comprehend-connector/logo.png"; + private static final String DUMMY_LOGO_URL = "https://raw.githubusercontent" + + ".com/axonivy-market/market/master/market/connector/amazon-comprehend-connector/logo.png"; @Test void testMappingByGHContent() throws IOException { @@ -39,6 +40,8 @@ void testMappingByGHContent() throws IOException { assertNotEquals(null, result); assertEquals("Amazon Comprehend", result.getNames().get(Language.EN.getValue())); assertEquals("Amazon Comprehend DE", result.getNames().get(Language.DE.getValue())); + Assertions.assertFalse(CollectionUtils.isEmpty(result.getArtifacts())); + Assertions.assertFalse(result.getArtifacts().get(0).isInvalidArtifact()); } @Test @@ -77,15 +80,15 @@ void testExtractSourceUrl() { @Test void testTransferComputedData() { - String initialVersion ="10.0.2"; + String initialVersion = "10.0.2"; Product product = new Product(); Product persistedData = new Product(); persistedData.setCustomOrder(1); persistedData.setReleasedVersions(List.of(initialVersion)); persistedData.setNewestReleaseVersion(initialVersion); - ProductFactory.transferComputedPersistedDataToProduct(persistedData,product); - assertEquals(1,product.getCustomOrder()); + ProductFactory.transferComputedPersistedDataToProduct(persistedData, product); + assertEquals(1, product.getCustomOrder()); assertEquals(initialVersion, product.getNewestReleaseVersion()); assertEquals(1, product.getReleasedVersions().size()); assertEquals(initialVersion, product.getReleasedVersions().get(0)); diff --git a/marketplace-service/src/test/java/com/axonivy/market/repository/impl/CustomProductRepositoryImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/repository/impl/CustomProductRepositoryImplTest.java index 3938e5d54..39888746b 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/repository/impl/CustomProductRepositoryImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/repository/impl/CustomProductRepositoryImplTest.java @@ -19,25 +19,18 @@ import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class CustomProductRepositoryImplTest extends BaseSetup { private static final String ID = "bmpn-statistic"; private static final String TAG = "v10.0.21"; - private Product mockProduct; - private Aggregation mockAggregation; - @Mock ProductModuleContentRepository contentRepo; - + private Product mockProduct; + private Aggregation mockAggregation; @Mock private MongoTemplate mongoTemplate; @@ -56,7 +49,8 @@ private void setUpMockAggregateResult() { mockAggregation = mock(Aggregation.class); AggregationResults aggregationResults = mock(AggregationResults.class); - when(mongoTemplate.aggregate(any(Aggregation.class), eq(MongoDBConstants.PRODUCT_COLLECTION), eq(Product.class))).thenReturn(aggregationResults); + when(mongoTemplate.aggregate(any(Aggregation.class), eq(MongoDBConstants.PRODUCT_COLLECTION), + eq(Product.class))).thenReturn(aggregationResults); mockProduct = new Product(); mockProduct.setId(ID); when(aggregationResults.getUniqueMappedResult()).thenReturn(mockProduct); @@ -66,7 +60,8 @@ private void setUpMockAggregateResult() { void testQueryProductByAggregation_WhenResultIsNull() { Aggregation aggregation = mock(Aggregation.class); AggregationResults aggregationResults = mock(AggregationResults.class); - when(mongoTemplate.aggregate(any(Aggregation.class), eq(MongoDBConstants.PRODUCT_COLLECTION), eq(Product.class))).thenReturn(aggregationResults); + when(mongoTemplate.aggregate(any(Aggregation.class), eq(MongoDBConstants.PRODUCT_COLLECTION), + eq(Product.class))).thenReturn(aggregationResults); when(aggregationResults.getUniqueMappedResult()).thenReturn(null); Product actualProduct = repo.queryProductByAggregation(aggregation); @@ -78,7 +73,8 @@ void testQueryProductByAggregation_WhenResultIsNull() { void testReleasedVersionsById_WhenResultIsNull() { AggregationResults aggregationResults = mock(AggregationResults.class); - when(mongoTemplate.aggregate(any(Aggregation.class), eq(MongoDBConstants.PRODUCT_COLLECTION), eq(Product.class))).thenReturn(aggregationResults); + when(mongoTemplate.aggregate(any(Aggregation.class), eq(MongoDBConstants.PRODUCT_COLLECTION), + eq(Product.class))).thenReturn(aggregationResults); when(aggregationResults.getUniqueMappedResult()).thenReturn(null); List results = repo.getReleasedVersionsById(ID); @@ -95,8 +91,7 @@ void testGetProductById() { @Test void testGetProductByIdAndTag() { setUpMockAggregateResult(); - when(contentRepo.findByTagAndProductId(TAG, ID)).thenReturn(null); - Product actualProduct = repo.getProductByIdAndTag(ID, TAG); + Product actualProduct = repo.getProductByIdWithTagOrVersion(ID, TAG); assertEquals(mockProduct, actualProduct); } @@ -113,15 +108,18 @@ void testIncreaseInstallationCount() { Product product = new Product(); product.setId(productId); product.setInstallationCount(5); - when(mongoTemplate.findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), eq(Product.class))).thenReturn(product); + when(mongoTemplate.findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), + eq(Product.class))).thenReturn(product); int updatedCount = repo.increaseInstallationCount(productId); assertEquals(5, updatedCount); - verify(mongoTemplate).findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), eq(Product.class)); + verify(mongoTemplate).findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), + eq(Product.class)); } @Test void testIncreaseInstallationCount_NullProduct() { - when(mongoTemplate.findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), eq(Product.class))).thenReturn(null); + when(mongoTemplate.findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), + eq(Product.class))).thenReturn(null); int updatedCount = repo.increaseInstallationCount(ID); assertEquals(0, updatedCount); } @@ -131,7 +129,9 @@ void testUpdateInitialCount() { setUpMockAggregateResult(); int initialCount = 10; repo.updateInitialCount(ID, initialCount); - verify(mongoTemplate).updateFirst(any(Query.class), eq(new Update().inc("InstallationCount", initialCount).set("SynchronizedInstallationCount", true)), eq(Product.class)); + verify(mongoTemplate).updateFirst(any(Query.class), + eq(new Update().inc("InstallationCount", initialCount).set("SynchronizedInstallationCount", true)), + eq(Product.class)); } @Test diff --git a/marketplace-service/src/test/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImplTest.java index bdcfeb12e..f6adc1434 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImplTest.java @@ -1,15 +1,10 @@ package com.axonivy.market.repository.impl; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -import java.util.List; - +import com.axonivy.market.BaseSetup; +import com.axonivy.market.criteria.ProductSearchCriteria; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.DocumentField; +import com.axonivy.market.enums.Language; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -19,11 +14,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.mongodb.core.MongoTemplate; -import com.axonivy.market.BaseSetup; -import com.axonivy.market.criteria.ProductSearchCriteria; -import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.DocumentField; -import com.axonivy.market.enums.Language; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ProductSearchRepositoryImplTest extends BaseSetup { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/FeedbackServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/FeedbackServiceImplTest.java index 45be07c10..d8af7a6c6 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/FeedbackServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/FeedbackServiceImplTest.java @@ -27,14 +27,8 @@ import java.util.List; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class FeedbackServiceImplTest { @@ -170,7 +164,7 @@ void testFindFeedbackByUserIdAndProductId_NotFound() { when(userRepository.findById(userId)).thenReturn(Optional.empty()); NotFoundException exception = assertThrows(NotFoundException.class, - () -> feedbackService.findFeedbackByUserIdAndProductId(userId, "product")); + () -> feedbackService.findFeedbackByUserIdAndProductId(userId, "product")); assertEquals(ErrorCode.USER_NOT_FOUND.getCode(), exception.getCode()); verify(userRepository, times(1)).findById(userId); } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/FileDownloadServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/FileDownloadServiceImplTest.java new file mode 100644 index 000000000..93f2bdb3c --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/FileDownloadServiceImplTest.java @@ -0,0 +1,90 @@ +package com.axonivy.market.service.impl; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; + +class FileDownloadServiceImplTest { + + @InjectMocks + private FileDownloadServiceImpl fileDownloadService; + + @Mock + private RestTemplate restTemplate; + + private static final String ZIP_FILE_PATH = "src/test/resources/zip/text.zip"; + private static final String EXTRACT_LOCATION = "src/test/resources/zip/data"; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void createFolder() { + String location = "testFolder"; + Path mockPath = Paths.get(location); + + try (MockedStatic mockedFiles = Mockito.mockStatic(Files.class)) { + mockedFiles.when(() -> Files.createDirectories(mockPath)).thenReturn(mockPath); + + Path resultPath = fileDownloadService.createFolder(location); + + mockedFiles.verify(() -> Files.createDirectories(mockPath), Mockito.times(1)); + Assertions.assertEquals(mockPath, resultPath); + } + } + + @Test + void testCreateFolderShouldNotThrowExceptionWhenIOExceptionOccurs() { + String location = "testFolder"; + Path mockPath = Paths.get(location); + + try (MockedStatic mockedFiles = Mockito.mockStatic(Files.class)) { + mockedFiles.when(() -> Files.createDirectories(mockPath)).thenThrow( + new IOException("Failed to create directory")); + + Path resultPath = fileDownloadService.createFolder(location); + + mockedFiles.verify(() -> Files.createDirectories(mockPath), Mockito.times(1)); + Assertions.assertEquals(mockPath, resultPath); + } + } + + @Test + void deleteDirectory_shouldDeleteAllFilesAndDirectories() { + // Arrange + Path mockPath = Mockito.mock(Path.class); + Path file1 = Mockito.mock(Path.class); + Path file2 = Mockito.mock(Path.class); + Stream mockStream = Stream.of(file1, file2); + + try (MockedStatic mockedFiles = Mockito.mockStatic(Files.class)) { + mockedFiles.when(() -> Files.walk(mockPath)).thenReturn(mockStream); + + fileDownloadService.deleteDirectory(mockPath); + + mockedFiles.verify(() -> Files.delete(file1), Mockito.times(1)); + mockedFiles.verify(() -> Files.delete(file2), Mockito.times(1)); + } + } + + @Test + void testUnzipFile() throws IOException { + int result = fileDownloadService.unzipFile(ZIP_FILE_PATH, EXTRACT_LOCATION); + Assertions.assertEquals(7, result); + fileDownloadService.deleteDirectory(Paths.get("src/test/resources/zip/data/text")); + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java index b4ebfce5e..ead00d738 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java @@ -1,26 +1,22 @@ package com.axonivy.market.service.impl; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - +import com.axonivy.market.bo.Artifact; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.MavenConstants; +import com.axonivy.market.constants.ProductJsonConstants; +import com.axonivy.market.constants.ReadmeConstants; +import com.axonivy.market.entity.Image; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.Language; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; +import com.axonivy.market.github.util.GitHubUtils; +import com.axonivy.market.repository.ProductJsonContentRepository; +import com.axonivy.market.service.ImageService; +import com.axonivy.market.util.MavenUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,26 +25,24 @@ import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHTag; import org.kohsuke.github.PagedIterable; -import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import com.axonivy.market.constants.CommonConstants; -import com.axonivy.market.constants.ProductJsonConstants; -import com.axonivy.market.constants.ReadmeConstants; -import com.axonivy.market.entity.Image; -import com.axonivy.market.entity.Product; -import com.axonivy.market.entity.ProductJsonContent; -import com.axonivy.market.enums.Language; -import com.axonivy.market.github.model.MavenArtifact; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; -import com.axonivy.market.repository.ProductJsonContentRepository; -import com.axonivy.market.service.ImageService; -import com.fasterxml.jackson.databind.JsonNode; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class GHAxonIvyProductRepoServiceImplTest { @@ -57,7 +51,7 @@ class GHAxonIvyProductRepoServiceImplTest { public static final String RELEASE_TAG = "v10.0.0"; public static final String IMAGE_NAME = "image.png"; public static final String DOCUWARE_CONNECTOR_PRODUCT = "docuware-connector-product"; - public static final String IMAGE_DOWNLOAD_URL = "https://raw.githubusercontent.com/image.png"; + private final ObjectMapper objectMapper = new ObjectMapper(); @Mock PagedIterable listTags; @@ -73,9 +67,6 @@ class GHAxonIvyProductRepoServiceImplTest { @Mock JsonNode dataNode; - @Mock - JsonNode childNode; - @Mock GHContent content = new GHContent(); @@ -123,41 +114,42 @@ void testContentFromGHRepoAndTag() throws IOException { } @Test - void testExtractMavenArtifactFromJsonNode() { - List artifacts = new ArrayList<>(); - boolean isDependency = true; - String nodeName = ProductJsonConstants.DEPENDENCIES; - - createListNodeForDataNoteByName(nodeName); - MavenArtifact mockArtifact = Mockito.mock(MavenArtifact.class); - Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl) - .createArtifactFromJsonNode(childNode, null, isDependency); - - axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); - - assertEquals(1, artifacts.size()); - assertSame(mockArtifact, artifacts.get(0)); - - isDependency = false; - nodeName = ProductJsonConstants.PROJECTS; - createListNodeForDataNoteByName(nodeName); + void testExtractMavenArtifactFromJsonNode() throws JsonProcessingException { + List artifacts = new ArrayList<>(); + // Arrange + String mockJsonNode = """ + { + "dependencies": [ + { + "groupId": "com.axonivy.market", + "artifactId": "octopus-util", + "version": "1.0.0" + }, + { + "groupId": "com.axonivy.market", + "artifactId": "octopus-util-demo", + "version": "1.0.0" + } + ] + } + """; - Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl) - .createArtifactFromJsonNode(childNode, null, isDependency); + dataNode = objectMapper.readTree(mockJsonNode); + boolean isDependency = true; - axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); + MavenUtils.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts, + MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); - assertEquals(2, artifacts.size()); - assertSame(mockArtifact, artifacts.get(1)); + assertEquals(2, artifacts.size()); // Assert that 2 artifacts were added + assertEquals("octopus-util", artifacts.get(0).getArtifactId()); // Validate first artifact + assertEquals("octopus-util-demo", artifacts.get(1).getArtifactId()); } - private void createListNodeForDataNoteByName(String nodeName) { - JsonNode sectionNode = Mockito.mock(JsonNode.class); - Iterator iterator = Mockito.mock(String.valueOf(Iterator.class)); - Mockito.when(dataNode.path(nodeName)).thenReturn(sectionNode); - Mockito.when(sectionNode.iterator()).thenReturn(iterator); - Mockito.when(iterator.hasNext()).thenReturn(true, false); - Mockito.when(iterator.next()).thenReturn(childNode); + private static GHContent createMockImage() { + GHContent mockImage = mock(GHContent.class); + when(mockImage.isFile()).thenReturn(true); + when(mockImage.getName()).thenReturn(IMAGE_NAME); + return mockImage; } @Test @@ -178,7 +170,7 @@ void testCreateArtifactFromJsonNode() { Mockito.when(dataNode.path(ProductJsonConstants.ARTIFACT_ID)).thenReturn(artifactIdNode); Mockito.when(dataNode.path(ProductJsonConstants.TYPE)).thenReturn(typeNode); - MavenArtifact artifact = axonivyProductRepoServiceImpl.createArtifactFromJsonNode(dataNode, repoUrl, isDependency); + Artifact artifact = MavenUtils.createArtifactFromJsonNode(dataNode, repoUrl, isDependency); assertEquals(repoUrl, artifact.getRepoUrl()); assertTrue(artifact.getIsDependency()); @@ -188,23 +180,10 @@ void testCreateArtifactFromJsonNode() { assertTrue(artifact.getIsProductArtifact()); } - @Test - void testConvertProductJsonToMavenProductInfo() throws IOException { - assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(null).size()); - assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); - - InputStream inputStream = getMockInputStream(); - Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(content)).thenReturn(inputStream); - assertEquals(2, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); - inputStream = getMockInputStreamWithOutProjectAndDependency(); - Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(content)).thenReturn(inputStream); - assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); - } - - @Test - void testExtractedContentStream() { - assertNull(axonivyProductRepoServiceImpl.extractedContentStream(null)); - assertNull(axonivyProductRepoServiceImpl.extractedContentStream(content)); + private static void getReadmeInputStream(String readmeContentString, GHContent mockContent) throws IOException { + InputStream mockReadmeInputStream = mock(InputStream.class); + when(mockContent.read()).thenReturn(mockReadmeInputStream); + when(mockReadmeInputStream.readAllBytes()).thenReturn(readmeContentString.getBytes()); } @Test @@ -216,27 +195,49 @@ void testGetOrganization() throws IOException { @Test void testGetReadmeAndProductContentsFromTag() throws IOException { - String readmeContentWithImage = "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (image.png)"; + String readmeContentWithImage = """ + #Product-name + Test README + ## Demo + Demo content + ## Setup + Setup content (image.png) + """; testGetReadmeAndProductContentsFromTagWithReadmeText(readmeContentWithImage); - String readmeContentWithoutHashProductName = "Test README\n## Demo\nDemo content\n## Setup\nSetup content (image.png)"; + String readmeContentWithoutHashProductName = """ + Test README + ## Demo + Demo content + ## Setup + Setup content (image.png) + """; testGetReadmeAndProductContentsFromTagWithReadmeText(readmeContentWithoutHashProductName); } private void testGetReadmeAndProductContentsFromTagWithReadmeText(String readmeContentWithImage) throws IOException { - GHContent mockContent = createMockProductFolderWithProductJson(); + //Mock readme content + GHContent mockContent = mock(GHContent.class); + when(mockContent.isDirectory()).thenReturn(true); + when(mockContent.isFile()).thenReturn(true); + when(mockContent.getName()).thenReturn(DOCUWARE_CONNECTOR_PRODUCT, ReadmeConstants.README_FILE); + + PagedIterable pagedIterable = Mockito.mock(String.valueOf(GHContent.class)); + when(mockContent.listDirectoryContent()).thenReturn(pagedIterable); + when(pagedIterable.toList()).thenReturn(List.of()); getReadmeInputStream(readmeContentWithImage, mockContent); - InputStream inputStream = getMockInputStream(); - Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(any())).thenReturn(inputStream); - Mockito.when(imageService.mappingImageFromGHContent(any(),any(),anyBoolean())).thenReturn(mockImage()); + + GHContent mockContent2 = createMockImage(); + + when(ghRepository.getDirectoryContent(CommonConstants.SLASH, RELEASE_TAG)).thenReturn( + List.of(mockContent, mockContent2)); + when(ghRepository.getDirectoryContent(DOCUWARE_CONNECTOR_PRODUCT, RELEASE_TAG)).thenReturn( + List.of(mockContent, mockContent2)); + when(imageService.mappingImageFromGHContent(any(), any(), anyBoolean())).thenReturn(mockImage()); var result = axonivyProductRepoServiceImpl.getReadmeAndProductContentsFromTag(createMockProduct(), ghRepository, RELEASE_TAG); assertEquals(RELEASE_TAG, result.getTag()); - assertTrue(result.getIsDependency()); - assertEquals("com.axonivy.utils.bpmnstatistic", result.getGroupId()); - assertEquals("bpmn-statistic", result.getArtifactId()); - assertEquals("iar", result.getType()); assertEquals("Test README", result.getDescription().get(Language.EN.getValue())); assertEquals("Demo content", result.getDemo().get(Language.EN.getValue())); assertEquals("Setup content (imageId-66e2b14868f2f95b2f95549a)", result.getSetup().get(Language.EN.getValue())); @@ -253,31 +254,78 @@ public static Image mockImage() { } @Test - void testGetReadmeAndProductContentFromTag_ImageFromFolder() throws IOException { - String readmeContentWithImageFolder = "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (./images/image.png)"; + void testGetReadmeAndProductContentFromTag_ImageFromRootFolder() { + String readmeContentWithImageFolder = """ + #Product-name + Test README + ## Demo + Demo content + ## Setup + Setup content (./image.png)"""; + + GHContent mockImageFile = mock(GHContent.class); + when(mockImageFile.getName()).thenReturn(IMAGE_NAME); + Mockito.when(imageService.mappingImageFromGHContent(any(), any(), anyBoolean())).thenReturn(mockImage()); + + String updatedReadme = axonivyProductRepoServiceImpl.updateImagesWithDownloadUrl(createMockProduct(), + List.of(mockImageFile), readmeContentWithImageFolder); + + assertEquals(""" + #Product-name + Test README + ## Demo + Demo content + ## Setup + Setup content (imageId-66e2b14868f2f95b2f95549a)""", + updatedReadme); + } + + @Test + void testGetReadmeAndProductContentFromTag_ImageFromChildFolder() throws IOException { + String readmeContentWithImageFolder = """ + #Product-name + Test README + ## Demo + Demo content + ## Setup + Setup content (.doc/img/image.png)"""; GHContent mockImageFile = mock(GHContent.class); - when(mockImageFile.getName()).thenReturn(ReadmeConstants.IMAGES, IMAGE_NAME); when(mockImageFile.isDirectory()).thenReturn(true); - Mockito.when(imageService.mappingImageFromGHContent(any(),any() ,anyBoolean())).thenReturn(mockImage()); + + GHContent mockImageFile2 = mock(GHContent.class); + when(mockImageFile2.isDirectory()).thenReturn(true); + + GHContent mockImageFile3 = mock(GHContent.class); + when(mockImageFile3.getName()).thenReturn(IMAGE_NAME); + PagedIterable pagedIterable = Mockito.mock(String.valueOf(GHContent.class)); when(mockImageFile.listDirectoryContent()).thenReturn(pagedIterable); - when(pagedIterable.toList()).thenReturn(List.of(mockImageFile)); + when(pagedIterable.toList()).thenReturn(List.of(mockImageFile2)); + + PagedIterable pagedIterable2 = Mockito.mock(String.valueOf(GHContent.class)); + when(mockImageFile2.listDirectoryContent()).thenReturn(pagedIterable2); + when(pagedIterable2.toList()).thenReturn(List.of(mockImageFile3)); + + Mockito.when(imageService.mappingImageFromGHContent(any(), any(), anyBoolean())).thenReturn(mockImage()); String updatedReadme = axonivyProductRepoServiceImpl.updateImagesWithDownloadUrl(createMockProduct(), List.of(mockImageFile), readmeContentWithImageFolder); - assertEquals( - "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (imageId-66e2b14868f2f95b2f95549a)", + assertEquals(""" + #Product-name + Test README + ## Demo + Demo content + ## Setup + Setup content (imageId-66e2b14868f2f95b2f95549a)""", updatedReadme); } @Test void testGetReadmeAndProductContentsFromTag_WithNoFullyThreeParts() throws IOException { String readmeContentString = "#Product-name\n Test README\n## Setup\nSetup content"; - GHContent mockContent = createMockProductFolder(); - getReadmeInputStream(readmeContentString, mockContent); var result = axonivyProductRepoServiceImpl.getReadmeAndProductContentsFromTag(createMockProduct(), ghRepository, @@ -290,9 +338,7 @@ void testGetReadmeAndProductContentsFromTag_WithNoFullyThreeParts() throws IOExc @Test void testGetReadmeAndProductContentsFromTag_SwitchPartsPosition() throws IOException { String readmeContentString = "#Product-name\n Test README\n## Setup\nSetup content\n## Demo\nDemo content"; - GHContent mockContent = createMockProductFolder(); - getReadmeInputStream(readmeContentString, mockContent); var result = axonivyProductRepoServiceImpl.getReadmeAndProductContentsFromTag(createMockProduct(), ghRepository, @@ -301,10 +347,17 @@ void testGetReadmeAndProductContentsFromTag_SwitchPartsPosition() throws IOExcep assertEquals("Setup content", result.getSetup().get(Language.EN.getValue())); } - private static void getReadmeInputStream(String readmeContentString, GHContent mockContent) throws IOException { - InputStream mockReadmeInputStream = mock(InputStream.class); - when(mockContent.read()).thenReturn(mockReadmeInputStream); - when(mockReadmeInputStream.readAllBytes()).thenReturn(readmeContentString.getBytes()); + @Test + void testConvertProductJsonToMavenProductInfo() throws IOException { + assertEquals(0, GitHubUtils.convertProductJsonToMavenProductInfo(null).size()); + assertEquals(0, GitHubUtils.convertProductJsonToMavenProductInfo(content).size()); + + InputStream inputStream = getMockInputStream(); + Mockito.when(GitHubUtils.extractedContentStream(content)).thenReturn(inputStream); + assertEquals(2, GitHubUtils.convertProductJsonToMavenProductInfo(content).size()); + inputStream = getMockInputStreamWithOutProjectAndDependency(); + Mockito.when(GitHubUtils.extractedContentStream(content)).thenReturn(inputStream); + assertEquals(0, GitHubUtils.convertProductJsonToMavenProductInfo(content).size()); } private static InputStream getMockInputStream() { @@ -312,7 +365,7 @@ private static InputStream getMockInputStream() { return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); } - private static String getMockProductJsonContent(){ + private static String getMockProductJsonContent() { return """ { "$schema": "https://json-schema.axonivy.com/market/10.0.0/product.json", @@ -368,25 +421,25 @@ private static String getMockProductJsonContent(){ private static InputStream getMockInputStreamWithOutProjectAndDependency() { String jsonContent = """ - { - "installers": [ { - "data": { - "repositories": [ - { - "url": "http://example.com/repo" + "installers": [ + { + "data": { + "repositories": [ + { + "url": "http://example.com/repo" + } + ] } - ] - } + } + ] } - ] - } - """; + """; return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); } private Product createMockProduct() { - Map names = Map.of("en","docuware-connector-name"); + Map names = Map.of("en", "docuware-connector-name"); Product product = new Product(); product.setId("docuware-connector"); product.setNames(names); @@ -406,52 +459,9 @@ private GHContent createMockProductFolder() throws IOException { return mockContent; } - private GHContent createMockProductFolderWithProductJson() throws IOException { - GHContent mockContent = mock(GHContent.class); - when(mockContent.isDirectory()).thenReturn(true); - when(mockContent.isFile()).thenReturn(true); - when(mockContent.getName()).thenReturn(DOCUWARE_CONNECTOR_PRODUCT, ReadmeConstants.README_FILE); - - GHContent mockContent2 = createMockProductJson(); - - when(ghRepository.getDirectoryContent(CommonConstants.SLASH, RELEASE_TAG)).thenReturn( - List.of(mockContent, mockContent2)); - when(ghRepository.getDirectoryContent(DOCUWARE_CONNECTOR_PRODUCT, RELEASE_TAG)).thenReturn( - List.of(mockContent, mockContent2)); - - return mockContent; - } - - private static GHContent createMockProductJson() { - GHContent mockProductJson = mock(GHContent.class); - when(mockProductJson.isFile()).thenReturn(true); - when(mockProductJson.getName()).thenReturn(ProductJsonConstants.PRODUCT_JSON_FILE, IMAGE_NAME); - return mockProductJson; - } - @Test - void test_insertProductJsonContent() throws IOException { - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ProductJsonContent.class); - String readmeContentWithImage = "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (image.png)"; - GHContent mockContent = createMockProductFolderWithProductJson(); - getReadmeInputStream(readmeContentWithImage, mockContent); - InputStream inputStream = getMockInputStream(); - Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(any())).thenReturn(inputStream); - Mockito.when(axonivyProductRepoServiceImpl.extractProductJsonContent(any(),anyString())).thenReturn(getMockProductJsonContent()); - - ProductJsonContent expectedProductJsonContent = new ProductJsonContent(); - expectedProductJsonContent.setProductId("docuware-connector"); - expectedProductJsonContent.setName("docuware-connector-name"); - expectedProductJsonContent.setVersion("10.0.0"); - expectedProductJsonContent.setContent(getMockProductJsonContent()); - - axonivyProductRepoServiceImpl.getReadmeAndProductContentsFromTag(createMockProduct(), ghRepository, - RELEASE_TAG); - - verify(productJsonContentRepository).save(argumentCaptor.capture()); - assertEquals("docuware-connector-name" , argumentCaptor.getValue().getName()); - assertEquals("10.0.0",argumentCaptor.getValue().getVersion()); - assertEquals("docuware-connector" , argumentCaptor.getValue().getProductId()); - assertEquals(getMockProductJsonContent().replace("${version}", "10.0.0"),argumentCaptor.getValue().getContent()); + void testExtractedContentStream() { + assertNull(GitHubUtils.extractedContentStream(null)); + assertNull(GitHubUtils.extractedContentStream(content)); } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/GitHubServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GitHubServiceImplTest.java index 5dd3ce8b6..1cfd8f00d 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/GitHubServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GitHubServiceImplTest.java @@ -17,9 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ImageServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ImageServiceImplTest.java index 9f95c35fa..5b22b8cda 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ImageServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ImageServiceImplTest.java @@ -1,16 +1,10 @@ package com.axonivy.market.service.impl; -import static com.axonivy.market.constants.CommonConstants.SLASH; -import static com.axonivy.market.constants.MetaConstants.META_FILE; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; - +import com.axonivy.market.entity.Image; +import com.axonivy.market.entity.Product; +import com.axonivy.market.repository.ImageRepository; +import com.axonivy.market.util.MavenUtils; +import org.bson.types.Binary; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHContent; @@ -18,23 +12,39 @@ import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.axonivy.market.entity.Image; -import com.axonivy.market.entity.Product; -import com.axonivy.market.repository.ImageRepository; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.META_FILE; +import static org.bson.assertions.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.never; @ExtendWith(MockitoExtension.class) class ImageServiceImplTest { + @Captor + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Image.class); @InjectMocks private ImageServiceImpl imageService; - @Mock private ImageRepository imageRepository; - @Captor - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Image.class); - @Test void testMappingImageFromGHContent() throws IOException { GHContent content = mock(GHContent.class); @@ -44,7 +54,7 @@ void testMappingImageFromGHContent() throws IOException { InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); when(content.read()).thenReturn(inputStream); - imageService.mappingImageFromGHContent(mockProduct(), content , true); + imageService.mappingImageFromGHContent(mockProduct(), content, true); Image expectedImage = new Image(); expectedImage.setProductId("google-maps-connector"); @@ -53,14 +63,89 @@ void testMappingImageFromGHContent() throws IOException { verify(imageRepository).save(argumentCaptor.capture()); - assertEquals(argumentCaptor.getValue().getProductId(),expectedImage.getProductId()); - assertEquals(argumentCaptor.getValue().getSha(),expectedImage.getSha()); - assertEquals(argumentCaptor.getValue().getImageUrl(),expectedImage.getImageUrl()); + assertEquals(argumentCaptor.getValue().getProductId(), expectedImage.getProductId()); + assertEquals(argumentCaptor.getValue().getSha(), expectedImage.getSha()); + assertEquals(argumentCaptor.getValue().getImageUrl(), expectedImage.getImageUrl()); + + when(imageRepository.findByProductIdAndSha(anyString(), anyString())).thenReturn(expectedImage); + Image result = imageService.mappingImageFromGHContent(mockProduct(), content, false); + assertEquals(expectedImage, result); + + } + + @Test + void testMappingImageFromDownloadedFolder() { + try (MockedStatic mockedMavenUtils = Mockito.mockStatic(MavenUtils.class)) { + Product product = new Product(); + product.setId("connectivity-demo"); + + byte[] newImageData = "connectivity-image-data".getBytes(); + + Path imagePath = Path.of("connectivity-image.png"); + ByteArrayInputStream inputStream = new ByteArrayInputStream(newImageData); + mockedMavenUtils.when(() -> MavenUtils.extractedContentStream(imagePath)).thenReturn(inputStream); + when(imageRepository.findByProductId(product.getId())).thenReturn(Collections.emptyList()); + + Image newImage = new Image(); + newImage.setImageData(new Binary(newImageData)); + newImage.setProductId(product.getId()); + + when(imageRepository.save(any(Image.class))).thenReturn(newImage); + + Image result = imageService.mappingImageFromDownloadedFolder(product, imagePath); + + assertNotNull(result); + assertEquals(newImage, result); + verify(imageRepository).save(any(Image.class)); + } + } + + @Test + void testMappingImageFromDownloadedFolderWhenImageExists() { + try (MockedStatic mockedMavenUtils = Mockito.mockStatic(MavenUtils.class)) { + Product product = new Product(); + product.setId("connectivity-demo"); + + byte[] existingImageData = "connectivity-image-data".getBytes(); + byte[] newImageData = "connectivity-image-data".getBytes(); + + Path imagePath = Path.of("connectivity-image.png"); + ByteArrayInputStream inputStream = new ByteArrayInputStream(newImageData); + mockedMavenUtils.when(() -> MavenUtils.extractedContentStream(imagePath)).thenReturn(inputStream); + + Image existingImage = new Image(); + existingImage.setImageData(new Binary(existingImageData)); + existingImage.setProductId(product.getId()); + + when(imageRepository.findByProductId(product.getId())).thenReturn(List.of(existingImage)); + + Image result = imageService.mappingImageFromDownloadedFolder(product, imagePath); + + assertNotNull(result); + assertEquals(existingImage, result); + verify(imageRepository, never()).save(any(Image.class)); + } + } + + @Test + void testMappingImageFromDownloadedFolder_ReturnNull() { + try (MockedStatic mockedMavenUtils = Mockito.mockStatic(MavenUtils.class)) { + Product product = new Product(); + product.setId("connectivity-demo"); + Path imagePath = Path.of("connectivity-image.png"); + mockedMavenUtils.when(() -> MavenUtils.extractedContentStream(imagePath)).thenThrow( + new NullPointerException("File not found")); + + Image result = imageService.mappingImageFromDownloadedFolder(product, imagePath); + + assertNull(result); + verify(imageRepository, times(0)).save(any(Image.class)); + } } @Test void testMappingImageFromGHContent_noGhContent() { - var result = imageService.mappingImageFromGHContent(mockProduct(), null , true); + var result = imageService.mappingImageFromGHContent(mockProduct(), null, true); assertNull(result); } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/JwtServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/JwtServiceImplTest.java index 923aae407..922af253e 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/JwtServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/JwtServiceImplTest.java @@ -10,10 +10,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; @ExtendWith(MockitoExtension.class) class JwtServiceImplTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/MetadataServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/MetadataServiceImplTest.java new file mode 100644 index 000000000..ee18d1af9 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/MetadataServiceImplTest.java @@ -0,0 +1,311 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.bo.Artifact; +import com.axonivy.market.constants.MavenConstants; +import com.axonivy.market.entity.MavenArtifactVersion; +import com.axonivy.market.entity.Metadata; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductJsonContent; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.model.MavenArtifactModel; +import com.axonivy.market.repository.MavenArtifactVersionRepository; +import com.axonivy.market.repository.MetadataRepository; +import com.axonivy.market.repository.MetadataSyncRepository; +import com.axonivy.market.repository.ProductJsonContentRepository; +import com.axonivy.market.repository.ProductModuleContentRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.util.MavenUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@ExtendWith(MockitoExtension.class) +class MetadataServiceImplTest { + @InjectMocks + MetadataServiceImpl metadataService; + @Mock + ProductRepository productRepo; + @Mock + MetadataSyncRepository metadataSyncRepo; + @Mock + ProductJsonContentRepository productJsonRepo; + @Mock + MavenArtifactVersionRepository mavenArtifactVersionRepo; + @Mock + MetadataRepository metadataRepo; + @Mock + ProductModuleContentRepository productContentRepo; + + public final String MOCK_SNAPSHOT = """ + + com.axonivy.utils.bpmnstatistic + bpmn-statistic + 8.0.5-SNAPSHOT + + + 20221011.124215 + 170 + + 20221011130000 + + + iar + 8.0.5-20221011.124215-170 + 20221011124215 + + + + + """; + private final String MOCK_METADATA = """ + + 1.0.2 + 1.0.1 + 20230924010101 + + 1.0.0 + + + """; + + private ProductJsonContent getMockProductJson() { + ProductJsonContent result = new ProductJsonContent(); + String mockContent = """ + { + "$schema": "https://json-schema.axonivy.com/market/10.0.0/product.json", + "installers": [ + { + "id": "maven-import", + "data": { + "projects": [ + { + "groupId": "com.axonivy.utils.bpmnstatistic", + "artifactId": "bpmn-statistic-demo", + "version": "${version}", + "type": "iar" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + }, + { + "id": "maven-dependency", + "data": { + "dependencies": [ + { + "groupId": "com.axonivy.utils.bpmnstatistic", + "artifactId": "bpmn-statistic", + "version": "${version}", + "type": "iar" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + } + ] + } + """; + result.setContent(mockContent); + return result; + } + + private static ProductModuleContent getMockProductModuleContent() { + ProductModuleContent mockProductModuleContent = new ProductModuleContent(); + mockProductModuleContent.setMavenVersions(new HashSet<>()); + return mockProductModuleContent; + } + + private MavenArtifactVersion getMockMavenArtifactVersion() { + return new MavenArtifactVersion(StringUtils.EMPTY, new HashMap<>(), + new HashMap<>()); + } + + private Metadata getMockMetadata() { + return Metadata.builder().productId("bpmn-statistic").artifactId("bpmn-statistic").groupId( + "com.axonivvy.util").isProductArtifact(true).repoUrl("https://maven.axonivy.com").type("iar").name("bpmn " + + "statistic (iar)").build(); + } + + private Artifact getMockArtifact() { + Artifact mockArtifact = new Artifact(); + mockArtifact.setArtifactId("bpmn-statistic"); + mockArtifact.setGroupId("com.axonivy.util"); + mockArtifact.setType("iar"); + mockArtifact.setRepoUrl(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + return mockArtifact; + } + + private List getMockProducts() { + Product mockProduct = + Product.builder().id("bpmn-statistic").releasedVersions(List.of("1.0.0")).artifacts(List.of(getMockArtifact())).build(); + return List.of(mockProduct); + } + + + @Test + void testGetArtifactsFromNonSyncedVersion() { + Mockito.when(productJsonRepo.findByProductIdAndVersion("bpmn-statistic", "1.0.0")).thenReturn( + List.of(getMockProductJson())); + Set artifacts = metadataService.getArtifactsFromNonSyncedVersion("bpmn-statistic", + Collections.emptyList()); + Assertions.assertEquals(0, artifacts.size()); + Mockito.verify(productJsonRepo, Mockito.never()).findByProductIdAndVersion(Mockito.anyString(), + Mockito.anyString()); + artifacts = metadataService.getArtifactsFromNonSyncedVersion("bpmn-statistic", List.of("1.0.0")); + Assertions.assertEquals(2, artifacts.size()); + Assertions.assertEquals("bpmn-statistic-demo", artifacts.iterator().next().getArtifactId()); + Assertions.assertEquals(2, artifacts.stream().filter(Artifact::getIsProductArtifact).toList().size()); + } + + @Test + void testUpdateMavenArtifactVersionCacheWithModel() { + MavenArtifactVersion mockMavenArtifactVersion = getMockMavenArtifactVersion(); + String version = "1.0.0"; + Metadata mockMetadata = getMockMetadata(); + metadataService.updateMavenArtifactVersionCacheWithModel(mockMavenArtifactVersion, version, mockMetadata); + List artifacts = mockMavenArtifactVersion.getProductArtifactsByVersion().get(version); + Assertions.assertEquals( + "https://maven.axonivy.com/com/axonivvy/util/bpmn-statistic/1.0.0/bpmn-statistic-1.0.0.iar", + artifacts.get(0).getDownloadUrl()); + Assertions.assertEquals(1, artifacts.size()); + metadataService.updateMavenArtifactVersionCacheWithModel(mockMavenArtifactVersion, version, mockMetadata); + Assertions.assertEquals(1, artifacts.size()); + + Assertions.assertEquals(0, mockMavenArtifactVersion.getAdditionalArtifactsByVersion().entrySet().size()); + mockMetadata.setProductArtifact(false); + metadataService.updateMavenArtifactVersionCacheWithModel(mockMavenArtifactVersion, version, mockMetadata); + Assertions.assertEquals(1, mockMavenArtifactVersion.getAdditionalArtifactsByVersion().entrySet().size()); + } + + @Test + void testUpdateMavenArtifactVersionForNonReleaseDevVersion() { + Metadata mockMetadata = getMockMetadata(); + String version = "1.0.0-SNAPSHOT"; + MavenArtifactVersion mockMavenArtifactVersion = getMockMavenArtifactVersion(); + try (MockedStatic mockUtils = Mockito.mockStatic(MavenUtils.class)) { + mockUtils.when(() -> MavenUtils.buildSnapShotMetadataFromVersion(mockMetadata, version)).thenReturn(mockMetadata); + mockUtils.when(() -> MavenUtils.getMetadataContentFromUrl( + "https://maven.axonivy.com/com/axonivvy/util/bpmn-statistic/1.0.0-SNAPSHOT/maven-metadata.xml")).thenReturn( + MOCK_SNAPSHOT); + mockUtils.when(() -> MavenUtils.buildSnapShotMetadataFromVersion(mockMetadata, version)).thenReturn(mockMetadata); + metadataService.updateMavenArtifactVersionForNonReleaseDevVersion(mockMavenArtifactVersion, mockMetadata, + version); + Assertions.assertEquals(1, mockMavenArtifactVersion.getProductArtifactsByVersion().entrySet().size()); + Assertions.assertEquals(1, mockMavenArtifactVersion.getProductArtifactsByVersion().get(version).size()); + } + } + + @Test + void testUpdateMavenArtifactVersionFromMetadata() { + Metadata mockMetadata = getMockMetadata(); + mockMetadata.setVersions(Set.of("1.0.0")); + MavenArtifactVersion mockMavenArtifactVersion = getMockMavenArtifactVersion(); + metadataService.updateMavenArtifactVersionFromMetadata(mockMavenArtifactVersion, mockMetadata); + Assertions.assertEquals(1, mockMavenArtifactVersion.getProductArtifactsByVersion().entrySet().size()); + Assertions.assertEquals(0, mockMavenArtifactVersion.getAdditionalArtifactsByVersion().entrySet().size()); + String snapshotVersion = "2.0.0-SNAPSHOT"; + mockMetadata.setVersions(Set.of(snapshotVersion, "1.0.0")); + try (MockedStatic mockUtils = Mockito.mockStatic(MavenUtils.class)) { + mockUtils.when(() -> MavenUtils.buildSnapShotMetadataFromVersion(mockMetadata, snapshotVersion)).thenReturn( + mockMetadata); + mockUtils.when(() -> MavenUtils.getMetadataContentFromUrl( + "https://maven.axonivy.com/com/axonivvy/util/bpmn-statistic/1.0.0-SNAPSHOT/maven-metadata.xml")).thenReturn( + MOCK_SNAPSHOT); + mockUtils.when(() -> MavenUtils.buildSnapShotMetadataFromVersion(mockMetadata, snapshotVersion)).thenReturn( + mockMetadata); + metadataService.updateMavenArtifactVersionFromMetadata(mockMavenArtifactVersion, mockMetadata); + Assertions.assertEquals(2, mockMavenArtifactVersion.getProductArtifactsByVersion().entrySet().size()); + Assertions.assertEquals(0, mockMavenArtifactVersion.getAdditionalArtifactsByVersion().entrySet().size()); + + mockMetadata.setProductArtifact(false); + metadataService.updateMavenArtifactVersionFromMetadata(mockMavenArtifactVersion, mockMetadata); + Assertions.assertEquals(2, mockMavenArtifactVersion.getAdditionalArtifactsByVersion().entrySet().size()); + } + } + @Test + void testSyncAllProductsMetadata() { + Mockito.when(productRepo.getAllProductsWithIdAndReleaseTagAndArtifact()).thenReturn(List.of(new Product())); + int result = metadataService.syncAllProductsMetadata(); + Assertions.assertEquals(1,result); + Mockito.when(productRepo.getAllProductsWithIdAndReleaseTagAndArtifact()).thenReturn(getMockProducts()); + result = metadataService.syncAllProductsMetadata(); + Assertions.assertEquals(0,result); + } + @Test + void testGetNonMatchSnapshotVersions() { + String productId = "connectivity-demo"; + List releasedVersion = List.of("1.0.0-SNAPSHOT"); + Set metaVersions = Set.of("1.0.0-SNAPSHOT"); + ProductModuleContent mockProductModuleContent = getMockProductModuleContent(); + Mockito.when(productContentRepo.findByTagAndProductId("v1.0.0-SNAPSHOT", productId)).thenReturn( + mockProductModuleContent); + Assertions.assertEquals(0, + metadataService.getNonMatchSnapshotVersions(productId, releasedVersion, metaVersions).size()); + metaVersions = Set.of("2.0.0-SNAPSHOT"); + Assertions.assertEquals(1, + metadataService.getNonMatchSnapshotVersions(productId, releasedVersion, metaVersions).size()); + metaVersions = Set.of("2.0.0"); + Assertions.assertEquals(0, + metadataService.getNonMatchSnapshotVersions(productId, releasedVersion, metaVersions).size()); + } + + + @Test + void testBuildProductFolderDownloadUrl() { + Metadata mockMetadata = getMockMetadata(); + Assertions.assertEquals("https://maven.axonivy.com/com/axonivvy/util/bpmn-statistic/1.0.0-SNAPSHOT/bpmn-statistic-1.0.0-SNAPSHOT.zip",metadataService.buildProductFolderDownloadUrl(mockMetadata,"1.0.0-SNAPSHOT")); + } + + @Test + void testUpdateMavenArtifactVersionData() { + String productId = "connectivity-demo"; + List releasedVersion = List.of("1.0.0"); + Metadata mockMetadata = getMockMetadata(); + mockMetadata.setVersions(new HashSet<>()); + mockMetadata.setUrl("https://maven.axonivy.com/com/axonivvy/util/bpmn-statistic/maven-metadata.xml"); + Set mockMetadataSet = Set.of(mockMetadata); + MavenArtifactVersion mockMavenArtifactVersion = getMockMavenArtifactVersion(); + metadataService.updateMavenArtifactVersionData(productId, releasedVersion, mockMetadataSet, + mockMavenArtifactVersion); + Assertions.assertEquals(0, mockMavenArtifactVersion.getAdditionalArtifactsByVersion().size()); + Assertions.assertEquals(0, mockMavenArtifactVersion.getProductArtifactsByVersion().size()); + try (MockedStatic mockUtils = Mockito.mockStatic(MavenUtils.class)) { + mockUtils.when(() -> MavenUtils.getMetadataContentFromUrl( + "https://maven.axonivy.com/com/axonivvy/util/bpmn-statistic/maven-metadata.xml")).thenReturn( + MOCK_METADATA); + Mockito.when(productContentRepo.findByTagAndProductId("v1.0.0", + productId)).thenReturn(getMockProductModuleContent()); + metadataService.updateMavenArtifactVersionData(productId, releasedVersion, mockMetadataSet, + mockMavenArtifactVersion); + Assertions.assertEquals(1, mockMavenArtifactVersion.getProductArtifactsByVersion().size()); + } + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductDesignerInstallationServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductDesignerInstallationServiceImplTest.java index 6e8998fef..b95be2443 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductDesignerInstallationServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductDesignerInstallationServiceImplTest.java @@ -34,8 +34,9 @@ public void setup() { @Test void testFindByProductId() { when(productDesignerInstallationRepository.findByProductId(any(), any())).thenReturn(this.mockResultReturn); - List results = productDesignerInstallationServiceImpl.findByProductId(BaseSetup.SAMPLE_PRODUCT_ID); - assertEquals(2,results.size()); + List results = productDesignerInstallationServiceImpl.findByProductId( + BaseSetup.SAMPLE_PRODUCT_ID); + assertEquals(2, results.size()); assertEquals("10.0.22", results.get(0).getDesignerVersion()); assertEquals(50, results.get(0).getNumberOfDownloads()); assertEquals("11.4.0", results.get(1).getDesignerVersion()); diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductJsonContentServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductJsonContentServiceImplTest.java new file mode 100644 index 000000000..521ae7b33 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductJsonContentServiceImplTest.java @@ -0,0 +1,65 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.constants.ProductJsonConstants; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductJsonContent; +import com.axonivy.market.repository.ProductJsonContentRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ProductJsonContentServiceImplTest { + @Mock + private ProductJsonContentRepository productJsonRepo; + + @InjectMocks + private ProductJsonContentServiceImpl productJsonContentService; + + @Test + void testUpdateProductJsonContent_ValidJsonContent() { + String jsonContent = "{\"version\":\"${version}\"}"; + String relatedTag = "v1.0.0"; + String currentVersion = "1.0.0"; + Product product = new Product(); + product.setId("123"); + HashMap names = new HashMap<>(); + names.put(ProductJsonConstants.EN_LANGUAGE, "Test Product"); + product.setNames(names); + + productJsonContentService.updateProductJsonContent(jsonContent, relatedTag, currentVersion, + ProductJsonConstants.VERSION_VALUE, product); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ProductJsonContent.class); + verify(productJsonRepo, times(1)).save(captor.capture()); + + ProductJsonContent savedContent = captor.getValue(); + assertEquals(currentVersion, savedContent.getVersion()); + assertEquals("123", savedContent.getProductId()); + assertEquals("Test Product", savedContent.getName()); + assertEquals("{\"version\":\"1.0.0\"}", savedContent.getContent()); + } + + @Test + void testUpdateProductJsonContent_EmptyJsonContent() { + String jsonContent = ""; + String relatedTag = "v1.0.0"; + String currentVersion = "1.0.0"; + Product product = new Product(); + + productJsonContentService.updateProductJsonContent(jsonContent, relatedTag, currentVersion, + ProductJsonConstants.VERSION_VALUE, product); + + verify(productJsonRepo, times(0)).save(any(ProductJsonContent.class)); + } +} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java index 2f466ab8b..c68482965 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java @@ -1,41 +1,34 @@ package com.axonivy.market.service.impl; -import static com.axonivy.market.constants.ProductJsonConstants.LOGO_FILE; -import static com.axonivy.market.constants.CommonConstants.SLASH; -import static com.axonivy.market.constants.MetaConstants.META_FILE; -import static com.axonivy.market.enums.DocumentField.SHORT_DESCRIPTIONS; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - +import com.axonivy.market.BaseSetup; +import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.criteria.ProductSearchCriteria; +import com.axonivy.market.entity.GitHubRepoMeta; +import com.axonivy.market.entity.MavenArtifactVersion; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductCustomSort; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.enums.FileStatus; +import com.axonivy.market.enums.FileType; +import com.axonivy.market.enums.Language; +import com.axonivy.market.enums.SortOption; +import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.exceptions.model.InvalidParamException; +import com.axonivy.market.github.model.GitHubFile; +import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.ProductCustomSortRequest; +import com.axonivy.market.repository.GitHubRepoMetaRepository; +import com.axonivy.market.repository.ImageRepository; +import com.axonivy.market.repository.MavenArtifactVersionRepository; +import com.axonivy.market.repository.ProductCustomSortRepository; +import com.axonivy.market.repository.ProductModuleContentRepository; +import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.ImageService; +import com.axonivy.market.util.MavenUtils; +import com.axonivy.market.util.VersionUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,6 +40,7 @@ import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; @@ -59,89 +53,76 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.test.util.ReflectionTestUtils; -import com.axonivy.market.BaseSetup; -import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.entity.GitHubRepoMeta; -import com.axonivy.market.entity.Product; -import com.axonivy.market.entity.ProductCustomSort; -import com.axonivy.market.entity.ProductModuleContent; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.enums.FileStatus; -import com.axonivy.market.enums.FileType; -import com.axonivy.market.enums.Language; -import com.axonivy.market.enums.SortOption; -import com.axonivy.market.enums.TypeOption; -import com.axonivy.market.exceptions.model.InvalidParamException; -import com.axonivy.market.github.model.GitHubFile; -import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; -import com.axonivy.market.github.service.GHAxonIvyProductRepoService; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.model.ProductCustomSortRequest; -import com.axonivy.market.repository.GitHubRepoMetaRepository; -import com.axonivy.market.repository.ProductCustomSortRepository; -import com.axonivy.market.repository.ProductModuleContentRepository; -import com.axonivy.market.repository.ProductRepository; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.META_FILE; +import static com.axonivy.market.constants.ProductJsonConstants.LOGO_FILE; +import static com.axonivy.market.enums.DocumentField.SHORT_DESCRIPTIONS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ProductServiceImplTest extends BaseSetup { + public static final String RELEASE_TAG = "v10.0.2"; private static final long LAST_CHANGE_TIME = 1718096290000L; private static final Pageable PAGEABLE = PageRequest.of(0, 20, Sort.by(SortOption.ALPHABETICALLY.getOption()).descending()); private static final String SHA1_SAMPLE = "35baa89091b2452b77705da227f1a964ecabc6c8"; - public static final String RELEASE_TAG = "v10.0.2"; private static final String INSTALLATION_FILE_PATH = "src/test/resources/installationCount.json"; private static final String EMPTY_SOURCE_URL_META_JSON_FILE = "/emptySourceUrlMeta.json"; - + @Captor + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Product.class); + @Captor + ArgumentCaptor> argumentCaptorProductModuleContents; + @Captor + ArgumentCaptor argumentCaptorProductModuleContent; + @Captor + ArgumentCaptor> productListArgumentCaptor; + @Captor + ArgumentCaptor productSearchCriteriaArgumentCaptor; private String keyword; private String language; private Page mockResultReturn; - @Mock private MongoTemplate mongoTemplate; - @Mock private GHRepository ghRepository; - @Mock private ProductRepository productRepository; - @Mock private ProductModuleContentRepository productModuleContentRepository; - @Mock private GHAxonIvyMarketRepoService marketRepoService; - @Mock private GitHubRepoMetaRepository repoMetaRepository; - @Mock private GitHubService gitHubService; @Mock - private ProductCustomSortRepository productCustomSortRepository; - - @Captor - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Product.class); - - @Captor - ArgumentCaptor> argumentCaptorProductModuleContents; - - @Captor - ArgumentCaptor argumentCaptorProductModuleContent; + private ImageRepository imageRepository; + @Mock + private ProductCustomSortRepository productCustomSortRepository; @Mock private GHAxonIvyProductRepoService ghAxonIvyProductRepoService; - @Mock private ImageService imageService; - - @Captor - ArgumentCaptor> productListArgumentCaptor; - - @Captor - ArgumentCaptor productSearchCriteriaArgumentCaptor; - + @Mock + private MavenArtifactVersionRepository mavenArtifactVersionRepo; @InjectMocks private ProductServiceImpl productService; @@ -278,7 +259,6 @@ void testSyncProductsAsUpdateLogoFromGitHub() throws IOException { when(mockCommit.getSHA1()).thenReturn(UUID.randomUUID().toString()); mockGitHubFile.setStatus(FileStatus.REMOVED); when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); - when(gitHubService.getGHContent(any(), anyString(), any())).thenReturn(mockGHContent); // Executes result = productService.syncLatestDataFromMarketRepo(); @@ -355,7 +335,7 @@ void testSyncProductsFirstTime() throws IOException { assertEquals(1, argumentCaptorProductModuleContents.getValue().size()); assertThat(argumentCaptorProductModuleContents.getValue().get(0).getId()) - .isEqualTo(mockReadmeProductContent().getId()); + .isEqualTo(mockReadmeProductContent().getId()); } @Test @@ -374,7 +354,8 @@ void testSyncProductsFirstTimeWithOutSourceUrl() throws IOException { List mockMetaJsonAndLogoList = new ArrayList<>(List.of(mockContent, mockContentLogo)); mockGHContentMap.put(SAMPLE_PRODUCT_ID, mockMetaJsonAndLogoList); when(marketRepoService.fetchAllMarketItems()).thenReturn(mockGHContentMap); - Mockito.when(imageService.mappingImageFromGHContent(any(),any(),anyBoolean())).thenReturn(GHAxonIvyProductRepoServiceImplTest.mockImage()); + Mockito.when(imageService.mappingImageFromGHContent(any(), any(), anyBoolean())).thenReturn( + GHAxonIvyProductRepoServiceImplTest.mockImage()); // Executes productService.syncLatestDataFromMarketRepo(); verify(productModuleContentRepository).save(argumentCaptorProductModuleContent.capture()); @@ -478,23 +459,44 @@ void testFetchProductDetail() { @Test void testFetchProductDetailByIdAndVersion() { String id = "amazon-comprehend"; + String version = "10.0.2"; + Product mockProduct = mockResultReturn.getContent().get(0); - when(productRepository.getProductByIdAndTag(id, RELEASE_TAG)).thenReturn(mockProduct); - Product result = productService.fetchProductDetailByIdAndVersion(id, "10.0.2"); + when(productRepository.getProductByIdWithTagOrVersion(id, version)).thenReturn(mockProduct); + + Product result = productService.fetchProductDetailByIdAndVersion(id, version); + assertEquals(mockProduct, result); - verify(productRepository, times(1)).getProductByIdAndTag(id, RELEASE_TAG); + verify(productRepository, times(1)).getProductByIdWithTagOrVersion(id, version); } @Test void testFetchBestMatchProductDetailByIdAndVersion() { String id = "amazon-comprehend"; - Product mockProduct = mockResultReturn.getContent().get(0); - mockProduct.setSynchronizedInstallationCount(true); - when(productRepository.getReleasedVersionsById(id)).thenReturn(List.of("10.0.2", "10.0.1")); - when(productRepository.getProductByIdAndTag(id, RELEASE_TAG)).thenReturn(mockProduct); - Product result = productService.fetchBestMatchProductDetail(id, "10.0.2"); - assertEquals(mockProduct, result); - verify(productRepository, times(1)).getProductByIdAndTag(id, RELEASE_TAG); + String version = "v10.0.2"; + String bestMatchVersion = "10.0.2"; + + MavenArtifactVersion mockMavenArtifactVersion = new MavenArtifactVersion(); + mockMavenArtifactVersion.getProductArtifactsByVersion().put(bestMatchVersion, Collections.emptyList()); + + List mockVersions = Arrays.asList("10.0.1", "10.0.2"); + when(mavenArtifactVersionRepo.findById(id)).thenReturn(Optional.of(mockMavenArtifactVersion)); + try (MockedStatic mockVersionUtils = Mockito.mockStatic(VersionUtils.class)) { + when(MavenUtils.getAllExistingVersions(mockMavenArtifactVersion, true, null)).thenReturn(mockVersions); + mockVersionUtils.when(() -> VersionUtils.getBestMatchVersion(mockVersions, version)).thenReturn(bestMatchVersion); + mockVersionUtils.when(() -> VersionUtils.convertVersionToTag(id, bestMatchVersion)).thenReturn(version); + + Product mockProduct = new Product(); + mockProduct.setSynchronizedInstallationCount(true); + when(productRepository.getProductByIdWithTagOrVersion(id, version)).thenReturn(mockProduct); + + Product result = productService.fetchBestMatchProductDetail(id, version); + + assertEquals(mockProduct, result); + assertEquals(bestMatchVersion, result.getBestMatchVersion()); + verify(mavenArtifactVersionRepo, times(1)).findById(id); + verify(productRepository, times(1)).getProductByIdWithTagOrVersion(id, version); + } } @Test @@ -634,8 +636,66 @@ private ProductModuleContent mockReadmeProductContent() { } private List mockProducts() { - Product product1 = Product.builder().repositoryName("axonivy-market/amazon-comprehend-connector") + Product product1 = Product.builder().id("amazon-comprehend-connector").repositoryName("axonivy-market/amazon-comprehend-connector") .productModuleContent(mockReadmeProductContent()).build(); return List.of(product1); } + + @Test + void testUpdateNewLogoFromGitHub_removeOldLogo() throws IOException { + // Start testing by adding new logo + mockMarketRepoMetaStatus(); + var mockCommit = mockGHCommitHasSHA1(UUID.randomUUID().toString()); + when(mockCommit.getCommitDate()).thenReturn(new Date()); + when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); + + var mockGitHubFile = new GitHubFile(); + mockGitHubFile.setFileName(LOGO_FILE); + mockGitHubFile.setType(FileType.LOGO); + mockGitHubFile.setStatus(FileStatus.ADDED); + when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); + var mockGHContent = mockGHContentAsMetaJSON(); + when(gitHubService.getGHContent(any(), anyString(), any())).thenReturn(mockGHContent); + + // Executes + var result = productService.syncLatestDataFromMarketRepo(); + assertFalse(result); + + // Start testing by deleting new logo + when(mockCommit.getSHA1()).thenReturn(UUID.randomUUID().toString()); + mockGitHubFile.setStatus(FileStatus.REMOVED); + when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); + when(imageRepository.findByImageUrlEndsWithIgnoreCase(anyString())) + .thenReturn(List.of(GHAxonIvyProductRepoServiceImplTest.mockImage())); + // Executes + result = productService.syncLatestDataFromMarketRepo(); + + verify(productRepository, times(1)).deleteById(anyString()); + verify(imageRepository, times(1)).deleteAllByProductId(anyString()); + verify(imageRepository, times(1)).findByImageUrlEndsWithIgnoreCase(anyString()); + assertFalse(result); + } + + @Test + void testUpdateNewLogoFromGitHub_ModifyLogo() throws IOException { + // Start testing by adding new logo + mockMarketRepoMetaStatus(); + var mockCommit = mockGHCommitHasSHA1(UUID.randomUUID().toString()); + when(mockCommit.getCommitDate()).thenReturn(new Date()); + when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); + + var mockGitHubFile = new GitHubFile(); + mockGitHubFile.setFileName("meta.json"); + mockGitHubFile.setType(FileType.META); + mockGitHubFile.setStatus(FileStatus.REMOVED); + when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); + when(productRepository.findByMarketDirectory(anyString())).thenReturn(mockProducts()); + + // Executes + var result = productService.syncLatestDataFromMarketRepo(); + + assertFalse(result); + verify(productRepository, times(1)).deleteById(anyString()); + verify(imageRepository, times(1)).deleteAllByProductId(anyString()); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java index 0e2b26bcd..94e022e1b 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java @@ -1,17 +1,16 @@ package com.axonivy.market.service.impl; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.verify; - +import com.axonivy.market.schedulingtask.ScheduledTasks; import org.awaitility.Awaitility; import org.awaitility.Durations; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; -import com.axonivy.market.schedulingtask.ScheduledTasks; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; -@SpringBootTest(properties = { "MONGODB_USERNAME=user", "MONGODB_PASSWORD=password", "MONGODB_HOST=mongoHost", +@SpringBootTest(properties = {"MONGODB_USERNAME=user", "MONGODB_PASSWORD=password", "MONGODB_HOST=mongoHost", "MONGODB_DATABASE=product", "MARKET_GITHUB_OAUTH_APP_CLIENT_ID=clientId", "MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=clientSecret", "MARKET_JWT_SECRET_KEY=jwtSecret", "MARKET_CORS_ALLOWED_ORIGIN=*", "MARKET_GITHUB_MARKET_BRANCH=master", "MARKET_MONGO_LOG_LEVEL=DEBUG"}) diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/UserServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/UserServiceImplTest.java index 389c87f3c..c6b907d9e 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/UserServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/UserServiceImplTest.java @@ -15,13 +15,8 @@ import java.util.List; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class UserServiceImplTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java index a45e845fa..2b10450fd 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java @@ -1,34 +1,28 @@ package com.axonivy.market.service.impl; -import com.axonivy.market.constants.MavenConstants; -import com.axonivy.market.entity.MavenArtifactModel; +import com.axonivy.market.bo.ArchivedArtifact; +import com.axonivy.market.bo.Artifact; import com.axonivy.market.entity.MavenArtifactVersion; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductJsonContent; -import com.axonivy.market.github.model.ArchivedArtifact; -import com.axonivy.market.github.model.MavenArtifact; import com.axonivy.market.github.service.GHAxonIvyProductRepoService; -import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.model.MavenArtifactModel; import com.axonivy.market.model.VersionAndUrlModel; import com.axonivy.market.repository.MavenArtifactVersionRepository; import com.axonivy.market.repository.ProductJsonContentRepository; +import com.axonivy.market.repository.ProductModuleContentRepository; import com.axonivy.market.repository.ProductRepository; -import com.axonivy.market.util.XmlReaderUtils; +import com.axonivy.market.util.MavenUtils; import org.apache.commons.lang3.StringUtils; -import org.assertj.core.api.Fail; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.kohsuke.github.GHContent; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; -import java.io.IOException; + import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -37,16 +31,10 @@ import java.util.Optional; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class VersionServiceImplTest { - private String repoName; - private Map> archivedArtifactsMap; - private List artifactsFromMeta; - private MavenArtifactVersion proceedDataCache; - private MavenArtifact metaProductArtifact; @Spy @InjectMocks private VersionServiceImpl versionService; @@ -63,314 +51,114 @@ class VersionServiceImplTest { @Mock private ProductJsonContentRepository productJsonContentRepository; - @BeforeEach() - void prepareBeforeTest() { - archivedArtifactsMap = new HashMap<>(); - artifactsFromMeta = new ArrayList<>(); - metaProductArtifact = new MavenArtifact(); - proceedDataCache = new MavenArtifactVersion(); - repoName = StringUtils.EMPTY; - ReflectionTestUtils.setField(versionService, "archivedArtifactsMap", archivedArtifactsMap); - ReflectionTestUtils.setField(versionService, "artifactsFromMeta", artifactsFromMeta); - ReflectionTestUtils.setField(versionService, "proceedDataCache", proceedDataCache); - ReflectionTestUtils.setField(versionService, "metaProductArtifact", metaProductArtifact); - } + @Mock + private ProductModuleContentRepository productModuleContentRepository; - private void setUpArtifactFromMeta() { - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - metaProductArtifact.setGroupId(groupId); - metaProductArtifact.setArtifactId(artifactId); - metaProductArtifact.setIsProductArtifact(true); - MavenArtifact additionalMavenArtifact = new MavenArtifact(repoUrl, "", groupId, artifactId, "", null, null, null); - artifactsFromMeta.add(metaProductArtifact); - artifactsFromMeta.add(additionalMavenArtifact); + private ProductJsonContent getMockProductJson() { + ProductJsonContent result = new ProductJsonContent(); + String mockContent = """ + { + "$schema": "https://json-schema.axonivy.com/market/10.0.0/product.json", + "installers": [ + { + "id": "maven-import", + "data": { + "projects": [ + { + "groupId": "com.axonivy.utils.bpmnstatistic", + "artifactId": "bpmn-statistic-demo", + "version": "${version}", + "type": "iar" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + }, + { + "id": "maven-dependency", + "data": { + "dependencies": [ + { + "groupId": "com.axonivy.utils.bpmnstatistic", + "artifactId": "bpmn-statistic", + "version": "${version}", + "type": "iar" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + } + ] + } + """; + result.setContent(mockContent); + return result; } @Test void testGetArtifactsAndVersionToDisplay() { - String productId = "adobe-acrobat-sign-connector"; + String productId = "bpmn-statistic"; String targetVersion = "10.0.10"; - setUpArtifactFromMeta(); - when(versionService.getProductMetaArtifacts(Mockito.anyString())).thenReturn(artifactsFromMeta); - when(versionService.getVersionsFromMavenArtifacts()).thenReturn( - List.of(targetVersion)); - when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.empty()); - ArrayList artifactsInVersion = new ArrayList<>(); - artifactsInVersion.add(new MavenArtifactModel()); - when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())).thenReturn( - artifactsInVersion); - Assertions.assertEquals(1, versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); - MavenArtifactVersion proceededData = new MavenArtifactVersion(); - proceededData.getProductArtifactWithVersionReleased().put(targetVersion, new ArrayList<>()); + when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.empty()); + when(mavenArtifactVersionRepository.findById("bpmn-statistic")).thenReturn( + Optional.ofNullable( + MavenArtifactVersion.builder().productId(productId).productArtifactsByVersion( + new HashMap<>()).additionalArtifactsByVersion(new HashMap<>()).build())); + Assertions.assertEquals(0, versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); + + MavenArtifactVersion proceededData = + MavenArtifactVersion.builder().productArtifactsByVersion(new HashMap<>()).additionalArtifactsByVersion( + new HashMap<>()).build(); + proceededData.getProductArtifactsByVersion().put(targetVersion, new ArrayList<>()); + proceededData.getAdditionalArtifactsByVersion().put(targetVersion, new ArrayList<>()); + + MavenArtifactModel mockModel = new MavenArtifactModel(); + mockModel.setName("bpmn-statistic"); + mockModel.setDownloadUrl("https://maven.axonivy.com"); + proceededData.getAdditionalArtifactsByVersion().put("10.0.10", List.of(mockModel)); when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.of(proceededData)); Assertions.assertEquals(1, versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); } - @Test - void testHandleArtifactForVersionToDisplay() { - String newVersionDetected = "10.0.10"; - List result = new ArrayList<>(); - List versionsToDisplay = List.of(newVersionDetected); - ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); - Assertions.assertTrue(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); - Assertions.assertEquals(1, result.size()); - Assertions.assertEquals(newVersionDetected, result.get(0).getVersion()); - Assertions.assertEquals(0, result.get(0).getArtifactsByVersion().size()); - - result = new ArrayList<>(); - ArrayList artifactsInVersion = new ArrayList<>(); - artifactsInVersion.add(new MavenArtifactModel()); - when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())).thenReturn( - artifactsInVersion); - Assertions.assertFalse(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); - Assertions.assertEquals(1, result.size()); - Assertions.assertEquals(1, result.get(0).getArtifactsByVersion().size()); - } @Test - void testGetProductMetaArtifacts() { - Product product = new Product(); - MavenArtifact artifact1 = new MavenArtifact(); - MavenArtifact artifact2 = new MavenArtifact(); - List artifacts = List.of(artifact1, artifact2); - product.setArtifacts(artifacts); - when(productRepository.findById(Mockito.anyString())).thenReturn(Optional.of(product)); - List result = versionService.getProductMetaArtifacts("portal"); - Assertions.assertEquals(artifacts, result); - Assertions.assertNull(versionService.getRepoName()); - - product.setRepositoryName("/market/portal"); - versionService.getProductMetaArtifacts("portal"); - Assertions.assertEquals("portal", versionService.getRepoName()); - } - - @Test - void testUpdateArtifactsInVersionWithProductArtifact() { - String version = "10.0.10"; - ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); - MavenArtifactModel artifactModel = new MavenArtifactModel(); - List mockMavenArtifactModels = List.of(artifactModel); - when(versionService.getProductJsonByVersion(Mockito.anyString())).thenReturn(List.of(new MavenArtifact())); - when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())).thenReturn( - mockMavenArtifactModels); - Assertions.assertEquals(mockMavenArtifactModels, - versionService.updateArtifactsInVersionWithProductArtifact(version)); - Assertions.assertEquals(1, proceedDataCache.getVersions().size()); - Assertions.assertEquals(1, proceedDataCache.getProductArtifactWithVersionReleased().size()); - Assertions.assertEquals(version, proceedDataCache.getVersions().get(0)); - } - - @Test - void testSanitizeMetaArtifactBeforeHandle() { - setUpArtifactFromMeta(); - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String archivedArtifactId1 = "adobe-acrobat-sign-connector"; - String archivedArtifactId2 = "adobe-acrobat-sign-connector"; - ArchivedArtifact archivedArtifact1 = new ArchivedArtifact("10.0.10", groupId, archivedArtifactId1); - ArchivedArtifact archivedArtifact2 = new ArchivedArtifact("10.0.20", groupId, archivedArtifactId2); - artifactsFromMeta.get(1).setArchivedArtifacts(List.of(archivedArtifact2, archivedArtifact1)); - - versionService.sanitizeMetaArtifactBeforeHandle(); - String artifactId = "adobe-acrobat-sign-connector"; - - Assertions.assertEquals(1, artifactsFromMeta.size()); - Assertions.assertEquals(1, archivedArtifactsMap.size()); - Assertions.assertEquals(2, archivedArtifactsMap.get(artifactId).size()); - Assertions.assertEquals(archivedArtifact1, archivedArtifactsMap.get(artifactId).get(0)); - } - + void testGetMavenArtifactsFromProductJsonByVersion() { + when(productJsonContentRepository.findByProductIdAndVersion("bpmn-statistic", "10.0.20")).thenReturn( + Collections.emptyList()); - @Test - void getVersionsFromMavenArtifacts() { - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - String archivedArtifactId = "adobe-sign-connector"; - artifactsFromMeta.add(new MavenArtifact(repoUrl, null, groupId, artifactId, null, null, null, null)); - ArrayList versionFromArtifact = new ArrayList<>(); - versionFromArtifact.add("10.0.6"); - versionFromArtifact.add("10.0.5"); - versionFromArtifact.add("10.0.4"); - - when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId)).thenReturn(versionFromArtifact); - Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); - - List archivedArtifacts = List.of(new ArchivedArtifact("10.0.9", groupId, archivedArtifactId)); - ArrayList versionFromArchivedArtifact = new ArrayList<>(); - versionFromArchivedArtifact.add("10.0.3"); - versionFromArchivedArtifact.add("10.0.2"); - versionFromArchivedArtifact.add("10.0.1"); - artifactsFromMeta.get(0).setArchivedArtifacts(archivedArtifacts); - when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, archivedArtifactId)).thenReturn( - versionFromArchivedArtifact); - versionFromArtifact.addAll(versionFromArchivedArtifact); - Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); - } - - @Test - void testGetVersionsFromArtifactDetails() { - - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - - ArrayList versionFromArtifact = new ArrayList<>(); - versionFromArtifact.add("10.0.16"); - versionFromArtifact.add("10.0.18"); - versionFromArtifact.add("10.0.19"); - versionFromArtifact.add("10.0.20"); - versionFromArtifact.add("10.0.21"); - versionFromArtifact.add("10.0.22"); - versionFromArtifact.add("10.0.23"); - versionFromArtifact.add("10.0.24"); - versionFromArtifact.add("10.0.25"); - - try (MockedStatic xmlUtils = Mockito.mockStatic(XmlReaderUtils.class)) { - xmlUtils.when(() -> XmlReaderUtils.readXMLFromUrl(Mockito.anyString())).thenReturn(versionFromArtifact); - Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, null, null), new ArrayList<>()); - Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId), - versionFromArtifact); - } - } - - @Test - void testBuildMavenMetadataUrlFromArtifact() { - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - String metadataUrl = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/maven-metadata.xml"; - Assertions.assertEquals(StringUtils.EMPTY, - versionService.buildMavenMetadataUrlFromArtifact(repoUrl, null, artifactId)); - Assertions.assertEquals(StringUtils.EMPTY, versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, null), - StringUtils.EMPTY); - Assertions.assertEquals(metadataUrl, - versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactId)); - } - - @Test - void testGetProductJsonByVersion() { - String targetArtifactId = "adobe-acrobat-sign-connector"; - String targetGroupId = "com.axonivy.connector.adobe.acrobat"; - GHContent mockContent = mock(GHContent.class); - repoName = "adobe-acrobat-sign-connector"; - ReflectionTestUtils.setField(versionService, "repoName", repoName); - ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); - MavenArtifact productArtifact = new MavenArtifact("https://maven.axonivy.com", null, targetGroupId, - targetArtifactId, "iar", null, true, null); - - metaProductArtifact.setRepoUrl("https://maven.axonivy.com"); - metaProductArtifact.setGroupId(targetGroupId); - metaProductArtifact.setArtifactId(targetArtifactId); - when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), - Mockito.anyString())).thenReturn(null); - Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); - - metaProductArtifact.setGroupId("com.axonivy.connector.adobe.acrobat.connector"); - when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), - Mockito.anyString())).thenReturn(mockContent); - - try { - when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)).thenReturn(List.of(productArtifact)); - Assertions.assertEquals(1, versionService.getProductJsonByVersion("10.0.20").size()); - - when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)).thenThrow( - new IOException("Mock IO Exception")); - Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); - } catch (IOException e) { - Fail.fail("Mock setup should not throw an exception"); - } - } + Assertions.assertEquals(0, + versionService.getMavenArtifactsFromProductJsonByTag("10.0.20", "bpmn-statistic").size()); - @Test - void testConvertMavenArtifactToModel() { - String downloadUrl = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/10.0.21/adobe-acrobat-sign-connector-10.0.21.iar"; - String artifactName = "Adobe Acrobat Sign Connector (iar)"; - - MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", - "adobe-acrobat-sign-connector", null, null, null, null); - - // Assert case handle artifact without name - MavenArtifactModel result = versionService.convertMavenArtifactToModel(targetArtifact, "10.0.21"); - MavenArtifactModel expectedResult = new MavenArtifactModel(artifactName, downloadUrl, null); - Assertions.assertEquals(expectedResult.getName(), result.getName()); - Assertions.assertEquals(expectedResult.getDownloadUrl(), result.getDownloadUrl()); - - // Assert case handle artifact with name - artifactName = "Adobe Connector"; - String expectedArtifactName = "Adobe Connector (iar)"; - targetArtifact.setName(artifactName); - result = versionService.convertMavenArtifactToModel(targetArtifact, "10.0.21"); - expectedResult = new MavenArtifactModel(artifactName, downloadUrl, null); - Assertions.assertEquals(expectedArtifactName, result.getName()); - Assertions.assertEquals(expectedResult.getDownloadUrl(), result.getDownloadUrl()); - } - @Test - void testConvertMavenArtifactsToModels() { - // Assert case param is empty - List result = versionService.convertMavenArtifactsToModels(Collections.emptyList(), "10.0.21"); - Assertions.assertEquals(Collections.emptyList(), result); - - // Assert case param is null - result = versionService.convertMavenArtifactsToModels(null, "10.0.21"); - Assertions.assertEquals(Collections.emptyList(), result); - - // Assert case param is a list with existed element - MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", - "adobe-acrobat-sign-connector", null, null, null, null); - result = versionService.convertMavenArtifactsToModels(List.of(targetArtifact), "10.0.21"); - Assertions.assertEquals(1, result.size()); + when(productJsonContentRepository.findByProductIdAndVersion("bpmn-statistic", "10.0.20")).thenReturn( + List.of(getMockProductJson())); + List results = versionService.getMavenArtifactsFromProductJsonByTag("10.0.20", "bpmn-statistic"); + Assertions.assertEquals(2, results.size()); } - @Test - void testBuildDownloadUrlFromArtifactAndVersion() { - // Set up artifact for testing - String targetArtifactId = "adobe-acrobat-sign-connector"; - String targetGroupId = "com.axonivy.connector"; - MavenArtifact targetArtifact = new MavenArtifact(null, null, targetGroupId, targetArtifactId, "iar", null, null, - null); - String targetVersion = "10.0.10"; - - // Assert case without archived artifact - String expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, - MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL, "com/axonivy/connector", targetArtifactId, targetVersion, - targetArtifactId, targetVersion, "iar"); - String result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); - Assertions.assertEquals(expectedResult, result); - - // Assert case with artifact not match & use custom repo - ArchivedArtifact adobeArchivedArtifactVersion9 = new ArchivedArtifact("10.0.9", "com.axonivy.adobe.connector", - "adobe-connector"); - ArchivedArtifact adobeArchivedArtifactVersion8 = new ArchivedArtifact("10.0.8", "com.axonivy.adobe.sign.connector", - "adobe-sign-connector"); - archivedArtifactsMap.put(targetArtifactId, List.of(adobeArchivedArtifactVersion9, adobeArchivedArtifactVersion8)); - String customRepoUrl = "https://nexus.axonivy.com"; - targetArtifact.setRepoUrl(customRepoUrl); - result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); - expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, customRepoUrl, "com/axonivy/connector", - targetArtifactId, targetVersion, targetArtifactId, targetVersion, "iar"); - Assertions.assertEquals(expectedResult, result); - - // Assert case with artifact got matching archived artifact & use custom file - // type - String customType = "zip"; - targetArtifact.setType(customType); - targetVersion = "10.0.9"; - result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, "10.0.9"); - expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, customRepoUrl, - "com/axonivy/adobe/connector", "adobe-connector", targetVersion, "adobe-connector", targetVersion, customType); - Assertions.assertEquals(expectedResult, result); - } @Test void testFindArchivedArtifactInfoBestMatchWithVersion() { - String targetArtifactId = "adobe-acrobat-sign-connector"; String targetVersion = "10.0.10"; - ArchivedArtifact result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, - targetVersion); + ArchivedArtifact result = MavenUtils.findArchivedArtifactInfoBestMatchWithVersion( + targetVersion, Collections.emptyList()); Assertions.assertNull(result); // Assert case with target version higher than all of latest version from @@ -382,53 +170,43 @@ void testFindArchivedArtifactInfoBestMatchWithVersion() { List archivedArtifacts = new ArrayList<>(); archivedArtifacts.add(adobeArchivedArtifactVersion8); archivedArtifacts.add(adobeArchivedArtifactVersion9); - archivedArtifactsMap.put(targetArtifactId, archivedArtifacts); - result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); + result = MavenUtils.findArchivedArtifactInfoBestMatchWithVersion(targetVersion, + archivedArtifacts); Assertions.assertNull(result); // Assert case with target version less than all of latest version from archived // artifact list - result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, "10.0.7"); + result = MavenUtils.findArchivedArtifactInfoBestMatchWithVersion("10.0.7", + archivedArtifacts); Assertions.assertEquals(adobeArchivedArtifactVersion8, result); // Assert case with target version is in range of archived artifact list ArchivedArtifact adobeArchivedArtifactVersion10 = new ArchivedArtifact("10.0.10", "com.axonivy.connector", "adobe-sign-connector"); - - archivedArtifactsMap.get(targetArtifactId).add(adobeArchivedArtifactVersion10); - result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); - Assertions.assertEquals(adobeArchivedArtifactVersion10, result); - } - - @Test - void testGetRepoNameFromMarketRepo() { - String defaultRepositoryName = "market/adobe-acrobat-connector"; - String expectedRepoName = "adobe-acrobat-connector"; - String result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); - Assertions.assertEquals(expectedRepoName, result); - - defaultRepositoryName = "market/utils/adobe-acrobat-connector"; - result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); - Assertions.assertEquals(expectedRepoName, result); - - defaultRepositoryName = "adobe-acrobat-connector"; - result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); - Assertions.assertEquals(expectedRepoName, result); + archivedArtifacts.add(adobeArchivedArtifactVersion10); + result = MavenUtils.findArchivedArtifactInfoBestMatchWithVersion(targetVersion, + archivedArtifacts); + Assertions.assertEquals(adobeArchivedArtifactVersion10.getArtifactId(), result.getArtifactId()); } @Test void testGetVersionsForDesigner() { - Mockito.when(productRepository.getReleasedVersionsById(anyString())) - .thenReturn(List.of("11.3.0", "11.1.1", "11.1.0", "10.0.2")); + MavenArtifactVersion mockMavenArtifactVersion = new MavenArtifactVersion(); + mockMavenArtifactVersion.getProductArtifactsByVersion().put("11.3.0-SNAPSHOT", new ArrayList<>()); + mockMavenArtifactVersion.getProductArtifactsByVersion().put("11.1.1", new ArrayList<>()); + mockMavenArtifactVersion.getProductArtifactsByVersion().put("11.1.0", new ArrayList<>()); + mockMavenArtifactVersion.getProductArtifactsByVersion().put("10.0.2", new ArrayList<>()); + + when(mavenArtifactVersionRepository.findById("portal")).thenReturn(Optional.of(mockMavenArtifactVersion)); - List result = versionService.getVersionsForDesigner("11.3.0"); + List result = versionService.getVersionsForDesigner("portal"); Assertions.assertEquals(result.stream().map(VersionAndUrlModel::getVersion).toList(), - List.of("11.3.0", "11.1.1", "11.1.0", "10.0.2")); - Assertions.assertEquals("/api/product-details/11.3.0/11.3.0/json", result.get(0).getUrl()); - Assertions.assertEquals("/api/product-details/11.3.0/11.1.1/json", result.get(1).getUrl()); - Assertions.assertEquals("/api/product-details/11.3.0/11.1.0/json", result.get(2).getUrl()); - Assertions.assertEquals("/api/product-details/11.3.0/10.0.2/json", result.get(3).getUrl()); + List.of("11.3.0-SNAPSHOT", "11.1.1", "11.1.0", "10.0.2")); + Assertions.assertTrue(result.get(0).getUrl().endsWith("/api/product-details/portal/11.3.0-SNAPSHOT/json")); + Assertions.assertTrue(result.get(1).getUrl().endsWith("/api/product-details/portal/11.1.1/json")); + Assertions.assertTrue(result.get(2).getUrl().endsWith("/api/product-details/portal/11.1.0/json")); + Assertions.assertTrue(result.get(3).getUrl().endsWith("/api/product-details/portal/10.0.2/json")); } @Test @@ -460,19 +238,47 @@ void testGetProductJsonContentByIdAndVersion() { mockProductJsonContent.setContent(mockContent); Mockito.when(productJsonContentRepository.findByProductIdAndVersion(anyString(), anyString())) - .thenReturn(mockProductJsonContent); + .thenReturn(List.of(mockProductJsonContent)); - Map result = versionService.getProductJsonContentByIdAndVersion("amazon-comprehend", "11.3.1"); + Map result = versionService.getProductJsonContentByIdAndTag("amazon-comprehend", "11.3.1"); Assertions.assertEquals("Amazon Comprehend", result.get("name")); } @Test void testGetProductJsonContentByIdAndVersion_noResult() { - Mockito.when(productJsonContentRepository.findByProductIdAndVersion(anyString(), anyString())).thenReturn(null); + Mockito.when(productJsonContentRepository.findByProductIdAndVersion(anyString(), anyString())).thenReturn( + Collections.emptyList()); - Map result = versionService.getProductJsonContentByIdAndVersion("amazon-comprehend", "11.3.1"); + Map result = versionService.getProductJsonContentByIdAndTag("amazon-comprehend", "11.3.1"); Assertions.assertEquals(new HashMap<>(), result); } + + @Test + void testGetPersistedVersions() { + String mockProductId = "portal"; + Assertions.assertEquals(0, versionService.getPersistedVersions(mockProductId).size()); + String mockVersion = "10.0.1"; + Product mocProduct = new Product(); + mocProduct.setId(mockProductId); + mocProduct.setReleasedVersions(List.of(mockVersion)); + when(productRepository.findById(mockProductId)).thenReturn(Optional.of(mocProduct)); + Assertions.assertEquals(1, versionService.getPersistedVersions(mockProductId).size()); + Assertions.assertEquals(mockVersion, versionService.getPersistedVersions(mockProductId).get(0)); + } + + @Test + void testGetAllExistingVersions() { + MavenArtifactVersion mockMavenArtifactVersion = new MavenArtifactVersion(); + Assertions.assertEquals(0, MavenUtils.getAllExistingVersions(mockMavenArtifactVersion, false, + StringUtils.EMPTY).size()); + Map> mockArtifactModelsByVersion = new HashMap<>(); + mockArtifactModelsByVersion.put("1.0.0-SNAPSHOT", new ArrayList<>()); + mockMavenArtifactVersion.setProductArtifactsByVersion(mockArtifactModelsByVersion); + Assertions.assertEquals(1, MavenUtils.getAllExistingVersions(mockMavenArtifactVersion, true, + StringUtils.EMPTY).size()); + Assertions.assertEquals(0, MavenUtils.getAllExistingVersions(mockMavenArtifactVersion, false, + StringUtils.EMPTY).size()); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/ImageUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/ImageUtilsTest.java index 07e7a218e..f5c2d897c 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/util/ImageUtilsTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/util/ImageUtilsTest.java @@ -1,14 +1,13 @@ package com.axonivy.market.util; -import java.util.HashMap; -import java.util.Map; - +import com.axonivy.market.entity.ProductModuleContent; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; -import com.axonivy.market.entity.ProductModuleContent; +import java.util.HashMap; +import java.util.Map; @ExtendWith(MockitoExtension.class) class ImageUtilsTest { @@ -21,7 +20,7 @@ void testMappingImageForProductModuleContent() { Assertions.assertEquals(expectedValue, result.getSetup().get("de")); } - private ProductModuleContent mockProductModuleContent(){ + private ProductModuleContent mockProductModuleContent() { ProductModuleContent productModuleContent = new ProductModuleContent(); productModuleContent.setDescription(mockDescriptionForProductModuleContent()); productModuleContent.setDemo(null); @@ -30,7 +29,7 @@ private ProductModuleContent mockProductModuleContent(){ return productModuleContent; } - private Map mockDescriptionForProductModuleContent(){ + private Map mockDescriptionForProductModuleContent() { Map mutableMap = new HashMap<>(); mutableMap.put("en", "Login or create a new account.[demo-process](imageId-66e2b13c68f2f95b2f95548c)"); mutableMap.put("de", "Login or create a new account.[demo-process](imageId-66e2b13c68f2f95b2f95548c)"); diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/MavenUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/MavenUtilsTest.java new file mode 100644 index 000000000..a4991c4da --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/util/MavenUtilsTest.java @@ -0,0 +1,211 @@ +package com.axonivy.market.util; + +import com.axonivy.market.bo.ArchivedArtifact; +import com.axonivy.market.bo.Artifact; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.MavenConstants; +import com.axonivy.market.constants.ProductJsonConstants; +import com.axonivy.market.entity.Metadata; +import com.axonivy.market.model.MavenArtifactModel; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +class MavenUtilsTest { + private static final String TEST_GROUP_ID = "com.axonivy.util"; + private static final String DEFAULT_TEST_MAVEN_URL = "https://maven.axonivy.com/com/axonivy/util/octopus/maven" + + "-metadata.xml"; + private static Metadata buildMocKMetadata() { + return Metadata.builder().url( + DEFAULT_TEST_MAVEN_URL).repoUrl("https://maven.axonivy.com").groupId(TEST_GROUP_ID).artifactId("octopus").type( + "zip").productId("octopus").build(); + } + + private static Artifact createMockArtifact() { + Artifact artifact = new Artifact(); + artifact.setArtifactId("octopus"); + artifact.setGroupId(TEST_GROUP_ID); + artifact.setRepoUrl(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + return artifact; + } + + @Test + void testConvertArtifactsToModels() { + // Assert case param is empty + List result = MavenUtils.convertArtifactsToModels(Collections.emptyList(), "10.0.21"); + Assertions.assertEquals(Collections.emptyList(), result); + + // Assert case param is null + result = MavenUtils.convertArtifactsToModels(null, "10.0.21"); + Assertions.assertEquals(Collections.emptyList(), result); + + // Assert case param is a list with existed element + Artifact targetArtifact = new Artifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", + "adobe-acrobat-sign-connector", null, null, null, null, false); + result = MavenUtils.convertArtifactsToModels(List.of(targetArtifact), "10.0.21"); + Assertions.assertEquals(1, result.size()); + } + + @Test + void testBuildDownloadUrlFromArtifactAndVersion() { + // Set up artifact for testing + String targetArtifactId = "adobe-acrobat-sign-connector"; + String targetGroupId = "com.axonivy.connector"; + Artifact targetArtifact = new Artifact(null, null, targetGroupId, targetArtifactId, "iar", null, null, null, false); + String targetVersion = "10.0.10"; + String artifactFileName = String.format(MavenConstants.ARTIFACT_FILE_NAME_FORMAT, targetArtifactId, targetVersion, + "iar"); + + // Assert case without archived artifact + String expectedResult = String.join(CommonConstants.SLASH, MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL, + "com/axonivy/connector", targetArtifactId, targetVersion, artifactFileName); + String result = MavenUtils.buildDownloadUrl(targetArtifact, targetVersion); + Assertions.assertEquals(expectedResult, result); + + // Assert case with artifact not match & use custom repo + ArchivedArtifact adobeArchivedArtifactVersion9 = new ArchivedArtifact("10.0.9", "com.axonivy.adobe.connector", + "adobe-connector"); + ArchivedArtifact adobeArchivedArtifactVersion8 = new ArchivedArtifact("10.0.8", "com.axonivy.adobe.sign.connector", + "adobe-sign-connector"); + String customRepoUrl = "https://nexus.axonivy.com"; + targetArtifact.setRepoUrl(customRepoUrl); + targetArtifact.setArchivedArtifacts(List.of(adobeArchivedArtifactVersion9, adobeArchivedArtifactVersion8)); + result = MavenUtils.buildDownloadUrl(targetArtifact, targetVersion); + artifactFileName = String.format(MavenConstants.ARTIFACT_FILE_NAME_FORMAT, targetArtifactId, targetVersion, "iar"); + expectedResult = String.join(CommonConstants.SLASH, customRepoUrl, "com/axonivy/connector", targetArtifactId, + targetVersion, artifactFileName); + Assertions.assertEquals(expectedResult, result); + + // Assert case with artifact got matching archived artifact & use custom file + // type + String customType = "zip"; + targetArtifact.setType(customType); + targetVersion = "10.0.9"; + targetArtifact.setArchivedArtifacts(List.of(adobeArchivedArtifactVersion9, adobeArchivedArtifactVersion8)); + result = MavenUtils.buildDownloadUrl(targetArtifact, "10.0.9"); + artifactFileName = String.format(MavenConstants.ARTIFACT_FILE_NAME_FORMAT, "adobe-connector", targetVersion, + customType); + expectedResult = String.join(CommonConstants.SLASH, customRepoUrl, "com/axonivy/adobe/connector", "adobe-connector", + targetVersion, artifactFileName); + Assertions.assertEquals(expectedResult, result); + } + + @Test + void testBuildSnapshotMetadataFromVersionUrlFromArtifactInfo() { + Assertions.assertEquals(StringUtils.EMPTY, + MavenUtils.buildSnapshotMetadataUrlFromArtifactInfo(null, null, null, null)); + Assertions.assertEquals( + "https://maven.axonivy.com/com/axonivy/util/octopus-demo/1.0.0-SNAPSHOT/maven-metadata.xml", + MavenUtils.buildSnapshotMetadataUrlFromArtifactInfo(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL, + TEST_GROUP_ID, "octopus-demo", "1.0.0-SNAPSHOT")); + } + + @Test + void testBuildMetadataUrlFromArtifactInfo() { + Assertions.assertEquals(StringUtils.EMPTY, MavenUtils.buildMetadataUrlFromArtifactInfo(null, null, null)); + + Assertions.assertEquals("https://maven.axonivy.com/com/axonivy/util/octopus-demo/maven-metadata.xml", + MavenUtils.buildMetadataUrlFromArtifactInfo(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL, TEST_GROUP_ID, + "octopus-demo")); + } + + @Test + void testConvertArtifactToMetadata() { + Artifact artifact = createMockArtifact(); + String metadataUrl = "https://maven.axonovy.com/com/axonivy/util/octopus-demo/maven-metadata.xml"; + Metadata result = MavenUtils.convertArtifactToMetadata("octopus", artifact, + metadataUrl); + Assertions.assertEquals(ProductJsonConstants.DEFAULT_PRODUCT_TYPE, result.getType()); + Assertions.assertEquals("Octopus (iar)", result.getName()); + Assertions.assertEquals(0, result.getVersions().size()); + Assertions.assertEquals(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL, result.getRepoUrl()); + + artifact.setName("octopus demo"); + result = MavenUtils.convertArtifactToMetadata("octopus", artifact, + metadataUrl); + Assertions.assertEquals("octopus demo (iar)", result.getName()); + } + + @Test + void testBuildSnapShotMetadataFromVersion() { + Metadata originalMetadata = buildMocKMetadata(); + Metadata snapShotMetadata = MavenUtils.buildSnapShotMetadataFromVersion(originalMetadata, "1.0.0-SNAPSHOT"); + Assertions.assertEquals(originalMetadata.getRepoUrl(), snapShotMetadata.getRepoUrl()); + Assertions.assertEquals(originalMetadata.getGroupId(), snapShotMetadata.getGroupId()); + Assertions.assertEquals(originalMetadata.getArtifactId(), snapShotMetadata.getArtifactId()); + Assertions.assertEquals(originalMetadata.getProductId(), snapShotMetadata.getProductId()); + Assertions.assertEquals(originalMetadata.getType(), snapShotMetadata.getType()); + Assertions.assertEquals("https://maven.axonivy.com/com/axonivy/util/octopus/1.0.0-SNAPSHOT/maven-metadata.xml", + snapShotMetadata.getUrl()); + } + + @Test + void testBuildMavenArtifactModelFromMetadata() { + Metadata mocKMetadata = buildMocKMetadata(); + mocKMetadata.setSnapshotVersionValue("20241111-111111"); + MavenArtifactModel result = MavenUtils.buildMavenArtifactModelFromMetadata("1.0.0-SNAPSHOT", mocKMetadata); + Assertions.assertEquals( + "https://maven.axonivy.com/com/axonivy/util/octopus/1.0.0-SNAPSHOT/octopus-20241111-111111.zip", + result.getDownloadUrl()); + } + + @Test + void testConvertArtifactsToMetadataSet() { + Artifact artifact = createMockArtifact(); + Set results = MavenUtils.convertArtifactsToMetadataSet(Set.of(artifact), "octopus"); + Assertions.assertEquals(1, results.size()); + Assertions.assertEquals(DEFAULT_TEST_MAVEN_URL, results.iterator().next().getUrl()); + results = MavenUtils.convertArtifactsToMetadataSet(Collections.emptySet(), "octopus"); + Assertions.assertEquals(0, results.size()); + + ArchivedArtifact mockArchivedArtifact = new ArchivedArtifact(); + mockArchivedArtifact.setArtifactId("octopus-test"); + mockArchivedArtifact.setGroupId("com.octopus.util"); + artifact.setArchivedArtifacts(List.of(mockArchivedArtifact)); + + results = MavenUtils.convertArtifactsToMetadataSet(Set.of(artifact), "octopus"); + Assertions.assertEquals(2, results.size()); + + } + + @Test + void testExtractMetaDataFromArchivedArtifacts() { + Set results = MavenUtils.extractMetaDataFromArchivedArtifacts("octopus", new Artifact()); + Assertions.assertEquals(0, results.size()); + + Artifact mockArtifact = createMockArtifact(); + ArchivedArtifact mockArchivedArtifact = new ArchivedArtifact(); + mockArchivedArtifact.setArtifactId("octopus-test"); + mockArchivedArtifact.setGroupId("com.octopus.util"); + mockArtifact.setArchivedArtifacts(List.of(mockArchivedArtifact)); + + results = MavenUtils.extractMetaDataFromArchivedArtifacts("octopus", mockArtifact); + + Assertions.assertEquals(1, results.size()); + Assertions.assertEquals("https://maven.axonivy.com/com/octopus/util/octopus-test/maven-metadata.xml", + results.iterator().next().getUrl()); + } + + @Test + void testBuildDownloadUrl() { + Metadata metadata = buildMocKMetadata(); + Assertions.assertEquals( + "https://maven.axonivy.com/com/axonivy/util/octopus/1.0.0-SNAPSHOT/octopus-1.0.0-SNAPSHOT.zip", + MavenUtils.buildDownloadUrl(metadata, "1.0.0-SNAPSHOT")); + } + + @Test + void testGetMetadataContent() { + Assertions.assertEquals(StringUtils.EMPTY, MavenUtils.getMetadataContentFromUrl("octopus.com")); + } + + @Test + void testFFilterNonProductArtifactFromList() { + Assertions.assertNull(MavenUtils.filterNonProductArtifactFromList(null)); + Assertions.assertEquals(0, MavenUtils.filterNonProductArtifactFromList(Collections.emptyList()).size()); + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/MetadataReaderUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/MetadataReaderUtilsTest.java new file mode 100644 index 000000000..d01c4e42c --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/util/MetadataReaderUtilsTest.java @@ -0,0 +1,82 @@ +package com.axonivy.market.util; + +import com.axonivy.market.constants.MavenConstants; +import com.axonivy.market.entity.Metadata; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@ExtendWith(MockitoExtension.class) +class MetadataReaderUtilsTest { + public final String MOCK_SNAPSHOT = """ + + com.axonivy.demo + workflow-demos + 8.0.5-SNAPSHOT + + + 20221011.124215 + 170 + + 20221011130000 + + + iar + 8.0.5-20221011.124215-170 + 20221011124215 + + + + + """; + private final String MOCK_METADATA = """ + + 1.0.2 + 1.0.1 + 20230924010101 + + 1.0.0 + 1.0.1 + 1.0.2 + + + """; + private static final String INVALID_METADATA = ""; + + private Metadata metadata; + + @BeforeEach + void setUp() { + metadata = Metadata.builder().build(); + } + + @Test + void testUpdateMetadataFromReleasesMavenXML() { + Metadata modifiedMetadata = MetadataReaderUtils.updateMetadataFromMavenXML(MOCK_METADATA, metadata, false); + LocalDateTime expectedLastUpdated = LocalDateTime.parse("20230924010101", + DateTimeFormatter.ofPattern(MavenConstants.DATE_TIME_FORMAT)); + + Assertions.assertEquals("1.0.2", modifiedMetadata.getLatest()); + Assertions.assertEquals("1.0.1", modifiedMetadata.getRelease()); + Assertions.assertEquals(expectedLastUpdated, modifiedMetadata.getLastUpdated()); + } + + @Test + void testUpdateMetadataFromInvalidSnapshotMavenXML() { + MetadataReaderUtils.updateMetadataFromMavenXML(INVALID_METADATA, metadata, true); + Assertions.assertNull(metadata.getLatest()); + Assertions.assertNull(metadata.getRelease()); + Assertions.assertNull(metadata.getLastUpdated()); + } + + @Test + void testUpdateMetadataFromSnapshotXml() { + MetadataReaderUtils.updateMetadataFromMavenXML(MOCK_SNAPSHOT, metadata, true); + Assertions.assertEquals("8.0.5-20221011.124215-170", metadata.getSnapshotVersionValue()); + } +} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/ProductContentUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/ProductContentUtilsTest.java new file mode 100644 index 000000000..4dd8ee2a3 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/util/ProductContentUtilsTest.java @@ -0,0 +1,44 @@ +package com.axonivy.market.util; + +import com.axonivy.market.bo.Artifact; +import com.axonivy.market.entity.ProductModuleContent; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +@ExtendWith(MockitoExtension.class) +class ProductContentUtilsTest { + @Test + void testUpdateProductModule() { + ProductModuleContent mockProductModuleContent = new ProductModuleContent(); + Artifact mockArtifact = new Artifact(); + mockArtifact.setIsDependency(true); + mockArtifact.setGroupId("com.axonivy.utils"); + mockArtifact.setArtifactId("octopus"); + mockArtifact.setType("zip"); + mockArtifact.setName("Octopus demo (zip)"); + ProductContentUtils.updateProductModule(mockProductModuleContent, List.of(mockArtifact)); + Assertions.assertEquals(mockArtifact.getGroupId(), mockProductModuleContent.getGroupId()); + Assertions.assertEquals(mockArtifact.getArtifactId(), mockProductModuleContent.getArtifactId()); + Assertions.assertEquals(mockArtifact.getName(), mockProductModuleContent.getName()); + Assertions.assertEquals(mockArtifact.getType(), mockProductModuleContent.getType()); + } + + @Test + void testRemoveFirstLine() { + Assertions.assertEquals(StringUtils.EMPTY, ProductContentUtils.removeFirstLine(null)); + Assertions.assertEquals(StringUtils.EMPTY, ProductContentUtils.removeFirstLine(" ")); + Assertions.assertEquals(StringUtils.EMPTY, ProductContentUtils.removeFirstLine("#")); + Assertions.assertEquals("Second line", ProductContentUtils.removeFirstLine("#First line\nSecond line")); + } + + @Test + void testGetReadmeFileLocale() { + Assertions.assertEquals(StringUtils.EMPTY, ProductContentUtils.getReadmeFileLocale("README.md")); + Assertions.assertEquals("DE", ProductContentUtils.getReadmeFileLocale("README_DE.md")); + } +} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/VersionUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/VersionUtilsTest.java index a1686746f..b1c70312c 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/util/VersionUtilsTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/util/VersionUtilsTest.java @@ -1,5 +1,6 @@ package com.axonivy.market.util; +import com.axonivy.market.entity.Product; import com.axonivy.market.enums.NonStandardProduct; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; @@ -12,171 +13,198 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Set; @ExtendWith(MockitoExtension.class) - class VersionUtilsTest { - @InjectMocks - private VersionUtils versionUtils; - - @Test - void testIsSnapshotVersion() { - String targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertTrue(VersionUtils.isSnapshotVersion(targetVersion)); - - targetVersion = "10.0.21-m1234"; - Assertions.assertFalse(VersionUtils.isSnapshotVersion(targetVersion)); - - targetVersion = "10.0.21"; - Assertions.assertFalse(VersionUtils.isSnapshotVersion(targetVersion)); - } - - @Test - void testIsSprintVersion() { - String targetVersion = "10.0.21-m1234"; - Assertions.assertTrue(VersionUtils.isSprintVersion(targetVersion)); - - targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertFalse(VersionUtils.isSprintVersion(targetVersion)); - - targetVersion = "10.0.21"; - Assertions.assertFalse(VersionUtils.isSprintVersion(targetVersion)); - } - - @Test - void testIsReleasedVersion() { - String targetVersion = "10.0.21"; - Assertions.assertTrue(VersionUtils.isReleasedVersion(targetVersion)); - - targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertFalse(VersionUtils.isReleasedVersion(targetVersion)); - - targetVersion = "10.0.21-m1231"; - Assertions.assertFalse(VersionUtils.isReleasedVersion(targetVersion)); - } - - @Test - void testIsMatchWithDesignerVersion() { - String designerVersion = "10.0.21"; - String targetVersion = "10.0.21.2"; - Assertions.assertTrue(VersionUtils.isMatchWithDesignerVersion(targetVersion, designerVersion)); - - targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertFalse(VersionUtils.isMatchWithDesignerVersion(targetVersion, designerVersion)); - - targetVersion = "10.0.19"; - Assertions.assertFalse(VersionUtils.isMatchWithDesignerVersion(targetVersion, designerVersion)); - } - - @Test - void testConvertVersionToTag() { - - String rawVersion = StringUtils.EMPTY; - Assertions.assertEquals(rawVersion, VersionUtils.convertVersionToTag(StringUtils.EMPTY, rawVersion)); - - rawVersion = "11.0.0"; - String tag = "11.0.0"; - Assertions.assertEquals(tag, VersionUtils.convertVersionToTag(NonStandardProduct.PORTAL.getId(), rawVersion)); - - tag = "v11.0.0"; - Assertions.assertEquals(tag, VersionUtils.convertVersionToTag(NonStandardProduct.GRAPHQL_DEMO.getId(), rawVersion)); - } - - @Test - void testGetVersionsToDisplay() { - ArrayList versionFromArtifact = new ArrayList<>(); - versionFromArtifact.add("10.0.6"); - versionFromArtifact.add("10.0.5"); - versionFromArtifact.add("10.0.4"); - versionFromArtifact.add("10.0.3-SNAPSHOT"); - Assertions.assertEquals(versionFromArtifact, VersionUtils.getVersionsToDisplay(versionFromArtifact, true, null)); - Assertions.assertEquals(List.of("10.0.5"), VersionUtils.getVersionsToDisplay(versionFromArtifact, null, "10.0.5")); - versionFromArtifact.remove("10.0.3-SNAPSHOT"); - Assertions.assertEquals(versionFromArtifact, VersionUtils.getVersionsToDisplay(versionFromArtifact, null, null)); - } - - - @Test - void testIsReleasedVersionOrUnReleaseDevVersion() { - String releasedVersion = "10.0.20"; - String snapshotVersion = "10.0.20-SNAPSHOT"; - String sprintVersion = "10.0.20-m1234"; - String minorSprintVersion = "10.0.20.1-m1234"; - String unreleasedSprintVersion = "10.0.21-m1235"; - List versions = List.of(releasedVersion, snapshotVersion, sprintVersion, unreleasedSprintVersion); - Assertions.assertTrue(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, releasedVersion)); - Assertions.assertFalse(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, sprintVersion)); - Assertions.assertFalse(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, snapshotVersion)); - Assertions.assertFalse(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, minorSprintVersion)); - Assertions.assertTrue(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, unreleasedSprintVersion)); - } - - @Test - void testGetBugfixVersion() { - String releasedVersion = "10.0.20"; - String shortReleasedVersion = "10.0"; - String snapshotVersion = "10.0.20-SNAPSHOT"; - String sprintVersion = "10.0.20-m1234"; - String minorSprintVersion = "10.0.20.1-m1234"; - Assertions.assertEquals(releasedVersion, VersionUtils.getBugfixVersion(releasedVersion)); - Assertions.assertEquals(releasedVersion, VersionUtils.getBugfixVersion(snapshotVersion)); - Assertions.assertEquals(releasedVersion, VersionUtils.getBugfixVersion(sprintVersion)); - Assertions.assertEquals(releasedVersion, VersionUtils.getBugfixVersion(minorSprintVersion)); - Assertions.assertEquals(shortReleasedVersion, VersionUtils.getBugfixVersion(shortReleasedVersion)); - - } - - @Test - void testGetBestMatchVersion() { - List releasedVersions = List.of("10.0.21-SNAPSHOT", "10.0.21", "10.0.19", "10.0.17"); - Assertions.assertEquals("10.0.19", VersionUtils.getBestMatchVersion(releasedVersions, "10.0.19")); - Assertions.assertEquals("10.0.21", VersionUtils.getBestMatchVersion(releasedVersions, "10.0.22")); - Assertions.assertEquals("10.0.17", VersionUtils.getBestMatchVersion(releasedVersions, "10.0.18")); - Assertions.assertEquals("10.0.21", VersionUtils.getBestMatchVersion(releasedVersions, "10.0.16")); - } - - @Test - void testConvertTagToVersion() { - Assertions.assertEquals("10.0.19", VersionUtils.convertTagToVersion("10.0.19")); - Assertions.assertEquals("10.0.19", VersionUtils.convertTagToVersion("v10.0.19")); - Assertions.assertEquals("", VersionUtils.convertTagToVersion("")); - } - - @Test - void testConvertTagsToVersions() { - List results = VersionUtils.convertTagsToVersions(List.of("10.0.1", "v10.0.2")); - Assertions.assertEquals(2, results.size()); - Assertions.assertEquals("10.0.1", results.get(0)); - Assertions.assertEquals("10.0.2", results.get(1)); - } - - @Test - void testGetOldestVersionWithEmptyTags() { - List tags = List.of(); - - String oldestTag = VersionUtils.getOldestVersion(tags); - - Assertions.assertEquals(StringUtils.EMPTY, oldestTag); - } - - @Test - void testGetOldestVersionWithNullTags() { - String oldestTag = VersionUtils.getOldestVersion(null); - - Assertions.assertEquals(StringUtils.EMPTY, oldestTag); - } - - @Test - void testGetOldestVersionWithNonNumericCharacters() { - GHTag tag1 = Mockito.mock(GHTag.class); - GHTag tag2 = Mockito.mock(GHTag.class); - Mockito.when(tag1.getName()).thenReturn("v1.0"); - Mockito.when(tag2.getName()).thenReturn("2.1"); - List tags = Arrays.asList(tag1, tag2); - - String oldestTag = VersionUtils.getOldestVersion(tags); - - Assertions.assertEquals("1.0", oldestTag); // Assuming the replacement of non-numeric characters works correctly - } + @InjectMocks + private VersionUtils versionUtils; + + @Test + void testIsSnapshotVersion() { + String targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertTrue(VersionUtils.isSnapshotVersion(targetVersion)); + + targetVersion = "10.0.21-m1234"; + Assertions.assertFalse(VersionUtils.isSnapshotVersion(targetVersion)); + + targetVersion = "10.0.21"; + Assertions.assertFalse(VersionUtils.isSnapshotVersion(targetVersion)); + } + + @Test + void testIsSprintVersion() { + String targetVersion = "10.0.21-m1234"; + Assertions.assertTrue(VersionUtils.isSprintVersion(targetVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(VersionUtils.isSprintVersion(targetVersion)); + + targetVersion = "10.0.21"; + Assertions.assertFalse(VersionUtils.isSprintVersion(targetVersion)); + } + + @Test + void testIsReleasedVersion() { + String targetVersion = "10.0.21"; + Assertions.assertTrue(VersionUtils.isReleasedVersion(targetVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(VersionUtils.isReleasedVersion(targetVersion)); + + targetVersion = "10.0.21-m1231"; + Assertions.assertFalse(VersionUtils.isReleasedVersion(targetVersion)); + } + + @Test + void testIsMatchWithDesignerVersion() { + String designerVersion = "10.0.21"; + String targetVersion = "10.0.21.2"; + Assertions.assertTrue(VersionUtils.isMatchWithDesignerVersion(targetVersion, designerVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(VersionUtils.isMatchWithDesignerVersion(targetVersion, designerVersion)); + + targetVersion = "10.0.19"; + Assertions.assertFalse(VersionUtils.isMatchWithDesignerVersion(targetVersion, designerVersion)); + } + + @Test + void testConvertVersionToTag() { + + String rawVersion = StringUtils.EMPTY; + Assertions.assertEquals(rawVersion, VersionUtils.convertVersionToTag(StringUtils.EMPTY, rawVersion)); + + rawVersion = "11.0.0"; + String tag = "11.0.0"; + Assertions.assertEquals(tag, VersionUtils.convertVersionToTag(NonStandardProduct.PORTAL.getId(), rawVersion)); + + tag = "v11.0.0"; + Assertions.assertEquals(tag, VersionUtils.convertVersionToTag(NonStandardProduct.GRAPHQL_DEMO.getId(), rawVersion)); + } + + @Test + void testGetVersionsToDisplay() { + ArrayList versionFromArtifact = new ArrayList<>(); + versionFromArtifact.add("10.0.6"); + versionFromArtifact.add("10.0.5"); + versionFromArtifact.add("10.0.4"); + versionFromArtifact.add("10.0.3-SNAPSHOT"); + Assertions.assertEquals(versionFromArtifact, VersionUtils.getVersionsToDisplay(versionFromArtifact, true, null)); + Assertions.assertEquals(List.of("10.0.5"), VersionUtils.getVersionsToDisplay(versionFromArtifact, null, "10.0.5")); + versionFromArtifact.remove("10.0.3-SNAPSHOT"); + Assertions.assertEquals(versionFromArtifact, VersionUtils.getVersionsToDisplay(versionFromArtifact, null, null)); + } + + + @Test + void testIsReleasedVersionOrUnReleaseDevVersion() { + String releasedVersion = "10.0.20"; + String snapshotVersion = "10.0.20-SNAPSHOT"; + String sprintVersion = "10.0.20-m1234"; + String minorSprintVersion = "10.0.20.1-m1234"; + String unreleasedSprintVersion = "10.0.21-m1235"; + List versions = List.of(releasedVersion, snapshotVersion, sprintVersion, unreleasedSprintVersion); + Assertions.assertTrue(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, releasedVersion)); + Assertions.assertFalse(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, sprintVersion)); + Assertions.assertFalse(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, snapshotVersion)); + Assertions.assertFalse(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, minorSprintVersion)); + Assertions.assertTrue(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, unreleasedSprintVersion)); + } + + @Test + void testGetBugfixVersion() { + String releasedVersion = "10.0.20"; + String shortReleasedVersion = "10.0"; + String snapshotVersion = "10.0.20-SNAPSHOT"; + String sprintVersion = "10.0.20-m1234"; + String minorSprintVersion = "10.0.20.1-m1234"; + Assertions.assertEquals(releasedVersion, VersionUtils.getBugfixVersion(releasedVersion)); + Assertions.assertEquals(releasedVersion, VersionUtils.getBugfixVersion(snapshotVersion)); + Assertions.assertEquals(releasedVersion, VersionUtils.getBugfixVersion(sprintVersion)); + Assertions.assertEquals(releasedVersion, VersionUtils.getBugfixVersion(minorSprintVersion)); + Assertions.assertEquals(shortReleasedVersion, VersionUtils.getBugfixVersion(shortReleasedVersion)); + + } + + @Test + void testGetBestMatchVersion() { + List releasedVersions = List.of("10.0.21-SNAPSHOT", "10.0.21", "10.0.19", "10.0.17"); + Assertions.assertEquals("10.0.19", VersionUtils.getBestMatchVersion(releasedVersions, "10.0.19")); + Assertions.assertEquals("10.0.21", VersionUtils.getBestMatchVersion(releasedVersions, "10.0.22")); + Assertions.assertEquals("10.0.17", VersionUtils.getBestMatchVersion(releasedVersions, "10.0.18")); + Assertions.assertEquals("10.0.21", VersionUtils.getBestMatchVersion(releasedVersions, "10.0.16")); + } + + @Test + void testConvertTagToVersion() { + Assertions.assertEquals("10.0.19", VersionUtils.convertTagToVersion("10.0.19")); + Assertions.assertEquals("10.0.19", VersionUtils.convertTagToVersion("v10.0.19")); + Assertions.assertEquals("", VersionUtils.convertTagToVersion("")); + } + + @Test + void testConvertTagsToVersions() { + List results = VersionUtils.convertTagsToVersions(List.of("10.0.1", "v10.0.2")); + Assertions.assertEquals(2, results.size()); + Assertions.assertEquals("10.0.1", results.get(0)); + Assertions.assertEquals("10.0.2", results.get(1)); + } + + @Test + void testGetOldestVersionWithEmptyTags() { + List tags = List.of(); + + String oldestTag = VersionUtils.getOldestVersion(tags); + + Assertions.assertEquals(StringUtils.EMPTY, oldestTag); + } + + @Test + void testGetOldestVersionWithNullTags() { + String oldestTag = VersionUtils.getOldestVersion(null); + + Assertions.assertEquals(StringUtils.EMPTY, oldestTag); + } + + @Test + void testGetOldestVersionWithNonNumericCharacters() { + GHTag tag1 = Mockito.mock(GHTag.class); + GHTag tag2 = Mockito.mock(GHTag.class); + Mockito.when(tag1.getName()).thenReturn("v1.0"); + Mockito.when(tag2.getName()).thenReturn("2.1"); + List tags = Arrays.asList(tag1, tag2); + + String oldestTag = VersionUtils.getOldestVersion(tags); + + Assertions.assertEquals("1.0", oldestTag); // Assuming the replacement of non-numeric characters works correctly + } + + @Test + void testRemoveSyncedVersionsFromReleasedVersions() { + Set syncVersion = Set.of("1.0.0"); + List releasedVersions = new ArrayList<>(); + releasedVersions.add("1.0.0"); + releasedVersions.add("2.0.0"); + List result = VersionUtils.removeSyncedVersionsFromReleasedVersions(releasedVersions, + Collections.emptySet()); + Assertions.assertEquals(2, result.size()); + result = VersionUtils.removeSyncedVersionsFromReleasedVersions(releasedVersions, + syncVersion); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("2.0.0", result.get(0)); + } + + @Test + void testGetReleaseTagsFromProduct() { + List result = VersionUtils.getReleaseTagsFromProduct(null); + Assertions.assertNotNull(result); + Assertions.assertEquals(0, result.size()); + Product mockProduct = Product.builder().id("portal").releasedVersions(List.of("1.0.0")).build(); + result = VersionUtils.getReleaseTagsFromProduct(mockProduct); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("1.0.0", result.get(0)); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/XmlReaderUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/XmlReaderUtilsTest.java deleted file mode 100644 index dc755b0ac..000000000 --- a/marketplace-service/src/test/java/com/axonivy/market/util/XmlReaderUtilsTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.axonivy.market.util; - -import org.apache.commons.lang3.StringUtils; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Collections; -import java.util.List; - -@ExtendWith(MockitoExtension.class) -class XmlReaderUtilsTest { - - @Test - void testExtractVersions() { - List versions = Collections.emptyList(); - XmlReaderUtils.extractVersions(StringUtils.EMPTY, versions); - Assertions.assertTrue(versions.isEmpty()); - } -} diff --git a/marketplace-service/src/test/resources/zip/text.zip b/marketplace-service/src/test/resources/zip/text.zip new file mode 100644 index 000000000..2b0a5b6e3 Binary files /dev/null and b/marketplace-service/src/test/resources/zip/text.zip differ diff --git a/marketplace-ui/src/app/app.component.html b/marketplace-ui/src/app/app.component.html index 7ee11ade9..016a69a82 100644 --- a/marketplace-ui/src/app/app.component.html +++ b/marketplace-ui/src/app/app.component.html @@ -25,8 +25,6 @@ } @if (loadingService.isLoading()) { -
-
-
+ } diff --git a/marketplace-ui/src/app/app.component.scss b/marketplace-ui/src/app/app.component.scss index d3196b6f5..dba00e98b 100644 --- a/marketplace-ui/src/app/app.component.scss +++ b/marketplace-ui/src/app/app.component.scss @@ -25,24 +25,6 @@ footer { } } -.spinner-container { - position: fixed; - height: 100%; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - top: 0; - left: 0; - background: rgba(0, 0, 0, 0.32); - z-index: 2000; -} - -.spinner-border { - width: 4rem; - height: 4rem; -} - .header-mobile { z-index: 1; position: absolute; diff --git a/marketplace-ui/src/app/app.component.ts b/marketplace-ui/src/app/app.component.ts index 2d15456b4..c4016a8f8 100644 --- a/marketplace-ui/src/app/app.component.ts +++ b/marketplace-ui/src/app/app.component.ts @@ -12,11 +12,12 @@ import { RouterOutlet, Event } from '@angular/router'; +import { LoadingSpinnerComponent } from "./shared/components/loading-spinner/loading-spinner.component"; @Component({ selector: 'app-root', standalone: true, - imports: [RouterOutlet, HeaderComponent, FooterComponent, CommonModule], + imports: [RouterOutlet, HeaderComponent, FooterComponent, CommonModule, LoadingSpinnerComponent], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) @@ -24,7 +25,7 @@ export class AppComponent { loadingService = inject(LoadingService); routingQueryParamService = inject(RoutingQueryParamService); route = inject(ActivatedRoute); - isMobileMenuCollapsed: boolean = true; + isMobileMenuCollapsed = true; constructor(private readonly router: Router) {} diff --git a/marketplace-ui/src/app/app.routes.ts b/marketplace-ui/src/app/app.routes.ts index 08f5524ec..7efb78add 100644 --- a/marketplace-ui/src/app/app.routes.ts +++ b/marketplace-ui/src/app/app.routes.ts @@ -1,11 +1,15 @@ import { Routes } from '@angular/router'; import { GithubCallbackComponent } from './auth/github-callback/github-callback.component'; -import { ErrorPageComponentComponent } from './shared/components/error-page-component/error-page-component.component'; +import { ErrorPageComponent } from './shared/components/error-page/error-page.component'; export const routes: Routes = [ { path: 'error-page', - component: ErrorPageComponentComponent + component: ErrorPageComponent + }, + { + path: 'error-page/:id', + component: ErrorPageComponent }, { path: '', @@ -20,4 +24,4 @@ export const routes: Routes = [ path: 'auth/github/callback', component: GithubCallbackComponent } -]; +]; \ No newline at end of file diff --git a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts index 19475e3d5..91b9b729c 100644 --- a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts +++ b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts @@ -6,7 +6,7 @@ import { import { environment } from '../../../environments/environment'; import { LoadingService } from '../services/loading/loading.service'; import { inject } from '@angular/core'; -import { catchError, finalize, throwError } from 'rxjs'; +import { catchError, EMPTY, finalize } from 'rxjs'; import { Router } from '@angular/router'; import { ERROR_CODES, ERROR_PAGE_PATH } from '../../shared/constants/common.constant'; @@ -45,9 +45,11 @@ export const apiInterceptor: HttpInterceptorFn = (req, next) => { return next(cloneReq).pipe( catchError(error => { if (ERROR_CODES.includes(error.status)) { + router.navigate([`${ERROR_PAGE_PATH}/${error.status}`]); + } else { router.navigate([ERROR_PAGE_PATH]); } - return throwError(() => new Error(error.message)); + return EMPTY; }), finalize(() => { loadingService.hide(); @@ -60,4 +62,4 @@ function addIvyHeaders(headers: HttpHeaders): HttpHeaders { return headers; } return headers.append(REQUEST_BY, IVY); -} +} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.html b/marketplace-ui/src/app/modules/product/product-card/product-card.component.html index dd414407c..687471ce6 100644 --- a/marketplace-ui/src/app/modules/product/product-card/product-card.component.html +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.html @@ -6,7 +6,8 @@ class="card-img-top rounded" width="70" height="70" - [ngSrc]="product | logo" + [ngSrc]="logoUrl" + (error)="onLogoError()" [alt]=" product.names | multilingualism: languageService.selectedLanguage() " diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts b/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts index 5de39f622..c268a4421 100644 --- a/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts @@ -4,14 +4,14 @@ import { TranslateModule } from '@ngx-translate/core'; import { LanguageService } from '../../../core/services/language/language.service'; import { ThemeService } from '../../../core/services/theme/theme.service'; import { Product } from '../../../shared/models/product.model'; -import { ProductLogoPipe } from '../../../shared/pipes/logo.pipe'; import { MultilingualismPipe } from '../../../shared/pipes/multilingualism.pipe'; import { ProductComponent } from '../product.component'; +import { DEFAULT_IMAGE_URL } from '../../../shared/constants/common.constant'; @Component({ selector: 'app-product-card', standalone: true, - imports: [CommonModule, ProductLogoPipe, MultilingualismPipe, TranslateModule, NgOptimizedImage], + imports: [CommonModule, MultilingualismPipe, TranslateModule, NgOptimizedImage], templateUrl: './product-card.component.html', styleUrl: './product-card.component.scss' }) @@ -21,4 +21,13 @@ export class ProductCardComponent { isShowInRESTClientEditor = inject(ProductComponent).isRESTClient(); @Input() product!: Product; + logoUrl = DEFAULT_IMAGE_URL; + + ngOnInit(): void { + this.logoUrl = this.product.logoUrl; + } + + onLogoError() { + this.logoUrl = DEFAULT_IMAGE_URL; + } } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.spec.ts index e0ff75b7c..d1f8273cd 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/show-feedbacks-dialog/show-feedbacks-dialog.component.spec.ts @@ -7,10 +7,7 @@ import { AppModalService } from '../../../../../shared/services/app-modal.servic import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { By } from '@angular/platform-browser'; -import { - provideHttpClient, - withInterceptorsFromDi -} from '@angular/common/http'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; describe('ShowFeedbacksDialogComponent', () => { @@ -74,4 +71,39 @@ describe('ShowFeedbacksDialogComponent', () => { expect(mockAppModalService.openAddFeedbackDialog).toHaveBeenCalled(); }); + + it('should call activeModal.dismiss when the window is resized to less than or equal to 767px', () => { + // Set up a media query list that matches + spyOn(window, 'matchMedia').and.returnValue({ + matches: true, + addListener: () => { + }, + removeListener: () => { + } + } as any); + + // Trigger the resize event + component.onResize(); + + // Check that dismiss was called + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + + it('should not call activeModal.dismiss when the window is resized to more than 767px', () => { + // Set up a media query list that does not match + spyOn(window, 'matchMedia').and.returnValue({ + matches: false, + addListener: () => { + }, + removeListener: () => { + } + } as any); + + // Trigger the resize event + component.onResize(); + + // Check that dismiss was not called + expect(mockActiveModal.dismiss).not.toHaveBeenCalled(); + }); + }); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html index 29048377e..e86f0ea13 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html @@ -1,26 +1,92 @@ @switch (actionType) { @case ('standard') { - + + + @if (isDropDownDisplayed()) { +
+
+
+
+ + +
+ + +
+ +
+
+ + +
+ + +
+
+ + @if (isDevVersionsDisplayed()) { + {{ 'common.product.detail.download.hideDevVersions' | translate }} + } @else { + {{ 'common.product.detail.download.showDevVersions' | translate }} + } - (minimum version 9.2.0) -

" - (click)="onUpdateInstallationCountForDesigner()" data-bs-custom-class="custom-tooltip" - [ngClass]="themeService.isDarkMode() ? 'btn-light' : 'btn-primary'"> - {{ 'common.product.detail.install.buttonLabel' | translate }} - - +
+ +
+
+ @if (isArtifactLoading()) { + + } +
+ } + } @case ('designerEnv') {
@@ -53,65 +119,4 @@ {{ 'common.product.detail.contactUs.label' | translate }} } -} - -@if (isDropDownDisplayed()) { - -} +} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.scss index 753ca8452..cdac2c7b4 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.scss +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.scss @@ -48,9 +48,6 @@ } } } -::ng-deep .dropdown-menu { - min-width: 100%; -} ::ng-deep .dropdown-item { font-size: 14px; @@ -86,12 +83,8 @@ } .btn__install { - margin-right: 10px; border: 0px; } -.btn__download { - border: 0.5px solid #ebebeb; -} .primary-color { color: var(--ivy-primary-bg); } @@ -105,8 +98,10 @@ line-height: 16.8px; } -::ng-deep .dropdown-menu.menu-bar { - margin-top: 4px; +::ng-deep #download-dropdown-menu .dropdown-menu.menu-bar { + margin-top: 0.2rem; + height: fit-content; + max-height: 176px; } ::ng-deep .install-designer-dropdown { @@ -115,22 +110,31 @@ text-align: start; } -::ng-deep .dropdown-menu { +.install-download-button-container { + position: relative; +} + +.btn__download { + border: 0.5px solid #ebebeb; +} + +::ng-deep #download-dropdown-menu { + position: absolute; + right: 0; + width: 400px; + height: 204px; + margin-top: 4rem; + background-color: var(--bs-body-bg); + z-index: 999; padding: 10px; border-radius: 5px; - gap: 15px; box-shadow: 0px 4px 30px 0px rgba(0, 0, 0, 0.1); - margin-top: 1.2rem; - &.maven-artifact-version__action { - min-width: 400px; - min-height: 204px; - @media (max-width: 500px) { - left: auto; - right: auto; - min-width: 80vw; - } + @media (max-width: 1399px) { + width: 100%; + } + &.maven-artifact-version__action { .form-group { gap: 7px; @@ -182,17 +186,28 @@ top: -10px; width: 20px; height: 20px; - left: 50%; + right: 12%; z-index: 1001; - @media (max-width: 767px) { - top: -8px; - width: 16px; - height: 16px; - } transform: translateX(-50%) rotate(45deg); border-right-color: transparent; border-bottom-color: transparent; background-color: var(--bs-body-bg); + + @media screen and (max-width: 1399px) { + right: 14%; + } + + @media screen and (max-width: 1199px) { + right: 21%; + } + + @media screen and (max-width: 767px) { + right: 18%; + } + + @media screen and (max-width: 575px) { + right: 15%; + } } .btn_contact-us { diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.spec.ts index 14f47fd83..1c9713990 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.spec.ts @@ -1,38 +1,60 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; -import { ProductDetailVersionActionComponent } from './product-detail-version-action.component'; +import { ProductDetailVersionActionComponent, versionParam } from './product-detail-version-action.component'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { ProductService } from '../../product.service'; import { provideHttpClient } from '@angular/common/http'; import { ElementRef } from '@angular/core'; import { ItemDropdown } from '../../../../shared/models/item-dropdown.model'; +import { CookieService } from 'ngx-cookie-service'; +import { ActivatedRoute, provideRouter, Router } from '@angular/router'; +import { CommonUtils } from '../../../../shared/utils/common.utils'; class MockElementRef implements ElementRef { nativeElement = { contains: jasmine.createSpy('contains') }; } + describe('ProductDetailVersionActionComponent', () => { + const productId = '123'; let component: ProductDetailVersionActionComponent; let fixture: ComponentFixture; let productServiceMock: any; + let router: Router; + let route: jasmine.SpyObj; beforeEach(() => { productServiceMock = jasmine.createSpyObj('ProductService', [ - 'sendRequestToProductDetailVersionAPI' , 'sendRequestToUpdateInstallationCount' + 'sendRequestToProductDetailVersionAPI', 'sendRequestToUpdateInstallationCount', 'sendRequestToGetProductVersionsForDesigner' ]); + const commonUtilsSpy = jasmine.createSpyObj('CommonUtils', ['getCookieValue']); + // const cookieServiceSpy = jasmine.createSpyObj('CookieService', ['get', 'set']); + const activatedRouteSpy = jasmine.createSpyObj('ActivatedRoute', [], { + snapshot: { + queryParams: {} + } + }); TestBed.configureTestingModule({ imports: [ProductDetailVersionActionComponent, TranslateModule.forRoot()], providers: [ TranslateService, + CookieService, provideHttpClient(), + provideRouter([]), { provide: ProductService, useValue: productServiceMock }, - { provide: ElementRef, useClass: MockElementRef } + { provide: ElementRef, useClass: MockElementRef }, + { provide: ActivatedRoute, useValue: { queryParams: of({}) } }, + { provide: CommonUtils, useValue: commonUtilsSpy }, + { provide: ActivatedRoute, useValue: activatedRouteSpy } ] }).compileComponents(); fixture = TestBed.createComponent(ProductDetailVersionActionComponent); component = fixture.componentInstance; + component.productId = productId; + router = TestBed.inject(Router); + route = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; fixture.detectChanges(); }); @@ -58,6 +80,90 @@ describe('ProductDetailVersionActionComponent', () => { expect(component.selectedArtifact).toEqual('https://example.com/download'); }); + it('should update selectedVersion, artifacts, selectedArtifactName, and selectedArtifact, and call addVersionParamToRoute', () => { + const version = '1.0'; + const artifacts = [{ + name: 'Example Artifact', + downloadUrl: 'https://example.com/download', + isProductArtifact: true + } as ItemDropdown]; + const versionMap = new Map(); + versionMap.set(version, artifacts); + + // Set up spies + spyOn(component.selectedVersion, 'set'); + spyOn(component as any, 'addVersionParamToRoute').and.callThrough(); + + // Mock data + component.versionMap = versionMap; + component.artifacts.set([]); + + // Call the method + component.onSelectVersion(version); + + // Expectations + expect(component.selectedVersion.set).toHaveBeenCalledWith(version); + expect(component.artifacts()).toEqual(artifacts); + expect(component.selectedArtifactName).toBe('Example Artifact'); + expect(component.selectedArtifact).toBe('https://example.com/download'); + expect(component.addVersionParamToRoute).toHaveBeenCalledWith(version); + }); + + it('should not update selected version or artifacts if the version is the same', () => { + // Arrange + const version = 'v1'; + component.selectedVersion.set(version); + spyOn(component.selectedVersion, 'set'); + spyOn(component.artifacts, 'set'); + spyOn(component.versionMap, 'get'); + spyOn(component, 'addVersionParamToRoute'); + + // Act + component.onSelectVersion(version); + + // Assert + expect(component.selectedVersion.set).not.toHaveBeenCalled(); + expect(component.artifacts.set).toHaveBeenCalled(); + expect(component.addVersionParamToRoute).toHaveBeenCalledWith(version); + }); + + it('should handle empty artifacts', () => { + // Arrange + const version = 'v1'; + spyOn(component.selectedVersion, 'set'); + spyOn(component.artifacts, 'set'); + spyOn(component.versionMap, 'get').and.returnValue([]); + spyOn(component, 'addVersionParamToRoute'); + + // Act + component.onSelectVersion(version); + + // Assert + expect(component.selectedVersion.set).toHaveBeenCalledWith(version); + expect(component.artifacts.set).toHaveBeenCalledWith([]); + expect(component.selectedArtifactName).toBe(''); + expect(component.selectedArtifact).toBe(''); + expect(component.addVersionParamToRoute).toHaveBeenCalledWith(version); + }); + + it('should navigate with the selected version in the query params', () => { + const version = '1.0'; + + // Set up spy for router.navigate + spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + + // Call the method + component.addVersionParamToRoute(version); + + // Expectations + expect(router.navigate).toHaveBeenCalledWith([], { + relativeTo: route, + queryParams: { [versionParam]: version }, + queryParamsHandling: 'merge' + }); + }); + + it('all of state should be reset before call rest api', () => { const selectedVersion = 'Version 10.0.2'; const artifact = { @@ -77,6 +183,8 @@ describe('ProductDetailVersionActionComponent', () => { component.sanitizeDataBeforeFetching(); expect(component.versions().length).toBe(0); expect(component.artifacts().length).toBe(0); + expect(component.selectedVersion()).toEqual(selectedVersion); + expect(component.selectedArtifact).toEqual(''); }); it('should call sendRequestToProductDetailVersionAPI and update versions and versionMap', () => { @@ -120,6 +228,7 @@ describe('ProductDetailVersionActionComponent', () => { }); it('should send Api to get DevVersion', () => { + spyOn(component.isDevVersionsDisplayed, 'set'); expect(component.isDevVersionsDisplayed()).toBeFalse(); mockApiWithExpectedResponse(); const event = new Event('click'); @@ -133,7 +242,7 @@ describe('ProductDetailVersionActionComponent', () => { const mockArtifact1 = { name: 'Example Artifact1', downloadUrl: 'https://example.com/download', - isProductArtifact: true, label: 'Example Artifact1', + isProductArtifact: true, label: 'Example Artifact1' } as ItemDropdown; const mockArtifact2 = { name: 'Example Artifact2', @@ -182,7 +291,7 @@ describe('ProductDetailVersionActionComponent', () => { const productId = 'octopus'; component.productId = productId; const newTabMock: Partial = { - blur: jasmine.createSpy('blur') + blur: jasmine.createSpy('blur') }; spyOn(window, 'open').and.returnValue(newTabMock as Window); spyOn(window, 'focus'); @@ -210,4 +319,38 @@ describe('ProductDetailVersionActionComponent', () => { ); expect(window.focus).toHaveBeenCalled(); }); + + it('should not call productService if versions are already populated', () => { + component.versions.set(['1.0', '1.1']); + fixture.detectChanges(); + component.getVersionInDesigner(); + + expect(productServiceMock.sendRequestToGetProductVersionsForDesigner).not.toHaveBeenCalled(); + }); + + it('should call productService and update versions if versions are empty', () => { + const productId = '123'; + component.versions.set([]); + const mockVersions = [{ version: '1.0' }, { version: '2.0' }]; + productServiceMock.sendRequestToGetProductVersionsForDesigner.and.returnValue(of(mockVersions)); + + // Act + component.getVersionInDesigner(); + + // Assert + expect(productServiceMock.sendRequestToGetProductVersionsForDesigner).toHaveBeenCalledWith(productId); + expect(component.versions()).toEqual(['Version 1.0', 'Version 2.0']); + }); + + it('should handle empty response from productService', () => { + component.versions.set([]); + productServiceMock.sendRequestToGetProductVersionsForDesigner.and.returnValue(of([])); + + // Act + component.getVersionInDesigner(); + + // Assert + expect(productServiceMock.sendRequestToGetProductVersionsForDesigner).toHaveBeenCalledWith(productId); + expect(component.versions()).toEqual([]); + }); }); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.ts index a6a59488a..5c23ae15c 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.ts @@ -5,14 +5,12 @@ import { computed, ElementRef, EventEmitter, - HostListener, inject, Input, model, Output, Signal, signal, - ViewChild, WritableSignal } from '@angular/core'; import { ThemeService } from '../../../../core/services/theme/theme.service'; @@ -29,6 +27,14 @@ import { VERSION } from '../../../../shared/constants/common.constant'; import { ProductDetailActionType } from '../../../../shared/enums/product-detail-action-type'; import { RoutingQueryParamService } from '../../../../shared/services/routing.query.param.service'; import { ProductDetail } from '../../../../shared/models/product-detail.model'; +import { LoadingSpinnerComponent } from '../../../../shared/components/loading-spinner/loading-spinner.component'; +import { CookieService } from 'ngx-cookie-service'; +import { CommonUtils } from '../../../../shared/utils/common.utils'; +import { ActivatedRoute, Router } from '@angular/router'; + +const delayTimeBeforeHideMessage = 2000; +const showDevVersionCookieName = 'showDevVersions'; +export const versionParam = 'version'; @Component({ selector: 'app-product-version-action', @@ -37,7 +43,8 @@ import { ProductDetail } from '../../../../shared/models/product-detail.model'; CommonModule, TranslateModule, FormsModule, - CommonDropdownComponent + CommonDropdownComponent, + LoadingSpinnerComponent ], templateUrl: './product-detail-version-action.component.html', styleUrl: './product-detail-version-action.component.scss' @@ -48,9 +55,6 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { @Input() productId!: string; @Input() actionType!: ProductDetailActionType; - @ViewChild('artifactDownloadButton') artifactDownloadButton!: ElementRef; - @ViewChild('artifactDownloadDialog') artifactDownloadDialog!: ElementRef; - @Input() product!: ProductDetail; selectedVersion = model(''); versions: WritableSignal = signal([]); @@ -64,8 +68,8 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { versionDropdownInDesigner: ItemDropdown[] = []; artifacts: WritableSignal = signal([]); - isDevVersionsDisplayed = signal(false); isDropDownDisplayed = signal(false); + isArtifactLoading = signal(false); designerVersion = ''; selectedArtifact: string | undefined = ''; selectedArtifactName: string | undefined = ''; @@ -77,6 +81,11 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { languageService = inject(LanguageService); routingQueryParamService = inject(RoutingQueryParamService); changeDetectorRef = inject(ChangeDetectorRef); + cookieService = inject(CookieService); + router = inject(Router); + route = inject(ActivatedRoute); + + isDevVersionsDisplayed: WritableSignal = signal(this.getShowDevVersionFromCookie()); ngAfterViewInit() { const tooltipTriggerList = [].slice.call( @@ -88,12 +97,18 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { } onSelectVersion(version: string) { - this.selectedVersion.set(version); - this.artifacts.set(this.versionMap.get(this.selectedVersion()) ?? []); - this.updateSelectedArtifact(); + if (this.selectedVersion() !== version) { + this.selectedVersion.set(version); + } + this.artifacts.set(this.versionMap.get(version) ?? []); + this.updateSelectedArtifact(version); + } + + private getShowDevVersionFromCookie() { + return CommonUtils.getCookieValue(this.cookieService, showDevVersionCookieName, false); } - private updateSelectedArtifact() { + private updateSelectedArtifact(version: string) { this.artifacts().forEach(artifact => { if (artifact.name) { artifact.label = artifact.name; @@ -103,6 +118,15 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { this.selectedArtifactName = this.artifacts()[0].name ?? ''; this.selectedArtifact = this.artifacts()[0].downloadUrl ?? ''; } + this.addVersionParamToRoute(version); + } + + addVersionParamToRoute(selectedVersion: string) { + this.router.navigate([], { + relativeTo: this.route, + queryParams: { [versionParam]: selectedVersion }, + queryParamsHandling: 'merge' + }).then(); } onSelectVersionInDesigner(version: string) { @@ -116,8 +140,9 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { onShowDevVersion(event: Event) { event.preventDefault(); - this.isDevVersionsDisplayed.set(!this.isDevVersionsDisplayed()); - this.getVersionWithArtifact(); + this.isDevVersionsDisplayed.update(oldValue => !oldValue); + this.cookieService.set(showDevVersionCookieName, this.isDevVersionsDisplayed().toString()); + this.getVersionWithArtifact(true); } onShowVersionAndArtifact() { @@ -125,50 +150,15 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { this.getVersionWithArtifact(); } this.isDropDownDisplayed.set(!this.isDropDownDisplayed()); - this.changeDetectorRef.detectChanges(); - this.reLocaleDialog(); } - - @HostListener('window:resize', ['$event']) - onResize() { - this.reLocaleDialog(); - } - - reLocaleDialog() { - const buttonPosition = this.getElementPosition(this.artifactDownloadButton); - const dialogPosition = this.getElementPosition(this.artifactDownloadDialog); - if (buttonPosition && dialogPosition) { - const dialogElement = this.artifactDownloadDialog.nativeElement; - - dialogElement.style.position = 'absolute'; - dialogElement.style.top = `${buttonPosition.y + buttonPosition.height}px`; - - // Align the dialog to the center of the button - const dialogWidth = dialogElement.offsetWidth; - const buttonCenterX = buttonPosition.x + buttonPosition.width / 2; - dialogElement.style.left = `${buttonCenterX - dialogWidth / 2}px`; - } - } - - getElementPosition(element: ElementRef) { - if (element?.nativeElement) { - const rect = element.nativeElement.getBoundingClientRect(); - return { - x: rect.left + window.scrollX, - y: rect.top + window.scrollY, - width: rect.width, - height: rect.height - }; - } - return null; - } - - getVersionWithArtifact() { + + getVersionWithArtifact(ignoreRouteVersion = false) { + this.isArtifactLoading.set(true); this.sanitizeDataBeforeFetching(); this.productService .sendRequestToProductDetailVersionAPI( this.productId, - this.isDevVersionsDisplayed(), + this.getShowDevVersionFromCookie(), this.designerVersion ) .subscribe(data => { @@ -184,12 +174,19 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { } }); if (this.versions().length !== 0) { - this.artifacts.set(this.versionMap.get(this.selectedVersion()) ?? []); - this.updateSelectedArtifact(); + this.onSelectVersion(this.getVersionFromRoute(ignoreRouteVersion) ?? this.versions()[0]); } + this.isArtifactLoading.set(false); }); } + getVersionFromRoute(ignoreRouteVersion: boolean): string | null { + if (ignoreRouteVersion) { + return null; + } + return this.route.snapshot.queryParams[versionParam] || null; + } + getVersionInDesigner(): void { if (this.versions().length === 0) { this.productService @@ -217,6 +214,7 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { sanitizeDataBeforeFetching() { this.versions.set([]); this.artifacts.set([]); + this.selectedArtifact = ''; } downloadArtifact() { diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html index b20928040..7cd309f2b 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html @@ -15,23 +15,24 @@
-
-
+
+
+ "/>

- {{ + class="product-title align-items-start default-cursor">{{ productDetail().names | multilingualism: languageService.selectedLanguage() }} @@ -67,118 +68,116 @@

-
-
- -
-
+
-
- -
- -