Initial commit

This commit is contained in:
Jim Martens 2024-08-30 14:39:25 +02:00
parent f604ee1577
commit 14bc2dad74
No known key found for this signature in database
GPG Key ID: CE04D842C9A68949
71 changed files with 3877 additions and 20 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

59
.gitignore vendored
View File

@ -1,24 +1,45 @@
# Compiled class file
*.class
/.*
!/.gitignore
!/.gitattributes
!/.editorconfig
!/.run/
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**
!**/src/test/**
**/*.log
**/*.hprof
**/generated/**
# Log file
*.log
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
# BlueJ files
*.ctxt
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
# Mobile Tools for Java (J2ME)
.mtj.tmp/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
### VS Code ###
.vscode/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
### Swap-Dateien und Backups ###
*.swp
*~
/lib/

View File

@ -0,0 +1,3 @@
{
".": "0.0.1"
}

1
CHANGELOG.md Normal file
View File

@ -0,0 +1 @@
# Changelog

View File

@ -1 +1,30 @@
# hackathon-2024-kotlin
# Timetable tool (backend) for Train Sim World
## Motivation
With the release of the Public Editor for Train Sim World 4 it is now possible to create
24h timetables in the Unreal Engine and then play those in the game. However, a lot of
planning has to happen before the first service SHOULD be created in the engine.
Until now, even Dovetail Games is manually copying timetable information to Excel sheets.
This is a time-consuming process that takes the fun away and makes it a very laborious process.
What if all the timetable data of passenger services could be automatically fetched without
manual copying? This timetable tool intents to do just that.
## Limitations
Some manual work in the planning
stage will remain: creating routes with portals and depots, formations, and providing some
data for services once they are fetched. This data includes formations and linking them together
into rotations.
The public timetable isn't enough? Freight trains, empty coaching stock movements or light loco
movements are not in the timetable and have to be entered manually. But the tool will support
as good as possible.
For starters, only the Deutsche Bahn API and hence Germany are supported. More countries could
be added later on once reliable data sources are identified.
Even then, this tool can only support with present day timetables. Vintage timetables are not
available via APIs or similar sources and the manual work will remain there.

17
build.gradle.kts Normal file
View File

@ -0,0 +1,17 @@
plugins {
id("hbt.versions")
id("hbt.nebula-release")
}
nebulaRelease {
addReleaseBranchPattern("/main/")
}
versionCatalogUpdate {
sortByKey.set(false)
keep {
keepUnusedVersions.set(true)
keepUnusedLibraries.set(true)
keepUnusedPlugins.set(true)
}
}

19
buildSrc/build.gradle.kts Normal file
View File

@ -0,0 +1,19 @@
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
mavenCentral()
}
dependencies {
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
implementation(libs.plugin.kotlin.gradle)
implementation(libs.plugin.springboot)
implementation(libs.plugin.lombok)
implementation(libs.plugin.nebula.release)
implementation(libs.plugin.gradle.versions)
implementation(libs.plugin.version.catalog)
implementation(libs.plugin.jib)
}

View File

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

View File

@ -0,0 +1,6 @@
plugins {
idea
}
val projectgroup: String = providers.gradleProperty("projectgroup").get()
group = projectgroup

View File

@ -0,0 +1,23 @@
plugins {
checkstyle
id("hbt.java-base")
}
checkstyle {
toolVersion = "10.0"
isIgnoreFailures = false
maxWarnings = 0
configFile = rootProject.file("config/checkstyle/checkstyle.xml")
configProperties = mapOf<String, String>(
"org.checkstyle.google.suppressionfilter.config" to
"${project.rootDir}/config/checkstyle/checkstyle-suppressions.xml")
}
tasks.withType<Checkstyle>().configureEach {
reports {
xml.required.set(true)
html.required.set(true)
}
}

View File

@ -0,0 +1,30 @@
plugins {
java
`java-library`
id("hbt.base")
application
}
val projectSourceCompatibility: String = rootProject.properties["projectSourceCompatibility"].toString()
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(projectSourceCompatibility))
}
}
repositories {
mavenCentral()
}
tasks.register("buildAll") {
group = "build"
dependsOn("build")
dependsOn("test")
}
tasks.clean {
doFirst {
delete("out")
}
}

View File

@ -0,0 +1,15 @@
plugins {
id("hbt.java-base")
}
tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.plusAssign("--enable-preview")
}
tasks.withType<Test>().configureEach {
jvmArgs.plusAssign("--enable-preview")
}
tasks.withType<JavaExec>().configureEach {
jvmArgs.plusAssign("--enable-preview")
}

View File

@ -0,0 +1,64 @@
import org.gradle.accessors.dm.LibrariesForLibs
plugins {
jacoco
id("io.freefair.lombok")
id("hbt.java-base")
id("hbt.checkstyle")
}
val libs = the<LibrariesForLibs>()
dependencies {
constraints.implementation(libs.bundles.logging)
implementation(libs.slf4j.api)
runtimeOnly(libs.bundles.logging)
testImplementation(libs.bundles.test)
testRuntimeOnly(libs.junit.launcher)
}
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")
}
}
tasks.withType<Test>().configureEach {
systemProperty("junit.jupiter.execution.parallel.enabled", true)
systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
useJUnitPlatform()
maxHeapSize = "4g"
workingDir = rootProject.projectDir
finalizedBy(tasks.jacocoTestReport)
}
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required.set(true)
}
}
normalization.runtimeClasspath.metaInf {
ignoreAttribute("Build-Timestamp")
}
tasks.register("cleanLibs") {
delete("${layout.buildDirectory.get().asFile}/libs")
}
tasks.build {
dependsOn("cleanLibs")
}

View File

@ -0,0 +1,21 @@
plugins {
id("com.google.cloud.tools.jib")
id("hbt.java-base")
}
tasks.named("jib") {
dependsOn("build")
}
tasks.named("jibDockerBuild") {
dependsOn("build")
}
tasks.named("build") {
dependsOn("cleanCache")
}
tasks.register("cleanCache") {
delete("${layout.buildDirectory.get().asFile}/jib-cache")
delete("${layout.buildDirectory.get().asFile}/libs")
}

View File

@ -0,0 +1,22 @@
import org.gradle.accessors.dm.LibrariesForLibs
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm")
}
val libs = the<LibrariesForLibs>()
dependencies {
implementation(libs.kotlin.logging)
implementation(kotlin("reflect"))
}
val projectSourceCompatibility: String = rootProject.properties["projectSourceCompatibility"].toString()
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjvm-default=all", "-Xjsr305=strict")
jvmTarget.set(JvmTarget.fromTarget(projectSourceCompatibility))
}
}

View File

@ -0,0 +1,18 @@
import java.nio.file.Files
plugins {
id("hbt.base")
}
apply(plugin="com.netflix.nebula.release")
tasks.register("writeVersionProperties") {
group = "version"
mustRunAfter("release")
outputs.file("${layout.buildDirectory.get().asFile}/version.properties")
val directory = layout.buildDirectory.get().asFile
doLast {
Files.createDirectories(directory.toPath())
File("${layout.buildDirectory.get().asFile}/version.properties").writeText("VERSION=${project.version}\n")
}
}

View File

@ -0,0 +1,75 @@
import org.gradle.accessors.dm.LibrariesForLibs
import org.springframework.boot.gradle.tasks.bundling.BootJar
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatter.ofPattern
plugins {
id("hbt.spring-boot-base")
}
val libs = the<LibrariesForLibs>()
dependencies {
implementation(libs.bundles.spring.boot)
testImplementation(libs.spring.boot.test)
}
sourceSets {
create("integration-test") {
java {
compileClasspath += sourceSets.main.get().output + sourceSets.test.get().output
runtimeClasspath += sourceSets.main.get().output + sourceSets.test.get().output
setSrcDirs(listOf("src/integration-test"))
}
}
}
idea {
module {
testSources.from(sourceSets["integration-test"].java.srcDirs)
}
}
val integrationTestImplementation: Configuration by configurations.getting {
extendsFrom(configurations.testImplementation.get())
}
tasks.register<Test>("integrationTest") {
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") {
dependsOn("integrationTest")
}
val formatter: DateTimeFormatter = ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
tasks.jar {
manifest {
attributes["Implementation-Title"] = rootProject.name
attributes["Implementation-Version"] = archiveVersion.get()
attributes["Implementation-Vendor"] = "Jim Martens"
attributes["Build-Timestamp"] = ZonedDateTime.now().format(formatter)
attributes["Created-By"] = "Gradle ${gradle.gradleVersion}"
attributes["Build-Jdk"] = "${providers.systemProperty("java.version").get()} (${providers.systemProperty("java.vendor").get()} ${providers.systemProperty("java.vm.version").get()})"
attributes["Build-OS"] = "${providers.systemProperty("os.name").get()} ${providers.systemProperty("os.arch").get()} ${providers.systemProperty("os.version").get()}"
}
}
springBoot {
buildInfo()
mainClass.set(project.properties["mainClass"].toString())
}
tasks.named<BootJar>("bootJar") {
enabled = true
}

View File

@ -0,0 +1,25 @@
import org.gradle.accessors.dm.LibrariesForLibs
import org.springframework.boot.gradle.tasks.bundling.BootJar
plugins {
id("org.springframework.boot")
id("hbt.java")
}
val libs = the<LibrariesForLibs>()
dependencies {
implementation(platform(libs.spring.boot))
implementation(libs.spring.boot.log4j)
}
configurations {
configureEach {
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
}
tasks.named<BootJar>("bootJar") {
enabled = false
}

View File

@ -0,0 +1,49 @@
import org.gradle.accessors.dm.LibrariesForLibs
plugins {
id("hbt.spring-boot-application")
id("hbt.spring-boot-cloud-base")
}
val libs = the<LibrariesForLibs>()
dependencies {
implementation(libs.bundles.spring.boot.server)
implementation(libs.spring.openapi)
implementation(libs.httpclient)
implementation(libs.prometheus)
}
sourceSets {
create("server-test") {
java {
compileClasspath += sourceSets.main.get().output + sourceSets.test.get().output
runtimeClasspath += sourceSets.main.get().output + sourceSets.test.get().output
setSrcDirs(listOf("src/server-test"))
}
}
}
idea {
module {
testSources.from(sourceSets["server-test"].java.srcDirs)
}
}
val serverTestImplementation: Configuration by configurations.getting {
extendsFrom(configurations["testImplementation"])
}
tasks.register<Test>("serverTest") {
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,11 @@
import org.gradle.accessors.dm.LibrariesForLibs
plugins {
id("hbt.spring-boot-base")
}
val libs = the<LibrariesForLibs>()
dependencies {
implementation(platform(libs.spring.cloud))
}

View File

@ -0,0 +1,32 @@
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
plugins {
id("com.github.ben-manes.versions")
id("nl.littlerobots.version-catalog-update")
}
tasks.withType<DependencyUpdatesTask>().configureEach {
revision = "release"
gradleReleaseChannel = "current"
}
fun String.isNonStable(): Boolean {
val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { uppercase().contains(it) }
val regex = "^[0-9,.v-]+(-r)?$".toRegex()
val isStable = stableKeyword || regex.matches(this)
return isStable.not()
}
tasks.withType<DependencyUpdatesTask> {
rejectVersionIf {
candidate.version.isNonStable()
}
}
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>

11
gradle.properties Normal file
View File

@ -0,0 +1,11 @@
projectname=routing
projectgroup=de.hbt
projectSourceCompatibility=21
mainClass=de.hbt.routing.MainApplicationKt
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
kapt.include.compile.classpath=false

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

@ -0,0 +1,144 @@
[versions]
spring-boot = "3.2.0"
spring-doc = "2.1.0"
spring-cloud = "2023.0.0"
spring-grpc = "2.14.0.RELEASE"
grpc = "1.57.0"
tomcat-annotations = "6.0.53"
httpclient = "5.2.1"
jaxb = "4.0.3"
jakarta-xml = "4.0.1"
slf4j = "2.0.7"
log4j = "2.20.0"
log4j-ecs = "1.5.0"
mapstruct = "1.5.5.Final"
junit = "5.10.0"
assertj = "3.24.2"
mockito = "5.4.0"
keycloak = "22.0.1"
kotlin-logging = "3.0.5"
kotlin-reflect = "1.9.0"
kotlin-lombok = "1.9.0"
plugin-nebula-release = "17.2.2"
plugin-lombok = "8.0.1"
plugin-gradle-versions = "0.46.0"
plugin-version-catalog = "0.8.0"
plugin-kotlin-gradle = "1.9.21"
plugin-jib = "3.3.2"
[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-security = { module = "org.springframework.boot:spring-boot-starter-security" }
spring-boot-oauth2-resource-server = { module = "org.springframework.boot:spring-boot-starter-oauth2-resource-server" }
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-cloud-starter-bus-kafka = { module = "org.springframework.cloud:spring-cloud-starter-bus-kafka" }
spring-cloud-starter-config = { module = "org.springframework.cloud:spring-cloud-starter-config" }
spring-cloud-config-server = { module = "org.springframework.cloud:spring-cloud-config-server" }
spring-cloud-leader-election = { module = "org.springframework.cloud:spring-cloud-kubernetes-fabric8-leader" }
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" }
jaxb-impl = { module = "com.sun.xml.bind:jaxb-impl", version.ref="jaxb" }
jakarta-xml-binding = { module = "jakarta.xml.bind:jakarta.xml.bind-api", version.ref = "jakarta-xml" }
keycloak-core = { module = "org.keycloak:keycloak-core", version.ref = "keycloak" }
keycloak-policy-enforcer = { module = "org.keycloak:keycloak-policy-enforcer", version.ref = "keycloak" }
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" }
junit-launcher = { module = "org.junit.platform:junit-platform-launcher" }
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" }
kotlin-logging = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "kotlin-logging" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
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" }
plugin-kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "plugin-kotlin-gradle" }
plugin-jib = { module = "com.google.cloud.tools:jib-gradle-plugin", version.ref = "plugin-jib" }
[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-log4j",
"spring-boot-starter",
]
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",
]
spring-boot-security = [
"keycloak-core",
"keycloak-policy-enforcer",
"spring-boot-oauth2-resource-server",
"spring-boot-security",
]
test = [
"assertj",
"junit-jupiter",
"junit-jupiter-api",
"mockito-core",
"mockito-inline",
"mockito-junit",
]
[plugins]
kotlin-lombok = { id = "org.jetbrains.kotlin.plugin.lombok", version.ref = "kotlin-lombok" }

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.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

240
gradlew vendored Executable 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 Executable 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

View File

@ -0,0 +1,15 @@
plugins {
id("hbt.spring-boot-cloud-application")
id("hbt.kotlin")
kotlin("kapt")
}
dependencies {
implementation(project(":support"))
implementation(libs.mapstruct.base)
annotationProcessor(libs.mapstruct.processor)
kapt(libs.mapstruct.processor)
implementation(libs.bundles.spring.boot.security)
}

View File

@ -0,0 +1,16 @@
package de.hbt.routing
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.data.mongodb.config.EnableMongoAuditing
import org.springframework.scheduling.annotation.EnableScheduling
@EnableMongoAuditing
@EnableScheduling
@SpringBootApplication(scanBasePackages = ["de.hbt.support", "de.hbt.routing"])
open class MainApplication
fun main(args: Array<String>) {
runApplication<MainApplication>(*args)
}

View File

@ -0,0 +1,81 @@
server:
port: 8080
shutdown: graceful
forward-headers-strategy: framework
http2:
enabled: true
### 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,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
# data.mongodb:
# uri: ${MONGODB_CONNECTION_STRING}
# auto-index-creation: true
# security:
# oauth2:
# resourceserver:
# jwt:
# jwk-set-uri: ${KEYCLOAK_URL:https://id.2martens.de}/realms/${KEYCLOAK_REALM:2martens}/protocol/openid-connect/certs
# application:
# name: timetable
springdoc:
swagger-ui:
display-request-duration: true
operationsSorter: method
disable-swagger-default-url: true
path: '/doc/v1/routing/'
default-produces-media-type: 'application/json'
api-docs:
path: '/api-docs/v1/routing'
openapi:
description: |
Open API Documentation for the Routing Assistant API
TODO
## Changelog
|Version | Change | Author | Date |
| ------ | -------------------------------------- | ------ | ---------- |
| | | N.N. | 13.07.2023 |
version: 1.0
title: 'Routing Assistant API'
resttemplate:
timeout:
readTimeoutRestHealthIndicatorInMillis: 5000
connectionRestHealthIndicatorTimeoutInMillis: 5000
readTimeoutRestTemplateInMillis: 5000
connectionRestTemplateTimeoutInMillis: 5000
de.hbt.routing:
de.hbt.support.health.greeting: "Good morning"
time:
defaultTimeZone: Europe/Berlin

View File

@ -0,0 +1,22 @@
<?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"/>
<Logger additivity="true" level="DEBUG" name="de.hbt.routing"/>
<Logger additivity="true" level="DEBUG" name="org.springframework.web.method.HandlerMethod"/>
</Loggers>
</Configuration>

View File

@ -0,0 +1,15 @@
package de.hbt.support.configuration
import de.hbt.support.property.TimeProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Clock
@Configuration
open class ClockConfiguration(private val properties: TimeProperties) {
@Bean
open fun clock(): Clock {
return Clock.system(properties.defaultTimeZone)
}
}

View File

@ -0,0 +1,13 @@
package de.hbt.support.configuration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.filter.ForwardedHeaderFilter
@Configuration
open class FilterConfiguration {
@Bean
open fun forwardedFilter(): ForwardedHeaderFilter {
return ForwardedHeaderFilter()
}
}

View File

@ -0,0 +1,20 @@
package de.hbt.support.configuration
import de.hbt.support.interceptor.HeaderInterceptorRest
import de.hbt.support.interceptor.LoggingInterceptorRest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Clock
@Configuration
open class InterceptorConfiguration {
@Bean
open fun loggingInterceptorRest(clock: Clock): LoggingInterceptorRest {
return LoggingInterceptorRest(clock)
}
@Bean
open fun headerInterceptorRest(): HeaderInterceptorRest {
return HeaderInterceptorRest()
}
}

View File

@ -0,0 +1,24 @@
package de.hbt.support.configuration
import de.hbt.support.interceptor.HeaderInterceptorRest
import de.hbt.support.interceptor.LoggingInterceptorRest
import de.hbt.support.property.RestTemplateTimeoutProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter
import org.springframework.web.client.RestClient
@Configuration
open class RestClientConfiguration {
@Bean("restClient")
open fun restClient(
headerInterceptorRest: HeaderInterceptorRest,
loggingInterceptor: LoggingInterceptorRest,
restTemplateTimeoutProperties: RestTemplateTimeoutProperties
): RestClient {
return RestClient.builder()
.messageConverters { it.add(Jaxb2RootElementHttpMessageConverter()) }
.requestInterceptors { listOf(headerInterceptorRest, loggingInterceptor) }
.build()
}
}

View File

@ -0,0 +1,37 @@
package de.hbt.support.configuration
import de.hbt.support.interceptor.HeaderInterceptorRest
import de.hbt.support.interceptor.LoggingInterceptorRest
import de.hbt.support.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
open class RestTemplateConfiguration {
@Bean("restTemplate")
open fun restTemplate(
headerInterceptorRest: HeaderInterceptorRest,
loggingInterceptor: LoggingInterceptorRest,
restTemplateTimeoutProperties: RestTemplateTimeoutProperties
): RestTemplate {
return RestTemplateBuilder()
.additionalInterceptors(headerInterceptorRest, loggingInterceptor)
.setConnectTimeout(restTemplateTimeoutProperties.connectionRestTemplateTimeoutInMillis)
.setReadTimeout(restTemplateTimeoutProperties.readTimeoutRestTemplateInMillis)
.build()
}
@Bean("restTemplateRestHealthIndicator")
open fun restTemplateRestHealthIndicator(
headerInterceptorRest: HeaderInterceptorRest,
restTemplateTimeoutProperties: RestTemplateTimeoutProperties
): RestTemplate {
return RestTemplateBuilder()
.additionalInterceptors(headerInterceptorRest)
.setConnectTimeout(restTemplateTimeoutProperties.connectionRestHealthIndicatorTimeoutInMillis)
.setReadTimeout(restTemplateTimeoutProperties.readTimeoutRestHealthIndicatorInMillis)
.build()
}
}

View File

@ -0,0 +1,25 @@
package de.hbt.support.configuration
import de.hbt.support.monitoring.statusprobe.CountBasedStatusProbe
import de.hbt.support.monitoring.statusprobe.StatusProbe
import de.hbt.support.monitoring.statusprobe.StatusProbeCriticality
import de.hbt.support.monitoring.statusprobe.StatusProbeLogger
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Clock
@Configuration
open class StatusProbeConfiguration(private val clock: Clock) {
@Bean
open fun statusProbeLogger(): StatusProbeLogger {
return StatusProbeLogger(clock)
}
@Bean
open fun testStatusProbe(statusProbeLogger: StatusProbeLogger): StatusProbe {
return CountBasedStatusProbe(
1,
clock, StatusProbeCriticality.K1, "testStatusProbe", statusProbeLogger
)
}
}

View File

@ -0,0 +1,36 @@
package de.hbt.support.controller
import de.hbt.support.exception.HttpStatusException
import de.hbt.support.model.dto.ErrorMessage
import mu.KotlinLogging
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])
class ExceptionController : ResponseEntityExceptionHandler() {
@ExceptionHandler(HttpStatusException::class)
fun handleException(e: HttpStatusException): ResponseEntity<ErrorMessage> {
if (e.cause != null) {
log.info(e.cause.toString(), e.cause)
} else {
log.info(e.toString())
}
return ResponseEntity.status(e.status)
.body(ErrorMessage(e.message!!))
}
@ExceptionHandler(RuntimeException::class)
fun handleRuntimeException(e: RuntimeException): ResponseEntity<ErrorMessage> {
log.error("unexpected exception occurred", e)
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorMessage(message = e.message!!))
}
companion object {
private val log = KotlinLogging.logger {}
}
}

View File

@ -0,0 +1,19 @@
package de.hbt.support.controller
import de.hbt.support.property.HealthCheckProperties
import io.swagger.v3.oas.annotations.Hidden
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping(value = ["/timetable"])
class HealthCheckController(private val properties: HealthCheckProperties) {
@Hidden
@GetMapping("/healthCheck")
fun healthCheck(message: String): String {
return properties.greeting.format(message)
}
}

View File

@ -0,0 +1,82 @@
package de.hbt.support.controller
import mu.KotlinLogging
import org.springframework.stereotype.Controller
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
import java.io.FileNotFoundException
import java.net.InetAddress
import java.net.UnknownHostException
import java.nio.charset.StandardCharsets
import java.nio.file.Paths
import java.util.jar.JarFile
@Controller
@RequestMapping(value = ["/timetable"])
class VersionHtmlController {
@GetMapping(path = ["/version"])
fun version(): String {
return "version"
}
@ModelAttribute("version")
private fun getApplicationVersion(): String {
return "${getTitle()} ${getVersion()}"
}
@ModelAttribute("footerString")
private fun getApplicationVersion(@RequestHeader("host") hostName: String): String {
return "${getTitle()} ${getVersion()} - $hostName"
}
private fun getTitle(): String {
val title = VersionHtmlController::class.java.`package`.implementationTitle
?: return "application"
return title.ifBlank {
"application"
}
}
private fun getVersion(): String {
val version = VersionHtmlController::class.java.`package`.implementationVersion
?: return "DEVELOPER"
return version.ifBlank {
"DEVELOPER"
}
}
@ModelAttribute("hostname")
private fun getHostname(): String {
try {
return InetAddress.getLocalHost().hostName
} catch (e: UnknownHostException) {
log.warn(e.toString(), e)
}
return ""
}
@ModelAttribute("manifest")
private fun getManifest(): Collection<String> {
try {
val location = javaClass.getProtectionDomain().codeSource.location
val jarFileName = Paths.get(location.toURI()).toString()
JarFile(jarFileName).use { jarFile ->
val entry = jarFile.getEntry(JarFile.MANIFEST_NAME)
jarFile.getInputStream(entry).use { `in` ->
return String(`in`.readAllBytes(), StandardCharsets.UTF_8).lines().toList()
}
}
} catch (ignored: FileNotFoundException) {
// do nothing if manifest file is not available
} catch (e: Exception) {
log.info(e.toString(), e)
}
return listOf("${getTitle()} ${getVersion()}")
}
companion object {
private val log = KotlinLogging.logger {}
}
}

View File

@ -0,0 +1,6 @@
package de.hbt.support.exception
import org.springframework.http.HttpStatus
class HttpStatusException(val status: HttpStatus, message: String, cause: Throwable?) :
RuntimeException(message, cause)

View File

@ -0,0 +1,39 @@
package de.hbt.support.interceptor
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatusCode
import org.springframework.http.client.ClientHttpResponse
import org.springframework.util.StreamUtils
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
class BufferingClientHttpResponseWrapper(private val response: ClientHttpResponse) : ClientHttpResponse {
private var body: ByteArray? = null
@Throws(IOException::class)
override fun getStatusCode(): HttpStatusCode {
return response.statusCode
}
@Throws(IOException::class)
override fun getStatusText(): String {
return response.statusText
}
override fun getHeaders(): HttpHeaders {
return response.headers
}
@Throws(IOException::class)
override fun getBody(): InputStream {
if (body == null) {
body = StreamUtils.copyToByteArray(response.body)
}
return ByteArrayInputStream(body)
}
override fun close() {
response.close()
}
}

View File

@ -0,0 +1,85 @@
package de.hbt.support.interceptor
import org.slf4j.MDC
import java.io.Closeable
import java.util.*
abstract class HeaderInterceptor {
companion object {
const val LOGGER_TRACE_ID = "trace.id"
const val LOGGER_REQTYPE_ID = "REQTYPE"
const val HEADER_FIELD_TRACE_ID = "X-TraceId"
const val HEADER_FIELD_B3_TRACE_ID = "x-b3-traceid"
const val HEADER_FIELD_TYPE_ID = "x-type"
const val REQ_TYPE_HEALTHCHECK = "HEALTH_CHECK"
const val REQ_TYPE_INTEGRATION_TEST = "INTEGRATION_TEST"
const val REQ_TYPE_SERVER_TEST = "SERVER_TEST"
const val REQ_TYPE_WARMUP = "WARMUP"
fun createNewTraceId(): String {
return UUID.randomUUID().toString()
}
fun getTraceId(): String {
val traceId = MDC.get(LOGGER_TRACE_ID)
return if (traceId.isNullOrBlank()) createNewTraceId() else traceId
}
fun getRequestType(): String? {
val type = MDC.get(LOGGER_REQTYPE_ID)
return if (type.isNullOrBlank()) null else type
}
private fun setTraceId(traceId: String): InterceptorCloseables {
return InterceptorCloseables(MDC.putCloseable(LOGGER_TRACE_ID, traceId))
}
private fun mark(requestType: String?): InterceptorCloseables {
return InterceptorCloseables(MDC.putCloseable(LOGGER_REQTYPE_ID, requestType))
}
fun set(traceId: String, requestType: String?): InterceptorCloseables {
return if (requestType != null) {
InterceptorCloseables(setTraceId(traceId), mark(requestType))
} else setTraceId(traceId)
}
}
fun markAsHealthCheck(): InterceptorCloseables {
return InterceptorCloseables(mark(REQ_TYPE_HEALTHCHECK), setTraceId(createNewTraceId()))
}
fun markAsIntegrationTest(): InterceptorCloseables {
return InterceptorCloseables(mark(REQ_TYPE_INTEGRATION_TEST), setTraceId(createNewTraceId()))
}
fun markAsServerTest(): InterceptorCloseables {
return InterceptorCloseables(mark(REQ_TYPE_SERVER_TEST), setTraceId(createNewTraceId()))
}
fun markAsWarmup(): InterceptorCloseables {
return InterceptorCloseables(mark(REQ_TYPE_WARMUP), setTraceId(createNewTraceId()))
}
class InterceptorCloseables(vararg closeables: Closeable) : Closeable {
private val closeables: Array<out Closeable>
init {
this.closeables = closeables
}
override fun close() {
closeables.forEach {
try {
it.close()
} catch (ignored: Exception) {
// do nothing
}
}
}
}
}

View File

@ -0,0 +1,88 @@
package de.hbt.support.interceptor
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
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.web.servlet.HandlerInterceptor
import java.io.IOException
import java.util.*
class HeaderInterceptorRest : HeaderInterceptor(), HandlerInterceptor, ClientHttpRequestInterceptor {
companion object {
val CLASS_NAME: String = HeaderInterceptorRest::class.java.getName()
fun extractTraceId(request: HttpServletRequest): String {
var traceId = request.getHeader(HEADER_FIELD_TRACE_ID)
if (traceId.isNullOrBlank()) traceId = request.getHeader(HEADER_FIELD_B3_TRACE_ID)
if (traceId.isNullOrBlank()) return createNewTraceId()
return traceId
}
fun extractTraceId(request: HttpRequest): String {
var traceId = request.headers[HEADER_FIELD_TRACE_ID]?.first()
if (traceId.isNullOrBlank()) traceId = request.headers[HEADER_FIELD_B3_TRACE_ID]?.first()
if (traceId.isNullOrBlank()) return UUID.randomUUID().toString()
return traceId
}
fun extractRequestType(request: HttpServletRequest): String? {
val type = request.getHeader(HEADER_FIELD_TYPE_ID)
if (type.isNullOrBlank()) return null
return type
}
fun extractRequestType(request: HttpRequest): String? {
val type = request.headers[HEADER_FIELD_TYPE_ID]?.first()
if (type.isNullOrBlank()) return null
return type
}
}
// ClientHttpRequestInterceptor
@Throws(IOException::class)
override fun intercept(
request: HttpRequest, body: ByteArray,
execution: ClientHttpRequestExecution
): ClientHttpResponse {
request.headers.add(HEADER_FIELD_TRACE_ID, getTraceId())
val requestType = getRequestType()
if (requestType != null) {
request.headers.add(HEADER_FIELD_TYPE_ID, requestType)
}
return try {
execution.execute(request, body)
} finally {
request.headers.remove(HEADER_FIELD_TRACE_ID)
request.headers.remove(HEADER_FIELD_TYPE_ID)
}
}
// HandlerInterceptor
override fun preHandle(
request: HttpServletRequest, response: HttpServletResponse,
handler: Any
): Boolean {
val traceId = extractTraceId(request)
val requestType = extractRequestType(request)
val closeable = set(traceId, requestType)
request.setAttribute(CLASS_NAME, closeable)
return true
}
// HandlerInterceptor
// override fun postHandle(
// request: HttpServletRequest,
// response: HttpServletResponse,
// handler: Any,
// modelAndView: ModelAndView?
// ) {
// val obj = request.getAttribute(CLASS_NAME)
// if (obj != null && obj is InterceptorCloseables) {
// obj.close()
// }
// }
}

View File

@ -0,0 +1,439 @@
package de.hbt.support.interceptor
import jakarta.servlet.*
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import mu.KotlinLogging
import org.apache.logging.log4j.message.StringMapMessage
import org.slf4j.Marker
import org.slf4j.MarkerFactory
import org.springframework.http.*
import org.springframework.http.client.ClientHttpRequestExecution
import org.springframework.http.client.ClientHttpRequestInterceptor
import org.springframework.http.client.ClientHttpResponse
import org.springframework.util.StreamUtils
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper
import java.io.IOException
import java.net.URI
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.*
import java.util.function.Function
class LoggingInterceptorRest(
private val requestLogBehaviour: FieldLogBehaviour,
private val responseLogBehaviour: FieldLogBehaviour,
val clock: Clock
) : Filter, ClientHttpRequestInterceptor {
constructor(clock: Clock) : this(FieldLogBehaviour.NEVER, FieldLogBehaviour.NEVER, clock)
@Throws(IOException::class, ServletException::class)
override fun doFilter(
request: ServletRequest, response: ServletResponse,
chain: FilterChain
) {
val requestTime = ZonedDateTime.now(clock)
val httpRequest = request as HttpServletRequest
val requestWrapper = ContentCachingRequestWrapper(httpRequest)
val responseWrapper = ContentCachingResponseWrapper(
(response as HttpServletResponse)
)
var fullResponseBytes: ByteArray? = null
var throwable: Throwable? = null
var responseBody: String? = null
var httpStatusCode = -1
try {
try {
chain.doFilter(
if (requestLogBehaviour != FieldLogBehaviour.NEVER) requestWrapper else httpRequest,
responseWrapper
)
if (responseLogBehaviour != FieldLogBehaviour.NEVER) {
fullResponseBytes = responseWrapper.contentAsByteArray
}
httpStatusCode = responseWrapper.status
} finally {
responseWrapper.copyBodyToResponse()
}
} catch (e: Exception) {
throwable = e
throw e
} finally {
try {
val responseSize = responseWrapper.contentSize
val responseHeaders = extractHeaders(
headerNames = responseWrapper.headerNames.iterator()
) { responseWrapper.getHeaders(it).iterator() }
if (
(responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|| responseLogBehaviour == FieldLogBehaviour.ALWAYS)
&& fullResponseBytes != null
) {
responseBody = String(
fullResponseBytes,
determineResponseEncoding()
)
}
val query = if (httpRequest.queryString != null) "?${httpRequest.queryString}" else ""
val requestUrl = URI.create("${httpRequest.requestURL}$query").toURL()
val requestHeaders = extractHeaders(
headerNames = httpRequest.headerNames.asIterator(),
ignoreList = listOf("authorization")) {
httpRequest.getHeaders(it).asIterator()
}
var requestBody: String? = null
var businessType: String? = null
if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|| requestLogBehaviour == FieldLogBehaviour.ALWAYS
) {
val fullRequestBytes = requestWrapper.contentAsByteArray
requestBody = String(fullRequestBytes, determineRequestEncoding())
businessType = determineBusinessType()
}
log(
LogMessage(
requestHeaders = requestHeaders,
responseHeaders = responseHeaders,
url = requestUrl,
method = httpRequest.method,
requestMimeType = typeToString(request.getContentType()),
responseMimeType = typeToString(response.getContentType()),
requestBody = requestBody,
responseBody = responseBody,
requestSize = httpRequest.contentLength,
responseSize = responseSize,
httpStatus = httpStatusCode,
direction = DIRECTION_IN,
requestTime = requestTime,
responseTime = ZonedDateTime.now(clock),
traceId = HeaderInterceptor.getTraceId(),
requestType = HeaderInterceptor.getRequestType(),
businessType = businessType,
throwable = throwable
)
)
val interceptorCloseables =
request.getAttribute(HeaderInterceptorRest.CLASS_NAME) as HeaderInterceptor.InterceptorCloseables
interceptorCloseables.close()
} catch (e: java.lang.RuntimeException) {
log.error(e.toString(), e)
}
}
}
private fun isError(httpStatusCode: Int): Boolean {
return httpStatusCode in 400..599
}
@Throws(IOException::class)
override fun intercept(
request: HttpRequest,
requestBytes: ByteArray, execution: ClientHttpRequestExecution
): ClientHttpResponse {
val requestTime = ZonedDateTime.now(clock)
var responseSize = 0
var responseHeaders: Map<String, Collection<String>> = emptyMap()
var responseMediaType: MediaType? = null
var httpStatusCode = -1
var throwable: Throwable? = null
var requestBody: String? = null
var responseBody: String? = null
var businessType: String? = null
return try {
val result = BufferingClientHttpResponseWrapper(
execution.execute(request, requestBytes)
)
val responseBytes = StreamUtils.copyToByteArray(result.getBody())
responseSize = responseBytes.size
responseHeaders = extractHeaders(result.headers)
responseMediaType = result.headers.getContentType()
httpStatusCode = result.statusCode.value()
if (responseLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|| responseLogBehaviour == FieldLogBehaviour.ALWAYS
) {
responseBody = String(
responseBytes,
determineRequestEncoding()
)
}
result
} catch (e: Exception) {
throwable = e
throw e
} finally {
try {
val url = request.uri.toURL()
val requestHeaders: Map<String, Collection<String>> = extractHeaders(request.headers)
if (requestLogBehaviour == FieldLogBehaviour.ONLY_ON_ERROR && isError(httpStatusCode)
|| requestLogBehaviour == FieldLogBehaviour.ALWAYS
) {
requestBody = String(
requestBytes,
determineRequestEncoding()
)
businessType = determineBusinessType()
}
log(
LogMessage(
requestHeaders = requestHeaders,
responseHeaders = responseHeaders,
url = url,
method = request.method.name(),
requestMimeType = typeToString(request.headers.getContentType()),
requestBody = requestBody,
responseBody = responseBody,
responseMimeType = typeToString(responseMediaType!!),
requestSize = requestBytes.size,
responseSize = responseSize,
httpStatus = httpStatusCode,
direction = DIRECTION_OUT,
requestTime = requestTime,
responseTime = ZonedDateTime.now(clock),
businessType = businessType,
throwable = throwable,
traceId = HeaderInterceptorRest.extractTraceId(request),
requestType = HeaderInterceptorRest.extractRequestType(request),
)
)
} catch (e: java.lang.RuntimeException) {
log.error(e.toString(), e)
}
}
}
private fun log(logMessage: LogMessage) {
val stringMapMessage = 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.host)
addLogString(
stringMapMessage, PARAM_URL_EXTENSION,
extractExtension(logMessage.url.path)
)
addLogString(stringMapMessage, PARAM_URL_PATH, logMessage.url.path)
addLogString(stringMapMessage, PARAM_URL_PORT, logMessage.url.port.toString())
addLogString(stringMapMessage, PARAM_URL_SCHEME, logMessage.url.protocol)
addLogString(stringMapMessage, PARAM_URL_QUERY, logMessage.url.query)
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, logMessage.requestSize.toString())
addLogString(stringMapMessage, PARAM_RESPONSE_BYTES, logMessage.responseSize.toString())
addLogString(stringMapMessage, PARAM_RESPONSE_STATUS, logMessage.httpStatus.toString())
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,
getDurationBetweenRequestAndResponseTime(logMessage).toNanos().toString()
)
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.asString(), logMessage.throwable)
}
private fun getDurationBetweenRequestAndResponseTime(logMessage: LogMessage): Duration {
return Duration.between(logMessage.requestTime, logMessage.responseTime)
}
private fun getHeader(headers: Map<String, Collection<String?>>, headerKey: String): String? {
return headers.entries.asSequence()
.filter { it.key.equals(headerKey, ignoreCase = true) }
.flatMap { it.value.asSequence() }
.firstOrNull()
}
private fun addLogString(stringMapMessage: StringMapMessage, key: String, value: String?) {
if (!value.isNullOrBlank()) {
stringMapMessage.with(key, value.trim { it <= ' ' })
}
}
/**
* usually returns null, but can be overridden to implement more complex logic
*/
private fun determineBusinessType(): String? {
return null
}
/**
* usually returns UTF-8, but can be overridden to implement more complex logic
*/
private fun determineRequestEncoding(): Charset {
return StandardCharsets.UTF_8
}
/**
* usually returns UTF-8, but can be overridden to implement more complex logic
*/
private fun determineResponseEncoding(): Charset {
return StandardCharsets.UTF_8
}
companion object {
private val log = KotlinLogging.logger {}
private const val MAX_LOG_SIZE = 20480 // 20 KB - logging could fail with bigger logmessages
val DATE_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
val MARKER: Marker = MarkerFactory.getMarker("communication")
const val DIRECTION_IN = "inbound"
const val DIRECTION_OUT = "outbound"
const val PROTOCOL_NAME = "http"
const val PARAM_URL_FULL = "url.full"
const val PARAM_URL_DOMAIN = "url.domain"
const val PARAM_URL_EXTENSION = "url.extension"
const val PARAM_URL_PATH = "url.path"
const val PARAM_URL_PORT = "url.port"
const val PARAM_URL_SCHEME = "url.scheme"
const val PARAM_URL_QUERY = "url.query"
const val PARAM_BUSINESS_TYPE = "http.request.type"
const val PARAM_REQUEST_BODY = "http.request.body.content"
const val PARAM_RESPONSE_BODY = "http.response.body.content"
const val PARAM_RESPONSE_STATUS = "http.response.status_code"
const val PARAM_REQUEST_HEADERS = "http.request.headers"
const val PARAM_RESPONSE_HEADERS = "http.response.headers"
const val PARAM_REQUEST_BYTES = "http.request.body.bytes"
const val PARAM_RESPONSE_BYTES = "http.response.body.bytes"
const val PARAM_REQUEST_MIMETYPE = "http.request.mime_type"
const val PARAM_RESPONSE_MIMETYPE = "http.response.mime_type"
const val PARAM_REQUEST_METHOD = "http.request.method"
const val PARAM_REQUEST_REFERER = "http.request.referrer"
const val PARAM_REQUEST_TIME = "event.start"
const val PARAM_RESPONSE_TIME = "event.end"
const val PARAM_DURATION = "event.duration"
const val PARAM_USER_AGENT = "user_agent.original"
const val PARAM_DIRECTION = "network.direction"
const val PARAM_PROTOCOL = "network.protocol"
private fun extractHeaders(
headerNames: Iterator<String>,
ignoreList: List<String> = emptyList(),
headerValuesSupplier: Function<String, Iterator<String>>
): Map<String, MutableCollection<String>> {
val requestHeaders: MutableMap<String, MutableCollection<String>> = mutableMapOf()
while (headerNames.hasNext()) {
val name = headerNames.next()
if (name in ignoreList) continue
val values = requestHeaders.computeIfAbsent(name) { mutableSetOf() }
val headerValues = headerValuesSupplier.apply(name)
while (headerValues.hasNext()) {
values.add(headerValues.next())
}
}
return requestHeaders
}
private fun extractHeaders(headers: HttpHeaders): Map<String, Collection<String>> {
val result: MutableMap<String, Collection<String>> = mutableMapOf()
for ((key, value) in headers) {
result[key] = value.toList()
}
return result
}
private fun toHeaderString(headerMap: Map<String, Collection<String>>): String {
return headerMap.entries.asSequence()
.flatMap { entry ->
val key = entry.key
entry.value.asSequence().map { Pair(key, it) }
}
.map { "${it.first}=${it.second}" }
.joinToString(separator = ",")
}
private fun typeToString(contentType: String?): String? {
return try {
if (contentType == null) return null
val mediaType = MediaType.parseMediaType(contentType)
return typeToString(mediaType)
} catch (e: InvalidMediaTypeException) {
log.info(e.toString(), e)
e.toString()
}
}
private fun typeToString(mediaType: MediaType?): String? {
return try {
if (mediaType == null) return null
"${mediaType.type}/${mediaType.subtype}"
} catch (e: RuntimeException) {
log.info(e.toString(), e)
e.toString()
}
}
private fun extractExtension(fileName: String?): String? {
if (fileName == null || !fileName.contains(".")) return null
return fileName.substring(fileName.lastIndexOf('.') + 1)
}
private fun cutToMaxLength(string: String?): String? {
return if (string != null && string.length > MAX_LOG_SIZE) {
string.substring(0, MAX_LOG_SIZE)
} else string
}
private data class LogMessage(
val requestHeaders: Map<String, Collection<String>>,
val responseHeaders: Map<String, Collection<String>>,
val url: URL,
val method: String,
val requestMimeType: String?,
val responseMimeType: String?,
val requestBody: String?,
val responseBody: String?,
val requestSize: Int,
val responseSize: Int,
val httpStatus: Int,
val direction: String,
val requestTime: ZonedDateTime,
val responseTime: ZonedDateTime,
val traceId: String?,
val requestType: String?,
val businessType: String?,
val throwable: Throwable?
)
enum class FieldLogBehaviour {
NEVER,
ONLY_ON_ERROR,
ALWAYS
}
}
}

