Initial commit

This commit is contained in:
Jim Martens 2023-07-01 21:49:36 +02:00
commit 05b15e01e3
77 changed files with 4249 additions and 0 deletions

669
.editorconfig Normal file
View File

@ -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

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
gradle/wrapper/gradle-wrapper.jar filter= diff= merge= -text
*.sh text eol=lf

45
.gitignore vendored Normal file
View File

@ -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/

View File

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="MainApplication" type="Application" factoryName="Application" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="de.twomartens.template.MainApplication" />
<module name="template.server.main" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="de.twomartens.template.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

201
LICENSE Normal file
View File

@ -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.

15
README.md Normal file
View File

@ -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.

13
build.gradle Normal file
View File

@ -0,0 +1,13 @@
plugins {
id 'twomartens.versions'
id 'twomartens.nebula-release'
}
versionCatalogUpdate {
sortByKey = false
keep {
keepUnusedVersions = true
keepUnusedLibraries = true
keepUnusedPlugins = true
}
}

15
buildSrc/build.gradle Normal file
View File

@ -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
}

9
buildSrc/settings.gradle Normal file
View File

@ -0,0 +1,9 @@
rootProject.name = 'twomartens.config'
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

View File

@ -0,0 +1,6 @@
plugins {
id 'idea'
id 'eclipse'
}
group = projectgroup

View File

@ -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
}
}
}
}

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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")

View File

@ -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")
}

View File

@ -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
}

View File

@ -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("")
}

View File

@ -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'
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
"-//Puppy Crawl//DTD Suppressions 1.0//EN"
"http://www.puppycrawl.com/dtds/suppressions_1_0.dtd">
<suppressions>
<suppress files="[/\\]build[/\\]" checks=".*"/>
<suppress checks="Javadoc" files="."/>
</suppressions>

View File

@ -0,0 +1,367 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!--
Checkstyle configuration that checks the Google coding conventions from Google Java Style
that can be found at https://google.github.io/styleguide/javaguide.html
Checkstyle is very configurable. Be sure to read the documentation at
http://checkstyle.org (or in your downloaded distribution).
To completely disable a check, just comment it out or delete it from the file.
To suppress certain violations please review suppression filters.
Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.
-->
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Excludes all 'module-info.java' files -->
<!-- See https://checkstyle.org/config_filefilters.html -->
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<!-- https://checkstyle.org/config_filters.html#SuppressionFilter -->
<module name="SuppressionFilter">
<property name="file" value="${org.checkstyle.google.suppressionfilter.config}"
default="checkstyle-suppressions.xml"/>
<property name="optional" value="true"/>
</module>
<!-- Checks for whitespace -->
<!-- See http://checkstyle.org/config_whitespace.html -->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<module name="LineLength">
<property name="fileExtensions" value="java"/>
<property name="max" value="120"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
</module>
<module name="TreeWalker">
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format"
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="AvoidStarImport"/>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap">
<property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/>
</module>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<module name="NeedBraces">
<property name="tokens"
value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/>
</module>
<module name="LeftCurly">
<property name="tokens"
value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,
INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT,
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,
OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
LITERAL_DO"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens"
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF,
COMPACT_CTOR_DEF"/>
</module>
<module name="SuppressionXpathSingleFilter">
<!-- suppresion is required till https://github.com/checkstyle/checkstyle/issues/7541 -->
<property name="id" value="RightCurlyAlone"/>
<property name="query" value="//RCURLY[parent::SLIST[count(./*)=1]
or preceding-sibling::*[last()][self::LCURLY]]"/>
</module>
<module name="WhitespaceAfter">
<property name="tokens"
value="COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE,
LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, DO_WHILE, ELLIPSIS,
LITERAL_SWITCH, LAMBDA"/>
</module>
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyLambdas" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<property name="ignoreEnhancedForColon" value="false"/>
<property name="tokens"
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,
BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,
LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,
LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,
LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded"
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="EmptyLineSeparator">
<property name="tokens"
value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF,
COMPACT_CTOR_DEF"/>
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/259 -->
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/258 -->
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
ANNOTATION_DEF, RECORD_DEF"/>
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LambdaParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="PatternVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Pattern variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="RecordComponentName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Record component name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="RecordTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Record type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed"
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded"
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow"
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded"
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="Indentation">
<property name="basicOffset" value="2"/>
<property name="braceAdjustment" value="2"/>
<property name="caseIndent" value="2"/>
<property name="throwsIndent" value="4"/>
<property name="lineWrappingIndentation" value="4"/>
<property name="arrayInitIndent" value="2"/>
</module>
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="3"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,
RECORD_COMPONENT_DEF"/>
</module>
<module name="NoWhitespaceBeforeCaseDefaultColon"/>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="VariableDeclarationUsageDistance">
<property name="allowedDistance" value="6"/>
</module>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="false"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
<property name="tokens" value="IMPORT, STATIC_IMPORT, PACKAGE_DEF"/>
</module>
<module name="MethodParamPad">
<property name="tokens"
value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF,
SUPER_CTOR_CALL, ENUM_CONSTANT_DEF, RECORD_DEF"/>
</module>
<module name="NoWhitespaceBefore">
<property name="tokens"
value="COMMA, SEMI, POST_INC, POST_DEC, DOT,
LABELED_STAT, METHOD_REF"/>
<property name="allowLineBreaks" value="true"/>
</module>
<module name="ParenPad">
<property name="tokens"
value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,
EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,
METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA,
RECORD_DEF"/>
</module>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens"
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF,
TYPE_EXTENSION_AND "/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF,
RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<module name="InvalidJavadocPosition"/>
<module name="JavadocTagContinuationIndentation"/>
<module name="SummaryJavadoc">
<property name="forbiddenSummaryFragments"
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
</module>
<module name="JavadocParagraph"/>
<module name="RequireEmptyLineBeforeBlockTagGroup"/>
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="accessModifiers" value="public"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="MissingJavadocMethod">
<property name="scope" value="public"/>
<property name="minLineCount" value="2"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF,
COMPACT_CTOR_DEF"/>
</module>
<module name="MissingJavadocType">
<property name="scope" value="protected"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
RECORD_DEF, ANNOTATION_DEF"/>
<property name="excludeScope" value="nothing"/>
</module>
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SingleLineJavadoc"/>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>
<module name="CommentsIndentation">
<property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/>
</module>
<!-- https://checkstyle.org/config_filters.html#SuppressionXpathFilter -->
<module name="SuppressionXpathFilter">
<property name="file" value="${org.checkstyle.google.suppressionxpathfilter.config}"
default="checkstyle-xpath-suppressions.xml"/>
<property name="optional" value="true"/>
</module>
</module>
</module>

