commit 05b15e01e319ca5d01e0025543fc1ad3daadf532 Author: Jim Martens Date: Sat Jul 1 21:49:36 2023 +0200 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5447b77 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,669 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = false +max_line_length = 100 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = none +ij_wrap_on_typing = false + +[*.java] +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 = false +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 = false +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_align_types_in_multi_catch = true +ij_java_annotation_parameter_wrap = off +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 = normal +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 = true +ij_java_binary_operation_wrap = normal +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 1 +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 = none +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 = 999 +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 = always +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_extends_keyword_wrap = off +ij_java_extends_list_wrap = normal +ij_java_field_annotation_wrap = split_into_lines +ij_java_finally_on_new_line = false +ij_java_for_brace_force = always +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = normal +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = always +ij_java_imports_layout = $*,|,* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = true +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 = 1 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = false +ij_java_keep_control_statement_in_one_line = false +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 = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_add_space_on_reformat = false +ij_java_line_comment_at_first_column = true +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = normal +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 = 999 +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_parameter_annotation_wrap = off +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_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_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_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = true +ij_java_ternary_operation_wrap = normal +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = normal +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 = always +ij_java_while_on_new_line = false +ij_java_wrap_comments = true +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false + +[*.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_continuation_indent_size = 2 +ij_xml_align_attributes = false +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 +ij_xml_use_custom_settings = true + +[{*.bash,*.sh,*.zsh}] +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}] +indent_size = 4 +tab_width = 4 +ij_continuation_indent_size = 8 +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}] +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}] +indent_size = 4 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = off +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = false +ij_kotlin_call_parameters_right_paren_on_new_line = false +ij_kotlin_call_parameters_wrap = off +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_continuation_indent_for_chained_calls = true +ij_kotlin_continuation_indent_for_expression_bodies = true +ij_kotlin_continuation_indent_in_argument_lists = true +ij_kotlin_continuation_indent_in_elvis = true +ij_kotlin_continuation_indent_in_if_conditions = true +ij_kotlin_continuation_indent_in_parameter_lists = true +ij_kotlin_continuation_indent_in_supertype_lists = true +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = off +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = false +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = off +ij_kotlin_method_parameters_new_line_after_left_paren = false +ij_kotlin_method_parameters_right_paren_on_new_line = false +ij_kotlin_method_parameters_wrap = off +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.** +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 0 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.markdown,*.md}] +indent_size = 4 +tab_width = 4 +ij_continuation_indent_size = 8 +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}] +indent_size = 4 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4b4f16c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +gradle/wrapper/gradle-wrapper.jar filter= diff= merge= -text +*.sh text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b310fab --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +/.* +!/.gitignore +!/.gitattributes +!/.editorconfig +!/.run/ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/** +!**/src/test/** +**/*.log +**/*.hprof +**/generated/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Swap-Dateien und Backups ### +*.swp +*~ +/lib/ diff --git a/.run/MainApplication.run.xml b/.run/MainApplication.run.xml new file mode 100644 index 0000000..1ad0379 --- /dev/null +++ b/.run/MainApplication.run.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c87425b --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Template + +Contains a template for a new Java project. By utilising the buildSrc Gradle pattern, +this template provides a fast start for plain Java projects as well as Spring Boot projects. + +It also contains some modules which each show a different way to use the template: +- module-server: A Spring Boot server project +- module-plain: A plain Java project +- module-lib: A Java library that can be used in other modules + +To use the template, simply copy the contents of this repository into a new repository. +Then, replace all occurrences of `template` and `Template` with the name of your project. +Finally, replace the contents of this README with the README of your project. + +The settings.gradle file needs to be changed to import the modules that you want to use. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..fe145dd --- /dev/null +++ b/build.gradle @@ -0,0 +1,13 @@ +plugins { + id 'twomartens.versions' + id 'twomartens.nebula-release' +} + +versionCatalogUpdate { + sortByKey = false + keep { + keepUnusedVersions = true + keepUnusedLibraries = true + keepUnusedPlugins = true + } +} diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..af471e1 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'groovy-gradle-plugin' +} + +repositories { + gradlePluginPortal() +} + +dependencies { + implementation libs.plugin.springboot + implementation libs.plugin.lombok + implementation libs.plugin.nebula.release + implementation libs.plugin.gradle.versions + implementation libs.plugin.version.catalog +} \ No newline at end of file diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle new file mode 100644 index 0000000..7cd729c --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1,9 @@ +rootProject.name = 'twomartens.config' + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/buildSrc/src/main/groovy/twomartens.base.gradle b/buildSrc/src/main/groovy/twomartens.base.gradle new file mode 100644 index 0000000..c6c8dd5 --- /dev/null +++ b/buildSrc/src/main/groovy/twomartens.base.gradle @@ -0,0 +1,6 @@ +plugins { + id 'idea' + id 'eclipse' +} + +group = projectgroup \ No newline at end of file diff --git a/buildSrc/src/main/groovy/twomartens.checkstyle.gradle b/buildSrc/src/main/groovy/twomartens.checkstyle.gradle new file mode 100644 index 0000000..ef5353a --- /dev/null +++ b/buildSrc/src/main/groovy/twomartens.checkstyle.gradle @@ -0,0 +1,23 @@ +plugins { + id 'checkstyle' + id 'twomartens.java-base' +} + +checkstyle { + toolVersion '10.0' + ignoreFailures = false + maxWarnings = 0 + + configFile rootProject.file('config/checkstyle/checkstyle.xml') + + configProperties = ['org.checkstyle.google.suppressionfilter.config': "${project.rootDir}/config/checkstyle/checkstyle-suppressions.xml"] + + tasks.withType(Checkstyle).tap { + configureEach { + reports { + xml.required = true + html.required = true + } + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/twomartens.java-base.gradle b/buildSrc/src/main/groovy/twomartens.java-base.gradle new file mode 100644 index 0000000..c85aa0d --- /dev/null +++ b/buildSrc/src/main/groovy/twomartens.java-base.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' + id 'java-library' + id 'twomartens.base' +} + +sourceCompatibility = projectSourceCompatibility +targetCompatibility = projectSourceCompatibility + +repositories { + mavenCentral() +} + +tasks.register('buildAll') { + group 'build' + dependsOn(build) + dependsOn(test) +} + +clean { + delete 'out' +} diff --git a/buildSrc/src/main/groovy/twomartens.java-preview.gradle b/buildSrc/src/main/groovy/twomartens.java-preview.gradle new file mode 100644 index 0000000..1459a9d --- /dev/null +++ b/buildSrc/src/main/groovy/twomartens.java-preview.gradle @@ -0,0 +1,15 @@ +plugins { + id 'twomartens.java-base' +} + +tasks.withType(JavaCompile).configureEach { + options.compilerArgs += "--enable-preview" +} + +tasks.withType(Test).configureEach { + jvmArgs += "--enable-preview" +} + +tasks.withType(JavaExec).configureEach { + jvmArgs += '--enable-preview' +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/twomartens.java.gradle b/buildSrc/src/main/groovy/twomartens.java.gradle new file mode 100644 index 0000000..9b61efa --- /dev/null +++ b/buildSrc/src/main/groovy/twomartens.java.gradle @@ -0,0 +1,71 @@ +import java.text.SimpleDateFormat + +plugins { + id 'jacoco' + id 'io.freefair.lombok' + id 'twomartens.java-base' + id 'twomartens.checkstyle' +} + +dependencies { + constraints.implementation libs.bundles.logging + + implementation libs.slf4j.api + runtimeOnly libs.bundles.logging + + testImplementation libs.bundles.test +} + +configurations { + configureEach { + exclude group: 'junit', module: 'junit' + // we are using log4j-slf4j2-impl, so we need to suppress spring include of log4j-slf4j-impl + exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl' + } +} + +test { + systemProperty 'junit.jupiter.execution.parallel.enabled', true + systemProperty 'junit.jupiter.execution.parallel.mode.default', "concurrent" + useJUnitPlatform() + maxHeapSize = "4g" + workingDir = rootProject.projectDir + finalizedBy jacocoTestReport +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +jacocoTestReport { + dependsOn(test) + reports { + xml.required = true + } +} + +jar { + doFirst { + manifest { + attributes 'Implementation-Title': rootProject.name, + 'Implementation-Version': archiveVersion.get(), + 'Implementation-Vendor': "Jim Martens", + 'Build-Timestamp': new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()), + 'Created-By': "Gradle ${gradle.gradleVersion}", + 'Build-Jdk': "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})", + 'Build-OS': "${System.properties['os.name']} ${System.properties['os.arch']} ${System.properties['os.version']}" + } + } +} + +normalization.runtimeClasspath.metaInf { + ignoreAttribute("Build-Timestamp") +} + +tasks.register('cleanLibs') { + delete("${buildDir}/libs") +} + +tasks.build.dependsOn("cleanLibs") diff --git a/buildSrc/src/main/groovy/twomartens.nebula-release.gradle b/buildSrc/src/main/groovy/twomartens.nebula-release.gradle new file mode 100644 index 0000000..4cf743a --- /dev/null +++ b/buildSrc/src/main/groovy/twomartens.nebula-release.gradle @@ -0,0 +1,16 @@ +plugins { + id 'com.netflix.nebula.release' + id 'twomartens.base' +} + +nebulaRelease { + addReleaseBranchPattern(/main/) +} + +task writeVersionProperties() { + group 'version' + buildDir.mkdirs() + file("$buildDir/version.properties").text = "VERSION=${project.version.toString()}\n" + mustRunAfter("release") + outputs.file("$buildDir/version.properties") +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/twomartens.spring-boot-cloud.gradle b/buildSrc/src/main/groovy/twomartens.spring-boot-cloud.gradle new file mode 100644 index 0000000..2feb1ed --- /dev/null +++ b/buildSrc/src/main/groovy/twomartens.spring-boot-cloud.gradle @@ -0,0 +1,40 @@ +plugins { + id 'twomartens.spring-boot' +} + +dependencies { + implementation platform(libs.spring.cloud) + implementation libs.bundles.spring.boot.server + implementation libs.spring.openapi + + implementation libs.httpclient + implementation libs.prometheus +} + +sourceSets { + "server-test" { + java { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/server-test/java') + } + resources.srcDir file('src/server-test/resources') + } +} + +configurations { + serverTestImplementation.extendsFrom testImplementation +} + +tasks.register('serverTest', Test) { + outputs.upToDateWhen { false } + systemProperty 'junit.jupiter.execution.parallel.enabled', true + systemProperty 'junit.jupiter.execution.parallel.mode.default', "concurrent" + systemProperty 'junit.jupiter.execution.parallel.mode.classes.default', "concurrent" + useJUnitPlatform() + maxHeapSize = "4g" + group = 'verification' + workingDir = rootProject.projectDir + testClassesDirs = sourceSets."server-test".output.classesDirs + classpath = sourceSets."server-test".runtimeClasspath +} diff --git a/buildSrc/src/main/groovy/twomartens.spring-boot.gradle b/buildSrc/src/main/groovy/twomartens.spring-boot.gradle new file mode 100644 index 0000000..f9ede99 --- /dev/null +++ b/buildSrc/src/main/groovy/twomartens.spring-boot.gradle @@ -0,0 +1,60 @@ +plugins { + id 'org.springframework.boot' + id 'twomartens.java' +} + +dependencies { + implementation platform(libs.spring.boot) + + implementation libs.bundles.spring.boot + testImplementation libs.spring.boot.test + annotationProcessor libs.spring.boot.config +} + +sourceSets { + "integration-test" { + java { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/java') + } + resources.srcDir file('src/integration-test/resources') + } +} + +configurations { + configureEach { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + integrationTestImplementation.extendsFrom testImplementation +} + +tasks.register('integrationTest', Test) { + systemProperty 'junit.jupiter.execution.parallel.enabled', true + systemProperty 'junit.jupiter.execution.parallel.mode.default', "concurrent" + systemProperty 'junit.jupiter.execution.parallel.mode.classes.default', "concurrent" + useJUnitPlatform() + maxHeapSize = "4g" + group = 'verification' + workingDir = rootProject.projectDir + testClassesDirs = sourceSets."integration-test".output.classesDirs + classpath = sourceSets."integration-test".runtimeClasspath +} + +tasks.named("buildAll").configure() { + dependsOn(integrationTest) +} + +springBoot { + buildInfo() +} + +bootJar { + enabled = false +} + +jar { + enabled = true + archiveClassifier.set("") +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/twomartens.versions.gradle b/buildSrc/src/main/groovy/twomartens.versions.gradle new file mode 100644 index 0000000..60eb26c --- /dev/null +++ b/buildSrc/src/main/groovy/twomartens.versions.gradle @@ -0,0 +1,29 @@ +plugins { + id "com.github.ben-manes.versions" + id "nl.littlerobots.version-catalog-update" +} + +dependencyUpdates { + revision = "release" + gradleReleaseChannel = "current" +} + +def isNonStable = { String version -> + def stableKeyword = ['RELEASE', 'FINAL', 'GA'].any { it -> version.toUpperCase().contains(it) } + def regex = /^[0-9,.v-]+(-r)?$/ + return !stableKeyword && !(version ==~ regex) +} + +tasks.named("dependencyUpdates").configure { + rejectVersionIf { + isNonStable(it.candidate.version) + } +} + +tasks.named("versionCatalogUpdate").configure { + group 'version' +} + +tasks.named("dependencyUpdates").configure { + group 'version' +} \ No newline at end of file diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml new file mode 100644 index 0000000..fb602c4 --- /dev/null +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..869599b --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,367 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..b360187 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,9 @@ +projectname=template +projectgroup=de.2martens +projectSourceCompatibility=17 +file.encoding=utf-8 +org.gradle.parallel=true +org.gradle.daemon=true +org.gradle.welcome=never +org.gradle.caching=true +org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..2552a79 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,113 @@ +[versions] +spring-boot = "3.0.5" +spring-doc = "2.1.0" +spring-cloud = "2022.0.2" +spring-grpc = "2.14.0.RELEASE" +grpc = "1.54.0" +tomcat-annotations = "6.0.53" +httpclient = "5.2.1" +slf4j = "2.0.7" +log4j = "2.20.0" +log4j-ecs = "1.5.0" +mapstruct = "1.5.3.Final" +junit = "5.9.2" +assertj = "3.24.2" +mockito = "5.3.0" +plugin-nebula-release = "17.1.0" +plugin-lombok = "8.0.1" +plugin-gradle-versions = "0.46.0" +plugin-version-catalog = "0.8.0" + +[libraries] +spring-boot = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" } +spring-boot-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator" } +spring-boot-web = { module = "org.springframework.boot:spring-boot-starter-web" } +spring-boot-log4j = { module = "org.springframework.boot:spring-boot-starter-log4j2" } +spring-boot-thymeleaf = { module = "org.springframework.boot:spring-boot-starter-thymeleaf" } +spring-boot-mongo = { module = "org.springframework.boot:spring-boot-starter-data-mongodb" } +spring-boot-validation = { module = "org.springframework.boot:spring-boot-starter-validation" } +spring-boot-test = { module = "org.springframework.boot:spring-boot-starter-test" } +spring-boot-config = { module = "org.springframework.boot:spring-boot-configuration-processor", version.ref = "spring-boot" } +spring-cloud = { module = "org.springframework.cloud:spring-cloud-dependencies", version.ref = "spring-cloud" } +spring-cloud-starter = { module = "org.springframework.cloud:spring-cloud-starter" } +spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter" } +spring-grpc = { module = "net.devh:grpc-spring-boot-starter", version.ref = "spring-grpc" } +spring-ui = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "spring-doc" } +spring-sec = { module = "org.springdoc:springdoc-openapi-starter-common", version.ref = "spring-doc" } +spring-openapi = { module = "org.springdoc:springdoc-openapi-starter-webmvc-api", version.ref = "spring-doc" } +grpc-api = { module = "io.grpc:grpc-api", version.ref = "grpc" } +grpc-context = { module = "io.grpc:grpc-context", version.ref = "grpc" } +grpc-core = { module = "io.grpc:grpc-core", version.ref = "grpc" } +grpc-netty = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpc" } +grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" } +grpc-proto-lite = { module = "io.grpc:grpc-protobuf-lite", version.ref = "grpc" } +grpc-services = { module = "io.grpc:grpc-services", version.ref = "grpc" } +grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } +grpc-protocjava = { module = "io.grpc:protoc-gen-grpc-java", version.ref = "grpc" } +grpc-testing = { module = "io.grpc:grpc-testing", version.ref = "grpc" } +tomcat-annotations = { module = "org.apache.tomcat:annotations-api", version.ref = "tomcat-annotations" } +httpclient = { module = "org.apache.httpcomponents.client5:httpclient5", version.ref = "httpclient" } +prometheus = { module = "io.micrometer:micrometer-registry-prometheus" } +mapstruct-base = { module = "org.mapstruct:mapstruct", version.ref = "mapstruct" } +mapstruct-processor = { module = "org.mapstruct:mapstruct-processor", version.ref = "mapstruct" } +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } +log4j-ecs = { module = "co.elastic.logging:log4j2-ecs-layout", version.ref = "log4j-ecs" } +log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } +log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } +log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" } +log4j-slf4j = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } +junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } +assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } +mockito-inline = "org.mockito:mockito-inline:5.2.0" +mockito-junit = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } +plugin-nebula-release = { module = "com.netflix.nebula:nebula-release-plugin", version.ref = "plugin-nebula-release" } +plugin-springboot = { module = "org.springframework.boot:spring-boot-gradle-plugin", version.ref = "spring-boot" } +plugin-lombok = { module = "io.freefair.gradle:lombok-plugin", version.ref = "plugin-lombok" } +plugin-gradle-versions = { module = "com.github.ben-manes:gradle-versions-plugin", version.ref = "plugin-gradle-versions" } +plugin-version-catalog = { module = "nl.littlerobots.vcu:plugin", version.ref = "plugin-version-catalog" } + +[bundles] +logging = [ + "log4j-api", + "log4j-core", + "log4j-ecs", + "log4j-jul", + "log4j-slf4j", + "slf4j-api", +] +grpc = [ + "grpc-api", + "grpc-context", + "grpc-core", + "grpc-netty", + "grpc-proto-lite", + "grpc-protobuf", + "grpc-services", + "grpc-stub", +] +spring-boot = [ + "spring-boot-starter", + "spring-boot-log4j", +] + +spring-boot-server = [ + "spring-boot-actuator", + "spring-boot-log4j", + "spring-boot-mongo", + "spring-boot-thymeleaf", + "spring-boot-validation", + "spring-boot-web", + "spring-cloud-starter", + "spring-sec", + "spring-ui", +] +test = [ + "assertj", + "junit-jupiter", + "junit-jupiter-api", + "mockito-core", + "mockito-inline", + "mockito-junit", +] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..fae0804 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..a69d9cb --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..2a64c64 --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +config.stopbubbling = true \ No newline at end of file diff --git a/module-lib/lib.gradle b/module-lib/lib.gradle new file mode 100644 index 0000000..aa3fd97 --- /dev/null +++ b/module-lib/lib.gradle @@ -0,0 +1,7 @@ +plugins { + id 'twomartens.java' +} + +dependencies { + +} \ No newline at end of file diff --git a/module-lib/src/main/java/de/twomartens/template/model/Name.java b/module-lib/src/main/java/de/twomartens/template/model/Name.java new file mode 100644 index 0000000..bc2c605 --- /dev/null +++ b/module-lib/src/main/java/de/twomartens/template/model/Name.java @@ -0,0 +1,5 @@ +package de.twomartens.template.model; + +public record Name(String value) { + +} diff --git a/module-plain/plain.gradle b/module-plain/plain.gradle new file mode 100644 index 0000000..db2effb --- /dev/null +++ b/module-plain/plain.gradle @@ -0,0 +1,7 @@ +plugins { + id 'twomartens.java' +} + +dependencies { + implementation project(':lib') +} \ No newline at end of file diff --git a/module-plain/src/main/java/de/twomartens/template/Main.java b/module-plain/src/main/java/de/twomartens/template/Main.java new file mode 100644 index 0000000..58e6980 --- /dev/null +++ b/module-plain/src/main/java/de/twomartens/template/Main.java @@ -0,0 +1,13 @@ +package de.twomartens.template; + +import de.twomartens.template.model.Name; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Main { + + public static void main(String[] args) { + Name name = new Name("World"); + log.info("Hello %s!".formatted(name)); + } +} diff --git a/module-plain/src/main/resources/log4j2.xml b/module-plain/src/main/resources/log4j2.xml new file mode 100644 index 0000000..24bce61 --- /dev/null +++ b/module-plain/src/main/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/module-server/server.gradle b/module-server/server.gradle new file mode 100644 index 0000000..d62c9f8 --- /dev/null +++ b/module-server/server.gradle @@ -0,0 +1,9 @@ +plugins { + id 'twomartens.spring-boot-cloud' +} + +dependencies { + implementation libs.mapstruct.base + annotationProcessor libs.mapstruct.processor + +} \ No newline at end of file diff --git a/module-server/src/main/java/de/twomartens/template/MainApplication.java b/module-server/src/main/java/de/twomartens/template/MainApplication.java new file mode 100644 index 0000000..344c0b3 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/MainApplication.java @@ -0,0 +1,35 @@ +package de.twomartens.template; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@SpringBootApplication +public class MainApplication extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { + return builder.sources(MainApplication.class); + } + + public static void main(String[] args) { + SpringApplication.run(MainApplication.class, args); + } + + @Bean + public OpenAPI customOpenAPI(@Value("${openapi.description}") String apiDesciption, + @Value("${openapi.version}") String apiVersion, @Value("${openapi.title}") String apiTitle) { + return new OpenAPI() + .info(new Info() + .title(apiTitle) + .version(apiVersion) + .description(apiDesciption)); + } +} diff --git a/module-server/src/main/java/de/twomartens/template/configuration/ClockConfiguration.java b/module-server/src/main/java/de/twomartens/template/configuration/ClockConfiguration.java new file mode 100644 index 0000000..dbfcccc --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/configuration/ClockConfiguration.java @@ -0,0 +1,20 @@ +package de.twomartens.template.configuration; + +import de.twomartens.template.property.ServiceProperties; +import java.time.Clock; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class ClockConfiguration { + + private final ServiceProperties serviceProperties; + + @Bean + public Clock clock() { + return Clock.system(serviceProperties.getDefaultTimeZone()); + } + +} \ No newline at end of file diff --git a/module-server/src/main/java/de/twomartens/template/configuration/InterceptorConfiguration.java b/module-server/src/main/java/de/twomartens/template/configuration/InterceptorConfiguration.java new file mode 100644 index 0000000..b574c8f --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/configuration/InterceptorConfiguration.java @@ -0,0 +1,22 @@ +package de.twomartens.template.configuration; + +import de.twomartens.template.interceptor.HeaderInterceptorRest; +import de.twomartens.template.interceptor.LoggingInterceptorRest; +import java.time.Clock; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class InterceptorConfiguration { + + @Bean + public LoggingInterceptorRest loggingInterceptorRest(Clock clock) { + return new LoggingInterceptorRest(clock); + } + + @Bean + public HeaderInterceptorRest headerInterceptorRest() { + return new HeaderInterceptorRest(); + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/configuration/OpenApiConfiguration.java b/module-server/src/main/java/de/twomartens/template/configuration/OpenApiConfiguration.java new file mode 100644 index 0000000..5b43c18 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/configuration/OpenApiConfiguration.java @@ -0,0 +1,18 @@ +package de.twomartens.template.configuration; + +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfiguration { + + @Bean + public GroupedOpenApi swaggerApi10() { + return GroupedOpenApi.builder() + .group("1.0") + .pathsToMatch("/template/v1/**") + .build(); + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/configuration/RestTemplateConfiguration.java b/module-server/src/main/java/de/twomartens/template/configuration/RestTemplateConfiguration.java new file mode 100644 index 0000000..e03c787 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/configuration/RestTemplateConfiguration.java @@ -0,0 +1,34 @@ +package de.twomartens.template.configuration; + +import de.twomartens.template.interceptor.HeaderInterceptorRest; +import de.twomartens.template.interceptor.LoggingInterceptorRest; +import de.twomartens.template.property.RestTemplateTimeoutProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfiguration { + + @Bean + public RestTemplate restTemplate(HeaderInterceptorRest headerInterceptorRest, + LoggingInterceptorRest loggingInterceptor, + RestTemplateTimeoutProperties restTemplateTimeoutProperties) { + return new RestTemplateBuilder() + .additionalInterceptors(headerInterceptorRest, loggingInterceptor) + .setConnectTimeout(restTemplateTimeoutProperties.getConnectionRestTemplateTimeoutInMillis()) + .setReadTimeout(restTemplateTimeoutProperties.getReadTimeoutRestTemplateInMillis()) + .build(); + } + + @Bean + public RestTemplate restTemplateRestHealthIndicator(HeaderInterceptorRest headerInterceptorRest, + RestTemplateTimeoutProperties restTemplateTimeoutProperties) { + return new RestTemplateBuilder() + .additionalInterceptors(headerInterceptorRest) + .setConnectTimeout(restTemplateTimeoutProperties.getConnectionRestHealthIndicatorTimeoutInMillis()) + .setReadTimeout(restTemplateTimeoutProperties.getReadTimeoutRestHealthIndicatorInMillis()) + .build(); + } +} diff --git a/module-server/src/main/java/de/twomartens/template/configuration/StatusProbeConfiguration.java b/module-server/src/main/java/de/twomartens/template/configuration/StatusProbeConfiguration.java new file mode 100644 index 0000000..de15d24 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/configuration/StatusProbeConfiguration.java @@ -0,0 +1,28 @@ +package de.twomartens.template.configuration; + +import de.twomartens.template.monitoring.statusprobe.CountBasedStatusProbe; +import de.twomartens.template.monitoring.statusprobe.StatusProbe; +import de.twomartens.template.monitoring.statusprobe.StatusProbeCriticality; +import de.twomartens.template.monitoring.statusprobe.StatusProbeLogger; +import java.time.Clock; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class StatusProbeConfiguration { + + private final Clock clock; + + @Bean + public StatusProbeLogger statusProbeLogger() { + return new StatusProbeLogger(clock); + } + + @Bean + public StatusProbe testStatusProbe(StatusProbeLogger statusProbeLogger) { + return new CountBasedStatusProbe(1, + clock, StatusProbeCriticality.K1, "testStatusProbe", statusProbeLogger); + } +} \ No newline at end of file diff --git a/module-server/src/main/java/de/twomartens/template/configuration/WebConfiguration.java b/module-server/src/main/java/de/twomartens/template/configuration/WebConfiguration.java new file mode 100644 index 0000000..3b6378c --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/configuration/WebConfiguration.java @@ -0,0 +1,28 @@ +package de.twomartens.template.configuration; + +import de.twomartens.template.interceptor.HeaderInterceptorRest; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class WebConfiguration implements WebMvcConfigurer { + + private final HeaderInterceptorRest headerInterceptorRest; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(headerInterceptorRest); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + CorsRegistration registration = registry.addMapping("/**"); + registration.allowedOrigins("*"); + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/controller/ExceptionController.java b/module-server/src/main/java/de/twomartens/template/controller/ExceptionController.java new file mode 100644 index 0000000..c09d4ab --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/controller/ExceptionController.java @@ -0,0 +1,37 @@ +package de.twomartens.template.controller; + +import de.twomartens.template.exception.HttpStatusException; +import de.twomartens.template.model.dto.ErrorMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice(annotations = RestController.class) +@Slf4j +public class ExceptionController extends ResponseEntityExceptionHandler { + + @ExceptionHandler(HttpStatusException.class) + public ResponseEntity handleException(HttpStatusException e) { + if (e.getCause() != null) { + log.info(e.getCause().toString(), e.getCause()); + } else { + log.info(e.toString()); + } + return ResponseEntity.status(e.getStatus()).body(ErrorMessage.builder() + .message(e.getMessage()) + .build()); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException e) { + log.error("unexpected exception occurred", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorMessage.builder() + .message(e.getMessage()) + .build()); + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/controller/VersionHtmlController.java b/module-server/src/main/java/de/twomartens/template/controller/VersionHtmlController.java new file mode 100644 index 0000000..2f23900 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/controller/VersionHtmlController.java @@ -0,0 +1,95 @@ +package de.twomartens.template.controller; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.URL; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * used to show version and title information on html pages + */ +@Slf4j +@Controller +@RequiredArgsConstructor +@RequestMapping(value = "/vorlage") +public class VersionHtmlController { + + @GetMapping(path = "/html/version.html") + public String version() { + return "version"; + } + + @ControllerAdvice + public static class VersionControllerAdvice { + + @ModelAttribute("version") + public String getApplicationVersion() { + return getTitle() + " " + getVersion(); + } + + @ModelAttribute("footerString") + public String getApplicationVersion(@RequestHeader("host") String hostName) { + return getTitle() + " " + getVersion() + " - " + hostName; + } + + private String getTitle() { + return Optional.ofNullable(VersionControllerAdvice.class.getPackage().getImplementationTitle()) + .filter(s -> !s.isBlank()) + .orElse("application"); + } + + public String getVersion() { + return Optional.ofNullable(VersionControllerAdvice.class.getPackage().getImplementationVersion()) + .filter(s -> !s.isBlank()) + .orElse("DEVELOPER"); + } + + @ModelAttribute("hostname") + public String getHostname() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + log.warn(e.toString(), e); + } + return ""; + } + + @ModelAttribute("manifest") + private Collection getManifest() { + try { + URL location = getClass().getProtectionDomain().getCodeSource().getLocation(); + String jarFileName = Paths.get(location.toURI()).toString(); + try (JarFile jarFile = new JarFile(jarFileName)) { + ZipEntry entry = jarFile.getEntry(JarFile.MANIFEST_NAME); + try (InputStream in = jarFile.getInputStream(entry)) { + return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines().toList(); + } + } + } catch (FileNotFoundException ignored) { + // do nothing if manifest file is not available + } catch (Exception e) { + log.info(e.toString(), e); + } + return List.of(getTitle() + " " + getVersion()); + } + + } + + +} diff --git a/module-server/src/main/java/de/twomartens/template/controller/v1/GreetingRestController.java b/module-server/src/main/java/de/twomartens/template/controller/v1/GreetingRestController.java new file mode 100644 index 0000000..0ac0e81 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/controller/v1/GreetingRestController.java @@ -0,0 +1,59 @@ +package de.twomartens.template.controller.v1; + +import de.twomartens.template.mapper.v1.GreetingMapper; +import de.twomartens.template.model.dto.v1.Greeting; +import de.twomartens.template.service.GreetingService; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.mapstruct.factory.Mappers; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping(value = "/template/v1") +@Tag(name = "Greeting Example", description = "all requests relating to greetings") +public class GreetingRestController { + + private final GreetingMapper mapper = Mappers.getMapper(GreetingMapper.class); + + private final GreetingService service; + + @Operation( + summary = "Returns a greeting message", + responses = {@ApiResponse( + responseCode = "200") + } + ) + @GetMapping("/greeting") + public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) { + return mapper.map(service.createGreeting(name)); + } + + @Operation( + summary = "Posts a greeting message to db", + responses = {@ApiResponse( + responseCode = "200") + } + ) + @PostMapping("/greeting") + public void postGreeting(@RequestBody Greeting greeting) { + service.postGreeting(mapper.map(greeting)); + } + + @Hidden + @GetMapping("/healthCheck") + public String checkHealth(@RequestParam(value = "message") String message) { + return message; + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/exception/HttpStatusException.java b/module-server/src/main/java/de/twomartens/template/exception/HttpStatusException.java new file mode 100644 index 0000000..8eac239 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/exception/HttpStatusException.java @@ -0,0 +1,20 @@ +package de.twomartens.template.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class HttpStatusException extends RuntimeException { + + private final HttpStatus status; + + public HttpStatusException(HttpStatus status, String message) { + super(message); + this.status = status; + } + + public HttpStatusException(HttpStatus status, String message, Throwable cause) { + super(message, cause); + this.status = status; + } +} diff --git a/module-server/src/main/java/de/twomartens/template/interceptor/BufferingClientHttpResponseWrapper.java b/module-server/src/main/java/de/twomartens/template/interceptor/BufferingClientHttpResponseWrapper.java new file mode 100644 index 0000000..4404be0 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/interceptor/BufferingClientHttpResponseWrapper.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.twomartens.template.interceptor; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.util.StreamUtils; + +/** + * Simple implementation of {@link ClientHttpResponse} that reads the response's body into memory, thus allowing for + * multiple invocations of {@link #getBody()}. + * + * @author Arjen Poutsma + * @since 3.1 + */ +public final class BufferingClientHttpResponseWrapper implements ClientHttpResponse { + + private final ClientHttpResponse response; + + @Nullable + private byte[] body; + + + public BufferingClientHttpResponseWrapper(ClientHttpResponse response) { + this.response = response; + } + + + @Override + public HttpStatusCode getStatusCode() throws IOException { + return this.response.getStatusCode(); + } + + @Override + @Deprecated + public int getRawStatusCode() throws IOException { + return this.response.getRawStatusCode(); + } + + @Override + public String getStatusText() throws IOException { + return this.response.getStatusText(); + } + + @Override + public HttpHeaders getHeaders() { + return this.response.getHeaders(); + } + + @Override + public InputStream getBody() throws IOException { + if (this.body == null) { + this.body = StreamUtils.copyToByteArray(this.response.getBody()); + } + return new ByteArrayInputStream(this.body); + } + + @Override + public void close() { + this.response.close(); + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptor.java b/module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptor.java new file mode 100644 index 0000000..a750e30 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptor.java @@ -0,0 +1,87 @@ +package de.twomartens.template.interceptor; + +import java.io.Closeable; +import java.util.Optional; +import java.util.UUID; +import org.slf4j.MDC; + +public abstract class HeaderInterceptor { + + public static final String LOGGER_TRACE_ID = "trace.id"; + public static final String LOGGER_REQTYPE_ID = "REQTYPE"; + + public static final String HEADER_FIELD_TRACE_ID = "X-TraceId"; + public static final String HEADER_FIELD_B3_TRACE_ID = "x-b3-traceid"; + public static final String HEADER_FIELD_TYPE_ID = "x-type"; + + public static final String REQ_TYPE_HEALTHCHECK = "HEALTH_CHECK"; + public static final String REQ_TYPE_INTEGRATION_TEST = "INTEGRATION_TEST"; + public static final String REQ_TYPE_SERVER_TEST = "SERVER_TEST"; + public static final String REQ_TYPE_WARMUP = "WARMUP"; + + public String getTraceId() { + return Optional.ofNullable(MDC.get(LOGGER_TRACE_ID)) + .filter(s -> !s.isBlank()) + .orElse(createNewTraceId()); + } + + public Optional getRequestType() { + return Optional.ofNullable(MDC.get(LOGGER_REQTYPE_ID)) + .filter(requestType -> !requestType.isBlank()); + } + + public InterceptorCloseables setTraceId(String traceId) { + return new InterceptorCloseables(MDC.putCloseable(LOGGER_TRACE_ID, traceId)); + } + + public InterceptorCloseables mark(String requestType) { + return new InterceptorCloseables(MDC.putCloseable(LOGGER_REQTYPE_ID, requestType)); + } + + public InterceptorCloseables set(String traceId, String requestType) { + return requestType != null + ? new InterceptorCloseables(setTraceId(traceId), mark(requestType)) + : setTraceId(traceId); + } + + public InterceptorCloseables markAsHealthCheck() { + return new InterceptorCloseables(mark(REQ_TYPE_HEALTHCHECK), setTraceId(createNewTraceId())); + } + + public InterceptorCloseables markAsIntegrationTest() { + return new InterceptorCloseables(mark(REQ_TYPE_INTEGRATION_TEST), setTraceId(createNewTraceId())); + } + + public InterceptorCloseables markAsServerTest() { + return new InterceptorCloseables(mark(REQ_TYPE_SERVER_TEST), setTraceId(createNewTraceId())); + } + + public InterceptorCloseables markAsWarmup() { + return new InterceptorCloseables(mark(REQ_TYPE_WARMUP), setTraceId(createNewTraceId())); + } + + private static String createNewTraceId() { + return UUID.randomUUID().toString(); + } + + public static class InterceptorCloseables implements Closeable { + + private final Closeable[] closeables; + + private InterceptorCloseables(Closeable... closeables) { + this.closeables = closeables; + } + + @Override + public void close() { + for (Closeable closeable : closeables) { + try { + closeable.close(); + } catch (Exception ignored) { + // do nothing + } + } + } + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptorRest.java b/module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptorRest.java new file mode 100644 index 0000000..97ddd4f --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/interceptor/HeaderInterceptorRest.java @@ -0,0 +1,68 @@ +package de.twomartens.template.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; +import java.util.UUID; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +public class HeaderInterceptorRest extends HeaderInterceptor + implements HandlerInterceptor, ClientHttpRequestInterceptor { + + public static final String CLASS_NAME = HeaderInterceptorRest.class.getName(); + + // ClientHttpRequestInterceptor + @Override + @NonNull + public ClientHttpResponse intercept(HttpRequest request, @NonNull byte[] body, + ClientHttpRequestExecution execution) throws IOException { + request.getHeaders().add(HEADER_FIELD_TRACE_ID, getTraceId()); + getRequestType().ifPresent(requestType -> request.getHeaders().add(HEADER_FIELD_TYPE_ID, requestType)); + try { + return execution.execute(request, body); + } finally { + request.getHeaders().remove(HEADER_FIELD_TRACE_ID); + request.getHeaders().remove(HEADER_FIELD_TYPE_ID); + } + } + + // HandlerInterceptor + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, + @NonNull Object handler) { + String traceId = extractTraceId(request); + String requestType = extractRequestType(request); + InterceptorCloseables closeable = set(traceId, requestType); + request.setAttribute(CLASS_NAME, closeable); + return true; + } + + public static String extractTraceId(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader(HEADER_FIELD_TRACE_ID)).filter(s -> !s.isBlank()) + .or(() -> Optional.ofNullable(request.getHeader(HEADER_FIELD_B3_TRACE_ID)).filter(s -> !s.isBlank())) + .orElse(UUID.randomUUID().toString()); + } + + public static String extractRequestType(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader(HEADER_FIELD_TYPE_ID)) + .filter(reqType -> !reqType.isBlank()) + .orElse(null); + } + + // HandlerInterceptor + @Override + public void postHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, + @NonNull Object handler, ModelAndView modelAndView) { + Optional.ofNullable(request.getAttribute(CLASS_NAME)) + .filter(InterceptorCloseables.class::isInstance) + .map(InterceptorCloseables.class::cast) + .ifPresent(InterceptorCloseables::close); + } +} diff --git a/module-server/src/main/java/de/twomartens/template/interceptor/LoggingInterceptorRest.java b/module-server/src/main/java/de/twomartens/template/interceptor/LoggingInterceptorRest.java new file mode 100644 index 0000000..be23359 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/interceptor/LoggingInterceptorRest.java @@ -0,0 +1,419 @@ +package de.twomartens.template.interceptor; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeSet; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.Builder; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager.Log4jMarker; +import org.apache.logging.log4j.message.StringMapMessage; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.lang.NonNull; +import org.springframework.util.StreamUtils; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +@Log4j2 +@RequiredArgsConstructor +public class LoggingInterceptorRest implements Filter, ClientHttpRequestInterceptor { + + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + public static final Marker MARKER = new Log4jMarker("communication"); + + private static final int MAX_LOG_SIZE = 20480; // 20 KB - logging could fail with bigger logmessages + + public static final String DIRECTION_IN = "inbound"; + public static final String DIRECTION_OUT = "outbound"; + public static final String PROTOCOL_NAME = "http"; + + public static final String PARAM_URL_FULL = "url.full"; + public static final String PARAM_URL_DOMAIN = "url.domain"; + public static final String PARAM_URL_EXTENSION = "url.extension"; + public static final String PARAM_URL_PATH = "url.path"; + public static final String PARAM_URL_PORT = "url.port"; + public static final String PARAM_URL_SCHEME = "url.scheme"; + public static final String PARAM_URL_QUERY = "url.query"; + public static final String PARAM_BUSINESS_TYPE = "http.request.type"; + public static final String PARAM_REQUEST_BODY = "http.request.body.content"; + public static final String PARAM_RESPONSE_BODY = "http.response.body.content"; + public static final String PARAM_RESPONSE_STATUS = "http.response.status_code"; + public static final String PARAM_REQUEST_HEADERS = "http.request.headers"; + public static final String PARAM_RESPONSE_HEADERS = "http.response.headers"; + public static final String PARAM_REQUEST_BYTES = "http.request.body.bytes"; + public static final String PARAM_RESPONSE_BYTES = "http.response.body.bytes"; + public static final String PARAM_REQUEST_MIMETYPE = "http.request.mime_type"; + public static final String PARAM_RESPONSE_MIMETYPE = "http.response.mime_type"; + public static final String PARAM_REQUEST_METHOD = "http.request.method"; + public static final String PARAM_REQUEST_REFERER = "http.request.referrer"; + public static final String PARAM_REQUEST_TIME = "event.start"; + public static final String PARAM_RESPONSE_TIME = "event.end"; + public static final String PARAM_DURATION = "event.duration"; + public static final String PARAM_USER_AGENT = "user_agent.original"; + public static final String PARAM_DIRECTION = "network.direction"; + public static final String PARAM_PROTOCOL = "network.protocol"; + + private final FieldLogBehaviour requestLogBehaviour; + private final FieldLogBehaviour responseLogBehaviour; + private final Clock clock; + + public LoggingInterceptorRest(Clock clock) { + this(FieldLogBehaviour.NEVER, FieldLogBehaviour.NEVER, clock); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + ZonedDateTime requestTime = ZonedDateTime.now(clock); + HttpServletRequest httpRequest = (HttpServletRequest) request; + ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpRequest); + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper( + (HttpServletResponse) response); + + byte[] fullResponseBytes = null; + Throwable throwable = null; + String responseBody = null; + int httpStatusCode = -1; + + try { + try { + chain.doFilter( + requestLogBehaviour != FieldLogBehaviour.NEVER ? requestWrapper : httpRequest, + responseWrapper); + if (responseLogBehaviour != FieldLogBehaviour.NEVER) { + fullResponseBytes = responseWrapper.getContentAsByteArray(); + } + httpStatusCode = responseWrapper.getStatus(); + } finally { + responseWrapper.copyBodyToResponse(); + } + } catch (Exception e) { + throwable = e; + throw e; + } finally { + try { + int responseSize = responseWrapper.getContentSize(); + Map> responseHeaders = extractHeaders( + responseWrapper.getHeaderNames().iterator(), + s -> responseWrapper.getHeaders(s).iterator()); + if ((responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode) + || responseLogBehaviour == FieldLogBehaviour.ALWAYS) && fullResponseBytes != null) { + responseBody = new String(fullResponseBytes, + determineResponseEncoding(responseHeaders, fullResponseBytes)); + } + URL requestUrl = new URL(Optional.ofNullable(httpRequest.getQueryString()) + .map(qs -> httpRequest.getRequestURL().toString() + "?" + qs) + .orElse(httpRequest.getRequestURL().toString())); + Map> requestHeaders = extractHeaders( + httpRequest.getHeaderNames().asIterator(), + s -> httpRequest.getHeaders(s).asIterator()); + String requestBody = null; + String businessType = null; + if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode) + || requestLogBehaviour == FieldLogBehaviour.ALWAYS) { + byte[] fullRequestBytes = requestWrapper.getContentAsByteArray(); + if (fullRequestBytes != null) { + requestBody = new String(fullRequestBytes, + determineRequestEncoding(requestHeaders, fullRequestBytes)); + } + businessType = determineBusinessType(requestUrl, requestBody); + } + log(LogMessage.builder() + .requestHeaders(requestHeaders) + .responseHeaders(responseHeaders) + .url(requestUrl) + .method(httpRequest.getMethod()) + .requestMimeType(typeToString(request.getContentType())) + .responseMimeType(typeToString(response.getContentType())) + .requestBody(requestBody) + .responseBody(responseBody) + .requestSize(httpRequest.getContentLength()) + .responseSize(responseSize) + .httpStatus(httpStatusCode) + .direction(DIRECTION_IN) + .requestTime(requestTime) + .responseTime(ZonedDateTime.now(clock)) + .traceId(HeaderInterceptorRest.extractTraceId(httpRequest)) + .requestType(HeaderInterceptorRest.extractRequestType(httpRequest)) + .businessType(businessType) + .throwable(throwable) + .build()); + } catch (RuntimeException e) { + log.error(e.toString(), e); + } + } + } + + private boolean isError(int httpStatusCode) { + return httpStatusCode >= 400 && httpStatusCode < 600; + } + + @NonNull + @Override + public ClientHttpResponse intercept(@NonNull HttpRequest request, + @NonNull byte[] requestBytes, @NonNull ClientHttpRequestExecution execution) + throws IOException { + + ZonedDateTime requestTime = ZonedDateTime.now(clock); + int responseSize = 0; + Map> responseHeaders = Collections.emptyMap(); + MediaType responseMediaType = null; + int httpStatusCode = -1; + Throwable throwable = null; + String requestBody = null; + String responseBody = null; + String businessType = null; + + try { + BufferingClientHttpResponseWrapper result = new BufferingClientHttpResponseWrapper( + execution.execute(request, requestBytes)); + byte[] responseBytes = StreamUtils.copyToByteArray(result.getBody()); + responseSize = responseBytes.length; + responseHeaders = extractHeaders(result.getHeaders()); + responseMediaType = result.getHeaders().getContentType(); + httpStatusCode = result.getStatusCode().value(); + if (responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode) + || responseLogBehaviour == FieldLogBehaviour.ALWAYS) { + responseBody = new String(responseBytes, + determineRequestEncoding(responseHeaders, responseBytes)); + } + return result; + } catch (Exception e) { + throwable = e; + throw e; + } finally { + try { + URL url = request.getURI().toURL(); + Map> requestHeaders = extractHeaders(request.getHeaders()); + if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode) + || requestLogBehaviour == FieldLogBehaviour.ALWAYS) { + requestBody = new String(requestBytes, + determineRequestEncoding(requestHeaders, requestBytes)); + businessType = determineBusinessType(url, requestBody); + } + log(LogMessage.builder() + .requestHeaders(requestHeaders) + .responseHeaders(responseHeaders) + .url(url) + .method(Objects.requireNonNull(request.getMethod()).toString()) + .requestMimeType(typeToString(request.getHeaders().getContentType())) + .requestBody(requestBody) + .responseBody(responseBody) + .responseMimeType(typeToString(responseMediaType)) + .requestSize(requestBytes.length) + .responseSize(responseSize) + .httpStatus(httpStatusCode) + .direction(DIRECTION_OUT) + .requestTime(requestTime) + .responseTime(ZonedDateTime.now(clock)) + .businessType(businessType) + .throwable(throwable) + .build()); + } catch (RuntimeException e) { + log.error(e.toString(), e); + } + } + } + + private void log(LogMessage logMessage) { + StringMapMessage stringMapMessage = new StringMapMessage(); + addLogString(stringMapMessage, PARAM_REQUEST_HEADERS, + toHeaderString(logMessage.requestHeaders())); + addLogString(stringMapMessage, PARAM_RESPONSE_HEADERS, + toHeaderString(logMessage.responseHeaders())); + addLogString(stringMapMessage, PARAM_URL_FULL, logMessage.url().toString()); + addLogString(stringMapMessage, PARAM_URL_DOMAIN, logMessage.url().getHost()); + addLogString(stringMapMessage, PARAM_URL_EXTENSION, + extractExtension(logMessage.url().getPath())); + addLogString(stringMapMessage, PARAM_URL_PATH, logMessage.url().getPath()); + addLogString(stringMapMessage, PARAM_URL_PORT, Integer.toString(logMessage.url().getPort())); + addLogString(stringMapMessage, PARAM_URL_SCHEME, logMessage.url().getProtocol()); + addLogString(stringMapMessage, PARAM_URL_QUERY, logMessage.url().getQuery()); + addLogString(stringMapMessage, PARAM_REQUEST_METHOD, logMessage.method()); + addLogString(stringMapMessage, PARAM_REQUEST_REFERER, + getHeader(logMessage.requestHeaders(), HttpHeaders.REFERER)); + addLogString(stringMapMessage, PARAM_REQUEST_MIMETYPE, logMessage.requestMimeType()); + addLogString(stringMapMessage, PARAM_RESPONSE_MIMETYPE, logMessage.responseMimeType()); + addLogString(stringMapMessage, PARAM_REQUEST_BYTES, Integer.toString(logMessage.requestSize())); + addLogString(stringMapMessage, PARAM_RESPONSE_BYTES, + Integer.toString(logMessage.responseSize())); + addLogString(stringMapMessage, PARAM_RESPONSE_STATUS, + Integer.toString(logMessage.httpStatus())); + addLogString(stringMapMessage, PARAM_DIRECTION, logMessage.direction()); + addLogString(stringMapMessage, PARAM_PROTOCOL, PROTOCOL_NAME); + addLogString(stringMapMessage, PARAM_REQUEST_TIME, + logMessage.requestTime().format(DATE_TIME_FORMATTER)); + addLogString(stringMapMessage, PARAM_RESPONSE_TIME, + logMessage.responseTime().format(DATE_TIME_FORMATTER)); + addLogString(stringMapMessage, PARAM_DURATION, + Long.toString(getDurationBetweenRequestAndResponseTime(logMessage).toNanos())); + addLogString(stringMapMessage, PARAM_USER_AGENT, + getHeader(logMessage.requestHeaders(), HttpHeaders.USER_AGENT)); + addLogString(stringMapMessage, HeaderInterceptor.LOGGER_TRACE_ID, logMessage.traceId()); + addLogString(stringMapMessage, HeaderInterceptor.LOGGER_REQTYPE_ID, logMessage.requestType()); + addLogString(stringMapMessage, PARAM_BUSINESS_TYPE, logMessage.businessType()); + addLogString(stringMapMessage, PARAM_REQUEST_BODY, cutToMaxLength(logMessage.requestBody())); + addLogString(stringMapMessage, PARAM_RESPONSE_BODY, cutToMaxLength(logMessage.responseBody())); + + log.debug(MARKER, stringMapMessage, logMessage.throwable()); + } + + private Duration getDurationBetweenRequestAndResponseTime(LogMessage logMessage) { + return Duration.between(logMessage.requestTime(), logMessage.responseTime()); + } + + private String getHeader(Map> headers, String headerKey) { + return headers.entrySet().stream() + .filter(e -> e.getKey().equalsIgnoreCase(headerKey)) + .flatMap(e -> e.getValue().stream()) + .findAny() + .orElse(null); + } + + private void addLogString(StringMapMessage stringMapMessage, String key, String value) { + if (value != null && !value.isBlank()) { + stringMapMessage.with(key, value.trim()); + } + } + + private static Map> extractHeaders(Iterator headerNames, + Function> headerValuesSupplier) { + Map> requestHeaders = new HashMap<>(); + while (headerNames.hasNext()) { + String name = headerNames.next(); + Collection values = requestHeaders.computeIfAbsent(name, n -> new TreeSet<>()); + Iterator headerValues = headerValuesSupplier.apply(name); + while (headerValues.hasNext()) { + values.add(headerValues.next()); + } + } + return requestHeaders; + } + + private static Map> extractHeaders(HttpHeaders headers) { + Map> result = new HashMap<>(); + for (Entry> entry : headers.entrySet()) { + result.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + return result; + } + + private static String toHeaderString(Map> headerMap) { + return headerMap.entrySet().stream() + .flatMap(es -> es.getValue().stream().map(v -> new KeyValuePair(es.getKey(), v))) + .map(kv -> kv.key() + "=" + kv.value()) + .collect(Collectors.joining("\n")); + } + + private static String typeToString(String contentType) { + try { + return Optional.ofNullable(contentType) + .map(MediaType::parseMediaType) + .map(LoggingInterceptorRest::typeToString) + .orElse(null); + } catch (RuntimeException e) { + log.info(e.toString(), e); + return e.toString(); + } + } + + private static String typeToString(MediaType mediaType) { + try { + return Optional.ofNullable(mediaType) + .map(m -> m.getType() + "/" + m.getSubtype()) + .orElse(null); + } catch (RuntimeException e) { + log.info(e.toString(), e); + return e.toString(); + } + } + + private static String extractExtension(String fileName) { + return Optional.ofNullable(fileName) + .filter(name -> name.contains(".")) + .map(name -> name.substring(name.lastIndexOf('.') + 1)) + .orElse(null); + } + + private static String cutToMaxLength(String string) { + if (string != null && string.length() > MAX_LOG_SIZE) { + return string.substring(0, MAX_LOG_SIZE); + } + return string; + } + + /** + * usually returns null, but can be overridden to implement more complex logic + */ + public String determineBusinessType(URL requestUrl, String requestBody) { + return null; + } + + /** + * usually returns UTF-8, but can be overridden to implement more complex logic + */ + public Charset determineRequestEncoding(Map> requestHeaders, + byte[] fullRequest) { + return StandardCharsets.UTF_8; + } + + /** + * usually returns UTF-8, but can be overridden to implement more complex logic + */ + public Charset determineResponseEncoding(Map> responseHeaders, + byte[] fullResponse) { + return StandardCharsets.UTF_8; + } + + @Builder + private record LogMessage(Map> requestHeaders, + Map> responseHeaders, URL url, String method, + String requestMimeType, String responseMimeType, String requestBody, + String responseBody, + int requestSize, int responseSize, int httpStatus, String direction, + ZonedDateTime requestTime, ZonedDateTime responseTime, String traceId, + String requestType, + String businessType, Throwable throwable) { + + } + + private record KeyValuePair(String key, String value) { + + } + + public enum FieldLogBehaviour { + NEVER, ONLY_ON_ERROR, ALWAYS + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/mapper/v1/GreetingMapper.java b/module-server/src/main/java/de/twomartens/template/mapper/v1/GreetingMapper.java new file mode 100644 index 0000000..7bc4d90 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/mapper/v1/GreetingMapper.java @@ -0,0 +1,21 @@ +package de.twomartens.template.mapper.v1; + +import de.twomartens.template.model.db.Greeting; +import org.mapstruct.CollectionMappingStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.ReportingPolicy; + +@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface GreetingMapper { + + de.twomartens.template.model.dto.v1.Greeting map(Greeting greeting); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "created", ignore = true) + @Mapping(target = "lastModified", ignore = true) + Greeting map(de.twomartens.template.model.dto.v1.Greeting greeting); +} diff --git a/module-server/src/main/java/de/twomartens/template/model/db/Greeting.java b/module-server/src/main/java/de/twomartens/template/model/db/Greeting.java new file mode 100644 index 0000000..ed137f0 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/model/db/Greeting.java @@ -0,0 +1,41 @@ +package de.twomartens.template.model.db; + +import java.util.Date; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.FieldDefaults; +import org.bson.types.ObjectId; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document +@Getter +@Setter +@Builder +@EqualsAndHashCode +@ToString +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Greeting { + + @Id + ObjectId id; + + @CreatedDate + Date created; + + @LastModifiedDate + Date lastModified; + + String message; + +} diff --git a/module-server/src/main/java/de/twomartens/template/model/dto/ErrorMessage.java b/module-server/src/main/java/de/twomartens/template/model/dto/ErrorMessage.java new file mode 100644 index 0000000..cb5a893 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/model/dto/ErrorMessage.java @@ -0,0 +1,24 @@ +package de.twomartens.template.model.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.FieldDefaults; + +@Getter +@Setter +@Builder +@EqualsAndHashCode +@ToString +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ErrorMessage { + + String message; +} diff --git a/module-server/src/main/java/de/twomartens/template/model/dto/v1/Greeting.java b/module-server/src/main/java/de/twomartens/template/model/dto/v1/Greeting.java new file mode 100644 index 0000000..9a8fdde --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/model/dto/v1/Greeting.java @@ -0,0 +1,31 @@ +package de.twomartens.template.model.dto.v1; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.FieldDefaults; +import org.springframework.lang.NonNull; + +@Getter +@Setter +@Builder +@EqualsAndHashCode +@ToString +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Schema(description = "Data container for a greeting message") +public class Greeting { + + @NonNull + @Schema(description = "Data container for a greeting message", example = "Hello Helmut!", + defaultValue = "Hello World!") + private final String message; + +} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractHealthIndicator.java b/module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractHealthIndicator.java new file mode 100644 index 0000000..5b0cfd3 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractHealthIndicator.java @@ -0,0 +1,84 @@ +package de.twomartens.template.monitoring.actuator; + +import java.io.Closeable; +import java.io.IOException; +import java.time.Clock; +import java.time.Duration; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.slf4j.MDC.MDCCloseable; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; + +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractHealthIndicator implements HealthIndicator { + + public static final String HOST = "localhost"; + public static final String HTTP_PREFIX = "http://"; + public static final String HOST_PORT_SEPERATOR = ":"; + public static final String PATH_SEPERATOR = "/"; + public static final String PARAMETER_SEPERATOR = "?"; + public static final String DETAIL_ENDPOINT_KEY = "endpoint"; + + private final String logStatusDownMessage = String.format("health indicator '%s' invoked with status '%s'", + indicatorName(), Status.DOWN.getCode()); + private final String logStatusUpMessage = String.format("health indicator '%s' invoked with status '%s'", + indicatorName(), Status.UP.getCode()); + private final Clock clock; + private final Preparable preparable; + private boolean firstTime = true; + + /** + * main method that determines the health of the service + */ + protected abstract Health determineHealth(); + + @Override + public Health health() { + try (Closeable ignored = preparable.prepare()) { + Health result = null; + Exception exception = null; + long start = clock.millis(); + try { + result = determineHealth(); + } catch (RuntimeException e) { + exception = e; + result = Health.down().withException(e).build(); + } finally { + logInvocation(result, exception, start, clock.millis()); + } + return result; + } catch (IOException e) { + log.error("unexpected exception occurred", e); + return Health.down(e).build(); + } + } + + private void logInvocation(Health health, Exception exception, long start, long end) { + Duration duration = Duration.ofMillis(end - start); + try (MDCCloseable ignored = MDC.putCloseable("event.duration", Long.toString(duration.toNanos()))) { + if (exception != null || health == null) { + log.error(logStatusDownMessage, exception); + firstTime = true; + } else if (health.getStatus() == Status.DOWN) { + log.warn(logStatusDownMessage); + firstTime = true; + } else if (firstTime) { + log.info(logStatusUpMessage); + firstTime = false; + } else { + log.trace(logStatusUpMessage); + } + } + } + + private String indicatorName() { + return this.getClass().getSimpleName() + .replace("HealthIndicator", "") + .toLowerCase(); + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractStatusProbeHealthIndicator.java b/module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractStatusProbeHealthIndicator.java new file mode 100644 index 0000000..9ee1ab8 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/monitoring/actuator/AbstractStatusProbeHealthIndicator.java @@ -0,0 +1,37 @@ +package de.twomartens.template.monitoring.actuator; + +import de.twomartens.template.monitoring.statusprobe.StatusProbe; +import java.time.Clock; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.boot.actuate.health.HealthIndicator; + +@Slf4j +public abstract class AbstractStatusProbeHealthIndicator extends AbstractHealthIndicator + implements HealthIndicator { + + public static final String MESSAGE_KEY = "message"; + public static final String LAST_STATUS_CHANGE_KEY = "lastStatusChange"; + + private final StatusProbe statusProbe; + + public AbstractStatusProbeHealthIndicator(Clock timeProvider, Preparable headerInterceptor, + StatusProbe statusProbe) { + super(timeProvider, headerInterceptor); + this.statusProbe = statusProbe; + } + + @Override + protected Health determineHealth() { + Builder healthBuilder = Health.status(statusProbe.getStatus()); + Optional.ofNullable(statusProbe.getLastStatusChange()) + .ifPresent(l -> healthBuilder.withDetail(LAST_STATUS_CHANGE_KEY, l)); + Optional.ofNullable(statusProbe.getThrowable()).ifPresent(healthBuilder::withException); + Optional.ofNullable(statusProbe.getMessage()) + .ifPresent(m -> healthBuilder.withDetail(MESSAGE_KEY, m)); + return healthBuilder.build(); + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/actuator/Preparable.java b/module-server/src/main/java/de/twomartens/template/monitoring/actuator/Preparable.java new file mode 100644 index 0000000..0b53049 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/monitoring/actuator/Preparable.java @@ -0,0 +1,10 @@ +package de.twomartens.template.monitoring.actuator; + +import java.io.Closeable; + +@FunctionalInterface +public interface Preparable { + + Closeable prepare(); + +} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/actuator/RestHealthIndicator.java b/module-server/src/main/java/de/twomartens/template/monitoring/actuator/RestHealthIndicator.java new file mode 100644 index 0000000..62c1a5e --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/monitoring/actuator/RestHealthIndicator.java @@ -0,0 +1,65 @@ +package de.twomartens.template.monitoring.actuator; + +import de.twomartens.template.interceptor.HeaderInterceptorRest; +import java.security.SecureRandom; +import java.time.Clock; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +/** + * A Health check which checks if the rest services are working. + *