View File

@ -0,0 +1,3 @@
package de.hbt.support.model.dto
data class ErrorMessage(val message: String)

View File

@ -0,0 +1,83 @@
package de.hbt.support.monitoring.actuator
import mu.KotlinLogging
import org.slf4j.MDC
import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator
import org.springframework.boot.actuate.health.Status
import java.io.IOException
import java.time.Clock
import java.time.Duration
import java.util.*
abstract class AbstractHealthIndicator(
private val clock: Clock,
private val preparable: Preparable
) : HealthIndicator {
private val logStatusDownMessage = "health indicator '${indicatorName()}' invoked with status '${Status.DOWN.code}'"
private val logStatusUpMessage = "health indicator '${indicatorName()}' invoked with status '${Status.UP.code}'"
private var firstTime = true
/**
* main method that determines the health of the service
*/
protected abstract fun determineHealth(): Health
override fun health(): Health {
try {
preparable.prepare().use {
var result: Health? = null
var exception: Exception? = null
val start = clock.millis()
try {
result = determineHealth()
} catch (e: RuntimeException) {
exception = e
result = Health.down().withException(e).build()
} finally {
logInvocation(result, exception, start, clock.millis())
}
return result!!
}
} catch (e: IOException) {
log.error("unexpected exception occurred", e)
return Health.down(e).build()
}
}
private fun logInvocation(health: Health?, exception: Exception?, start: Long, end: Long) {
val duration = Duration.ofMillis(end - start)
MDC.putCloseable("event.duration", duration.toNanos().toString()).use {
if (exception != null || health == null) {
log.error(logStatusDownMessage, exception)
firstTime = true
} else if (health.status === Status.DOWN) {
log.warn(logStatusDownMessage)
firstTime = true
} else if (firstTime) {
log.info(logStatusUpMessage)
firstTime = false
} else {
log.trace(logStatusUpMessage)
}
}
}
private fun indicatorName(): String {
return this.javaClass.getSimpleName()
.replace("HealthIndicator", "")
.lowercase(Locale.getDefault())
}
companion object {
const val HOST = "localhost"
const val HTTP_PREFIX = "http://"
const val HOST_PORT_SEPARATOR = ":"
const val PATH_SEPARATOR = "/"
const val PARAMETER_SEPARATOR = "?"
const val DETAIL_ENDPOINT_KEY = "endpoint"
private val log = KotlinLogging.logger {}
}
}