9
gradle.properties Normal file
View File

@ -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

113
gradle/libs.versions.toml Normal file
View File

@ -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",
]

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -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

240
gradlew vendored Normal file
View File

@ -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" "$@"

91
gradlew.bat vendored Normal file
View File

@ -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

1
lombok.config Normal file
View File

@ -0,0 +1 @@
config.stopbubbling = true

7
module-lib/lib.gradle Normal file
View File

@ -0,0 +1,7 @@
plugins {
id 'twomartens.java'
}
dependencies {
}

View File

@ -0,0 +1,5 @@
package de.twomartens.template.model;
public record Name(String value) {
}

View File

@ -0,0 +1,7 @@
plugins {
id 'twomartens.java'
}
dependencies {
implementation project(':lib')
}

View File

@ -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));
}
}

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Configuration status="warn" monitorInterval="30">
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout alwaysWriteExceptions="true"
pattern="%5p %d{HH:mm:ss.SSS} (%F:%L) %notEmpty{[%X{REQTYPE}] }%notEmpty{[%marker] }%K{event.end}%m%n%xEx{full}"/>
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="console"/>
</Root>
</Loggers>
</Configuration>

View File

@ -0,0 +1,9 @@
plugins {
id 'twomartens.spring-boot-cloud'
}
dependencies {
implementation libs.mapstruct.base
annotationProcessor libs.mapstruct.processor
}

View File

@ -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));
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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("*");
}
}

View File

@ -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<ErrorMessage> 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<ErrorMessage> handleRuntimeException(RuntimeException e) {
log.error("unexpected exception occurred", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorMessage.builder()
.message(e.getMessage())
.build());
}
}

View File

@ -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<String> 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());
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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<String> 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
}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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<String, Collection<String>> 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<String, Collection<String>> 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<String, Collection<String>> 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<String, Collection<String>> 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<String, Collection<String>> 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<String, Collection<String>> extractHeaders(Iterator<String> headerNames,
Function<String, Iterator<String>> headerValuesSupplier) {
Map<String, Collection<String>> requestHeaders = new HashMap<>();
while (headerNames.hasNext()) {
String name = headerNames.next();
Collection<String> values = requestHeaders.computeIfAbsent(name, n -> new TreeSet<>());
Iterator<String> headerValues = headerValuesSupplier.apply(name);
while (headerValues.hasNext()) {
values.add(headerValues.next());
}
}
return requestHeaders;
}
private static Map<String, Collection<String>> extractHeaders(HttpHeaders headers) {
Map<String, Collection<String>> result = new HashMap<>();
for (Entry<String, List<String>> entry : headers.entrySet()) {
result.put(entry.getKey(), new ArrayList<>(entry.getValue()));
}
return result;
}
private static String toHeaderString(Map<String, Collection<String>> 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<String, Collection<String>> requestHeaders,
byte[] fullRequest) {
return StandardCharsets.UTF_8;
}
/**
* usually returns UTF-8, but can be overridden to implement more complex logic
*/
public Charset determineResponseEncoding(Map<String, Collection<String>> responseHeaders,
byte[] fullResponse) {
return StandardCharsets.UTF_8;
}
@Builder
private record LogMessage(Map<String, Collection<String>> requestHeaders,
Map<String, Collection<String>> 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
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,10 @@
package de.twomartens.template.monitoring.actuator;
import java.io.Closeable;
@FunctionalInterface
public interface Preparable {
Closeable prepare();
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<String> 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();
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
package de.twomartens.template.monitoring.statusprobe;
public enum StatusProbeCriticality {
K1, K2, K3
}

View File

@ -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<ProbeIdent, Status> 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<StatusProbeCriticality> 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<ProbeIdent> 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<ProbeIdent> probesDown, StatusProbeCriticality criticality) {
List<String> 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) {
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<Greeting, String> {
Optional<Greeting> findByMessageIgnoreCase(String message);
}

View File

@ -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);
}
}