+ * If you have a complex service, you should think about an easy greeting or echo service, which + * only tests the + * network/service stack and not the full application. + *

+ * The health check will be called by kubernetes to check if the container/pod should be in load + * balancing. It is possible + * to have as much health checks as you like. + *

+ * There should be a health check which is ok not before all data is loaded. + */ +@Slf4j +@Component +public class RestHealthIndicator extends AbstractHealthIndicator implements HealthIndicator { + + private static final String URL_PATH = "/template/v1/healthCheck"; + private static final String GET_PARAMETER = "message="; + + private final SecureRandom randomizer = new SecureRandom(); + private final RestTemplate restTemplateRestHealthIndicator; + private final String urlPrefix; + + public RestHealthIndicator(Clock clock, HeaderInterceptorRest interceptor, + ServerProperties serverProperties, RestTemplate restTemplateRestHealthIndicator) { + super(clock, interceptor::markAsHealthCheck); + this.restTemplateRestHealthIndicator = restTemplateRestHealthIndicator; + urlPrefix = HTTP_PREFIX + HOST + HOST_PORT_SEPERATOR + serverProperties.getPort() + + URL_PATH + PARAMETER_SEPERATOR + GET_PARAMETER; + } + + /** + * main method that determines the health of the service + */ + @Override + protected Health determineHealth() { + String random = Integer.toString(randomizer.nextInt(100000, 999999)); + String url = urlPrefix + "{random}"; + ResponseEntity response = restTemplateRestHealthIndicator.getForEntity(url, String.class, random); + Status status = Optional.ofNullable(response.getBody()) + .filter(random::equals) + .map(m -> Status.UP) + .orElse(Status.DOWN); + return Health.status(status) + .withDetail(DETAIL_ENDPOINT_KEY, url) + .build(); + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/CountBasedStatusProbe.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/CountBasedStatusProbe.java new file mode 100644 index 0000000..799337e --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/CountBasedStatusProbe.java @@ -0,0 +1,33 @@ +package de.twomartens.template.monitoring.statusprobe; + +import java.time.Clock; +import java.util.concurrent.atomic.AtomicInteger; +import org.springframework.boot.actuate.health.Status; + +public class CountBasedStatusProbe extends StatusProbe { + + private final AtomicInteger failureCount = new AtomicInteger(0); + + private final int maxFailureCount; + + public CountBasedStatusProbe(int maxFailureCount, Clock clock, StatusProbeCriticality criticality, String name, + StatusProbeLogger statusProbeLogger) { + super(clock, criticality, name, statusProbeLogger); + this.maxFailureCount = maxFailureCount; + } + + @Override + protected synchronized void setStatus(Status status, Throwable throwable, String message) { + if (status == Status.DOWN) { + int failureCount = this.failureCount.incrementAndGet(); + if (failureCount > maxFailureCount) { + super.setStatus(status, throwable, message); + } + } else if (status == Status.UP) { + this.failureCount.set(0); + super.setStatus(status, throwable, message); + } + } + + +} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/PercentageBasedStatusProbe.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/PercentageBasedStatusProbe.java new file mode 100644 index 0000000..a0ae97c --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/PercentageBasedStatusProbe.java @@ -0,0 +1,63 @@ +package de.twomartens.template.monitoring.statusprobe; + +import java.time.Clock; +import java.time.Duration; +import org.springframework.boot.actuate.health.Status; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +/** + * uses the percentage of down statuses within a given period (default: 1 min) to determine if status of probe is down. + * this is meant to be used to avoid flickering status probes on services that have lots of status updates. When there + * is no significant amount of requests during one scheduling period, the behavior may be arbitrary. + */ +public class PercentageBasedStatusProbe extends StatusProbe implements ScheduledStatusProbe { + + private final int maxFailurePercent; + + private int requestCount = 0; + private int downCount = 0; + private Throwable throwable; + private String message; + + public PercentageBasedStatusProbe(int maxFailurePercent, Clock clock, + ThreadPoolTaskScheduler threadPoolTaskScheduler, StatusProbeCriticality criticality, String name, + StatusProbeLogger statusProbeLogger) { + this(maxFailurePercent, clock, threadPoolTaskScheduler, Duration.ofMinutes(1), criticality, name, + statusProbeLogger); + } + + public PercentageBasedStatusProbe(int maxFailurePercent, Clock clock, + ThreadPoolTaskScheduler threadPoolTaskScheduler, Duration schedulePeriod, StatusProbeCriticality criticality, + String name, StatusProbeLogger statusProbeLogger) { + super(clock, criticality, name, statusProbeLogger); + this.maxFailurePercent = maxFailurePercent; + scheduleTask(threadPoolTaskScheduler, schedulePeriod); + } + + @Override + protected synchronized void setStatus(Status status, Throwable throwable, String message) { + if (status == Status.DOWN) { + downCount++; + this.throwable = throwable; + this.message = message; + } + requestCount++; + } + + private void reset() { + requestCount = 0; + downCount = 0; + throwable = null; + message = null; + } + + public synchronized void runScheduledTask() { + if (requestCount > 0 && (downCount * 100.0 / requestCount) > maxFailurePercent) { + super.setStatus(Status.DOWN, throwable, message); + } else { + super.setStatus(Status.UP, null, null); + } + reset(); + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/ScheduledStatusProbe.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/ScheduledStatusProbe.java new file mode 100644 index 0000000..8a19fd1 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/ScheduledStatusProbe.java @@ -0,0 +1,18 @@ +package de.twomartens.template.monitoring.statusprobe; + +import java.time.Duration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.support.PeriodicTrigger; + +public interface ScheduledStatusProbe { + + void runScheduledTask(); + + default void scheduleTask(ThreadPoolTaskScheduler threadPoolTaskScheduler, + Duration schedulePeriod) { + PeriodicTrigger periodicTrigger = new PeriodicTrigger( + Duration.ofSeconds(schedulePeriod.toSeconds())); + threadPoolTaskScheduler.schedule(this::runScheduledTask, periodicTrigger); + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbe.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbe.java new file mode 100644 index 0000000..36a0bf9 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbe.java @@ -0,0 +1,62 @@ +package de.twomartens.template.monitoring.statusprobe; + +import java.time.Clock; +import java.time.ZonedDateTime; +import lombok.Getter; +import org.springframework.boot.actuate.health.Status; + +@Getter +public class StatusProbe { + + private final Clock clock; + + private Status status = Status.UP; + private Throwable throwable = null; + private String message = null; + private ZonedDateTime lastStatusChange; + private final StatusProbeLogger statusProbeLogger; + private final String name; + + public StatusProbe(Clock clock, StatusProbeCriticality criticality, String name, + StatusProbeLogger statusProbeLogger) { + this.clock = clock; + this.name = name; + this.statusProbeLogger = statusProbeLogger; + statusProbeLogger.registerStatusProbe(name, criticality); + } + + protected void setStatus(Status status, Throwable throwable, String message) { + if (status != this.status) { + lastStatusChange = ZonedDateTime.now(clock); + statusProbeLogger.logStatusChange(name, message, status, lastStatusChange, throwable); + } + this.status = status; + this.throwable = throwable; + this.message = message; + } + + public void up() { + setStatus(Status.UP, null, null); + } + + public void up(String message) { + setStatus(Status.UP, null, message); + } + + public void down() { + setStatus(Status.DOWN, null, null); + } + + public void down(Throwable throwable) { + setStatus(Status.DOWN, throwable, null); + } + + public void down(String message) { + setStatus(Status.DOWN, null, message); + } + + protected void down(Throwable throwable, String message) { + setStatus(Status.DOWN, throwable, message); + } + +} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeCriticality.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeCriticality.java new file mode 100644 index 0000000..f7f4f55 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeCriticality.java @@ -0,0 +1,7 @@ +package de.twomartens.template.monitoring.statusprobe; + + +public enum StatusProbeCriticality { + K1, K2, K3 +} + diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeLogger.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeLogger.java new file mode 100644 index 0000000..9ddfa43 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/StatusProbeLogger.java @@ -0,0 +1,124 @@ +package de.twomartens.template.monitoring.statusprobe; + +import static de.twomartens.template.monitoring.statusprobe.StatusProbeCriticality.K1; +import static de.twomartens.template.monitoring.statusprobe.StatusProbeCriticality.K2; +import static de.twomartens.template.monitoring.statusprobe.StatusProbeCriticality.K3; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; +import org.apache.logging.log4j.message.StringMapMessage; +import org.springframework.boot.actuate.health.Status; + +public class StatusProbeLogger { + + private static final Marker MARKER = MarkerManager.getMarker("statusprobe"); + + private static final String LABEL_CRITICALITY = "label.status.criticality"; + private static final String LABEL_STATUS = "label.status.status"; + private static final String LABEL_REASON = "label.status.reason"; + private static final String LABEL_MESSAGE = "label.status.description"; + private static final String LABEL_LAST_STATUS_CHANGE = "label.status.last_change"; + + private final Clock clock; + private final Logger commLog; + private final Map statusProbeToStatus = new HashMap<>(); + + public StatusProbeLogger(Clock clock) { + this(clock, LogManager.getLogger("statusprobe")); + } + + /** + * only for testing purposes + */ + StatusProbeLogger(Clock clock, Logger commLog) { + this.clock = clock; + this.commLog = commLog; + } + + public void registerStatusProbe(String name, StatusProbeCriticality criticality) { + statusProbeToStatus.put(new ProbeIdent(name, criticality), Status.UP); + logStatusChange(name, "Startup", Status.UP, ZonedDateTime.now(clock), null); + } + + public void logStatusChange(String name, String message, Status status, ZonedDateTime lastStatusChange, + Throwable throwable) { + ProbeIdent probeIdent = getProbeIdent(name); + if (probeIdent == null) { + probeIdent = new ProbeIdent(name, K1); + } + statusProbeToStatus.put(probeIdent, status); + createLog(message, lastStatusChange, throwable); + } + + private ProbeIdent getProbeIdent(String name) { + return statusProbeToStatus.keySet().stream().filter(key -> key.name.equals(name)).findFirst().orElse(null); + } + + private void createLog(String message, ZonedDateTime lastStatusChange, Throwable throwable) { + Status overallStatus = getOverallStatus(); + StatusProbeCriticality criticality = getOverallCriticality(); + if (message == null) { + message = ""; + } + if (Status.UP.equals(overallStatus)) { + commLog.info(MARKER, new StringMapMessage() + .with(LABEL_CRITICALITY, criticality) + .with(LABEL_STATUS, overallStatus) + .with(LABEL_MESSAGE, message) + .with(LABEL_LAST_STATUS_CHANGE, lastStatusChange)); + } else { + commLog.error(MARKER, new StringMapMessage() + .with(LABEL_CRITICALITY, criticality) + .with(LABEL_STATUS, overallStatus) + .with(LABEL_MESSAGE, message) + .with(LABEL_REASON, getReason()) + .with(LABEL_LAST_STATUS_CHANGE, lastStatusChange), throwable); + } + } + + private StatusProbeCriticality getOverallCriticality() { + List crits = statusProbeToStatus.keySet().stream().map(key -> key.criticality).toList(); + return crits.contains(K1) ? K1 : crits.contains(K2) ? K2 : K3; + } + + private Status getOverallStatus() { + if (statusProbeToStatus.containsValue(Status.DOWN)) { + return Status.DOWN; + } + return Status.UP; + } + + private String getReason() { + + List probesDown = statusProbeToStatus.entrySet().stream() + .filter(entry -> Status.DOWN.equals(entry.getValue())) + .map(Map.Entry::getKey) + .toList(); + + String reasonK1 = getDownStatusProbes(probesDown, K1); + String reasonK2 = getDownStatusProbes(probesDown, K2); + String reasonK3 = getDownStatusProbes(probesDown, K3); + + return "%s%s%s".formatted(reasonK1, reasonK2, reasonK3).trim(); + } + + private String getDownStatusProbes(List probesDown, StatusProbeCriticality criticality) { + List downProbeNames = probesDown.stream().filter(probe -> probe.criticality.equals(criticality)) + .map(probe -> probe.name).toList(); + if (downProbeNames.size() > 0) { + return criticality + " failed: " + String.join(",", downProbeNames) + "\n"; + } + return ""; + } + + public record ProbeIdent(String name, StatusProbeCriticality criticality) { + + } +} diff --git a/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/TimeBasedStatusProbe.java b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/TimeBasedStatusProbe.java new file mode 100644 index 0000000..250dcba --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/monitoring/statusprobe/TimeBasedStatusProbe.java @@ -0,0 +1,64 @@ +package de.twomartens.template.monitoring.statusprobe; + +import java.time.Clock; +import java.time.Duration; +import java.time.ZonedDateTime; +import org.springframework.boot.actuate.health.Status; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +public class TimeBasedStatusProbe extends StatusProbe implements ScheduledStatusProbe { + + private final Clock clock; + + private final Duration maxFailureDuration; + + private ZonedDateTime lastSuccess; + + private Throwable throwable = null; + + private String message = null; + + public TimeBasedStatusProbe(Duration maxFailureDuration, Clock clock, + ThreadPoolTaskScheduler threadPoolTaskScheduler, StatusProbeCriticality criticality, String name, + StatusProbeLogger statusProbeLogger) { + this(maxFailureDuration, clock, threadPoolTaskScheduler, Duration.ofMinutes(1), criticality, name, + statusProbeLogger); + } + + public TimeBasedStatusProbe(Duration maxFailureDuration, Clock clock, + ThreadPoolTaskScheduler threadPoolTaskScheduler, Duration schedulePeriod, StatusProbeCriticality criticality, + String name, StatusProbeLogger statusProbeLogger) { + super(clock, criticality, name, statusProbeLogger); + this.clock = clock; + this.maxFailureDuration = maxFailureDuration; + this.lastSuccess = null; + scheduleTask(threadPoolTaskScheduler, schedulePeriod); + } + + @Override + protected synchronized void setStatus(Status status, Throwable throwable, String message) { + if (status == Status.DOWN) { + this.throwable = throwable; + this.message = message; + } else if (status == Status.UP) { + lastSuccess = ZonedDateTime.now(clock); + super.setStatus(status, throwable, message); + } + } + + private boolean isOverdue() { + if (lastSuccess == null) { + return false; + } + Duration timeSinceLastSuccess = Duration.between(lastSuccess, ZonedDateTime.now(clock)); + return maxFailureDuration.minus(timeSinceLastSuccess).isNegative(); + } + + public synchronized void runScheduledTask() { + if (isOverdue()) { + super.setStatus(Status.DOWN, throwable, message); + } + } + + +} diff --git a/module-server/src/main/java/de/twomartens/template/property/RestTemplateTimeoutProperties.java b/module-server/src/main/java/de/twomartens/template/property/RestTemplateTimeoutProperties.java new file mode 100644 index 0000000..d2673a7 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/property/RestTemplateTimeoutProperties.java @@ -0,0 +1,34 @@ +package de.twomartens.template.property; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.stereotype.Component; + +@Getter +@Setter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +@ConfigurationProperties(prefix = "resttemplate.timeout") +@Component +public class RestTemplateTimeoutProperties { + + @DurationUnit(ChronoUnit.MILLIS) + private Duration readTimeoutRestHealthIndicatorInMillis; + @DurationUnit(ChronoUnit.MILLIS) + private Duration connectionRestHealthIndicatorTimeoutInMillis; + @DurationUnit(ChronoUnit.MILLIS) + private Duration readTimeoutRestTemplateInMillis; + @DurationUnit(ChronoUnit.MILLIS) + private Duration connectionRestTemplateTimeoutInMillis; +} + diff --git a/module-server/src/main/java/de/twomartens/template/property/ServiceProperties.java b/module-server/src/main/java/de/twomartens/template/property/ServiceProperties.java new file mode 100644 index 0000000..4b3f4fb --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/property/ServiceProperties.java @@ -0,0 +1,21 @@ +package de.twomartens.template.property; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.ZoneId; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +@Data +@RefreshScope +@Configuration +@ConfigurationProperties(prefix = "de.twomartens.template") +@Schema(description = "Properties, to configure this Application") +public class ServiceProperties { + + private ZoneId defaultTimeZone; + + private String greeting; + +} \ No newline at end of file diff --git a/module-server/src/main/java/de/twomartens/template/property/StatusProbeProperties.java b/module-server/src/main/java/de/twomartens/template/property/StatusProbeProperties.java new file mode 100644 index 0000000..b1a0095 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/property/StatusProbeProperties.java @@ -0,0 +1,28 @@ +package de.twomartens.template.property; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +@Data +@RefreshScope +@Configuration +@ConfigurationProperties(prefix = "de.twomartens.template.statusprobe") +@Schema(description = "Properties, to configure this Application") +public class StatusProbeProperties { + + @DurationUnit(ChronoUnit.SECONDS) + private Duration scheduleDuration; + + @DurationUnit(ChronoUnit.MINUTES) + private Duration maxKafkaFailureDuration; + + private int maxBlobFailureCount; + + private int maxFailurePercent; +} diff --git a/module-server/src/main/java/de/twomartens/template/repository/GreetingRepository.java b/module-server/src/main/java/de/twomartens/template/repository/GreetingRepository.java new file mode 100644 index 0000000..aad78c3 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/repository/GreetingRepository.java @@ -0,0 +1,11 @@ +package de.twomartens.template.repository; + +import de.twomartens.template.model.db.Greeting; +import java.util.Optional; +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface GreetingRepository extends MongoRepository { + + Optional findByMessageIgnoreCase(String message); + +} diff --git a/module-server/src/main/java/de/twomartens/template/service/GreetingService.java b/module-server/src/main/java/de/twomartens/template/service/GreetingService.java new file mode 100644 index 0000000..994384b --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/service/GreetingService.java @@ -0,0 +1,40 @@ +package de.twomartens.template.service; + +import de.twomartens.template.model.db.Greeting; +import de.twomartens.template.property.ServiceProperties; +import de.twomartens.template.repository.GreetingRepository; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class GreetingService { + + private final MeterRegistry meterRegistry; + private final ServiceProperties serviceProperties; + private final Counter counter; + + private final GreetingRepository greetingRepository; + + public GreetingService(MeterRegistry meterRegistry, ServiceProperties serviceProperties, + GreetingRepository greetingRepository) { + this.meterRegistry = meterRegistry; + this.serviceProperties = serviceProperties; + this.greetingRepository = greetingRepository; + counter = meterRegistry.counter("template.callCounter"); + } + + public Greeting createGreeting(String name) { + log.info("Create greeting for '{}'", name); + counter.increment(); + meterRegistry.gauge("template.nameLength", name.length()); + String greeting = serviceProperties.getGreeting(); + return Greeting.builder().message(String.format(greeting, name)).build(); + } + + public void postGreeting(Greeting greeting) { + greetingRepository.save(greeting); + } +} diff --git a/module-server/src/main/java/de/twomartens/template/service/PropertyReloadService.java b/module-server/src/main/java/de/twomartens/template/service/PropertyReloadService.java new file mode 100644 index 0000000..5864846 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/template/service/PropertyReloadService.java @@ -0,0 +1,37 @@ +package de.twomartens.template.service; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.logging.log4j.message.StringMapMessage; +import org.springframework.cloud.context.refresh.ContextRefresher; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +/** + * automatically reloads application*.yaml. reload can also be triggered manually by doing a post + * request on http://localhost:12001/actuator/refresh + */ +@Log4j2 +@Service +@RequiredArgsConstructor +public class PropertyReloadService { + + public static final int REFRESH_SECONDS = 60; + public static final String PARAM_MESSAGE = "message"; + public static final String PARAM_PROPERTIES = "labels.properties"; + + private final ContextRefresher contextRefresher; + + @Scheduled(fixedDelay = REFRESH_SECONDS, initialDelay = REFRESH_SECONDS, timeUnit = TimeUnit.SECONDS) + public void refresh() { + Set properties = contextRefresher.refresh(); + if (!properties.isEmpty()) { + log.info(new StringMapMessage() + .with(PARAM_MESSAGE, "properties changed") + .with(PARAM_PROPERTIES, String.join("\n", properties))); + } + } + +} diff --git a/module-server/src/main/resources/application-dev.yaml b/module-server/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..c64a463 --- /dev/null +++ b/module-server/src/main/resources/application-dev.yaml @@ -0,0 +1,6 @@ +#TODO zufaellige Ports eintragen +server.port: 12000 +grpc.server.port: 12002 +management.server.port: 12001 + +de.twomartens.template.name: "Template" \ No newline at end of file diff --git a/module-server/src/main/resources/application.yaml b/module-server/src/main/resources/application.yaml new file mode 100644 index 0000000..4211d10 --- /dev/null +++ b/module-server/src/main/resources/application.yaml @@ -0,0 +1,67 @@ +server: + port: 8080 + shutdown: graceful +grpc.server.port: 9090 + +### technical configurations ### +management: + # for security, don't use same port as application + server.port: 8081 + health: + livenessState.enabled: true + readinessState.enabled: true + endpoint: + health: + show-details: always + probes.enabled: true + group: + readiness.include: readinessState,diskSpace,grpcChannel,grpc,rest + liveness.include: livenessState + endpoints.web.exposure.include: '*' + # activate percentiles for web and grpc requests + metrics.distribution.percentiles-histogram: + grpc.server.processing.duration: true + http.server.requests: true + +spring: + main: + banner-mode: off + profiles: + default: dev + +# properties for application +de.twomartens.template: + defaultTimeZone: Europe/Berlin + greeting: "Hello %s" + +springdoc: + swagger-ui: + path: '/template/doc/' + api-docs: + path: '/template/api-docs/' + default-produces-media-type: 'application/json' + +openapi: + #TODO write description/ changelog + description: | + Open API Documentation for the Template + + ## Header 2 + + __YOUR TEXT HERE__ + + ## Changelog + + |Version | Change | Author | Date | + | ------ | -------------------------------------- | ------ | ---------- | + | 1.0 | added API Versioning (and changelog) | Jim | 28.05.2023 | + + version: 1.0 + title: 'Template API' + +resttemplate: + timeout: + readTimeoutRestHealthIndicatorInMillis: 5000 + connectionRestHealthIndicatorTimeoutInMillis: 5000 + readTimeoutRestTemplateInMillis: 5000 + connectionRestTemplateTimeoutInMillis: 5000 \ No newline at end of file diff --git a/module-server/src/main/resources/log4j2.xml b/module-server/src/main/resources/log4j2.xml new file mode 100644 index 0000000..31a9745 --- /dev/null +++ b/module-server/src/main/resources/log4j2.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/module-server/src/main/resources/templates/error.html b/module-server/src/main/resources/templates/error.html new file mode 100644 index 0000000..a2d5c37 --- /dev/null +++ b/module-server/src/main/resources/templates/error.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/module-server/src/main/resources/templates/error/404.html b/module-server/src/main/resources/templates/error/404.html new file mode 100644 index 0000000..6f9e73e --- /dev/null +++ b/module-server/src/main/resources/templates/error/404.html @@ -0,0 +1,9 @@ + + + + + +

The requested URL was not found on this server.

+ + + diff --git a/module-server/src/main/resources/templates/errorIncludes.html b/module-server/src/main/resources/templates/errorIncludes.html new file mode 100644 index 0000000..78f10ae --- /dev/null +++ b/module-server/src/main/resources/templates/errorIncludes.html @@ -0,0 +1,28 @@ + + + + Error <th:block th:text="${status}"/> (<th:block th:text="${error}"/>) + + + + +

Template

+

+ +

+
+
+ + diff --git a/module-server/src/main/resources/templates/version.html b/module-server/src/main/resources/templates/version.html new file mode 100644 index 0000000..c9da798 --- /dev/null +++ b/module-server/src/main/resources/templates/version.html @@ -0,0 +1,37 @@ + + + + <th:block th:text="${version}"/> + + + +

+ +

+

+ Hostname: + +

+

+ + +
+
+

+ + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..a4e51f1 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +rootProject.name = projectname + +include 'plain' +include 'server' +include 'lib' + +rootProject.children.each { subproject -> + subproject.projectDir = file("module-" + subproject.name) + subproject.buildFileName = "${subproject.name}.gradle" +} \ No newline at end of file