View File

@ -0,0 +1,30 @@
package de.hbt.support.monitoring.actuator
import de.hbt.support.monitoring.statusprobe.StatusProbe
import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator
import java.time.Clock
abstract class AbstractStatusProbeHealthIndicator(
timeProvider: Clock, headerInterceptor: Preparable,
private val statusProbe: StatusProbe
) : AbstractHealthIndicator(timeProvider, headerInterceptor), HealthIndicator {
override fun determineHealth(): Health {
val healthBuilder = Health.status(statusProbe.status)
if (statusProbe.lastStatusChange != null) {
healthBuilder.withDetail(LAST_STATUS_CHANGE_KEY, statusProbe.lastStatusChange)
}
if (statusProbe.throwable != null) {
healthBuilder.withException(statusProbe.throwable)
}
if (statusProbe.message != null) {
healthBuilder.withDetail(MESSAGE_KEY, statusProbe.message)
}
return healthBuilder.build()
}
companion object {
const val MESSAGE_KEY = "message"
const val LAST_STATUS_CHANGE_KEY = "lastStatusChange"
}
}

View File

@ -0,0 +1,7 @@
package de.hbt.support.monitoring.actuator
import java.io.Closeable
fun interface Preparable {
fun prepare(): Closeable
}

View File

@ -0,0 +1,59 @@
package de.hbt.support.monitoring.actuator
import de.hbt.support.interceptor.HeaderInterceptorRest
import de.hbt.support.property.HealthCheckProperties
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.stereotype.Component
import org.springframework.web.client.RestTemplate
import java.security.SecureRandom
import java.time.Clock
/**
* A Health check which checks if the rest services are working.
*
*
* If you have a complex service, you should think about an easy greeting or echo service, which
* only tests the
* network/service stack and not the full application.
*
*
* The health check will be called by kubernetes to check if the container/pod should be in load
* balancing. It is possible
* to have as much health checks as you like.
*
*
* There should be a health check which is ok not before all data is loaded.
*/
@Component
class RestHealthIndicator(
clock: Clock, interceptor: HeaderInterceptorRest,
serverProperties: ServerProperties,
private val restTemplateRestHealthIndicator: RestTemplate,
private val serviceProperties: HealthCheckProperties
) : AbstractHealthIndicator(clock, Preparable { interceptor.markAsHealthCheck() }), HealthIndicator {
private val randomizer = SecureRandom()
private val urlPrefix: String = (HTTP_PREFIX + HOST + HOST_PORT_SEPARATOR
+ serverProperties.port
+ URL_PATH + PARAMETER_SEPARATOR + GET_PARAMETER)
/**
* main method that determines the health of the service
*/
override fun determineHealth(): Health {
val random = randomizer.nextInt(100000, 999999).toString()
val url = "$urlPrefix{random}"
val response = restTemplateRestHealthIndicator.getForEntity(url, String::class.java, random)
val status = if (response.body == serviceProperties.greeting.format(random)) Status.UP else Status.DOWN
return Health.status(status)
.withDetail(DETAIL_ENDPOINT_KEY, url)
.build()
}
companion object {
private const val URL_PATH = "/timetable/healthCheck"
private const val GET_PARAMETER = "message="
}
}