View File

@ -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<String> properties = contextRefresher.refresh();
if (!properties.isEmpty()) {
log.info(new StringMapMessage()
.with(PARAM_MESSAGE, "properties changed")
.with(PARAM_PROPERTIES, String.join("\n", properties)));
}
}
}

View File

@ -0,0 +1,6 @@
#TODO zufaellige Ports eintragen
server.port: 12000
grpc.server.port: 12002
management.server.port: 12001
de.twomartens.template.name: "Template"

View File

@ -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

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Configuration status="warn" monitorInterval="30">
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout alwaysWriteExceptions="true"
pattern="%5p %d{HH:mm:ss.SSS} (%F:%L) %notEmpty{[%X{REQTYPE}] }%notEmpty{[%marker] }%K{event.end}%m%n%xEx{full}"/>
</Console>
<Console name="interceptor" target="SYSTEM_OUT">
<!-- <EcsLayout servicename="${env:spring_application_name}" includeOrigin="true" includeMarkers="true"/>-->
<PatternLayout alwaysWriteExceptions="true"
pattern="%5p %d{HH:mm:ss.SSS} (%F:%L) %notEmpty{[%K{REQTYPE}] }%notEmpty{[%X{REQTYPE}] }%K{network.direction} %K{http.request.method} %K{http.response.status_code} %K{url.full} (duration=%K{event.duration} request-size=%K{http.request.body.bytes} response-size=%K{http.response.body.bytes} trace-id=%notEmpty{%K{trace.id}}%notEmpty{%X{trace.id}})%n%xEx{full}"/>
</Console>
</Appenders>
<Loggers>
<Root level="WARN">
<AppenderRef ref="console"/>
</Root>
<Logger additivity="true" level="INFO" name="statusprobe"/>
</Loggers>
</Configuration>

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{errorIncludes :: head}"/>
<body>
<th:block th:insert="~{errorIncludes :: mainText}"/>
<th:block th:insert="~{errorIncludes :: footer}"/>
</body>
</html>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{errorIncludes :: head}" />
<body>
<th:block th:insert="~{errorIncludes :: mainText}" />
<p>The requested URL <code th:text="${path}"></code> was not found on this server.</p>
<th:block th:insert="~{errorIncludes :: footer}" />
</body>
</html>

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
<title>Error <th:block th:text="${status}"/> (<th:block th:text="${error}"/>)</title>
<style>
@media (prefers-color-scheme: dark) {
html{background:#000000;color:#ffffff;font:15px/22px arial,sans-serif}
}
@media (prefers-color-scheme: light) {
html{background:#ffffff;color:#000000;font:15px/22px arial,sans-serif}
}
body{margin:50px}
h1,footer{margin:20px 0}
p{margin:0}
code{font-family: monospace,monospace}
.foot{color:#777777;font-size:80%}
</style>
</head>
<body>
<th:block th:fragment="mainText">
<h1>Template</h1>
<p>
<b th:text="${status}"></b> <th:block th:text="${error}" />
</p>
</th:block>
<footer class="foot" th:fragment="footer"><th:block th:text="${footerString}" /></footer>
</body>
</html>

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
<title><th:block th:text="${version}"/></title>
<style>
@media (prefers-color-scheme: dark) {
html{background:#000000;color:#ffffff;font:15px/22px arial,sans-serif}
}
@media (prefers-color-scheme: light) {
html{background:#ffffff;color:#000000;font:15px/22px arial,sans-serif}
}
body{margin:50px}
h1,footer{margin:20px 0}
p{margin:0}
code{font-family: monospace,monospace}
.foot{color:#777777;font-size:80%}
</style>
</head>
<body style="font-family:sans-serif; line-height: 1.4em;">
<h2>
<th:block th:text="${version}"/>
</h2>
<p>
Hostname:
<th:block th:text="${hostname}"/>
</p>
<p>
<th:block th:each="item : ${manifest}">
<th:block th:text="${item}"/>
<br/>
</th:block>
</p>
<footer class="foot" th:fragment="footer">
<th:block th:text="${footerString}"/>
</footer>
</body>
</html>

10
settings.gradle Normal file
View File

@ -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"
}