View File

@ -0,0 +1,25 @@
package de.hbt.support.monitoring.statusprobe
import org.springframework.boot.actuate.health.Status
import java.time.Clock
import java.util.concurrent.atomic.AtomicInteger
class CountBasedStatusProbe(
private val maxFailureCount: Int, clock: Clock, criticality: StatusProbeCriticality, name: String,
statusProbeLogger: StatusProbeLogger
) : StatusProbe(clock, criticality, name, statusProbeLogger) {
private val failureCount = AtomicInteger(0)
@Synchronized
override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
if (status === Status.DOWN) {
val failureCount = failureCount.incrementAndGet()
if (failureCount > maxFailureCount) {
super.setStatus(status, throwable, message)
}
} else if (status === Status.UP) {
failureCount.set(0)
super.setStatus(status, throwable, message)
}
}
}

View File

@ -0,0 +1,51 @@
package de.hbt.support.monitoring.statusprobe
import org.springframework.boot.actuate.health.Status
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import java.time.Clock
import java.time.Duration
/**
* 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.
*/
class PercentageBasedStatusProbe(
private val maxFailurePercent: Int, clock: Clock,
threadPoolTaskScheduler: ThreadPoolTaskScheduler, schedulePeriod: Duration, criticality: StatusProbeCriticality,
name: String, statusProbeLogger: StatusProbeLogger
) : StatusProbe(clock, criticality, name, statusProbeLogger), ScheduledStatusProbe {
private var requestCount = 0
private var downCount = 0
private var temporaryMessage: String? = null
private var temporaryThrowable: Throwable? = null
init {
scheduleTask(threadPoolTaskScheduler, schedulePeriod)
}
@Synchronized
override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
if (status === Status.DOWN) {
downCount++
this.temporaryThrowable = throwable
this.temporaryMessage = message
}
requestCount++
}
private fun reset() {
requestCount = 0
downCount = 0
}
@Synchronized
override fun runScheduledTask() {
if (requestCount > 0 && downCount * 100.0 / requestCount > maxFailurePercent) {
super.setStatus(Status.DOWN, temporaryThrowable, temporaryMessage)
} else {
super.setStatus(Status.UP, null, null)
}
reset()
}
}

View File

@ -0,0 +1,22 @@
package de.hbt.support.monitoring.statusprobe
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import org.springframework.scheduling.support.PeriodicTrigger
import java.time.Duration
interface ScheduledStatusProbe {
fun runScheduledTask()
fun scheduleTask(
threadPoolTaskScheduler: ThreadPoolTaskScheduler,
schedulePeriod: Duration
) {
val periodicTrigger = PeriodicTrigger(
Duration.ofSeconds(schedulePeriod.toSeconds())
)
threadPoolTaskScheduler.schedule(periodicTrigger) { runScheduledTask() }
}
}
fun ThreadPoolTaskScheduler.schedule(periodicTrigger: PeriodicTrigger, task: Runnable) {
this.schedule(task, periodicTrigger)
}

View File

@ -0,0 +1,59 @@
package de.hbt.support.monitoring.statusprobe
import lombok.Getter
import org.springframework.boot.actuate.health.Status
import java.time.Clock
import java.time.ZonedDateTime
@Getter
open class StatusProbe(
private val clock: Clock, criticality: StatusProbeCriticality, private val name: String,
private val statusProbeLogger: StatusProbeLogger
) {
var status: Status = Status.UP
private set
var throwable: Throwable? = null
private set
var message: String? = null
private set
var lastStatusChange: ZonedDateTime? = null
private set
init {
statusProbeLogger.registerStatusProbe(name, criticality)
}
protected open fun setStatus(status: Status, throwable: Throwable?, message: String?) {
if (status !== this.status) {
lastStatusChange = ZonedDateTime.now(clock)
statusProbeLogger.logStatusChange(name, message, status, lastStatusChange, throwable)
}
this.status = status
this.throwable = throwable
this.message = message
}
fun up() {
setStatus(Status.UP, null, null)
}
fun up(message: String) {
setStatus(Status.UP, null, message)
}
fun down() {
setStatus(Status.DOWN, null, null)
}
fun down(throwable: Throwable) {
setStatus(Status.DOWN, throwable, null)
}
fun down(message: String) {
setStatus(Status.DOWN, null, message)
}
protected fun down(throwable: Throwable, message: String) {
setStatus(Status.DOWN, throwable, message)
}
}

View File

@ -0,0 +1,7 @@
package de.hbt.support.monitoring.statusprobe
enum class StatusProbeCriticality {
K1,
K2,
K3
}

View File

@ -0,0 +1,109 @@
package de.hbt.support.monitoring.statusprobe
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.apache.logging.log4j.MarkerManager
import org.apache.logging.log4j.message.StringMapMessage
import org.springframework.boot.actuate.health.Status
import java.time.Clock
import java.time.ZonedDateTime
class StatusProbeLogger internal constructor(private val clock: Clock, private val commLog: Logger) {
private val statusProbeToStatus: MutableMap<ProbeIdent, Status> = mutableMapOf()
constructor(clock: Clock) : this(clock, LogManager.getLogger("statusprobe"))
fun registerStatusProbe(name: String, criticality: StatusProbeCriticality) {
statusProbeToStatus[ProbeIdent(name, criticality)] = Status.UP
logStatusChange(name, "Startup", Status.UP, ZonedDateTime.now(clock), null)
}
fun logStatusChange(
name: String, message: String?, status: Status, lastStatusChange: ZonedDateTime?,
throwable: Throwable?
) {
var probeIdent = getProbeIdent(name)
if (probeIdent == null) {
probeIdent = ProbeIdent(name, StatusProbeCriticality.K1)
}
statusProbeToStatus[probeIdent] = status
createLog(message, lastStatusChange, throwable)
}
private fun getProbeIdent(name: String): ProbeIdent? {
return statusProbeToStatus.keys.asSequence()
.filter { it.name == name }
.firstOrNull()
}
private fun createLog(message: String?, lastStatusChange: ZonedDateTime?, throwable: Throwable?) {
val cleanedMessage = message ?: ""
val overallStatus = overallStatus
val criticality = overallCriticality
if (Status.UP == overallStatus) {
commLog.info(
MARKER, StringMapMessage()
.with(LABEL_CRITICALITY, criticality)
.with(LABEL_STATUS, overallStatus)
.with(LABEL_MESSAGE, cleanedMessage)
.with(LABEL_LAST_STATUS_CHANGE, lastStatusChange)
)
} else {
commLog.error(
MARKER, StringMapMessage()
.with(LABEL_CRITICALITY, criticality)
.with(LABEL_STATUS, overallStatus)
.with(LABEL_MESSAGE, cleanedMessage)
.with(LABEL_REASON, reason)
.with(LABEL_LAST_STATUS_CHANGE, lastStatusChange), throwable
)
}
}
private val overallCriticality: StatusProbeCriticality
get() {
val crits = statusProbeToStatus.keys.asSequence()
.map { it.criticality }
.toList()
return if (crits.contains(StatusProbeCriticality.K1)) StatusProbeCriticality.K1
else if (crits.contains(StatusProbeCriticality.K2)) StatusProbeCriticality.K2
else StatusProbeCriticality.K3
}
private val overallStatus: Status
get() = if (statusProbeToStatus.containsValue(Status.DOWN)) Status.DOWN
else Status.UP
private val reason: String
get() {
val probesDown = statusProbeToStatus.entries.asSequence()
.filter { it.value == Status.DOWN }
.map { it.key }
.toList()
val reasonK1 = getDownStatusProbes(probesDown, StatusProbeCriticality.K1)
val reasonK2 = getDownStatusProbes(probesDown, StatusProbeCriticality.K2)
val reasonK3 = getDownStatusProbes(probesDown, StatusProbeCriticality.K3)
return "$reasonK1$reasonK2$reasonK3".trim { it <= ' ' }
}
private fun getDownStatusProbes(probesDown: List<ProbeIdent>, criticality: StatusProbeCriticality): String {
val downProbeNames = probesDown.asSequence()
.filter { it.criticality == criticality }
.map { it.name }
.toList()
return if (downProbeNames.isNotEmpty()) {
"$criticality failed: ${downProbeNames.joinToString(separator = ",")}\n"
} else ""
}
data class ProbeIdent(val name: String, val criticality: StatusProbeCriticality)
companion object {
private val MARKER = MarkerManager.getMarker("statusprobe")
private const val LABEL_CRITICALITY = "label.status.criticality"
private const val LABEL_STATUS = "label.status.status"
private const val LABEL_REASON = "label.status.reason"
private const val LABEL_MESSAGE = "label.status.description"
private const val LABEL_LAST_STATUS_CHANGE = "label.status.last_change"
}
}

View File

@ -0,0 +1,48 @@
package de.hbt.support.monitoring.statusprobe
import org.springframework.boot.actuate.health.Status
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import java.time.Clock
import java.time.Duration
import java.time.ZonedDateTime
class TimeBasedStatusProbe(
private val maxFailureDuration: Duration, private val clock: Clock,
threadPoolTaskScheduler: ThreadPoolTaskScheduler, schedulePeriod: Duration, criticality: StatusProbeCriticality,
name: String, statusProbeLogger: StatusProbeLogger
) : StatusProbe(clock, criticality, name, statusProbeLogger), ScheduledStatusProbe {
private var lastSuccess: ZonedDateTime? = null
private var temporaryThrowable: Throwable? = null
private var temporaryMessage: String? = null
init {
scheduleTask(threadPoolTaskScheduler, schedulePeriod)
}
@Synchronized
override fun setStatus(status: Status, throwable: Throwable?, message: String?) {
if (status === Status.DOWN) {
this.temporaryThrowable = throwable
this.temporaryMessage = message
} else if (status === Status.UP) {
lastSuccess = ZonedDateTime.now(clock)
super.setStatus(status, throwable, message)
}
}
private val isOverdue: Boolean
get() {
if (lastSuccess == null) {
return false
}
val timeSinceLastSuccess = Duration.between(lastSuccess, ZonedDateTime.now(clock))
return maxFailureDuration.minus(timeSinceLastSuccess).isNegative
}
@Synchronized
override fun runScheduledTask() {
if (isOverdue) {
super.setStatus(Status.DOWN, temporaryThrowable, temporaryMessage)
}
}
}

View File

@ -0,0 +1,10 @@
package de.hbt.support.property
import io.swagger.v3.oas.annotations.media.Schema
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties(prefix = "de.hbt.support.health")
@Schema(description = "Properties, to configure health check")
class HealthCheckProperties {
lateinit var greeting: String
}

View File

@ -0,0 +1,15 @@
package de.hbt.support.property
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.ConstructorBinding
import org.springframework.boot.convert.DurationUnit
import java.time.Duration
import java.time.temporal.ChronoUnit
@ConfigurationProperties(prefix = "resttemplate.timeout")
data class RestTemplateTimeoutProperties @ConstructorBinding constructor(
@DurationUnit(ChronoUnit.MILLIS) val readTimeoutRestHealthIndicatorInMillis: Duration,
@DurationUnit(ChronoUnit.MILLIS) val connectionRestHealthIndicatorTimeoutInMillis: Duration,
@DurationUnit(ChronoUnit.MILLIS) val readTimeoutRestTemplateInMillis: Duration,
@DurationUnit(ChronoUnit.MILLIS) val connectionRestTemplateTimeoutInMillis: Duration
)

View File

@ -0,0 +1,19 @@
package de.hbt.support.property
import io.swagger.v3.oas.annotations.media.Schema
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.ConstructorBinding
import org.springframework.boot.convert.DurationUnit
import org.springframework.cloud.context.config.annotation.RefreshScope
import java.time.Duration
import java.time.temporal.ChronoUnit
@RefreshScope
@ConfigurationProperties(prefix = "de.hbt.routing.statusprobe")
@Schema(description = "Properties, to configure this Application")
data class StatusProbeProperties @ConstructorBinding constructor(
@DurationUnit(ChronoUnit.SECONDS) val scheduleDuration: Duration,
@DurationUnit(ChronoUnit.MINUTES) val maxKafkaFailureDuration: Duration,
val maxBlobFailureCount: Int,
val maxFailurePercent: Int
)

View File

@ -0,0 +1,14 @@
package de.hbt.support.property
import io.swagger.v3.oas.annotations.media.Schema
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.ConstructorBinding
import org.springframework.cloud.context.config.annotation.RefreshScope
import java.time.ZoneId
@RefreshScope
@ConfigurationProperties(prefix = "time")
@Schema(description = "Properties to configure time")
data class TimeProperties @ConstructorBinding constructor(
val defaultTimeZone: ZoneId
)

View File

@ -0,0 +1,65 @@
package de.hbt.support.security
import mu.KotlinLogging
import org.keycloak.AuthorizationContext
import org.keycloak.adapters.authorization.PolicyEnforcer
import org.keycloak.adapters.authorization.spi.HttpRequest
import org.keycloak.adapters.authorization.spi.HttpResponse
import org.keycloak.authorization.client.ClientAuthorizationContext
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode
import org.keycloak.representations.idm.authorization.Permission
class SpringPolicyEnforcer(private val policyEnforcer: PolicyEnforcer,
private val policyEnforcerConfig: PolicyEnforcerConfig) {
fun enforce(request: HttpRequest, response: HttpResponse): AuthorizationContext {
if (log.isDebugEnabled) {
log.debug("Policy enforcement is enabled. Enforcing policy decisions for path [{}].", request.uri)
}
val context = authorize(request, response)
if (log.isDebugEnabled) {
log.debug("Policy enforcement result for path [{}] is : {}", request.uri, if (context.isGranted) "GRANTED" else "DENIED")
log.debug("Returning authorization context with permissions:")
for (permission in context.permissions) {
log.debug(permission.toString())
}
}
return context
}
private fun authorize(request: HttpRequest, response: HttpResponse): AuthorizationContext {
val enforcementMode = policyEnforcerConfig.enforcementMode
return if (EnforcementMode.DISABLED == enforcementMode) {
createAuthorizedContext()
} else policyEnforcer.enforce(request, response)
}
private fun createAuthorizedContext(): AuthorizationContext {
return object : ClientAuthorizationContext(policyEnforcer.authzClient) {
override fun hasPermission(resourceName: String, scopeName: String): Boolean {
return true
}
override fun hasResourcePermission(resourceName: String): Boolean {
return true
}
override fun hasScopePermission(scopeName: String): Boolean {
return true
}
override fun getPermissions(): List<Permission> {
return emptyList()
}
override fun isGranted(): Boolean {
return true
}
}
}
companion object {
private val log = KotlinLogging.logger {}
}
}

View File

@ -0,0 +1,74 @@
package de.hbt.support.security
import jakarta.servlet.*
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import mu.KotlinLogging
import org.keycloak.AuthorizationContext
import org.keycloak.adapters.authorization.PolicyEnforcer
import org.keycloak.adapters.authorization.integration.elytron.ServletHttpRequest
import org.keycloak.adapters.authorization.integration.elytron.ServletHttpResponse
import org.keycloak.adapters.authorization.spi.ConfigurationResolver
import org.keycloak.adapters.authorization.spi.HttpRequest
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolver) : Filter {
private val policyEnforcer: MutableMap<PolicyEnforcerConfig, SpringPolicyEnforcer> = ConcurrentHashMap()
@Throws(IOException::class, ServletException::class)
override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse?, filterChain: FilterChain) {
val request = servletRequest as HttpServletRequest
val response = servletResponse as HttpServletResponse?
val httpRequest = ServletHttpRequest(request) { extractBearerToken(request) }
val policyEnforcer = getOrCreatePolicyEnforcer(httpRequest)
val authzContext = policyEnforcer.enforce(httpRequest, ServletHttpResponse(response))
request.setAttribute(AuthorizationContext::class.java.name, authzContext)
if (authzContext.isGranted) {
log.debug("Request authorized, continuing the filter chain")
filterChain.doFilter(servletRequest, servletResponse)
} else {
log.debug("Unauthorized request to path [{}], aborting the filter chain", request.requestURI)
}
}
private fun extractBearerToken(request: HttpServletRequest): String? {
val authorizationHeaderValues = request.getHeaders("Authorization")
while (authorizationHeaderValues.hasMoreElements()) {
val value = authorizationHeaderValues.nextElement()
val parts = value.trim()
.split("\\s+".toRegex())
.dropLastWhile { it.isEmpty() }
.toTypedArray()
if (parts.size != 2) {
continue
}
val bearer = parts[0]
if (bearer.equals("Bearer", ignoreCase = true)) {
return parts[1]
}
}
return null
}
private fun getOrCreatePolicyEnforcer(request: HttpRequest): SpringPolicyEnforcer {
return policyEnforcer.computeIfAbsent(configResolver.resolve(request)) { createPolicyEnforcer(it) }
}
private fun createPolicyEnforcer(enforcerConfig: PolicyEnforcerConfig): SpringPolicyEnforcer {
val authServerUrl = enforcerConfig.authServerUrl
return SpringPolicyEnforcer(PolicyEnforcer.builder()
.authServerUrl(authServerUrl)
.realm(enforcerConfig.realm)
.clientId(enforcerConfig.resource)
.credentials(enforcerConfig.credentials)
.bearerOnly(false)
.enforcerConfig(enforcerConfig).build(), enforcerConfig)
}
companion object {
private val log = KotlinLogging.logger {}
}
}

View File

@ -0,0 +1,13 @@
plugins {
id("hbt.spring-boot-cloud-base")
id("hbt.kotlin")
kotlin("kapt")
}
dependencies {
implementation(libs.spring.boot.actuator)
implementation(libs.spring.boot.web)
implementation(libs.spring.openapi)
implementation(libs.bundles.spring.boot.security)
}

View File

@ -0,0 +1,41 @@
{
"release-type": "simple",
"draft": "true",
"bump-minor-pre-major": "true",
"bump-patch-for-minor-pre-major": "true",
"changelog-sections": [
{
"type": "feat",
"section": "Added",
"hidden": false
},
{
"type": "change",
"section": "Changed",
"hidden": false
},
{
"type": "deprecate",
"section": "Deprecated",
"hidden": false
},
{
"type": "fix",
"section": "Fixed",
"hidden": false
},
{
"type": "remove",
"section": "Removed",
"hidden": false
},
{
"type": "security",
"section": "Security",
"hidden": false
}
],
"packages": {
".": {}
}
}

10
settings.gradle.kts Normal file
View File

@ -0,0 +1,10 @@
val projectname: String = providers.gradleProperty("projectname").get()
rootProject.name = projectname
include("support")
include("server")
for (subproject in rootProject.children) {
subproject.projectDir = file("module-" + subproject.name)
subproject.buildFileName = "${subproject.name}.gradle.kts"
}

1
version.txt Normal file
View File

@ -0,0 +1 @@
0.0.1