use google java formatter to avoid downloading eclipse during build

This commit is contained in:
Athou
2026-07-05 10:18:30 +02:00
parent f4dc279ec9
commit 1bd9b71215
200 changed files with 13923 additions and 12754 deletions

View File

@@ -1,401 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<profiles version="23">
<profile kind="CodeFormatterProfile" name="CommaFeed" version="23">
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.align_with_spaces" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
<setting id="org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_record_components" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_logical_operator" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line" value="one_line_if_empty"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_shift_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_type_parameters" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_loops" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_switch_case_arrow_operator" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_enum_constant" value="49"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.text_block_indentation" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_module_statements" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_permitted_types" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_type_annotations" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_assertion_message_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines" value="2147483647"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="80"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="80"/>
<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_not_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_type_arguments" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package" value="49"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_permitted_types_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.javadoc_do_not_separate_block_tags" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_tag_description" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_record_constructor" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_string_concatenation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_shift_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_shift_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_additive_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_relational_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_logical_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.keep_switch_body_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_switch_case_with_arrow" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="140"/>
<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.keep_method_body_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_additive_operator" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_relational_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line" value="one_line_if_empty"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_lambda_body" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_relational_operator" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_additive_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.align_selector_in_method_invocation_on_expression_first_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_record_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_switch_case_with_arrow_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_switch_case_with_colon" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type" value="49"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable" value="49"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_additive_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field" value="49"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_conditional_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_shift_operator" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.keep_code_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_assignment_operator" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_switch_case_with_arrow" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method" value="49"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assertion_message" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_logical_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_relational_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_logical_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_permitted_types" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="tab"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_string_concatenation" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="140"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
</profile>
</profiles>

View File

@@ -40,11 +40,6 @@
<module name="AvoidStarImport" /> <module name="AvoidStarImport" />
<module name="AvoidStaticImport" /> <module name="AvoidStaticImport" />
<module name="IllegalImport" /> <module name="IllegalImport" />
<module name="ImportOrder">
<property name="groups" value="/^java\./,javax,jakarta,org,com" />
<property name="ordered" value="true" />
<property name="separated" value="true" />
</module>
<module name="RedundantImport" /> <module name="RedundantImport" />
<module name="UnusedImports" /> <module name="UnusedImports" />
@@ -113,14 +108,6 @@
<module name="NoLineWrap" /> <module name="NoLineWrap" />
<module name="NoWhitespaceBefore" /> <module name="NoWhitespaceBefore" />
<module name="ParenPad" /> <module name="ParenPad" />
<module name="RegexpSinglelineJava">
<property name="format" value="^\t* +\t*\S" />
<property name="message" value="Line has leading space characters; indentation should be performed with tabs only." />
<property name="ignoreComments" value="true" />
</module>
<module name="WhitespaceAround">
<property name="allowEmptyTypes" value="true" />
</module>
</module> </module>
</module> </module>

View File

@@ -1,7 +0,0 @@
#Organize Import Order
#Wed Jan 29 15:15:04 CET 2025
0=java
1=javax
2=jakarta
3=org
4=com

View File

@@ -285,12 +285,9 @@
<configuration> <configuration>
<encoding>UTF-8</encoding> <encoding>UTF-8</encoding>
<java> <java>
<eclipse> <googleJavaFormat>
<file>${project.basedir}/dev/EclipseCodeFormatter.xml</file> <style>AOSP</style>
</eclipse> </googleJavaFormat>
<importOrder>
<file>${project.basedir}/dev/eclipse.importorder</file>
</importOrder>
</java> </java>
</configuration> </configuration>
</plugin> </plugin>

View File

@@ -1,15 +1,13 @@
package com.commafeed; package com.commafeed;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Singleton;
import com.commafeed.backend.feed.FeedRefreshEngine; import com.commafeed.backend.feed.FeedRefreshEngine;
import com.commafeed.backend.feed.ImageProxyUrl; import com.commafeed.backend.feed.ImageProxyUrl;
import com.commafeed.backend.task.TaskScheduler; import com.commafeed.backend.task.TaskScheduler;
import com.commafeed.security.password.PasswordConstraintValidator; import com.commafeed.security.password.PasswordConstraintValidator;
import io.quarkus.runtime.ShutdownEvent; import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent; import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -25,7 +23,8 @@ public class CommaFeedApplication {
public void start(@Observes StartupEvent ev) { public void start(@Observes StartupEvent ev) {
log.info("starting up..."); log.info("starting up...");
PasswordConstraintValidator.setMinimumPasswordLength(config.users().minimumPasswordLength()); PasswordConstraintValidator.setMinimumPasswordLength(
config.users().minimumPasswordLength());
if (config.imageProxyEnabled()) { if (config.imageProxyEnabled()) {
ImageProxyUrl.generateKey(); ImageProxyUrl.generateKey();
@@ -41,5 +40,4 @@ public class CommaFeedApplication {
feedRefreshEngine.stop(); feedRefreshEngine.stop();
taskScheduler.stop(); taskScheduler.stop();
} }
} }

View File

@@ -1,25 +1,23 @@
package com.commafeed; package com.commafeed;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import com.commafeed.backend.feed.FeedRefreshIntervalCalculator; import com.commafeed.backend.feed.FeedRefreshIntervalCalculator;
import io.quarkus.runtime.annotations.ConfigDocSection; import io.quarkus.runtime.annotations.ConfigDocSection;
import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot; import io.quarkus.runtime.annotations.ConfigRoot;
import io.quarkus.runtime.configuration.MemorySize; import io.quarkus.runtime.configuration.MemorySize;
import io.smallrye.config.ConfigMapping; import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault; import io.smallrye.config.WithDefault;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
/** /**
* CommaFeed configuration * CommaFeed configuration
* *
* Default values are for production, they can be overridden in application.properties for other profiles * <p>Default values are for production, they can be overridden in application.properties for other
* profiles
*/ */
@ConfigMapping(prefix = "commafeed") @ConfigMapping(prefix = "commafeed")
@ConfigRoot(phase = ConfigPhase.RUN_TIME) @ConfigRoot(phase = ConfigPhase.RUN_TIME)
@@ -31,9 +29,11 @@ public interface CommaFeedConfiguration {
boolean hideFromWebCrawlers(); boolean hideFromWebCrawlers();
/** /**
* If enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser. * If enabled, images in feed entries will be proxied through the server instead of accessed
* directly by the browser.
* *
* This is useful if commafeed is accessed through a restricting proxy that blocks some feeds that are followed. * <p>This is useful if commafeed is accessed through a restricting proxy that blocks some feeds
* that are followed.
*/ */
@WithDefault("false") @WithDefault("false")
boolean imageProxyEnabled(); boolean imageProxyEnabled();
@@ -41,59 +41,44 @@ public interface CommaFeedConfiguration {
/** /**
* Enable password recovery via email. * Enable password recovery via email.
* *
* Quarkus mailer will need to be configured. * <p>Quarkus mailer will need to be configured.
*/ */
@WithDefault("false") @WithDefault("false")
boolean passwordRecoveryEnabled(); boolean passwordRecoveryEnabled();
/** /** Message displayed in a notification at the bottom of the page. */
* Message displayed in a notification at the bottom of the page.
*/
Optional<String> announcement(); Optional<String> announcement();
/** /** Google Auth key for fetching Youtube channel favicons. */
* Google Auth key for fetching Youtube channel favicons.
*/
Optional<String> googleAuthKey(); Optional<String> googleAuthKey();
/** /** HTTP client configuration */
* HTTP client configuration
*/
@ConfigDocSection @ConfigDocSection
HttpClient httpClient(); HttpClient httpClient();
/** /** Feed refresh engine settings. */
* Feed refresh engine settings.
*/
@ConfigDocSection @ConfigDocSection
FeedRefresh feedRefresh(); FeedRefresh feedRefresh();
/** /** Push notification settings. */
* Push notification settings.
*/
@ConfigDocSection @ConfigDocSection
PushNotifications pushNotifications(); PushNotifications pushNotifications();
/** /** Database settings. */
* Database settings.
*/
@ConfigDocSection @ConfigDocSection
Database database(); Database database();
/** /** Users settings. */
* Users settings.
*/
@ConfigDocSection @ConfigDocSection
Users users(); Users users();
/** /** Websocket settings. */
* Websocket settings.
*/
@ConfigDocSection @ConfigDocSection
Websocket websocket(); Websocket websocket();
/** /**
* Duration to wait for the feed refresh engine and the task scheduler to stop when the application is shutting down. * Duration to wait for the feed refresh engine and the task scheduler to stop when the
* application is shutting down.
*/ */
@WithDefault("2s") @WithDefault("2s")
Duration shutdownTimeout(); Duration shutdownTimeout();
@@ -104,171 +89,150 @@ public interface CommaFeedConfiguration {
*/ */
Optional<String> userAgent(); Optional<String> userAgent();
/** /** Time to wait for a connection to be established. */
* Time to wait for a connection to be established.
*/
@WithDefault("5s") @WithDefault("5s")
Duration connectTimeout(); Duration connectTimeout();
/** /** Time to wait for SSL handshake to complete. */
* Time to wait for SSL handshake to complete.
*/
@WithDefault("5s") @WithDefault("5s")
Duration sslHandshakeTimeout(); Duration sslHandshakeTimeout();
/** /** Time to wait between two packets before timeout. */
* Time to wait between two packets before timeout.
*/
@WithDefault("10s") @WithDefault("10s")
Duration socketTimeout(); Duration socketTimeout();
/** /** Time to wait for the full response to be received. */
* Time to wait for the full response to be received.
*/
@WithDefault("10s") @WithDefault("10s")
Duration responseTimeout(); Duration responseTimeout();
/** /** Time to live for a connection in the pool. */
* Time to live for a connection in the pool.
*/
@WithDefault("30s") @WithDefault("30s")
Duration connectionTimeToLive(); Duration connectionTimeToLive();
/** /** Time between eviction runs for idle connections. */
* Time between eviction runs for idle connections.
*/
@WithDefault("1m") @WithDefault("1m")
Duration idleConnectionsEvictionInterval(); Duration idleConnectionsEvictionInterval();
/** /**
* If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed. * If a feed is larger than this, it will be discarded to prevent memory issues while
* parsing the feed.
*/ */
@WithDefault("5M") @WithDefault("5M")
MemorySize maxResponseSize(); MemorySize maxResponseSize();
/** /**
* Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal * Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks,
* resources. * which could potentially expose internal resources.
* *
* You may want to enable this if you host a public instance of CommaFeed with registrations open. * <p>You may want to enable this if you host a public instance of CommaFeed with
* registrations open.
*/ */
@WithDefault("false") @WithDefault("false")
boolean blockLocalAddresses(); boolean blockLocalAddresses();
/** /** HTTP client cache configuration */
* HTTP client cache configuration
*/
@ConfigDocSection @ConfigDocSection
HttpClientCache cache(); HttpClientCache cache();
} }
interface HttpClientCache { interface HttpClientCache {
/** /**
* Whether to enable the cache. This cache is used to avoid spamming feeds in short bursts (e.g. when subscribing to a feed for the * Whether to enable the cache. This cache is used to avoid spamming feeds in short bursts
* first time or when clicking "fetch all my feeds now"). * (e.g. when subscribing to a feed for the first time or when clicking "fetch all my feeds
* now").
*/ */
@WithDefault("true") @WithDefault("true")
boolean enabled(); boolean enabled();
/** /** Maximum amount of memory the cache can use. */
* Maximum amount of memory the cache can use.
*/
@WithDefault("10M") @WithDefault("10M")
MemorySize maximumMemorySize(); MemorySize maximumMemorySize();
/** /** Duration after which an entry is removed from the cache. */
* Duration after which an entry is removed from the cache.
*/
@WithDefault("1m") @WithDefault("1m")
Duration expiration(); Duration expiration();
} }
interface FeedRefresh { interface FeedRefresh {
/** /** Default amount of time CommaFeed will wait before refreshing a feed. */
* Default amount of time CommaFeed will wait before refreshing a feed.
*/
@WithDefault("5m") @WithDefault("5m")
Duration interval(); Duration interval();
/** /**
* Maximum amount of time CommaFeed will wait before refreshing a feed. This is used as an upper bound when: * Maximum amount of time CommaFeed will wait before refreshing a feed. This is used as an
* upper bound when:
* *
* <ul> * <ul>
* <li>an error occurs while refreshing a feed and we're backing off exponentially</li> * <li>an error occurs while refreshing a feed and we're backing off exponentially
* <li>we receive a Cache-Control header from the feed</li> * <li>we receive a Cache-Control header from the feed
* <li>we receive a Retry-After header from the feed</li> * <li>we receive a Retry-After header from the feed
* </ul> * </ul>
*/ */
@WithDefault("4h") @WithDefault("4h")
Duration maxInterval(); Duration maxInterval();
/** /**
* If enabled, CommaFeed will calculate the next refresh time based on the feed's average time between entries and the time since * If enabled, CommaFeed will calculate the next refresh time based on the feed's average
* the last entry was published. The interval will be sometimes between the default refresh interval * time between entries and the time since the last entry was published. The interval will
* (`commafeed.feed-refresh.interval`) and the maximum refresh interval (`commafeed.feed-refresh.max-interval`). * be sometimes between the default refresh interval (`commafeed.feed-refresh.interval`) and
* the maximum refresh interval (`commafeed.feed-refresh.max-interval`).
* *
* See {@link FeedRefreshIntervalCalculator} for details. * <p>See {@link FeedRefreshIntervalCalculator} for details.
*/ */
@WithDefault("true") @WithDefault("true")
boolean intervalEmpirical(); boolean intervalEmpirical();
/** /** Feed refresh engine error handling settings. */
* Feed refresh engine error handling settings.
*/
@ConfigDocSection @ConfigDocSection
FeedRefreshErrorHandling errors(); FeedRefreshErrorHandling errors();
/** /** Amount of http threads used to fetch feeds. */
* Amount of http threads used to fetch feeds.
*/
@Min(1) @Min(1)
@WithDefault("3") @WithDefault("3")
int httpThreads(); int httpThreads();
/** /** Amount of threads used to insert new entries in the database. */
* Amount of threads used to insert new entries in the database.
*/
@Min(1) @Min(1)
@WithDefault("1") @WithDefault("1")
int databaseThreads(); int databaseThreads();
/** /**
* Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again. * Duration after which a user is considered inactive. Feeds for inactive users are not
* refreshed until they log in again.
* *
* 0 to disable. * <p>0 to disable.
*/ */
@WithDefault("0") @WithDefault("0")
Duration userInactivityPeriod(); Duration userInactivityPeriod();
/** /**
* Duration after which the evaluation of a filtering expresion to mark an entry as read is considered to have timed out. * Duration after which the evaluation of a filtering expresion to mark an entry as read is
* considered to have timed out.
*/ */
@WithDefault("500ms") @WithDefault("500ms")
Duration filteringExpressionEvaluationTimeout(); Duration filteringExpressionEvaluationTimeout();
/** /**
* Duration after which the "Fetch all my feeds now" action is available again after use to avoid spamming feeds. * Duration after which the "Fetch all my feeds now" action is available again after use to
* avoid spamming feeds.
*/ */
@WithDefault("0") @WithDefault("0")
Duration forceRefreshCooldownDuration(); Duration forceRefreshCooldownDuration();
} }
interface PushNotifications { interface PushNotifications {
/** /** Whether to enable push notifications to notify users of new entries in their feeds. */
* Whether to enable push notifications to notify users of new entries in their feeds.
*/
@WithDefault("true") @WithDefault("true")
boolean enabled(); boolean enabled();
/** /** Amount of threads used to send external notifications about new entries. */
* Amount of threads used to send external notifications about new entries.
*/
@Min(1) @Min(1)
@WithDefault("5") @WithDefault("5")
int threads(); int threads();
/** /**
* Maximum amount of notifications that can be queued before new notifications are discarded. * Maximum amount of notifications that can be queued before new notifications are
* discarded.
*/ */
@Min(1) @Min(1)
@WithDefault("100") @WithDefault("100")
@@ -276,15 +240,14 @@ public interface CommaFeedConfiguration {
} }
interface FeedRefreshErrorHandling { interface FeedRefreshErrorHandling {
/** /** Number of retries before backoff is applied. */
* Number of retries before backoff is applied.
*/
@Min(0) @Min(0)
@WithDefault("3") @WithDefault("3")
int retriesBeforeBackoff(); int retriesBeforeBackoff();
/** /**
* Duration to wait before retrying after an error. Will be multiplied by the number of errors since the last successful fetch. * Duration to wait before retrying after an error. Will be multiplied by the number of
* errors since the last successful fetch.
*/ */
@WithDefault("1h") @WithDefault("1h")
Duration backoffInterval(); Duration backoffInterval();
@@ -294,14 +257,12 @@ public interface CommaFeedConfiguration {
/** /**
* Timeout applied to all database queries. * Timeout applied to all database queries.
* *
* 0 to disable. * <p>0 to disable.
*/ */
@WithDefault("0") @WithDefault("0")
Duration queryTimeout(); Duration queryTimeout();
/** /** Database cleanup settings. */
* Database cleanup settings.
*/
@ConfigDocSection @ConfigDocSection
Cleanup cleanup(); Cleanup cleanup();
@@ -309,15 +270,16 @@ public interface CommaFeedConfiguration {
/** /**
* Maximum age of feed entries in the database. Older entries will be deleted. * Maximum age of feed entries in the database. Older entries will be deleted.
* *
* 0 to disable. * <p>0 to disable.
*/ */
@WithDefault("365d") @WithDefault("365d")
Duration entriesMaxAge(); Duration entriesMaxAge();
/** /**
* Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted. * Maximum age of feed entry statuses (read/unread) in the database. Older statuses will
* be deleted.
* *
* 0 to disable. * <p>0 to disable.
*/ */
@WithDefault("0") @WithDefault("0")
Duration statusesMaxAge(); Duration statusesMaxAge();
@@ -325,7 +287,7 @@ public interface CommaFeedConfiguration {
/** /**
* Maximum number of entries per feed to keep in the database. * Maximum number of entries per feed to keep in the database.
* *
* 0 to disable. * <p>0 to disable.
*/ */
@WithDefault("500") @WithDefault("500")
int maxFeedCapacity(); int maxFeedCapacity();
@@ -333,74 +295,66 @@ public interface CommaFeedConfiguration {
/** /**
* Limit the number of feeds a user can subscribe to. * Limit the number of feeds a user can subscribe to.
* *
* 0 to disable. * <p>0 to disable.
*/ */
@WithDefault("0") @WithDefault("0")
int maxFeedsPerUser(); int maxFeedsPerUser();
/** /** Rows to delete per query while cleaning up old entries. */
* Rows to delete per query while cleaning up old entries.
*/
@Positive @Positive
@WithDefault("100") @WithDefault("100")
int batchSize(); int batchSize();
/** /** Whether to keep starred entries when cleaning up old entries. */
* Whether to keep starred entries when cleaning up old entries.
*/
@WithDefault("true") @WithDefault("true")
boolean keepStarredEntries(); boolean keepStarredEntries();
default Instant statusesInstantThreshold() { default Instant statusesInstantThreshold() {
return statusesMaxAge().toMillis() > 0 ? Instant.now().minus(statusesMaxAge()) : null; return statusesMaxAge().toMillis() > 0
? Instant.now().minus(statusesMaxAge())
: null;
} }
} }
} }
interface Users { interface Users {
/** /** Whether to let users create accounts for themselves. */
* Whether to let users create accounts for themselves.
*/
@WithDefault("false") @WithDefault("false")
boolean allowRegistrations(); boolean allowRegistrations();
/** /** Minimum password length for user accounts. */
* Minimum password length for user accounts.
*/
@WithDefault("4") @WithDefault("4")
int minimumPasswordLength(); int minimumPasswordLength();
/** /** Whether an email address is required when creating a user account. */
* Whether an email address is required when creating a user account.
*/
@WithDefault("false") @WithDefault("false")
boolean emailAddressRequired(); boolean emailAddressRequired();
/** /** Whether to create a demo account the first time the app starts. */
* Whether to create a demo account the first time the app starts.
*/
@WithDefault("false") @WithDefault("false")
boolean createDemoAccount(); boolean createDemoAccount();
} }
interface Websocket { interface Websocket {
/** /**
* Enable websocket connection so the server can notify web clients that there are new entries for feeds. * Enable websocket connection so the server can notify web clients that there are new
* entries for feeds.
*/ */
@WithDefault("true") @WithDefault("true")
boolean enabled(); boolean enabled();
/** /**
* Interval at which the client will send a ping message on the websocket to keep the connection alive. * Interval at which the client will send a ping message on the websocket to keep the
* connection alive.
*/ */
@WithDefault("15m") @WithDefault("15m")
Duration pingInterval(); Duration pingInterval();
/** /**
* If the websocket connection is disabled or the connection is lost, the client will reload the feed tree at this interval. * If the websocket connection is disabled or the connection is lost, the client will reload
* the feed tree at this interval.
*/ */
@WithDefault("30s") @WithDefault("30s")
Duration treeReloadInterval(); Duration treeReloadInterval();
} }
} }

View File

@@ -1,11 +1,9 @@
package com.commafeed; package com.commafeed;
import java.time.InstantSource; import com.codahale.metrics.MetricRegistry;
import jakarta.enterprise.inject.Produces; import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
import java.time.InstantSource;
import com.codahale.metrics.MetricRegistry;
@Singleton @Singleton
public class CommaFeedProducers { public class CommaFeedProducers {
@@ -21,5 +19,4 @@ public class CommaFeedProducers {
public MetricRegistry metricRegistry() { public MetricRegistry metricRegistry() {
return new MetricRegistry(); return new MetricRegistry();
} }
} }

View File

@@ -1,11 +1,9 @@
package com.commafeed; package com.commafeed;
import jakarta.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Properties; import java.util.Properties;
import jakarta.inject.Singleton;
import lombok.Getter; import lombok.Getter;
@Singleton @Singleton
@@ -26,5 +24,4 @@ public class CommaFeedVersion {
this.version = properties.getProperty("git.build.version", "unknown"); this.version = properties.getProperty("git.build.version", "unknown");
this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown"); this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown");
} }
} }

View File

@@ -1,22 +1,19 @@
package com.commafeed; package com.commafeed;
import com.commafeed.security.CookieService;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.UnauthorizedException;
import jakarta.annotation.Priority; import jakarta.annotation.Priority;
import jakarta.validation.ValidationException; import jakarta.validation.ValidationException;
import jakarta.ws.rs.core.NewCookie; import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.ext.Provider; import jakarta.ws.rs.ext.Provider;
import lombok.RequiredArgsConstructor;
import org.jboss.resteasy.reactive.RestResponse; import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.RestResponse.ResponseBuilder; import org.jboss.resteasy.reactive.RestResponse.ResponseBuilder;
import org.jboss.resteasy.reactive.RestResponse.Status; import org.jboss.resteasy.reactive.RestResponse.Status;
import org.jboss.resteasy.reactive.server.ServerExceptionMapper; import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
import com.commafeed.security.CookieService;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.UnauthorizedException;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
@Provider @Provider
@Priority(1) @Priority(1)
@@ -27,13 +24,18 @@ public class ExceptionMappers {
@ServerExceptionMapper(UnauthorizedException.class) @ServerExceptionMapper(UnauthorizedException.class)
public RestResponse<UnauthorizedResponse> unauthorized(UnauthorizedException e) { public RestResponse<UnauthorizedResponse> unauthorized(UnauthorizedException e) {
return RestResponse.status(Status.UNAUTHORIZED, new UnauthorizedResponse(e.getMessage(), config.users().allowRegistrations())); return RestResponse.status(
Status.UNAUTHORIZED,
new UnauthorizedResponse(e.getMessage(), config.users().allowRegistrations()));
} }
@ServerExceptionMapper(AuthenticationFailedException.class) @ServerExceptionMapper(AuthenticationFailedException.class)
public RestResponse<AuthenticationFailed> authenticationFailed(AuthenticationFailedException e) { public RestResponse<AuthenticationFailed> authenticationFailed(
AuthenticationFailedException e) {
NewCookie logoutCookie = cookieService.buildLogoutCookie(); NewCookie logoutCookie = cookieService.buildLogoutCookie();
return ResponseBuilder.create(Status.UNAUTHORIZED, new AuthenticationFailed(e.getMessage())).cookie(logoutCookie).build(); return ResponseBuilder.create(Status.UNAUTHORIZED, new AuthenticationFailed(e.getMessage()))
.cookie(logoutCookie)
.build();
} }
@ServerExceptionMapper(ValidationException.class) @ServerExceptionMapper(ValidationException.class)

View File

@@ -1,16 +1,13 @@
package com.commafeed; package com.commafeed;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Singleton;
import com.codahale.metrics.json.MetricsModule; import com.codahale.metrics.json.MetricsModule;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.quarkus.jackson.ObjectMapperCustomizer; import io.quarkus.jackson.ObjectMapperCustomizer;
import jakarta.inject.Singleton;
import java.util.concurrent.TimeUnit;
@Singleton @Singleton
public class JacksonCustomizer implements ObjectMapperCustomizer { public class JacksonCustomizer implements ObjectMapperCustomizer {
@@ -19,7 +16,8 @@ public class JacksonCustomizer implements ObjectMapperCustomizer {
objectMapper.registerModule(new JavaTimeModule()); objectMapper.registerModule(new JavaTimeModule());
// read and write instants as milliseconds instead of nanoseconds // read and write instants as milliseconds instead of nanoseconds
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true) objectMapper
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false) .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);

View File

@@ -6,221 +6,343 @@ import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer; import com.codahale.metrics.Timer;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection( @RegisterForReflection(
targets = { targets = {
// metrics // metrics
MetricRegistry.class, Meter.class, Gauge.class, Counter.class, Timer.class, Histogram.class, MetricRegistry.class,
Meter.class,
Gauge.class,
Counter.class,
Timer.class,
Histogram.class,
// rome // rome
java.util.Date.class, com.rometools.opml.feed.synd.impl.TreeCategoryImpl.class, java.util.Date.class,
com.rometools.rome.feed.synd.SyndFeedImpl.class, com.rometools.rome.feed.module.DCSubjectImpl.class, com.rometools.opml.feed.synd.impl.TreeCategoryImpl.class,
com.rometools.rome.feed.synd.SyndEntryImpl.class, com.rometools.modules.psc.types.SimpleChapter.class, com.rometools.rome.feed.synd.SyndFeedImpl.class,
com.rometools.rome.feed.synd.SyndCategoryImpl.class, com.rometools.rome.feed.synd.SyndImageImpl.class, com.rometools.rome.feed.module.DCSubjectImpl.class,
com.rometools.rome.feed.synd.SyndContentImpl.class, com.rometools.rome.feed.synd.SyndEnclosureImpl.class, com.rometools.rome.feed.synd.SyndEntryImpl.class,
com.rometools.modules.psc.types.SimpleChapter.class,
com.rometools.rome.feed.synd.SyndCategoryImpl.class,
com.rometools.rome.feed.synd.SyndImageImpl.class,
com.rometools.rome.feed.synd.SyndContentImpl.class,
com.rometools.rome.feed.synd.SyndEnclosureImpl.class,
// rome cloneable // rome cloneable
com.rometools.modules.activitystreams.types.Article.class, com.rometools.modules.activitystreams.types.Audio.class, com.rometools.modules.activitystreams.types.Article.class,
com.rometools.modules.activitystreams.types.Bookmark.class, com.rometools.modules.activitystreams.types.Comment.class, com.rometools.modules.activitystreams.types.Audio.class,
com.rometools.modules.activitystreams.types.Event.class, com.rometools.modules.activitystreams.types.File.class, com.rometools.modules.activitystreams.types.Bookmark.class,
com.rometools.modules.activitystreams.types.Folder.class, com.rometools.modules.activitystreams.types.List.class, com.rometools.modules.activitystreams.types.Comment.class,
com.rometools.modules.activitystreams.types.Note.class, com.rometools.modules.activitystreams.types.Person.class, com.rometools.modules.activitystreams.types.Event.class,
com.rometools.modules.activitystreams.types.Photo.class, com.rometools.modules.activitystreams.types.PhotoAlbum.class, com.rometools.modules.activitystreams.types.File.class,
com.rometools.modules.activitystreams.types.Place.class, com.rometools.modules.activitystreams.types.Playlist.class, com.rometools.modules.activitystreams.types.Folder.class,
com.rometools.modules.activitystreams.types.Product.class, com.rometools.modules.activitystreams.types.Review.class, com.rometools.modules.activitystreams.types.List.class,
com.rometools.modules.activitystreams.types.Service.class, com.rometools.modules.activitystreams.types.Song.class, com.rometools.modules.activitystreams.types.Note.class,
com.rometools.modules.activitystreams.types.Status.class, com.rometools.modules.base.types.DateTimeRange.class, com.rometools.modules.activitystreams.types.Person.class,
com.rometools.modules.base.types.FloatUnit.class, com.rometools.modules.base.types.GenderEnumeration.class, com.rometools.modules.activitystreams.types.Photo.class,
com.rometools.modules.base.types.IntUnit.class, com.rometools.modules.base.types.PriceTypeEnumeration.class, com.rometools.modules.activitystreams.types.PhotoAlbum.class,
com.rometools.modules.base.types.ShippingType.class, com.rometools.modules.base.types.ShortDate.class, com.rometools.modules.activitystreams.types.Place.class,
com.rometools.modules.base.types.Size.class, com.rometools.modules.base.types.YearType.class, com.rometools.modules.activitystreams.types.Playlist.class,
com.rometools.modules.content.ContentItem.class, com.rometools.modules.georss.GeoRSSPoint.class, com.rometools.modules.activitystreams.types.Product.class,
com.rometools.modules.georss.geometries.Envelope.class, com.rometools.modules.georss.geometries.LineString.class, com.rometools.modules.activitystreams.types.Review.class,
com.rometools.modules.georss.geometries.LinearRing.class, com.rometools.modules.georss.geometries.Point.class, com.rometools.modules.activitystreams.types.Service.class,
com.rometools.modules.georss.geometries.Polygon.class, com.rometools.modules.georss.geometries.Position.class, com.rometools.modules.activitystreams.types.Song.class,
com.rometools.modules.georss.geometries.PositionList.class, com.rometools.modules.mediarss.types.MediaGroup.class, com.rometools.modules.activitystreams.types.Status.class,
com.rometools.modules.mediarss.types.Metadata.class, com.rometools.modules.mediarss.types.Thumbnail.class, com.rometools.modules.base.types.DateTimeRange.class,
com.rometools.modules.opensearch.entity.OSQuery.class, com.rometools.modules.photocast.types.PhotoDate.class, com.rometools.modules.base.types.FloatUnit.class,
com.rometools.modules.sle.types.DateValue.class, com.rometools.modules.sle.types.Group.class, com.rometools.modules.base.types.GenderEnumeration.class,
com.rometools.modules.sle.types.NumberValue.class, com.rometools.modules.sle.types.Sort.class, com.rometools.modules.base.types.IntUnit.class,
com.rometools.modules.sle.types.StringValue.class, com.rometools.modules.yahooweather.types.Astronomy.class, com.rometools.modules.base.types.PriceTypeEnumeration.class,
com.rometools.modules.yahooweather.types.Atmosphere.class, com.rometools.modules.yahooweather.types.Condition.class, com.rometools.modules.base.types.ShippingType.class,
com.rometools.modules.yahooweather.types.Forecast.class, com.rometools.modules.yahooweather.types.Location.class, com.rometools.modules.base.types.ShortDate.class,
com.rometools.modules.yahooweather.types.Units.class, com.rometools.modules.yahooweather.types.Wind.class, com.rometools.modules.base.types.Size.class,
com.rometools.opml.feed.opml.Attribute.class, com.rometools.opml.feed.opml.Opml.class, com.rometools.modules.base.types.YearType.class,
com.rometools.opml.feed.opml.Outline.class, com.rometools.rome.feed.atom.Category.class, com.rometools.modules.content.ContentItem.class,
com.rometools.rome.feed.atom.Content.class, com.rometools.rome.feed.atom.Entry.class, com.rometools.modules.georss.GeoRSSPoint.class,
com.rometools.rome.feed.atom.Feed.class, com.rometools.rome.feed.atom.Generator.class, com.rometools.modules.georss.geometries.Envelope.class,
com.rometools.rome.feed.atom.Link.class, com.rometools.rome.feed.atom.Person.class, com.rometools.modules.georss.geometries.LineString.class,
com.rometools.rome.feed.rss.Category.class, com.rometools.rome.feed.rss.Channel.class, com.rometools.modules.georss.geometries.LinearRing.class,
com.rometools.rome.feed.rss.Cloud.class, com.rometools.rome.feed.rss.Content.class, com.rometools.modules.georss.geometries.Point.class,
com.rometools.rome.feed.rss.Description.class, com.rometools.rome.feed.rss.Enclosure.class, com.rometools.modules.georss.geometries.Polygon.class,
com.rometools.rome.feed.rss.Guid.class, com.rometools.rome.feed.rss.Image.class, com.rometools.rome.feed.rss.Item.class, com.rometools.modules.georss.geometries.Position.class,
com.rometools.rome.feed.rss.Source.class, com.rometools.rome.feed.rss.TextInput.class, com.rometools.modules.georss.geometries.PositionList.class,
com.rometools.rome.feed.synd.SyndLinkImpl.class, com.rometools.rome.feed.synd.SyndPersonImpl.class, com.rometools.modules.mediarss.types.MediaGroup.class,
com.rometools.modules.mediarss.types.Metadata.class,
com.rometools.modules.mediarss.types.Thumbnail.class,
com.rometools.modules.opensearch.entity.OSQuery.class,
com.rometools.modules.photocast.types.PhotoDate.class,
com.rometools.modules.sle.types.DateValue.class,
com.rometools.modules.sle.types.Group.class,
com.rometools.modules.sle.types.NumberValue.class,
com.rometools.modules.sle.types.Sort.class,
com.rometools.modules.sle.types.StringValue.class,
com.rometools.modules.yahooweather.types.Astronomy.class,
com.rometools.modules.yahooweather.types.Atmosphere.class,
com.rometools.modules.yahooweather.types.Condition.class,
com.rometools.modules.yahooweather.types.Forecast.class,
com.rometools.modules.yahooweather.types.Location.class,
com.rometools.modules.yahooweather.types.Units.class,
com.rometools.modules.yahooweather.types.Wind.class,
com.rometools.opml.feed.opml.Attribute.class,
com.rometools.opml.feed.opml.Opml.class,
com.rometools.opml.feed.opml.Outline.class,
com.rometools.rome.feed.atom.Category.class,
com.rometools.rome.feed.atom.Content.class,
com.rometools.rome.feed.atom.Entry.class,
com.rometools.rome.feed.atom.Feed.class,
com.rometools.rome.feed.atom.Generator.class,
com.rometools.rome.feed.atom.Link.class,
com.rometools.rome.feed.atom.Person.class,
com.rometools.rome.feed.rss.Category.class,
com.rometools.rome.feed.rss.Channel.class,
com.rometools.rome.feed.rss.Cloud.class,
com.rometools.rome.feed.rss.Content.class,
com.rometools.rome.feed.rss.Description.class,
com.rometools.rome.feed.rss.Enclosure.class,
com.rometools.rome.feed.rss.Guid.class,
com.rometools.rome.feed.rss.Image.class,
com.rometools.rome.feed.rss.Item.class,
com.rometools.rome.feed.rss.Source.class,
com.rometools.rome.feed.rss.TextInput.class,
com.rometools.rome.feed.synd.SyndLinkImpl.class,
com.rometools.rome.feed.synd.SyndPersonImpl.class,
java.util.ArrayList.class, java.util.ArrayList.class,
// rome modules // rome modules
com.rometools.modules.sse.modules.Conflict.class, com.rometools.modules.sse.modules.Conflicts.class, com.rometools.modules.sse.modules.Conflict.class,
com.rometools.modules.cc.CreativeCommonsImpl.class, com.rometools.modules.feedpress.modules.FeedpressModuleImpl.class, com.rometools.modules.sse.modules.Conflicts.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleImpl.class, com.rometools.modules.sse.modules.Sharing.class, com.rometools.modules.cc.CreativeCommonsImpl.class,
com.rometools.modules.georss.SimpleModuleImpl.class, com.rometools.modules.atom.modules.AtomLinkModuleImpl.class, com.rometools.modules.feedpress.modules.FeedpressModuleImpl.class,
com.rometools.modules.itunes.EntryInformationImpl.class, com.rometools.modules.sse.modules.Update.class, com.rometools.modules.opensearch.impl.OpenSearchModuleImpl.class,
com.rometools.modules.photocast.PhotocastModuleImpl.class, com.rometools.modules.itunes.FeedInformationImpl.class, com.rometools.modules.sse.modules.Sharing.class,
com.rometools.modules.yahooweather.YWeatherModuleImpl.class, com.rometools.modules.feedburner.FeedBurnerImpl.class, com.rometools.modules.georss.SimpleModuleImpl.class,
com.rometools.modules.sse.modules.Related.class, com.rometools.modules.fyyd.modules.FyydModuleImpl.class, com.rometools.modules.atom.modules.AtomLinkModuleImpl.class,
com.rometools.modules.psc.modules.PodloveSimpleChapterModuleImpl.class, com.rometools.modules.thr.ThreadingModuleImpl.class, com.rometools.modules.itunes.EntryInformationImpl.class,
com.rometools.modules.sse.modules.Sync.class, com.rometools.modules.sle.SimpleListExtensionImpl.class, com.rometools.modules.sse.modules.Update.class,
com.rometools.modules.slash.SlashImpl.class, com.rometools.modules.sse.modules.History.class, com.rometools.modules.photocast.PhotocastModuleImpl.class,
com.rometools.modules.georss.GMLModuleImpl.class, com.rometools.modules.base.CustomTagsImpl.class, com.rometools.modules.itunes.FeedInformationImpl.class,
com.rometools.modules.base.GoogleBaseImpl.class, com.rometools.modules.sle.SleEntryImpl.class, com.rometools.modules.yahooweather.YWeatherModuleImpl.class,
com.rometools.modules.mediarss.MediaEntryModuleImpl.class, com.rometools.modules.content.ContentModuleImpl.class, com.rometools.modules.feedburner.FeedBurnerImpl.class,
com.rometools.modules.georss.W3CGeoModuleImpl.class, com.rometools.rome.feed.module.DCModuleImpl.class, com.rometools.modules.sse.modules.Related.class,
com.rometools.modules.mediarss.MediaModuleImpl.class, com.rometools.rome.feed.module.SyModuleImpl.class, com.rometools.modules.fyyd.modules.FyydModuleImpl.class,
com.rometools.modules.psc.modules.PodloveSimpleChapterModuleImpl.class,
com.rometools.modules.thr.ThreadingModuleImpl.class,
com.rometools.modules.sse.modules.Sync.class,
com.rometools.modules.sle.SimpleListExtensionImpl.class,
com.rometools.modules.slash.SlashImpl.class,
com.rometools.modules.sse.modules.History.class,
com.rometools.modules.georss.GMLModuleImpl.class,
com.rometools.modules.base.CustomTagsImpl.class,
com.rometools.modules.base.GoogleBaseImpl.class,
com.rometools.modules.sle.SleEntryImpl.class,
com.rometools.modules.mediarss.MediaEntryModuleImpl.class,
com.rometools.modules.content.ContentModuleImpl.class,
com.rometools.modules.georss.W3CGeoModuleImpl.class,
com.rometools.rome.feed.module.DCModuleImpl.class,
com.rometools.modules.mediarss.MediaModuleImpl.class,
com.rometools.rome.feed.module.SyModuleImpl.class,
// extracted from all 3 rome.properties files of rome library // extracted from all 3 rome.properties files of rome library
com.rometools.rome.io.impl.RSS090Parser.class, com.rometools.rome.io.impl.RSS091NetscapeParser.class, com.rometools.rome.io.impl.RSS090Parser.class,
com.rometools.rome.io.impl.RSS091UserlandParser.class, com.rometools.rome.io.impl.RSS092Parser.class, com.rometools.rome.io.impl.RSS091NetscapeParser.class,
com.rometools.rome.io.impl.RSS093Parser.class, com.rometools.rome.io.impl.RSS094Parser.class, com.rometools.rome.io.impl.RSS091UserlandParser.class,
com.rometools.rome.io.impl.RSS10Parser.class, com.rometools.rome.io.impl.RSS20wNSParser.class, com.rometools.rome.io.impl.RSS092Parser.class,
com.rometools.rome.io.impl.RSS20Parser.class, com.rometools.rome.io.impl.Atom10Parser.class, com.rometools.rome.io.impl.RSS093Parser.class,
com.rometools.rome.io.impl.RSS094Parser.class,
com.rometools.rome.io.impl.RSS10Parser.class,
com.rometools.rome.io.impl.RSS20wNSParser.class,
com.rometools.rome.io.impl.RSS20Parser.class,
com.rometools.rome.io.impl.Atom10Parser.class,
com.rometools.rome.io.impl.Atom03Parser.class, com.rometools.rome.io.impl.Atom03Parser.class,
com.rometools.rome.io.impl.SyModuleParser.class,
com.rometools.rome.io.impl.SyModuleParser.class, com.rometools.rome.io.impl.DCModuleParser.class, com.rometools.rome.io.impl.DCModuleParser.class,
com.rometools.rome.io.impl.RSS090Generator.class,
com.rometools.rome.io.impl.RSS090Generator.class, com.rometools.rome.io.impl.RSS091NetscapeGenerator.class, com.rometools.rome.io.impl.RSS091NetscapeGenerator.class,
com.rometools.rome.io.impl.RSS091UserlandGenerator.class, com.rometools.rome.io.impl.RSS092Generator.class, com.rometools.rome.io.impl.RSS091UserlandGenerator.class,
com.rometools.rome.io.impl.RSS093Generator.class, com.rometools.rome.io.impl.RSS094Generator.class, com.rometools.rome.io.impl.RSS092Generator.class,
com.rometools.rome.io.impl.RSS10Generator.class, com.rometools.rome.io.impl.RSS20Generator.class, com.rometools.rome.io.impl.RSS093Generator.class,
com.rometools.rome.io.impl.Atom10Generator.class, com.rometools.rome.io.impl.Atom03Generator.class, com.rometools.rome.io.impl.RSS094Generator.class,
com.rometools.rome.io.impl.RSS10Generator.class,
com.rometools.rome.feed.synd.impl.ConverterForAtom10.class, com.rometools.rome.feed.synd.impl.ConverterForAtom03.class, com.rometools.rome.io.impl.RSS20Generator.class,
com.rometools.rome.io.impl.Atom10Generator.class,
com.rometools.rome.io.impl.Atom03Generator.class,
com.rometools.rome.feed.synd.impl.ConverterForAtom10.class,
com.rometools.rome.feed.synd.impl.ConverterForAtom03.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS090.class, com.rometools.rome.feed.synd.impl.ConverterForRSS090.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS091Netscape.class, com.rometools.rome.feed.synd.impl.ConverterForRSS091Netscape.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS091Userland.class, com.rometools.rome.feed.synd.impl.ConverterForRSS091Userland.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS092.class, com.rometools.rome.feed.synd.impl.ConverterForRSS093.class, com.rometools.rome.feed.synd.impl.ConverterForRSS092.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS094.class, com.rometools.rome.feed.synd.impl.ConverterForRSS10.class, com.rometools.rome.feed.synd.impl.ConverterForRSS093.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS094.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS10.class,
com.rometools.rome.feed.synd.impl.ConverterForRSS20.class, com.rometools.rome.feed.synd.impl.ConverterForRSS20.class,
com.rometools.modules.mediarss.io.RSS20YahooParser.class, com.rometools.modules.mediarss.io.RSS20YahooParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.content.io.ContentModuleParser.class, com.rometools.modules.content.io.ContentModuleParser.class,
com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.itunes.io.ITunesParser.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.atom.io.AtomModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
com.rometools.modules.georss.W3CGeoParser.class,
com.rometools.modules.photocast.io.Parser.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.atom.io.AtomModuleParser.class,
com.rometools.modules.itunes.io.ITunesParserOldNamespace.class, com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ModuleParser.class, com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class, com.rometools.modules.sle.io.ModuleParser.class,
com.rometools.modules.yahooweather.io.WeatherModuleParser.class,
com.rometools.modules.feedpress.io.FeedpressParser.class,
com.rometools.modules.fyyd.io.FyydParser.class, com.rometools.modules.fyyd.io.FyydParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.content.io.ContentModuleParser.class, com.rometools.modules.content.io.ContentModuleParser.class,
com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.itunes.io.ITunesParser.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.atom.io.AtomModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
com.rometools.modules.georss.W3CGeoParser.class,
com.rometools.modules.photocast.io.Parser.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.atom.io.AtomModuleParser.class,
com.rometools.modules.itunes.io.ITunesParserOldNamespace.class, com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ModuleParser.class, com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class, com.rometools.modules.sle.io.ModuleParser.class,
com.rometools.modules.yahooweather.io.WeatherModuleParser.class,
com.rometools.modules.feedpress.io.FeedpressParser.class,
com.rometools.modules.fyyd.io.FyydParser.class, com.rometools.modules.fyyd.io.FyydParser.class,
com.rometools.modules.cc.io.ModuleParserRSS1.class,
com.rometools.modules.cc.io.ModuleParserRSS1.class, com.rometools.modules.content.io.ContentModuleParser.class, com.rometools.modules.content.io.ContentModuleParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.georss.SimpleParser.class,
com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
com.rometools.modules.photocast.io.Parser.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.georss.SimpleParser.class,
com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
com.rometools.modules.photocast.io.Parser.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
com.rometools.modules.feedpress.io.FeedpressParser.class, com.rometools.modules.fyyd.io.FyydParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class,
com.rometools.modules.fyyd.io.FyydParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class, com.rometools.modules.cc.io.ModuleParserRSS2.class,
com.rometools.modules.content.io.ContentModuleParser.class, com.rometools.modules.slash.io.SlashModuleParser.class, com.rometools.modules.base.io.GoogleBaseParser.class,
com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.content.io.ContentModuleParser.class,
com.rometools.modules.atom.io.AtomModuleParser.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.itunes.io.ITunesParser.class,
com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.itunes.io.ITunesParserOldNamespace.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ItemParser.class, com.rometools.modules.atom.io.AtomModuleParser.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
com.rometools.modules.georss.SimpleParser.class,
com.rometools.modules.georss.W3CGeoParser.class,
com.rometools.modules.photocast.io.Parser.class,
com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
com.rometools.modules.sle.io.ItemParser.class,
com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.yahooweather.io.WeatherModuleParser.class,
com.rometools.modules.psc.io.PodloveSimpleChapterParser.class, com.rometools.modules.psc.io.PodloveSimpleChapterParser.class,
com.rometools.modules.cc.io.ModuleParserRSS1.class,
com.rometools.modules.cc.io.ModuleParserRSS1.class, com.rometools.modules.base.io.GoogleBaseParser.class, com.rometools.modules.base.io.GoogleBaseParser.class,
com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.content.io.ContentModuleParser.class, com.rometools.modules.base.io.CustomTagParser.class,
com.rometools.modules.content.io.ContentModuleParser.class,
com.rometools.modules.slash.io.SlashModuleParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class, com.rometools.modules.base.io.GoogleBaseParser.class,
com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.slash.io.SlashModuleParser.class, com.rometools.modules.base.io.CustomTagParser.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
com.rometools.modules.georss.SimpleParser.class,
com.rometools.modules.georss.W3CGeoParser.class,
com.rometools.modules.photocast.io.Parser.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class,
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class, com.rometools.modules.base.io.GoogleBaseParser.class,
com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.slash.io.SlashModuleParser.class, com.rometools.modules.base.io.CustomTagParser.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
com.rometools.modules.georss.SimpleParser.class,
com.rometools.modules.georss.W3CGeoParser.class,
com.rometools.modules.photocast.io.Parser.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
com.rometools.modules.thr.io.ThreadingModuleParser.class, com.rometools.modules.psc.io.PodloveSimpleChapterParser.class, com.rometools.modules.thr.io.ThreadingModuleParser.class,
com.rometools.modules.psc.io.PodloveSimpleChapterParser.class,
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class, com.rometools.modules.cc.io.CCModuleGenerator.class,
com.rometools.modules.itunes.io.ITunesGenerator.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class,
com.rometools.modules.sle.io.ModuleGenerator.class, com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
com.rometools.modules.feedpress.io.FeedpressGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class,
com.rometools.modules.content.io.ContentModuleGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
com.rometools.modules.itunes.io.ITunesGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class,
com.rometools.modules.photocast.io.Generator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class,
com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.sle.io.ModuleGenerator.class,
com.rometools.modules.feedpress.io.FeedpressGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class, com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
com.rometools.modules.feedpress.io.FeedpressGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class,
com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
com.rometools.modules.slash.io.SlashModuleGenerator.class, com.rometools.modules.itunes.io.ITunesGenerator.class, com.rometools.modules.cc.io.CCModuleGenerator.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.georss.SimpleGenerator.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class,
com.rometools.modules.photocast.io.Generator.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
com.rometools.modules.georss.SimpleGenerator.class,
com.rometools.modules.georss.W3CGeoGenerator.class,
com.rometools.modules.photocast.io.Generator.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
com.rometools.modules.feedpress.io.FeedpressGenerator.class,
com.rometools.modules.fyyd.io.FyydGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class,
com.rometools.modules.base.io.GoogleBaseGenerator.class,
com.rometools.modules.base.io.CustomTagGenerator.class,
com.rometools.modules.content.io.ContentModuleGenerator.class,
com.rometools.modules.slash.io.SlashModuleGenerator.class,
com.rometools.modules.itunes.io.ITunesGenerator.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
com.rometools.modules.georss.SimpleGenerator.class,
com.rometools.modules.georss.W3CGeoGenerator.class,
com.rometools.modules.photocast.io.Generator.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
com.rometools.modules.atom.io.AtomModuleGenerator.class,
com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class, com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class, com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class,
com.rometools.modules.base.io.GoogleBaseGenerator.class,
com.rometools.modules.base.io.GoogleBaseGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
com.rometools.modules.slash.io.SlashModuleGenerator.class, com.rometools.modules.slash.io.SlashModuleGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class,
com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.slash.io.SlashModuleGenerator.class, com.rometools.modules.base.io.CustomTagGenerator.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.slash.io.SlashModuleGenerator.class,
com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
com.rometools.modules.georss.SimpleGenerator.class,
com.rometools.modules.georss.W3CGeoGenerator.class,
com.rometools.modules.photocast.io.Generator.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class,
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.base.io.CustomTagGenerator.class,
com.rometools.modules.slash.io.SlashModuleGenerator.class, com.rometools.modules.slash.io.SlashModuleGenerator.class,
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.georss.SimpleGenerator.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.thr.io.ThreadingModuleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class,
com.rometools.modules.photocast.io.Generator.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
com.rometools.modules.thr.io.ThreadingModuleGenerator.class,
com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class, com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class,
com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
com.rometools.opml.io.impl.OPML10Generator.class,
com.rometools.opml.io.impl.OPML10Generator.class, com.rometools.opml.io.impl.OPML20Generator.class, com.rometools.opml.io.impl.OPML20Generator.class,
com.rometools.opml.io.impl.OPML10Parser.class,
com.rometools.opml.io.impl.OPML10Parser.class, com.rometools.opml.io.impl.OPML20Parser.class, com.rometools.opml.io.impl.OPML20Parser.class,
com.rometools.opml.feed.synd.impl.ConverterForOPML10.class,
com.rometools.opml.feed.synd.impl.ConverterForOPML10.class, com.rometools.opml.feed.synd.impl.ConverterForOPML20.class, }) com.rometools.opml.feed.synd.impl.ConverterForOPML20.class,
})
public class NativeImageClasses { public class NativeImageClasses {}
}

View File

@@ -1,10 +1,8 @@
package com.commafeed.backend; package com.commafeed.backend;
import java.nio.charset.StandardCharsets;
import com.google.common.hash.HashFunction; import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
import java.nio.charset.StandardCharsets;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
@UtilityClass @UtilityClass

View File

@@ -1,5 +1,12 @@
package com.commafeed.backend; package com.commafeed.backend;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedVersion;
import com.google.common.net.HttpHeaders;
import inet.ipaddr.IPAddress;
import inet.ipaddr.IPAddressNetwork;
import inet.ipaddr.IPAddressString;
import jakarta.inject.Singleton;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.URI; import java.net.URI;
import java.net.UnknownHostException; import java.net.UnknownHostException;
@@ -8,9 +15,9 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.SequencedMap; import java.util.SequencedMap;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
import lombok.RequiredArgsConstructor;
import jakarta.inject.Singleton; import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.apache5.util.Apache5SslUtils;
import org.apache.hc.client5.http.DnsResolver; import org.apache.hc.client5.http.DnsResolver;
import org.apache.hc.client5.http.SystemDefaultDnsResolver; import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.ConnectionConfig;
@@ -35,17 +42,6 @@ import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout; import org.apache.hc.core5.util.Timeout;
import org.brotli.dec.BrotliInputStream; import org.brotli.dec.BrotliInputStream;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedVersion;
import com.google.common.net.HttpHeaders;
import inet.ipaddr.IPAddress;
import inet.ipaddr.IPAddressNetwork;
import inet.ipaddr.IPAddressString;
import lombok.RequiredArgsConstructor;
import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.apache5.util.Apache5SslUtils;
@Singleton @Singleton
@RequiredArgsConstructor @RequiredArgsConstructor
public class HttpClientFactory { public class HttpClientFactory {
@@ -57,14 +53,23 @@ public class HttpClientFactory {
private final CommaFeedVersion version; private final CommaFeedVersion version;
public CloseableHttpClient newClient(int poolSize) { public CloseableHttpClient newClient(int poolSize) {
PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config, poolSize); PoolingHttpClientConnectionManager connectionManager =
String userAgent = config.httpClient() newConnectionManager(config, poolSize);
String userAgent =
config.httpClient()
.userAgent() .userAgent()
.orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", version.getVersion())); .orElseGet(
() ->
String.format(
"CommaFeed/%s (https://github.com/Athou/commafeed)",
version.getVersion()));
return newClient(config, connectionManager, userAgent); return newClient(config, connectionManager, userAgent);
} }
private CloseableHttpClient newClient(CommaFeedConfiguration config, HttpClientConnectionManager connectionManager, String userAgent) { private CloseableHttpClient newClient(
CommaFeedConfiguration config,
HttpClientConnectionManager connectionManager,
String userAgent) {
List<Header> headers = new ArrayList<>(); List<Header> headers = new ArrayList<>();
headers.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en")); headers.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en"));
headers.add(new BasicHeader(HttpHeaders.PRAGMA, "No-cache")); headers.add(new BasicHeader(HttpHeaders.PRAGMA, "No-cache"));
@@ -75,7 +80,8 @@ public class HttpClientFactory {
contentDecoderMap.put(ContentCoding.DEFLATE.token(), DeflateInputStream::new); contentDecoderMap.put(ContentCoding.DEFLATE.token(), DeflateInputStream::new);
contentDecoderMap.put(ContentCoding.BROTLI.token(), BrotliInputStream::new); contentDecoderMap.put(ContentCoding.BROTLI.token(), BrotliInputStream::new);
RedirectStrategy redirectStrategy = config.httpClient().blockLocalAddresses() RedirectStrategy redirectStrategy =
config.httpClient().blockLocalAddresses()
? new BlockLocalAddressesRedirectStrategy(DNS_RESOLVER) ? new BlockLocalAddressesRedirectStrategy(DNS_RESOLVER)
: new DefaultRedirectStrategy(); : new DefaultRedirectStrategy();
@@ -88,30 +94,40 @@ public class HttpClientFactory {
.setDefaultHeaders(headers) .setDefaultHeaders(headers)
.setConnectionManager(connectionManager) .setConnectionManager(connectionManager)
.evictExpiredConnections() .evictExpiredConnections()
.evictIdleConnections(TimeValue.of(config.httpClient().idleConnectionsEvictionInterval())) .evictIdleConnections(
TimeValue.of(config.httpClient().idleConnectionsEvictionInterval()))
.setContentDecoderRegistry(new LinkedHashMap<>(contentDecoderMap)) .setContentDecoderRegistry(new LinkedHashMap<>(contentDecoderMap))
.setRedirectStrategy(redirectStrategy) .setRedirectStrategy(redirectStrategy)
.build(); .build();
} }
private PoolingHttpClientConnectionManager newConnectionManager(CommaFeedConfiguration config, int poolSize) { private PoolingHttpClientConnectionManager newConnectionManager(
SSLFactory sslFactory = SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build(); CommaFeedConfiguration config, int poolSize) {
DnsResolver dnsResolver = config.httpClient().blockLocalAddresses() ? new BlockLocalAddressesDnsResolver(DNS_RESOLVER) SSLFactory sslFactory =
SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build();
DnsResolver dnsResolver =
config.httpClient().blockLocalAddresses()
? new BlockLocalAddressesDnsResolver(DNS_RESOLVER)
: DNS_RESOLVER; : DNS_RESOLVER;
return PoolingHttpClientConnectionManagerBuilder.create() return PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(Apache5SslUtils.toTlsSocketStrategy(sslFactory)) .setTlsSocketStrategy(Apache5SslUtils.toTlsSocketStrategy(sslFactory))
.setDefaultConnectionConfig(ConnectionConfig.custom() .setDefaultConnectionConfig(
ConnectionConfig.custom()
.setConnectTimeout(Timeout.of(config.httpClient().connectTimeout())) .setConnectTimeout(Timeout.of(config.httpClient().connectTimeout()))
.setSocketTimeout(Timeout.of(config.httpClient().socketTimeout())) .setSocketTimeout(Timeout.of(config.httpClient().socketTimeout()))
.setTimeToLive(Timeout.of(config.httpClient().connectionTimeToLive())) .setTimeToLive(
Timeout.of(config.httpClient().connectionTimeToLive()))
.build())
.setDefaultTlsConfig(
TlsConfig.custom()
.setHandshakeTimeout(
Timeout.of(config.httpClient().sslHandshakeTimeout()))
.build()) .build())
.setDefaultTlsConfig(TlsConfig.custom().setHandshakeTimeout(Timeout.of(config.httpClient().sslHandshakeTimeout())).build())
.setMaxConnPerRoute(poolSize) .setMaxConnPerRoute(poolSize)
.setMaxConnTotal(poolSize) .setMaxConnTotal(poolSize)
.setDnsResolver(dnsResolver) .setDnsResolver(dnsResolver)
.build(); .build();
} }
private static boolean isLocalAddress(InetAddress address) { private static boolean isLocalAddress(InetAddress address) {
@@ -125,7 +141,8 @@ public class HttpClientFactory {
InetAddress[] addresses = delegate.resolve(host); InetAddress[] addresses = delegate.resolve(host);
for (InetAddress addr : addresses) { for (InetAddress addr : addresses) {
if (isLocalAddress(addr)) { if (isLocalAddress(addr)) {
throw new UnknownHostException("Access to local address blocked: " + addr.getHostAddress()); throw new UnknownHostException(
"Access to local address blocked: " + addr.getHostAddress());
} }
} }
return addresses; return addresses;
@@ -143,7 +160,8 @@ public class HttpClientFactory {
private final DnsResolver delegate; private final DnsResolver delegate;
@Override @Override
public URI getLocationURI(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException { public URI getLocationURI(HttpRequest request, HttpResponse response, HttpContext context)
throws HttpException {
URI redirectUri = super.getLocationURI(request, response, context); URI redirectUri = super.getLocationURI(request, response, context);
String host = redirectUri.getHost(); String host = redirectUri.getHost();
@@ -160,12 +178,12 @@ public class HttpClientFactory {
for (InetAddress addr : addresses) { for (InetAddress addr : addresses) {
if (isLocalAddress(addr)) { if (isLocalAddress(addr)) {
throw new HttpException("Access to local address blocked: " + addr.getHostAddress()); throw new HttpException(
"Access to local address blocked: " + addr.getHostAddress());
} }
} }
return redirectUri; return redirectUri;
} }
} }
} }

View File

@@ -1,5 +1,15 @@
package com.commafeed.backend; package com.commafeed.backend;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedConfiguration.HttpClientCache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteStreams;
import com.google.common.net.HttpHeaders;
import jakarta.inject.Singleton;
import jakarta.ws.rs.core.CacheControl;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI; import java.net.URI;
@@ -8,10 +18,12 @@ import java.time.Instant;
import java.time.InstantSource; import java.time.InstantSource;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import lombok.Builder;
import jakarta.inject.Singleton; import lombok.EqualsAndHashCode;
import jakarta.ws.rs.core.CacheControl; import lombok.Getter;
import lombok.Lombok;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.config.RequestConfig;
@@ -27,25 +39,7 @@ import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.util.Timeout; import org.apache.hc.core5.util.Timeout;
import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate; import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate;
import com.codahale.metrics.MetricRegistry; /** Smart HTTP getter: handles gzip, ssl, last modified and etag headers */
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedConfiguration.HttpClientCache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteStreams;
import com.google.common.net.HttpHeaders;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Lombok;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Smart HTTP getter: handles gzip, ssl, last modified and etag headers
*/
@Singleton @Singleton
@Slf4j @Slf4j
public class HttpGetter { public class HttpGetter {
@@ -54,24 +48,42 @@ public class HttpGetter {
private final CloseableHttpClient client; private final CloseableHttpClient client;
private final Cache<HttpRequest, HttpResponse> cache; private final Cache<HttpRequest, HttpResponse> cache;
public HttpGetter(CommaFeedConfiguration config, InstantSource instantSource, HttpClientFactory httpClientFactory, public HttpGetter(
CommaFeedConfiguration config,
InstantSource instantSource,
HttpClientFactory httpClientFactory,
MetricRegistry metrics) { MetricRegistry metrics) {
this.config = config; this.config = config;
this.instantSource = instantSource; this.instantSource = instantSource;
this.client = httpClientFactory.newClient(config.feedRefresh().httpThreads()); this.client = httpClientFactory.newClient(config.feedRefresh().httpThreads());
this.cache = newCache(config); this.cache = newCache(config);
metrics.registerGauge(MetricRegistry.name(getClass(), "cache", "size"), () -> cache == null ? 0 : cache.size()); metrics.registerGauge(
metrics.registerGauge(MetricRegistry.name(getClass(), "cache", "memoryUsage"), MetricRegistry.name(getClass(), "cache", "size"),
() -> cache == null ? 0 : cache.asMap().values().stream().mapToInt(e -> ArrayUtils.getLength(e.content)).sum()); () -> cache == null ? 0 : cache.size());
metrics.registerGauge(
MetricRegistry.name(getClass(), "cache", "memoryUsage"),
() ->
cache == null
? 0
: cache.asMap().values().stream()
.mapToInt(e -> ArrayUtils.getLength(e.content))
.sum());
} }
public HttpResult get(String url) throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException { public HttpResult get(String url)
throws IOException,
NotModifiedException,
TooManyRequestsException,
SchemeNotAllowedException {
return get(HttpRequest.builder(url).build()); return get(HttpRequest.builder(url).build());
} }
public HttpResult get(HttpRequest request) public HttpResult get(HttpRequest request)
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException { throws IOException,
NotModifiedException,
TooManyRequestsException,
SchemeNotAllowedException {
URI uri = URI.create(request.getUrl()); URI uri = URI.create(request.getUrl());
ensureHttpScheme(uri.getScheme()); ensureHttpScheme(uri.getScheme());
@@ -91,7 +103,8 @@ public class HttpGetter {
} }
int code = response.code(); int code = response.code();
if (code == HttpStatus.SC_TOO_MANY_REQUESTS || code == HttpStatus.SC_SERVICE_UNAVAILABLE && response.retryAfter() != null) { if (code == HttpStatus.SC_TOO_MANY_REQUESTS
|| code == HttpStatus.SC_SERVICE_UNAVAILABLE && response.retryAfter() != null) {
throw new TooManyRequestsException(response.retryAfter()); throw new TooManyRequestsException(response.retryAfter());
} }
@@ -106,12 +119,18 @@ public class HttpGetter {
String lastModifiedHeader = response.lastModifiedHeader(); String lastModifiedHeader = response.lastModifiedHeader();
String eTagHeader = response.eTagHeader(); String eTagHeader = response.eTagHeader();
Duration validFor = Optional.ofNullable(response.cacheControl()) Duration validFor =
Optional.ofNullable(response.cacheControl())
.filter(cc -> cc.getMaxAge() >= 0) .filter(cc -> cc.getMaxAge() >= 0)
.map(cc -> Duration.ofSeconds(cc.getMaxAge())) .map(cc -> Duration.ofSeconds(cc.getMaxAge()))
.orElse(Duration.ZERO); .orElse(Duration.ZERO);
return new HttpResult(response.content(), response.contentType(), lastModifiedHeader, eTagHeader, response.urlAfterRedirect(), return new HttpResult(
response.content(),
response.contentType(),
lastModifiedHeader,
eTagHeader,
response.urlAfterRedirect(),
validFor); validFor);
} }
@@ -125,7 +144,8 @@ public class HttpGetter {
log.debug("fetching {}", request.getUrl()); log.debug("fetching {}", request.getUrl());
HttpClientContext context = HttpClientContext.create(); HttpClientContext context = HttpClientContext.create();
context.setRequestConfig(RequestConfig.custom() context.setRequestConfig(
RequestConfig.custom()
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())) .setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
// causes issues with some feeds // causes issues with some feeds
// see https://github.com/Athou/commafeed/issues/1572 // see https://github.com/Athou/commafeed/issues/1572
@@ -133,39 +153,62 @@ public class HttpGetter {
.setProtocolUpgradeEnabled(false) .setProtocolUpgradeEnabled(false)
.build()); .build());
return client.execute(request.toClassicHttpRequest(), context, resp -> { return client.execute(
byte[] content = resp.getEntity() == null ? null request.toClassicHttpRequest(),
: toByteArray(resp.getEntity(), config.httpClient().maxResponseSize().asLongValue()); context,
resp -> {
byte[] content =
resp.getEntity() == null
? null
: toByteArray(
resp.getEntity(),
config.httpClient().maxResponseSize().asLongValue());
int code = resp.getCode(); int code = resp.getCode();
String lastModifiedHeader = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.LAST_MODIFIED)) String lastModifiedHeader =
Optional.ofNullable(resp.getFirstHeader(HttpHeaders.LAST_MODIFIED))
.map(NameValuePair::getValue) .map(NameValuePair::getValue)
.map(StringUtils::trimToNull) .map(StringUtils::trimToNull)
.orElse(null); .orElse(null);
String eTagHeader = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.ETAG)) String eTagHeader =
Optional.ofNullable(resp.getFirstHeader(HttpHeaders.ETAG))
.map(NameValuePair::getValue) .map(NameValuePair::getValue)
.map(StringUtils::trimToNull) .map(StringUtils::trimToNull)
.orElse(null); .orElse(null);
CacheControl cacheControl = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.CACHE_CONTROL)) CacheControl cacheControl =
Optional.ofNullable(resp.getFirstHeader(HttpHeaders.CACHE_CONTROL))
.map(NameValuePair::getValue) .map(NameValuePair::getValue)
.map(StringUtils::trimToNull) .map(StringUtils::trimToNull)
.map(HttpGetter::toCacheControl) .map(HttpGetter::toCacheControl)
.orElse(null); .orElse(null);
Instant retryAfter = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.RETRY_AFTER)) Instant retryAfter =
Optional.ofNullable(resp.getFirstHeader(HttpHeaders.RETRY_AFTER))
.map(NameValuePair::getValue) .map(NameValuePair::getValue)
.map(StringUtils::trimToNull) .map(StringUtils::trimToNull)
.map(this::toInstant) .map(this::toInstant)
.orElse(null); .orElse(null);
String contentType = Optional.ofNullable(resp.getEntity()).map(HttpEntity::getContentType).orElse(null); String contentType =
String urlAfterRedirect = Optional.ofNullable(context.getRedirectLocations()) Optional.ofNullable(resp.getEntity())
.map(HttpEntity::getContentType)
.orElse(null);
String urlAfterRedirect =
Optional.ofNullable(context.getRedirectLocations())
.map(RedirectLocations::getAll) .map(RedirectLocations::getAll)
.map(l -> Iterables.getLast(l, null)) .map(l -> Iterables.getLast(l, null))
.map(URI::toString) .map(URI::toString)
.orElse(request.getUrl()); .orElse(request.getUrl());
return new HttpResponse(code, lastModifiedHeader, eTagHeader, cacheControl, retryAfter, content, contentType, urlAfterRedirect); return new HttpResponse(
code,
lastModifiedHeader,
eTagHeader,
cacheControl,
retryAfter,
content,
contentType,
urlAfterRedirect);
}); });
} }
@@ -193,7 +236,8 @@ public class HttpGetter {
private static byte[] toByteArray(HttpEntity entity, long maxBytes) throws IOException { private static byte[] toByteArray(HttpEntity entity, long maxBytes) throws IOException {
if (entity.getContentLength() > maxBytes) { if (entity.getContentLength() > maxBytes) {
throw new IOException( throw new IOException(
"Response size (%s bytes) exceeds the maximum allowed size (%s bytes)".formatted(entity.getContentLength(), maxBytes)); "Response size (%s bytes) exceeds the maximum allowed size (%s bytes)"
.formatted(entity.getContentLength(), maxBytes));
} }
try (InputStream input = entity.getContent()) { try (InputStream input = entity.getContent()) {
@@ -203,7 +247,9 @@ public class HttpGetter {
byte[] bytes = ByteStreams.limit(input, maxBytes + 1).readAllBytes(); byte[] bytes = ByteStreams.limit(input, maxBytes + 1).readAllBytes();
if (bytes.length > maxBytes) { if (bytes.length > maxBytes) {
throw new IOException("Response size exceeds the maximum allowed size (%s bytes)".formatted(maxBytes)); throw new IOException(
"Response size exceeds the maximum allowed size (%s bytes)"
.formatted(maxBytes));
} }
return bytes; return bytes;
} }
@@ -216,7 +262,9 @@ public class HttpGetter {
} }
return CacheBuilder.newBuilder() return CacheBuilder.newBuilder()
.weigher((HttpRequest key, HttpResponse value) -> value.content() != null ? value.content().length : 0) .weigher(
(HttpRequest key, HttpResponse value) ->
value.content() != null ? value.content().length : 0)
.maximumWeight(cacheConfig.maximumMemorySize().asLongValue()) .maximumWeight(cacheConfig.maximumMemorySize().asLongValue())
.expireAfterWrite(cacheConfig.expiration()) .expireAfterWrite(cacheConfig.expiration())
.build(); .build();
@@ -234,21 +282,18 @@ public class HttpGetter {
public static class NotModifiedException extends Exception { public static class NotModifiedException extends Exception {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /** if the value of this header changed, this is its new value */
* if the value of this header changed, this is its new value
*/
private final String newLastModifiedHeader; private final String newLastModifiedHeader;
/** /** if the value of this header changed, this is its new value */
* if the value of this header changed, this is its new value
*/
private final String newEtagHeader; private final String newEtagHeader;
public NotModifiedException(String message) { public NotModifiedException(String message) {
this(message, null, null); this(message, null, null);
} }
public NotModifiedException(String message, String newLastModifiedHeader, String newEtagHeader) { public NotModifiedException(
String message, String newLastModifiedHeader, String newEtagHeader) {
super(message); super(message);
this.newLastModifiedHeader = newLastModifiedHeader; this.newLastModifiedHeader = newLastModifiedHeader;
this.newEtagHeader = newEtagHeader; this.newEtagHeader = newEtagHeader;
@@ -299,10 +344,21 @@ public class HttpGetter {
} }
} }
private record HttpResponse(int code, String lastModifiedHeader, String eTagHeader, CacheControl cacheControl, Instant retryAfter, private record HttpResponse(
byte[] content, String contentType, String urlAfterRedirect) {} int code,
String lastModifiedHeader,
String eTagHeader,
CacheControl cacheControl,
Instant retryAfter,
byte[] content,
String contentType,
String urlAfterRedirect) {}
public record HttpResult(byte[] content, String contentType, String lastModifiedSince, String eTag, String urlAfterRedirect, public record HttpResult(
byte[] content,
String contentType,
String lastModifiedSince,
String eTag,
String urlAfterRedirect,
Duration validFor) {} Duration validFor) {}
} }

View File

@@ -2,14 +2,12 @@ package com.commafeed.backend;
import java.net.URI; import java.net.URI;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.Strings;
import org.netpreserve.urlcanon.Canonicalizer; import org.netpreserve.urlcanon.Canonicalizer;
import org.netpreserve.urlcanon.ParsedUrl; import org.netpreserve.urlcanon.ParsedUrl;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
@UtilityClass @UtilityClass
@Slf4j @Slf4j
public class Urls { public class Urls {
@@ -29,13 +27,9 @@ public class Urls {
} }
/** /**
* * @param relativeUrl the url of the entry
* @param relativeUrl * @param feedLink the url of the feed as described in the feed
* the url of the entry * @param feedUrl the url of the feed that we used to fetch the feed
* @param feedLink
* the url of the feed as described in the feed
* @param feedUrl
* the url of the feed that we used to fetch the feed
* @return an absolute url pointing to the entry * @return an absolute url pointing to the entry
*/ */
public static String toAbsolute(String relativeUrl, String feedLink, String feedUrl) { public static String toAbsolute(String relativeUrl, String feedLink, String feedUrl) {
@@ -47,7 +41,11 @@ public class Urls {
try { try {
return URI.create(baseUrl).resolve(relativeUrl).toString(); return URI.create(baseUrl).resolve(relativeUrl).toString();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
log.debug("Unable to create absolute url from relative url: {} base: {}", relativeUrl, baseUrl, e); log.debug(
"Unable to create absolute url from relative url: {} base: {}",
relativeUrl,
baseUrl,
e);
return null; return null;
} }
} }
@@ -64,7 +62,8 @@ public class Urls {
} }
/** /**
* Normalize the url. The resulting url is not meant to be fetched but rather used as a mean to identify a feed and avoid duplicates * Normalize the url. The resulting url is not meant to be fetched but rather used as a mean to
* identify a feed and avoid duplicates
*/ */
public static String normalize(String url) { public static String normalize(String url) {
if (url == null) { if (url == null) {

View File

@@ -1,16 +1,14 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.util.List;
import java.util.Objects;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.QFeedCategory; import com.commafeed.backend.model.QFeedCategory;
import com.commafeed.backend.model.QUser; import com.commafeed.backend.model.QUser;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.querydsl.core.types.Predicate; import com.querydsl.core.types.Predicate;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Objects;
@Singleton @Singleton
public class FeedCategoryDAO extends GenericDAO<FeedCategory> { public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
@@ -22,11 +20,17 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
} }
public List<FeedCategory> findAll(User user) { public List<FeedCategory> findAll(User user) {
return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user)).join(CATEGORY.user, QUser.user).fetchJoin().fetch(); return query().selectFrom(CATEGORY)
.where(CATEGORY.user.eq(user))
.join(CATEGORY.user, QUser.user)
.fetchJoin()
.fetch();
} }
public FeedCategory findById(User user, Long id) { public FeedCategory findById(User user, Long id) {
return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), CATEGORY.id.eq(id)).fetchOne(); return query().selectFrom(CATEGORY)
.where(CATEGORY.user.eq(user), CATEGORY.id.eq(id))
.fetchOne();
} }
public FeedCategory findByName(User user, String name, FeedCategory parent) { public FeedCategory findByName(User user, String name, FeedCategory parent) {
@@ -36,7 +40,9 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
} else { } else {
parentPredicate = CATEGORY.parent.eq(parent); parentPredicate = CATEGORY.parent.eq(parent);
} }
return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), CATEGORY.name.eq(name), parentPredicate).fetchOne(); return query().selectFrom(CATEGORY)
.where(CATEGORY.user.eq(user), CATEGORY.name.eq(name), parentPredicate)
.fetchOne();
} }
public List<FeedCategory> findByParent(User user, FeedCategory parent) { public List<FeedCategory> findByParent(User user, FeedCategory parent) {
@@ -67,5 +73,4 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
} }
return isChild; return isChild;
} }
} }

View File

@@ -1,18 +1,15 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.time.Instant;
import java.util.List;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import org.apache.commons.lang3.Strings;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.QFeed; import com.commafeed.backend.model.QFeed;
import com.commafeed.backend.model.QFeedSubscription; import com.commafeed.backend.model.QFeedSubscription;
import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import java.time.Instant;
import java.util.List;
import org.apache.commons.lang3.Strings;
@Singleton @Singleton
public class FeedDAO extends GenericDAO<Feed> { public class FeedDAO extends GenericDAO<Feed> {
@@ -29,12 +26,16 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
public List<Feed> findNextUpdatable(int count, Instant lastLoginThreshold) { public List<Feed> findNextUpdatable(int count, Instant lastLoginThreshold) {
JPAQuery<Feed> query = query().selectFrom(FEED) JPAQuery<Feed> query =
query().selectFrom(FEED)
.distinct() .distinct()
// join on subscriptions to only refresh feeds that have subscribers // join on subscriptions to only refresh feeds that have subscribers
.join(SUBSCRIPTION) .join(SUBSCRIPTION)
.on(SUBSCRIPTION.feed.eq(FEED)) .on(SUBSCRIPTION.feed.eq(FEED))
.where(FEED.disabledUntil.isNull().or(FEED.disabledUntil.lt(Instant.now()))); .where(
FEED.disabledUntil
.isNull()
.or(FEED.disabledUntil.lt(Instant.now())));
if (lastLoginThreshold != null) { if (lastLoginThreshold != null) {
query.join(SUBSCRIPTION.user).where(SUBSCRIPTION.user.lastLogin.gt(lastLoginThreshold)); query.join(SUBSCRIPTION.user).where(SUBSCRIPTION.user.lastLogin.gt(lastLoginThreshold));
@@ -48,7 +49,8 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
public Feed findByUrl(String normalizedUrl, String normalizedUrlHash) { public Feed findByUrl(String normalizedUrl, String normalizedUrlHash) {
return query().selectFrom(FEED) return query()
.selectFrom(FEED)
.where(FEED.normalizedUrlHash.eq(normalizedUrlHash)) .where(FEED.normalizedUrlHash.eq(normalizedUrlHash))
.fetch() .fetch()
.stream() .stream()
@@ -59,6 +61,9 @@ public class FeedDAO extends GenericDAO<Feed> {
public List<Feed> findWithoutSubscriptions(int max) { public List<Feed> findWithoutSubscriptions(int max) {
QFeedSubscription sub = QFeedSubscription.feedSubscription; QFeedSubscription sub = QFeedSubscription.feedSubscription;
return query().selectFrom(FEED).where(JPAExpressions.selectOne().from(sub).where(sub.feed.eq(FEED)).notExists()).limit(max).fetch(); return query().selectFrom(FEED)
.where(JPAExpressions.selectOne().from(sub).where(sub.feed.eq(FEED)).notExists())
.limit(max)
.fetch();
} }
} }

View File

@@ -1,13 +1,11 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.util.List;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.QFeedEntry; import com.commafeed.backend.model.QFeedEntry;
import com.commafeed.backend.model.QFeedEntryContent; import com.commafeed.backend.model.QFeedEntryContent;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import java.util.List;
@Singleton @Singleton
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> { public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
@@ -20,11 +18,15 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
} }
public List<FeedEntryContent> findExisting(String contentHash, String titleHash) { public List<FeedEntryContent> findExisting(String contentHash, String titleHash) {
return query().select(CONTENT).from(CONTENT).where(CONTENT.contentHash.eq(contentHash), CONTENT.titleHash.eq(titleHash)).fetch(); return query().select(CONTENT)
.from(CONTENT)
.where(CONTENT.contentHash.eq(contentHash), CONTENT.titleHash.eq(titleHash))
.fetch();
} }
public long deleteWithoutEntries(int max) { public long deleteWithoutEntries(int max) {
List<Long> ids = query().select(CONTENT.id) List<Long> ids =
query().select(CONTENT.id)
.from(CONTENT) .from(CONTENT)
.leftJoin(ENTRY) .leftJoin(ENTRY)
.on(ENTRY.content.id.eq(CONTENT.id)) .on(ENTRY.content.id.eq(CONTENT.id))

View File

@@ -1,14 +1,5 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.QFeedEntry; import com.commafeed.backend.model.QFeedEntry;
@@ -16,6 +7,13 @@ import com.google.common.collect.Lists;
import com.querydsl.core.Tuple; import com.querydsl.core.Tuple;
import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Singleton @Singleton
public class FeedEntryDAO extends GenericDAO<FeedEntry> { public class FeedEntryDAO extends GenericDAO<FeedEntry> {
@@ -28,7 +26,11 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
} }
public FeedEntry findExisting(String guidHash, Feed feed) { public FeedEntry findExisting(String guidHash, Feed feed) {
return query().select(ENTRY).from(ENTRY).where(ENTRY.guidHash.eq(guidHash), ENTRY.feed.eq(feed)).limit(1).fetchOne(); return query().select(ENTRY)
.from(ENTRY)
.where(ENTRY.guidHash.eq(guidHash), ENTRY.feed.eq(feed))
.limit(1)
.fetchOne();
} }
public Set<String> findExistingGuidHashes(Set<String> guidHashes, Feed feed) { public Set<String> findExistingGuidHashes(Set<String> guidHashes, Feed feed) {
@@ -37,13 +39,19 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
} }
Set<String> result = new HashSet<>(); Set<String> result = new HashSet<>();
for (List<String> batch : Lists.partition(new ArrayList<>(guidHashes), IN_CLAUSE_BATCH_SIZE)) { for (List<String> batch :
result.addAll(query().select(ENTRY.guidHash).from(ENTRY).where(ENTRY.feed.eq(feed), ENTRY.guidHash.in(batch)).fetch()); Lists.partition(new ArrayList<>(guidHashes), IN_CLAUSE_BATCH_SIZE)) {
result.addAll(
query().select(ENTRY.guidHash)
.from(ENTRY)
.where(ENTRY.feed.eq(feed), ENTRY.guidHash.in(batch))
.fetch());
} }
return result; return result;
} }
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max, boolean keepStarredEntries) { public List<FeedCapacity> findFeedsExceedingCapacity(
long maxCapacity, long max, boolean keepStarredEntries) {
NumberExpression<Long> count = ENTRY.id.count(); NumberExpression<Long> count = ENTRY.id.count();
JPAQuery<Tuple> query = query().select(ENTRY.feed.id, count).from(ENTRY); JPAQuery<Tuple> query = query().select(ENTRY.feed.id, count).from(ENTRY);
@@ -51,25 +59,21 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
query.where(Predicates.isNotStarred(ENTRY)); query.where(Predicates.isNotStarred(ENTRY));
} }
return query.groupBy(ENTRY.feed) return query.groupBy(ENTRY.feed).having(count.gt(maxCapacity)).limit(max).fetch().stream()
.having(count.gt(maxCapacity))
.limit(max)
.fetch()
.stream()
.map(t -> new FeedCapacity(t.get(ENTRY.feed.id), t.get(count))) .map(t -> new FeedCapacity(t.get(ENTRY.feed.id), t.get(count)))
.toList(); .toList();
} }
public int delete(Long feedId, long max) { public int delete(Long feedId, long max) {
List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).limit(max).fetch(); List<FeedEntry> list =
query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).limit(max).fetch();
return delete(list); return delete(list);
} }
/** /** Delete entries older than a certain date */
* Delete entries older than a certain date
*/
public int deleteEntriesOlderThan(Instant olderThan, long max, boolean keepStarredEntries) { public int deleteEntriesOlderThan(Instant olderThan, long max, boolean keepStarredEntries) {
JPAQuery<FeedEntry> query = query().selectFrom(ENTRY) JPAQuery<FeedEntry> query =
query().selectFrom(ENTRY)
.where(ENTRY.published.lt(olderThan)) .where(ENTRY.published.lt(olderThan))
.orderBy(ENTRY.published.asc()) .orderBy(ENTRY.published.asc())
.limit(max); .limit(max);
@@ -81,11 +85,13 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
return delete(query.fetch()); return delete(query.fetch());
} }
/** /** Delete the oldest entries of a feed */
* Delete the oldest entries of a feed
*/
public int deleteOldEntries(Long feedId, long max, boolean keepStarredEntries) { public int deleteOldEntries(Long feedId, long max, boolean keepStarredEntries) {
JPAQuery<FeedEntry> query = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).orderBy(ENTRY.published.asc()).limit(max); JPAQuery<FeedEntry> query =
query().selectFrom(ENTRY)
.where(ENTRY.feed.id.eq(feedId))
.orderBy(ENTRY.published.asc())
.limit(max);
if (keepStarredEntries) { if (keepStarredEntries) {
query.where(Predicates.isNotStarred(ENTRY)); query.where(Predicates.isNotStarred(ENTRY));

View File

@@ -1,16 +1,5 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import org.apache.commons.collections4.CollectionUtils;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.feed.FeedEntryKeyword; import com.commafeed.backend.feed.FeedEntryKeyword;
import com.commafeed.backend.feed.FeedEntryKeyword.Mode; import com.commafeed.backend.feed.FeedEntryKeyword.Mode;
@@ -32,6 +21,14 @@ import com.querydsl.core.Tuple;
import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
@Singleton @Singleton
public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> { public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
@@ -46,25 +43,33 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
private final FeedEntryTagDAO feedEntryTagDAO; private final FeedEntryTagDAO feedEntryTagDAO;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
public FeedEntryStatusDAO(EntityManager entityManager, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) { public FeedEntryStatusDAO(
EntityManager entityManager,
FeedEntryTagDAO feedEntryTagDAO,
CommaFeedConfiguration config) {
super(entityManager, FeedEntryStatus.class); super(entityManager, FeedEntryStatus.class);
this.feedEntryTagDAO = feedEntryTagDAO; this.feedEntryTagDAO = feedEntryTagDAO;
this.config = config; this.config = config;
} }
public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) { public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) {
List<FeedEntryStatus> statuses = query().selectFrom(STATUS).where(STATUS.entry.eq(entry), STATUS.subscription.eq(sub)).fetch(); List<FeedEntryStatus> statuses =
query().selectFrom(STATUS)
.where(STATUS.entry.eq(entry), STATUS.subscription.eq(sub))
.fetch();
FeedEntryStatus status = statuses.stream().findFirst().orElse(null); FeedEntryStatus status = statuses.stream().findFirst().orElse(null);
return handleStatus(user, status, sub, entry); return handleStatus(user, status, sub, entry);
} }
/** /** creates an artificial "unread" status if status is null */
* creates an artificial "unread" status if status is null private FeedEntryStatus handleStatus(
*/ User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
if (status == null) { if (status == null) {
Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold(); Instant statusesInstantThreshold =
boolean read = statusesInstantThreshold != null && entry.getPublished().isBefore(statusesInstantThreshold); config.database().cleanup().statusesInstantThreshold();
boolean read =
statusesInstantThreshold != null
&& entry.getPublished().isBefore(statusesInstantThreshold);
status = new FeedEntryStatus(user, sub, entry); status = new FeedEntryStatus(user, sub, entry);
status.setRead(read); status.setRead(read);
status.setMarkable(!read); status.setMarkable(!read);
@@ -75,17 +80,25 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
} }
private void fetchTags(User user, List<FeedEntryStatus> statuses) { private void fetchTags(User user, List<FeedEntryStatus> statuses) {
Map<Long, List<FeedEntryTag>> tagsByEntryIds = feedEntryTagDAO.findByEntries(user, Map<Long, List<FeedEntryTag>> tagsByEntryIds =
statuses.stream().map(FeedEntryStatus::getEntry).toList()); feedEntryTagDAO.findByEntries(
user, statuses.stream().map(FeedEntryStatus::getEntry).toList());
for (FeedEntryStatus status : statuses) { for (FeedEntryStatus status : statuses) {
List<FeedEntryTag> tags = tagsByEntryIds.get(status.getEntry().getId()); List<FeedEntryTag> tags = tagsByEntryIds.get(status.getEntry().getId());
status.setTags(tags == null ? List.of() : tags); status.setTags(tags == null ? List.of() : tags);
} }
} }
public List<FeedEntryStatus> findStarred(User user, List<FeedEntryKeyword> keywords, Instant newerThan, int offset, int limit, public List<FeedEntryStatus> findStarred(
ReadingOrder order, boolean includeContent) { User user,
JPAQuery<FeedEntryStatus> query = query().selectFrom(STATUS).where(STATUS.user.eq(user), STATUS.starred.isTrue()); List<FeedEntryKeyword> keywords,
Instant newerThan,
int offset,
int limit,
ReadingOrder order,
boolean includeContent) {
JPAQuery<FeedEntryStatus> query =
query().selectFrom(STATUS).where(STATUS.user.eq(user), STATUS.starred.isTrue());
if (includeContent || CollectionUtils.isNotEmpty(keywords)) { if (includeContent || CollectionUtils.isNotEmpty(keywords)) {
query.join(STATUS.entry).fetchJoin(); query.join(STATUS.entry).fetchJoin();
query.join(STATUS.entry.content, CONTENT).fetchJoin(); query.join(STATUS.entry.content, CONTENT).fetchJoin();
@@ -124,10 +137,21 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
return statuses; return statuses;
} }
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly, public List<FeedEntryStatus> findBySubscriptions(
List<FeedEntryKeyword> keywords, Instant newerThan, int offset, int limit, ReadingOrder order, boolean includeContent, User user,
String tag, Long minEntryId, Long maxEntryId) { List<FeedSubscription> subs,
Map<Long, List<FeedSubscription>> subsByFeedId = subs.stream().collect(Collectors.groupingBy(s -> s.getFeed().getId())); boolean unreadOnly,
List<FeedEntryKeyword> keywords,
Instant newerThan,
int offset,
int limit,
ReadingOrder order,
boolean includeContent,
String tag,
Long minEntryId,
Long maxEntryId) {
Map<Long, List<FeedSubscription>> subsByFeedId =
subs.stream().collect(Collectors.groupingBy(s -> s.getFeed().getId()));
JPAQuery<Tuple> query = query().select(ENTRY, STATUS).from(ENTRY); JPAQuery<Tuple> query = query().select(ENTRY, STATUS).from(ENTRY);
query.leftJoin(ENTRY.statuses, STATUS).on(STATUS.subscription.in(subs)); query.leftJoin(ENTRY.statuses, STATUS).on(STATUS.subscription.in(subs));
@@ -212,7 +236,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
} }
public UnreadCount getUnreadCount(FeedSubscription sub) { public UnreadCount getUnreadCount(FeedSubscription sub) {
JPAQuery<Tuple> query = query().select(ENTRY.count(), ENTRY.published.max()) JPAQuery<Tuple> query =
query().select(ENTRY.count(), ENTRY.published.max())
.from(ENTRY) .from(ENTRY)
.leftJoin(ENTRY.statuses, STATUS) .leftJoin(ENTRY.statuses, STATUS)
.on(STATUS.subscription.eq(sub)) .on(STATUS.subscription.eq(sub))
@@ -239,7 +264,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
} }
public long deleteOldStatuses(Instant olderThan, int limit) { public long deleteOldStatuses(Instant olderThan, int limit) {
List<Long> ids = query().select(STATUS.id) List<Long> ids =
query().select(STATUS.id)
.from(STATUS) .from(STATUS)
.where(STATUS.entryInserted.lt(olderThan), STATUS.starred.isFalse()) .where(STATUS.entryInserted.lt(olderThan), STATUS.starred.isFalse())
.limit(limit) .limit(limit)
@@ -254,13 +280,15 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
where.and(SUBSCRIPTION.autoMarkAsReadAfterDays.isNotNull()); where.and(SUBSCRIPTION.autoMarkAsReadAfterDays.isNotNull());
where.and(SUBSCRIPTION.autoMarkAsReadAfterDays.gt(0)); where.and(SUBSCRIPTION.autoMarkAsReadAfterDays.gt(0));
NumberExpression<Integer> daysDiff = Expressions.numberTemplate(Integer.class, "TIMESTAMPDIFF(DAY, {0}, {1})", ENTRY.published, NumberExpression<Integer> daysDiff =
now); Expressions.numberTemplate(
Integer.class, "TIMESTAMPDIFF(DAY, {0}, {1})", ENTRY.published, now);
where.and(daysDiff.goe(SUBSCRIPTION.autoMarkAsReadAfterDays)); where.and(daysDiff.goe(SUBSCRIPTION.autoMarkAsReadAfterDays));
where.and(buildUnreadPredicate()); where.and(buildUnreadPredicate());
List<Tuple> tuples = query().select(ENTRY, STATUS, SUBSCRIPTION) List<Tuple> tuples =
query().select(ENTRY, STATUS, SUBSCRIPTION)
.from(ENTRY) .from(ENTRY)
.join(ENTRY.feed, FEED) .join(ENTRY.feed, FEED)
.join(SUBSCRIPTION) .join(SUBSCRIPTION)
@@ -274,7 +302,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
long updated = 0; long updated = 0;
// Update existing statuses // Update existing statuses
List<Long> statusIdsToUpdate = tuples.stream() List<Long> statusIdsToUpdate =
tuples.stream()
.map(t -> t.get(STATUS)) .map(t -> t.get(STATUS))
.filter(s -> s != null && s.getId() != null) .filter(s -> s != null && s.getId() != null)
.map(FeedEntryStatus::getId) .map(FeedEntryStatus::getId)
@@ -282,7 +311,11 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
.toList(); .toList();
if (!statusIdsToUpdate.isEmpty()) { if (!statusIdsToUpdate.isEmpty()) {
updated += updateQuery(STATUS).where(STATUS.id.in(statusIdsToUpdate)).set(STATUS.read, true).execute(); updated +=
updateQuery(STATUS)
.where(STATUS.id.in(statusIdsToUpdate))
.set(STATUS.read, true)
.execute();
} }
// Insert new statuses for entries without existing status // Insert new statuses for entries without existing status
@@ -300,5 +333,4 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
return updated; return updated;
} }
} }

View File

@@ -1,16 +1,14 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.QFeedEntryTag; import com.commafeed.backend.model.QFeedEntryTag;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Singleton @Singleton
public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> { public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
@@ -30,7 +28,8 @@ public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
} }
public Map<Long, List<FeedEntryTag>> findByEntries(User user, List<FeedEntry> entries) { public Map<Long, List<FeedEntryTag>> findByEntries(User user, List<FeedEntry> entries) {
return query().selectFrom(TAG) return query()
.selectFrom(TAG)
.where(TAG.user.eq(user), TAG.entry.in(entries)) .where(TAG.user.eq(user), TAG.entry.in(entries))
.fetch() .fetch()
.stream() .stream()

View File

@@ -1,20 +1,5 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.PostCommitInsertEventListener;
import org.hibernate.event.spi.PostInsertEvent;
import org.hibernate.persister.entity.EntityPersister;
import com.commafeed.backend.model.AbstractModel; import com.commafeed.backend.model.AbstractModel;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
@@ -23,6 +8,18 @@ import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.QFeedSubscription; import com.commafeed.backend.model.QFeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.querydsl.jpa.JPQLQuery; import com.querydsl.jpa.JPQLQuery;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.PostCommitInsertEventListener;
import org.hibernate.event.spi.PostInsertEvent;
import org.hibernate.persister.entity.EntityPersister;
@Singleton @Singleton
public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> { public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
@@ -37,12 +34,14 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
} }
public void onPostCommitInsert(Consumer<FeedSubscription> consumer) { public void onPostCommitInsert(Consumer<FeedSubscription> consumer) {
entityManager.unwrap(SharedSessionContractImplementor.class) entityManager
.unwrap(SharedSessionContractImplementor.class)
.getFactory() .getFactory()
.getServiceRegistry() .getServiceRegistry()
.getService(EventListenerRegistry.class) .getService(EventListenerRegistry.class)
.getEventListenerGroup(EventType.POST_COMMIT_INSERT) .getEventListenerGroup(EventType.POST_COMMIT_INSERT)
.appendListener(new PostCommitInsertEventListener() { .appendListener(
new PostCommitInsertEventListener() {
@Override @Override
public void onPostInsert(PostInsertEvent event) { public void onPostInsert(PostInsertEvent event) {
if (event.getEntity() instanceof FeedSubscription s) { if (event.getEntity() instanceof FeedSubscription s) {
@@ -63,7 +62,8 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
} }
public FeedSubscription findById(User user, Long id) { public FeedSubscription findById(User user, Long id) {
List<FeedSubscription> subs = query().selectFrom(SUBSCRIPTION) List<FeedSubscription> subs =
query().selectFrom(SUBSCRIPTION)
.where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.id.eq(id)) .where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.id.eq(id))
.leftJoin(SUBSCRIPTION.feed) .leftJoin(SUBSCRIPTION.feed)
.fetchJoin() .fetchJoin()
@@ -79,7 +79,8 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
} }
public FeedSubscription findByFeed(User user, Feed feed) { public FeedSubscription findByFeed(User user, Feed feed) {
List<FeedSubscription> subs = query().selectFrom(SUBSCRIPTION) List<FeedSubscription> subs =
query().selectFrom(SUBSCRIPTION)
.where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.feed.eq(feed)) .where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.feed.eq(feed))
.fetch(); .fetch();
FeedSubscription sub = subs.stream().findFirst().orElse(null); FeedSubscription sub = subs.stream().findFirst().orElse(null);
@@ -87,7 +88,8 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
} }
public List<FeedSubscription> findAll(User user) { public List<FeedSubscription> findAll(User user) {
List<FeedSubscription> subs = query().selectFrom(SUBSCRIPTION) List<FeedSubscription> subs =
query().selectFrom(SUBSCRIPTION)
.where(SUBSCRIPTION.user.eq(user)) .where(SUBSCRIPTION.user.eq(user))
.leftJoin(SUBSCRIPTION.feed) .leftJoin(SUBSCRIPTION.feed)
.fetchJoin() .fetchJoin()
@@ -98,11 +100,15 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
} }
public Long count(User user) { public Long count(User user) {
return query().select(SUBSCRIPTION.count()).from(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user)).fetchOne(); return query().select(SUBSCRIPTION.count())
.from(SUBSCRIPTION)
.where(SUBSCRIPTION.user.eq(user))
.fetchOne();
} }
public List<FeedSubscription> findByCategory(User user, FeedCategory category) { public List<FeedSubscription> findByCategory(User user, FeedCategory category) {
JPQLQuery<FeedSubscription> query = query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user)); JPQLQuery<FeedSubscription> query =
query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user));
if (category == null) { if (category == null) {
query.where(SUBSCRIPTION.category.isNull()); query.where(SUBSCRIPTION.category.isNull());
} else { } else {
@@ -112,8 +118,14 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
} }
public List<FeedSubscription> findByCategories(User user, List<FeedCategory> categories) { public List<FeedSubscription> findByCategories(User user, List<FeedCategory> categories) {
Set<Long> categoryIds = categories.stream().map(AbstractModel::getId).collect(Collectors.toSet()); Set<Long> categoryIds =
return findAll(user).stream().filter(s -> s.getCategory() != null && categoryIds.contains(s.getCategory().getId())).toList(); categories.stream().map(AbstractModel::getId).collect(Collectors.toSet());
return findAll(user).stream()
.filter(
s ->
s.getCategory() != null
&& categoryIds.contains(s.getCategory().getId()))
.toList();
} }
private List<FeedSubscription> initRelations(List<FeedSubscription> list) { private List<FeedSubscription> initRelations(List<FeedSubscription> list) {

View File

@@ -1,20 +1,16 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.time.Duration;
import java.util.Collection;
import jakarta.persistence.EntityManager;
import org.hibernate.jpa.SpecHints;
import com.commafeed.backend.model.AbstractModel; import com.commafeed.backend.model.AbstractModel;
import com.querydsl.core.types.EntityPath; import com.querydsl.core.types.EntityPath;
import com.querydsl.jpa.impl.JPADeleteClause; import com.querydsl.jpa.impl.JPADeleteClause;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAQueryFactory;
import com.querydsl.jpa.impl.JPAUpdateClause; import com.querydsl.jpa.impl.JPAUpdateClause;
import jakarta.persistence.EntityManager;
import java.time.Duration;
import java.util.Collection;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.hibernate.jpa.SpecHints;
@RequiredArgsConstructor @RequiredArgsConstructor
public abstract class GenericDAO<T extends AbstractModel> { public abstract class GenericDAO<T extends AbstractModel> {
@@ -62,5 +58,4 @@ public abstract class GenericDAO<T extends AbstractModel> {
query.setHint(SpecHints.HINT_SPEC_QUERY_TIMEOUT, Math.toIntExact(timeout.toMillis())); query.setHint(SpecHints.HINT_SPEC_QUERY_TIMEOUT, Math.toIntExact(timeout.toMillis()));
} }
} }
} }

View File

@@ -4,7 +4,6 @@ import com.commafeed.backend.model.QFeedEntry;
import com.commafeed.backend.model.QFeedEntryStatus; import com.commafeed.backend.model.QFeedEntryStatus;
import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPAExpressions;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
@UtilityClass @UtilityClass
@@ -13,6 +12,9 @@ public class Predicates {
private static final QFeedEntryStatus STATUS = QFeedEntryStatus.feedEntryStatus; private static final QFeedEntryStatus STATUS = QFeedEntryStatus.feedEntryStatus;
public static BooleanExpression isNotStarred(QFeedEntry entry) { public static BooleanExpression isNotStarred(QFeedEntry entry) {
return JPAExpressions.selectOne().from(STATUS).where(STATUS.entry.eq(entry).and(STATUS.starred.isTrue())).notExists(); return JPAExpressions.selectOne()
.from(STATUS)
.where(STATUS.entry.eq(entry).and(STATUS.starred.isTrue()))
.notExists();
} }
} }

View File

@@ -1,10 +1,8 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.util.concurrent.Callable;
import jakarta.inject.Singleton;
import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.narayana.jta.QuarkusTransaction;
import jakarta.inject.Singleton;
import java.util.concurrent.Callable;
@Singleton @Singleton
public class UnitOfWork { public class UnitOfWork {

View File

@@ -1,10 +1,9 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import com.commafeed.backend.model.QUser; import com.commafeed.backend.model.QUser;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
@Singleton @Singleton
public class UserDAO extends GenericDAO<User> { public class UserDAO extends GenericDAO<User> {

View File

@@ -1,16 +1,14 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import com.commafeed.backend.model.QUserRole; import com.commafeed.backend.model.QUserRole;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole; import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Singleton @Singleton
public class UserRoleDAO extends GenericDAO<UserRole> { public class UserRoleDAO extends GenericDAO<UserRole> {

View File

@@ -1,11 +1,10 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
import com.commafeed.backend.model.QUserSettings; import com.commafeed.backend.model.QUserSettings;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings; import com.commafeed.backend.model.UserSettings;
import jakarta.inject.Singleton;
import jakarta.persistence.EntityManager;
@Singleton @Singleton
public class UserSettingsDAO extends GenericDAO<UserSettings> { public class UserSettingsDAO extends GenericDAO<UserSettings> {

View File

@@ -1,21 +1,17 @@
package com.commafeed.backend.favicon; package com.commafeed.backend.favicon;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.net.URIBuilder;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.net.URIBuilder;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -37,7 +33,9 @@ public class FacebookFaviconFetcher implements FaviconFetcher {
return null; return null;
} }
String iconUrl = String.format("https://graph.facebook.com/%s/picture?type=square&height=16", userName); String iconUrl =
String.format(
"https://graph.facebook.com/%s/picture?type=square&height=16", userName);
try { try {
log.debug("Getting Facebook user's icon, {}", url); log.debug("Getting Facebook user's icon, {}", url);
@@ -60,7 +58,10 @@ public class FacebookFaviconFetcher implements FaviconFetcher {
} }
List<NameValuePair> params = new URIBuilder(uri).getQueryParams(); List<NameValuePair> params = new URIBuilder(uri).getQueryParams();
return params.stream().filter(p -> "id".equals(p.getName())).map(NameValuePair::getValue).findFirst().orElse(null); return params.stream()
.filter(p -> "id".equals(p.getName()))
.map(NameValuePair::getValue)
.findFirst()
.orElse(null);
} }
} }

View File

@@ -1,7 +1,6 @@
package com.commafeed.backend.favicon; package com.commafeed.backend.favicon;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@@ -17,7 +16,8 @@ public record Favicon(byte[] icon, MediaType mediaType) {
try { try {
return MediaType.valueOf(contentType); return MediaType.valueOf(contentType);
} catch (Exception e) { } catch (Exception e) {
log.debug("invalid content type '{}' received, returning default value", contentType, e); log.debug(
"invalid content type '{}' received, returning default value", contentType, e);
return DEFAULT_MEDIA_TYPE; return DEFAULT_MEDIA_TYPE;
} }
} }

View File

@@ -5,5 +5,4 @@ import com.commafeed.backend.model.Feed;
public interface FaviconFetcher { public interface FaviconFetcher {
Favicon fetch(Feed feed); Favicon fetch(Feed feed);
} }

View File

@@ -1,18 +1,14 @@
package com.commafeed.backend.favicon; package com.commafeed.backend.favicon;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /** Fetch favicon from the url declared in the feed. */
* Fetch favicon from the url declared in the feed.
*/
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@Singleton @Singleton

View File

@@ -1,23 +1,18 @@
package com.commafeed.backend.favicon; package com.commafeed.backend.favicon;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
import jakarta.annotation.Priority; import jakarta.annotation.Priority;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;
import com.commafeed.backend.HttpGetter; /** Extracts favicon url from html page. */
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Extracts favicon url from html page.
*/
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@Singleton @Singleton

View File

@@ -1,20 +1,15 @@
package com.commafeed.backend.favicon; package com.commafeed.backend.favicon;
import java.net.URI;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import java.net.URI;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /** Fetches favicon from root of the domain (e.g. https://example.com/favicon.ico) */
* Fetches favicon from root of the domain (e.g. https://example.com/favicon.ico)
*/
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@Singleton @Singleton

View File

@@ -1,18 +1,5 @@
package com.commafeed.backend.favicon; package com.commafeed.backend.favicon;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import jakarta.ws.rs.core.UriBuilder;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.net.URIBuilder;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.HttpResult;
@@ -23,9 +10,18 @@ import com.commafeed.backend.model.Feed;
import com.fasterxml.jackson.core.JsonPointer; import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import jakarta.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.net.URIBuilder;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -35,8 +31,10 @@ public class YoutubeFaviconFetcher implements FaviconFetcher {
private static final String PART_SNIPPET = "snippet"; private static final String PART_SNIPPET = "snippet";
private static final JsonPointer CHANNEL_THUMBNAIL_URL = JsonPointer.compile("/items/0/snippet/thumbnails/default/url"); private static final JsonPointer CHANNEL_THUMBNAIL_URL =
private static final JsonPointer PLAYLIST_CHANNEL_ID = JsonPointer.compile("/items/0/snippet/channelId"); JsonPointer.compile("/items/0/snippet/thumbnails/default/url");
private static final JsonPointer PLAYLIST_CHANNEL_ID =
JsonPointer.compile("/items/0/snippet/channelId");
private final HttpGetter getter; private final HttpGetter getter;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
@@ -57,9 +55,18 @@ public class YoutubeFaviconFetcher implements FaviconFetcher {
try { try {
List<NameValuePair> params = new URIBuilder(url).getQueryParams(); List<NameValuePair> params = new URIBuilder(url).getQueryParams();
Optional<NameValuePair> userId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("user")).findFirst(); Optional<NameValuePair> userId =
Optional<NameValuePair> channelId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("channel_id")).findFirst(); params.stream()
Optional<NameValuePair> playlistId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("playlist_id")).findFirst(); .filter(nvp -> nvp.getName().equalsIgnoreCase("user"))
.findFirst();
Optional<NameValuePair> channelId =
params.stream()
.filter(nvp -> nvp.getName().equalsIgnoreCase("channel_id"))
.findFirst();
Optional<NameValuePair> playlistId =
params.stream()
.filter(nvp -> nvp.getName().equalsIgnoreCase("playlist_id"))
.findFirst();
byte[] response = null; byte[] response = null;
if (userId.isPresent()) { if (userId.isPresent()) {
@@ -92,8 +99,12 @@ public class YoutubeFaviconFetcher implements FaviconFetcher {
} }
private byte[] fetchForUser(String googleAuthKey, String userId) private byte[] fetchForUser(String googleAuthKey, String userId)
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException { throws IOException,
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels") NotModifiedException,
TooManyRequestsException,
SchemeNotAllowedException {
URI uri =
UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
.queryParam("part", PART_SNIPPET) .queryParam("part", PART_SNIPPET)
.queryParam("key", googleAuthKey) .queryParam("key", googleAuthKey)
.queryParam("forUsername", userId) .queryParam("forUsername", userId)
@@ -102,8 +113,12 @@ public class YoutubeFaviconFetcher implements FaviconFetcher {
} }
private byte[] fetchForChannel(String googleAuthKey, String channelId) private byte[] fetchForChannel(String googleAuthKey, String channelId)
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException { throws IOException,
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels") NotModifiedException,
TooManyRequestsException,
SchemeNotAllowedException {
URI uri =
UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
.queryParam("part", PART_SNIPPET) .queryParam("part", PART_SNIPPET)
.queryParam("key", googleAuthKey) .queryParam("key", googleAuthKey)
.queryParam("id", channelId) .queryParam("id", channelId)
@@ -112,8 +127,12 @@ public class YoutubeFaviconFetcher implements FaviconFetcher {
} }
private byte[] fetchForPlaylist(String googleAuthKey, String playlistId) private byte[] fetchForPlaylist(String googleAuthKey, String playlistId)
throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException { throws IOException,
URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/playlists") NotModifiedException,
TooManyRequestsException,
SchemeNotAllowedException {
URI uri =
UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/playlists")
.queryParam("part", PART_SNIPPET) .queryParam("part", PART_SNIPPET)
.queryParam("key", googleAuthKey) .queryParam("key", googleAuthKey)
.queryParam("id", playlistId) .queryParam("id", playlistId)
@@ -127,5 +146,4 @@ public class YoutubeFaviconFetcher implements FaviconFetcher {
return fetchForChannel(googleAuthKey, channelId.asText()); return fetchForChannel(googleAuthKey, channelId.asText());
} }
} }

View File

@@ -2,16 +2,14 @@ package com.commafeed.backend.feed;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
/** /** A keyword used in a search query */
* A keyword used in a search query
*/
public record FeedEntryKeyword(String keyword, Mode mode) { public record FeedEntryKeyword(String keyword, Mode mode) {
public enum Mode { public enum Mode {
INCLUDE, EXCLUDE INCLUDE,
EXCLUDE
} }
public static List<FeedEntryKeyword> fromQueryString(String keywords) { public static List<FeedEntryKeyword> fromQueryString(String keywords) {

View File

@@ -1,16 +1,5 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import com.commafeed.backend.Digests; import com.commafeed.backend.Digests;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpRequest; import com.commafeed.backend.HttpGetter.HttpRequest;
@@ -22,13 +11,18 @@ import com.commafeed.backend.feed.parser.FeedParser;
import com.commafeed.backend.feed.parser.FeedParser.FeedParsingException; import com.commafeed.backend.feed.parser.FeedParser.FeedParsingException;
import com.commafeed.backend.feed.parser.FeedParserResult; import com.commafeed.backend.feed.parser.FeedParserResult;
import com.commafeed.backend.urlprovider.FeedURLProvider; import com.commafeed.backend.urlprovider.FeedURLProvider;
import io.quarkus.arc.All; import io.quarkus.arc.All;
import jakarta.inject.Singleton;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
/** /** Fetches a feed then parses it */
* Fetches a feed then parses it
*/
@Slf4j @Slf4j
@Singleton @Singleton
public class FeedFetcher { public class FeedFetcher {
@@ -37,18 +31,31 @@ public class FeedFetcher {
private final HttpGetter getter; private final HttpGetter getter;
private final List<FeedURLProvider> urlProviders; private final List<FeedURLProvider> urlProviders;
public FeedFetcher(FeedParser parser, HttpGetter getter, @All List<FeedURLProvider> urlProviders) { public FeedFetcher(
FeedParser parser, HttpGetter getter, @All List<FeedURLProvider> urlProviders) {
this.parser = parser; this.parser = parser;
this.getter = getter; this.getter = getter;
this.urlProviders = urlProviders; this.urlProviders = urlProviders;
} }
public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag, public FeedFetcherResult fetch(
Instant lastPublishedDate, String lastContentHash) throws FeedParsingException, IOException, NotModifiedException, String feedUrl,
TooManyRequestsException, SchemeNotAllowedException, NoFeedFoundException { boolean extractFeedUrlFromHtml,
String lastModified,
String eTag,
Instant lastPublishedDate,
String lastContentHash)
throws FeedParsingException,
IOException,
NotModifiedException,
TooManyRequestsException,
SchemeNotAllowedException,
NoFeedFoundException {
log.debug("Fetching feed {}", feedUrl); log.debug("Fetching feed {}", feedUrl);
HttpResult result = getter.get(HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build()); HttpResult result =
getter.get(
HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build());
byte[] content = result.content(); byte[] content = result.content();
FeedParserResult parserResult; FeedParserResult parserResult;
@@ -56,11 +63,20 @@ public class FeedFetcher {
parserResult = parser.parse(result.urlAfterRedirect(), content); parserResult = parser.parse(result.urlAfterRedirect(), content);
} catch (FeedParsingException e) { } catch (FeedParsingException e) {
if (extractFeedUrlFromHtml) { if (extractFeedUrlFromHtml) {
String extractedUrl = extractFeedUrl(urlProviders, feedUrl, new String(result.content(), StandardCharsets.UTF_8)); String extractedUrl =
extractFeedUrl(
urlProviders,
feedUrl,
new String(result.content(), StandardCharsets.UTF_8));
if (StringUtils.isNotBlank(extractedUrl)) { if (StringUtils.isNotBlank(extractedUrl)) {
feedUrl = extractedUrl; feedUrl = extractedUrl;
result = getter.get(HttpRequest.builder(extractedUrl).lastModified(lastModified).eTag(eTag).build()); result =
getter.get(
HttpRequest.builder(extractedUrl)
.lastModified(lastModified)
.eTag(eTag)
.build());
content = result.content(); content = result.content();
parserResult = parser.parse(result.urlAfterRedirect(), content); parserResult = parser.parse(result.urlAfterRedirect(), content);
} else { } else {
@@ -75,27 +91,39 @@ public class FeedFetcher {
throw new IOException("Feed content is empty."); throw new IOException("Feed content is empty.");
} }
boolean lastModifiedHeaderValueChanged = !Strings.CS.equals(lastModified, result.lastModifiedSince()); boolean lastModifiedHeaderValueChanged =
!Strings.CS.equals(lastModified, result.lastModifiedSince());
boolean etagHeaderValueChanged = !Strings.CS.equals(eTag, result.eTag()); boolean etagHeaderValueChanged = !Strings.CS.equals(eTag, result.eTag());
String hash = Digests.sha1Hex(content); String hash = Digests.sha1Hex(content);
if (lastContentHash != null && lastContentHash.equals(hash)) { if (lastContentHash != null && lastContentHash.equals(hash)) {
log.debug("content hash not modified: {}", feedUrl); log.debug("content hash not modified: {}", feedUrl);
throw new NotModifiedException("content hash not modified", lastModifiedHeaderValueChanged ? result.lastModifiedSince() : null, throw new NotModifiedException(
"content hash not modified",
lastModifiedHeaderValueChanged ? result.lastModifiedSince() : null,
etagHeaderValueChanged ? result.eTag() : null); etagHeaderValueChanged ? result.eTag() : null);
} }
if (lastPublishedDate != null && lastPublishedDate.equals(parserResult.lastPublishedDate())) { if (lastPublishedDate != null
&& lastPublishedDate.equals(parserResult.lastPublishedDate())) {
log.debug("publishedDate not modified: {}", feedUrl); log.debug("publishedDate not modified: {}", feedUrl);
throw new NotModifiedException("publishedDate not modified", lastModifiedHeaderValueChanged ? result.lastModifiedSince() : null, throw new NotModifiedException(
"publishedDate not modified",
lastModifiedHeaderValueChanged ? result.lastModifiedSince() : null,
etagHeaderValueChanged ? result.eTag() : null); etagHeaderValueChanged ? result.eTag() : null);
} }
return new FeedFetcherResult(parserResult, result.urlAfterRedirect(), result.lastModifiedSince(), result.eTag(), hash, return new FeedFetcherResult(
parserResult,
result.urlAfterRedirect(),
result.lastModifiedSince(),
result.eTag(),
hash,
result.validFor()); result.validFor());
} }
private static String extractFeedUrl(List<FeedURLProvider> urlProviders, String url, String urlContent) { private static String extractFeedUrl(
List<FeedURLProvider> urlProviders, String url, String urlContent) {
return urlProviders.stream() return urlProviders.stream()
.flatMap(provider -> provider.get(url, urlContent).stream()) .flatMap(provider -> provider.get(url, urlContent).stream())
.filter(StringUtils::isNotBlank) .filter(StringUtils::isNotBlank)
@@ -103,8 +131,13 @@ public class FeedFetcher {
.orElse(null); .orElse(null);
} }
public record FeedFetcherResult(FeedParserResult feed, String urlAfterRedirect, String lastModifiedHeader, String lastETagHeader, public record FeedFetcherResult(
String contentHash, Duration validFor) {} FeedParserResult feed,
String urlAfterRedirect,
String lastModifiedHeader,
String lastETagHeader,
String contentHash,
Duration validFor) {}
public static class NoFeedFoundException extends Exception { public static class NoFeedFoundException extends Exception {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -113,5 +146,4 @@ public class FeedFetcher {
super("This URL does not point to an RSS feed or a website with an RSS feed.", cause); super("This URL does not point to an RSS feed or a website with an RSS feed.", cause);
} }
} }
} }

View File

@@ -1,19 +1,5 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Singleton;
import com.codahale.metrics.Gauge; import com.codahale.metrics.Gauge;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
@@ -25,7 +11,18 @@ import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import jakarta.inject.Singleton;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@@ -49,8 +46,14 @@ public class FeedRefreshEngine {
private ThreadPoolExecutor databaseUpdaterExecutor; private ThreadPoolExecutor databaseUpdaterExecutor;
private ThreadPoolExecutor notifierExecutor; private ThreadPoolExecutor notifierExecutor;
public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater, public FeedRefreshEngine(
FeedUpdateNotifier notifier, CommaFeedConfiguration config, MetricRegistry metrics) { UnitOfWork unitOfWork,
FeedDAO feedDAO,
FeedRefreshWorker worker,
FeedRefreshUpdater updater,
FeedUpdateNotifier notifier,
CommaFeedConfiguration config,
MetricRegistry metrics) {
this.unitOfWork = unitOfWork; this.unitOfWork = unitOfWork;
this.feedDAO = feedDAO; this.feedDAO = feedDAO;
this.worker = worker; this.worker = worker;
@@ -61,12 +64,20 @@ public class FeedRefreshEngine {
this.queue = new LinkedBlockingDeque<>(); this.queue = new LinkedBlockingDeque<>();
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size); metrics.register(
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge<Integer>) () -> workerExecutor.getActiveCount()); MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
metrics.register(MetricRegistry.name(getClass(), "updater", "active"), metrics.register(
MetricRegistry.name(getClass(), "worker", "active"),
(Gauge<Integer>) () -> workerExecutor.getActiveCount());
metrics.register(
MetricRegistry.name(getClass(), "updater", "active"),
(Gauge<Integer>) () -> databaseUpdaterExecutor.getActiveCount()); (Gauge<Integer>) () -> databaseUpdaterExecutor.getActiveCount());
metrics.register(MetricRegistry.name(getClass(), "notifier", "active"), (Gauge<Integer>) () -> notifierExecutor.getActiveCount()); metrics.register(
metrics.register(MetricRegistry.name(getClass(), "notifier", "queue"), (Gauge<Integer>) () -> notifierExecutor.getQueue().size()); MetricRegistry.name(getClass(), "notifier", "active"),
(Gauge<Integer>) () -> notifierExecutor.getActiveCount());
metrics.register(
MetricRegistry.name(getClass(), "notifier", "queue"),
(Gauge<Integer>) () -> notifierExecutor.getQueue().size());
} }
private void createExecutors() { private void createExecutors() {
@@ -74,8 +85,11 @@ public class FeedRefreshEngine {
this.refillLoopExecutor = Executors.newSingleThreadExecutor(); this.refillLoopExecutor = Executors.newSingleThreadExecutor();
this.refillExecutor = newDiscardingSingleThreadExecutorService(); this.refillExecutor = newDiscardingSingleThreadExecutorService();
this.workerExecutor = newBlockingExecutorService(config.feedRefresh().httpThreads()); this.workerExecutor = newBlockingExecutorService(config.feedRefresh().httpThreads());
this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads()); this.databaseUpdaterExecutor =
this.notifierExecutor = newDiscardingExecutorService(config.pushNotifications().threads(), newBlockingExecutorService(config.feedRefresh().databaseThreads());
this.notifierExecutor =
newDiscardingExecutorService(
config.pushNotifications().threads(),
config.pushNotifications().queueCapacity()); config.pushNotifications().queueCapacity());
} }
@@ -87,17 +101,21 @@ public class FeedRefreshEngine {
private void startFeedProcessingLoop() { private void startFeedProcessingLoop() {
// take a feed from the queue, process it, rince, repeat // take a feed from the queue, process it, rince, repeat
feedProcessingLoopExecutor.submit(() -> { feedProcessingLoopExecutor.submit(
() -> {
while (!feedProcessingLoopExecutor.isShutdown()) { while (!feedProcessingLoopExecutor.isShutdown()) {
try { try {
// take() is blocking until a feed is available from the queue // take() is blocking until a feed is available from the queue
Feed feed = queue.take(); Feed feed = queue.take();
// send the feed to be processed // send the feed to be processed
log.debug("got feed {} from the queue, send it for processing", feed.getId()); log.debug(
"got feed {} from the queue, send it for processing",
feed.getId());
processFeedAsync(feed); processFeedAsync(feed);
// we removed a feed from the queue, try to refill it as it may now be empty // we removed a feed from the queue, try to refill it as it may now be
// empty
if (queue.isEmpty()) { if (queue.isEmpty()) {
log.debug("took the last feed from the queue, try to refill"); log.debug("took the last feed from the queue, try to refill");
refillQueueAsync(); refillQueueAsync();
@@ -114,7 +132,8 @@ public class FeedRefreshEngine {
private void startRefillLoop() { private void startRefillLoop() {
// refill the queue at regular intervals if it's empty // refill the queue at regular intervals if it's empty
refillLoopExecutor.submit(() -> { refillLoopExecutor.submit(
() -> {
while (!refillLoopExecutor.isShutdown()) { while (!refillLoopExecutor.isShutdown()) {
try { try {
if (queue.isEmpty()) { if (queue.isEmpty()) {
@@ -142,7 +161,8 @@ public class FeedRefreshEngine {
} }
private void refillQueueAsync() { private void refillQueueAsync() {
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(
() -> {
if (!queue.isEmpty()) { if (!queue.isEmpty()) {
return; return;
} }
@@ -150,14 +170,19 @@ public class FeedRefreshEngine {
refill.mark(); refill.mark();
List<Feed> nextUpdatableFeeds = getNextUpdatableFeeds(getBatchSize()); List<Feed> nextUpdatableFeeds = getNextUpdatableFeeds(getBatchSize());
log.debug("found {} feeds that are up for refresh", nextUpdatableFeeds.size()); log.debug(
"found {} feeds that are up for refresh",
nextUpdatableFeeds.size());
for (Feed feed : nextUpdatableFeeds) { for (Feed feed : nextUpdatableFeeds) {
// add the feed only if it was not already queued // add the feed only if it was not already queued
if (queue.stream().noneMatch(f -> f.getId().equals(feed.getId()))) { if (queue.stream().noneMatch(f -> f.getId().equals(feed.getId()))) {
queue.addLast(feed); queue.addLast(feed);
} }
} }
}, refillExecutor).whenComplete((data, ex) -> { },
refillExecutor)
.whenComplete(
(data, ex) -> {
if (ex != null) { if (ex != null) {
log.error("error while refilling the queue", ex); log.error("error while refilling the queue", ex);
} }
@@ -167,31 +192,51 @@ public class FeedRefreshEngine {
private void processFeedAsync(Feed feed) { private void processFeedAsync(Feed feed) {
CompletableFuture.supplyAsync(() -> worker.update(feed), workerExecutor) CompletableFuture.supplyAsync(() -> worker.update(feed), workerExecutor)
.thenApplyAsync(r -> updater.update(r.feed(), r.entries()), databaseUpdaterExecutor) .thenApplyAsync(r -> updater.update(r.feed(), r.entries()), databaseUpdaterExecutor)
.thenCompose(r -> { .thenCompose(
List<CompletableFuture<Void>> futures = r.insertedUnreadEntriesBySubscription().entrySet().stream().map(e -> { r -> {
List<CompletableFuture<Void>> futures =
r.insertedUnreadEntriesBySubscription().entrySet().stream()
.map(
e -> {
FeedSubscription sub = e.getKey(); FeedSubscription sub = e.getKey();
List<FeedEntry> entries = e.getValue(); List<FeedEntry> entries = e.getValue();
notifier.notifyOverWebsocket(sub, entries); notifier.notifyOverWebsocket(sub, entries);
return CompletableFuture.runAsync(() -> notifier.sendPushNotifications(sub, entries), notifierExecutor); return CompletableFuture.runAsync(
}).toList(); () ->
return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)); notifier
.sendPushNotifications(
sub,
entries),
notifierExecutor);
}) })
.exceptionally(ex -> { .toList();
return CompletableFuture.allOf(
futures.toArray(CompletableFuture[]::new));
})
.exceptionally(
ex -> {
log.error("error while processing feed {}", feed.getUrl(), ex); log.error("error while processing feed {}", feed.getUrl(), ex);
return null; return null;
}); });
} }
private List<Feed> getNextUpdatableFeeds(int max) { private List<Feed> getNextUpdatableFeeds(int max) {
return unitOfWork.call(() -> { return unitOfWork.call(
Instant lastLoginThreshold = config.feedRefresh().userInactivityPeriod().isZero() ? null () -> {
: Instant.now().minus(config.feedRefresh().userInactivityPeriod()); Instant lastLoginThreshold =
config.feedRefresh().userInactivityPeriod().isZero()
? null
: Instant.now()
.minus(config.feedRefresh().userInactivityPeriod());
List<Feed> feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold); List<Feed> feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold);
if (!feeds.isEmpty()) { if (!feeds.isEmpty()) {
// update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable() // update disabledUntil to prevent feeds from being returned again by
Instant nextUpdateDate = Instant.now().plus(config.feedRefresh().interval()); // feedDAO.findNextUpdatable()
feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).toList(), nextUpdateDate); Instant nextUpdateDate =
Instant.now().plus(config.feedRefresh().interval());
feedDAO.setDisabledUntil(
feeds.stream().map(AbstractModel::getId).toList(), nextUpdateDate);
} }
return feeds; return feeds;
}); });
@@ -202,41 +247,50 @@ public class FeedRefreshEngine {
} }
public void stop() { public void stop() {
MoreExecutors.shutdownAndAwaitTermination(this.feedProcessingLoopExecutor, config.shutdownTimeout()); MoreExecutors.shutdownAndAwaitTermination(
MoreExecutors.shutdownAndAwaitTermination(this.refillLoopExecutor, config.shutdownTimeout()); this.feedProcessingLoopExecutor, config.shutdownTimeout());
MoreExecutors.shutdownAndAwaitTermination(
this.refillLoopExecutor, config.shutdownTimeout());
MoreExecutors.shutdownAndAwaitTermination(this.refillExecutor, config.shutdownTimeout()); MoreExecutors.shutdownAndAwaitTermination(this.refillExecutor, config.shutdownTimeout());
MoreExecutors.shutdownAndAwaitTermination(this.workerExecutor, config.shutdownTimeout()); MoreExecutors.shutdownAndAwaitTermination(this.workerExecutor, config.shutdownTimeout());
MoreExecutors.shutdownAndAwaitTermination(this.databaseUpdaterExecutor, config.shutdownTimeout()); MoreExecutors.shutdownAndAwaitTermination(
this.databaseUpdaterExecutor, config.shutdownTimeout());
MoreExecutors.shutdownAndAwaitTermination(this.notifierExecutor, config.shutdownTimeout()); MoreExecutors.shutdownAndAwaitTermination(this.notifierExecutor, config.shutdownTimeout());
queue.clear(); queue.clear();
} }
/** /**
* returns an ExecutorService with a single thread that discards tasks if a task is already running * returns an ExecutorService with a single thread that discards tasks if a task is already
* running
*/ */
private ThreadPoolExecutor newDiscardingSingleThreadExecutorService() { private ThreadPoolExecutor newDiscardingSingleThreadExecutorService() {
ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>()); ThreadPoolExecutor pool =
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
return pool; return pool;
} }
/** /** returns an ExecutorService that discards tasks if the queue is full */
* returns an ExecutorService that discards tasks if the queue is full
*/
private ThreadPoolExecutor newDiscardingExecutorService(int threads, int queueCapacity) { private ThreadPoolExecutor newDiscardingExecutorService(int threads, int queueCapacity) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, ThreadPoolExecutor pool =
new ThreadPoolExecutor(
threads,
threads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(queueCapacity)); new LinkedBlockingQueue<>(queueCapacity));
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
return pool; return pool;
} }
/** /** returns an ExecutorService that blocks submissions until a thread is available */
* returns an ExecutorService that blocks submissions until a thread is available
*/
private ThreadPoolExecutor newBlockingExecutorService(int threads) { private ThreadPoolExecutor newBlockingExecutorService(int threads) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>()); ThreadPoolExecutor pool =
pool.setRejectedExecutionHandler((r, e) -> { new ThreadPoolExecutor(
threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
pool.setRejectedExecutionHandler(
(r, e) -> {
if (e.isShutdown()) { if (e.isShutdown()) {
return; return;
} }

View File

@@ -1,17 +1,14 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.time.Duration;
import java.time.Instant;
import java.time.InstantSource;
import java.time.temporal.ChronoUnit;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.ObjectUtils;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedConfiguration.FeedRefreshErrorHandling; import com.commafeed.CommaFeedConfiguration.FeedRefreshErrorHandling;
import com.google.common.primitives.Longs; import com.google.common.primitives.Longs;
import jakarta.inject.Singleton;
import java.time.Duration;
import java.time.Instant;
import java.time.InstantSource;
import java.time.temporal.ChronoUnit;
import org.apache.commons.lang3.ObjectUtils;
@Singleton @Singleton
public class FeedRefreshIntervalCalculator { public class FeedRefreshIntervalCalculator {
@@ -22,7 +19,8 @@ public class FeedRefreshIntervalCalculator {
private final FeedRefreshErrorHandling errorHandling; private final FeedRefreshErrorHandling errorHandling;
private final InstantSource instantSource; private final InstantSource instantSource;
public FeedRefreshIntervalCalculator(CommaFeedConfiguration config, InstantSource instantSource) { public FeedRefreshIntervalCalculator(
CommaFeedConfiguration config, InstantSource instantSource) {
this.interval = config.feedRefresh().interval(); this.interval = config.feedRefresh().interval();
this.maxInterval = config.feedRefresh().maxInterval(); this.maxInterval = config.feedRefresh().maxInterval();
this.empirical = config.feedRefresh().intervalEmpirical(); this.empirical = config.feedRefresh().intervalEmpirical();
@@ -30,8 +28,11 @@ public class FeedRefreshIntervalCalculator {
this.instantSource = instantSource; this.instantSource = instantSource;
} }
public Instant onFetchSuccess(Instant publishedDate, Long averageEntryInterval, Duration validFor) { public Instant onFetchSuccess(
Instant instant = empirical ? computeEmpiricalRefreshInterval(publishedDate, averageEntryInterval) Instant publishedDate, Long averageEntryInterval, Duration validFor) {
Instant instant =
empirical
? computeEmpiricalRefreshInterval(publishedDate, averageEntryInterval)
: instantSource.instant().plus(interval); : instantSource.instant().plus(interval);
return constrainToBounds(ObjectUtils.max(instant, instantSource.instant().plus(validFor))); return constrainToBounds(ObjectUtils.max(instant, instantSource.instant().plus(validFor)));
} }
@@ -49,11 +50,15 @@ public class FeedRefreshIntervalCalculator {
return constrainToBounds(instantSource.instant().plus(interval)); return constrainToBounds(instantSource.instant().plus(interval));
} }
Duration retryInterval = errorHandling.backoffInterval().multipliedBy(errorCount - errorHandling.retriesBeforeBackoff() + 1L); Duration retryInterval =
errorHandling
.backoffInterval()
.multipliedBy(errorCount - errorHandling.retriesBeforeBackoff() + 1L);
return constrainToBounds(instantSource.instant().plus(retryInterval)); return constrainToBounds(instantSource.instant().plus(retryInterval));
} }
private Instant computeEmpiricalRefreshInterval(Instant publishedDate, Long averageEntryInterval) { private Instant computeEmpiricalRefreshInterval(
Instant publishedDate, Long averageEntryInterval) {
Instant now = instantSource.instant(); Instant now = instantSource.instant();
if (publishedDate == null) { if (publishedDate == null) {
@@ -70,7 +75,11 @@ public class FeedRefreshIntervalCalculator {
} else if (averageEntryInterval != null) { } else if (averageEntryInterval != null) {
// use average time between entries to decide when to refresh next, divided by factor // use average time between entries to decide when to refresh next, divided by factor
int factor = 2; int factor = 2;
long millis = Longs.constrainToRange(averageEntryInterval / factor, interval.toMillis(), maxInterval.dividedBy(4).toMillis()); long millis =
Longs.constrainToRange(
averageEntryInterval / factor,
interval.toMillis(),
maxInterval.dividedBy(4).toMillis());
return now.plusMillis(millis); return now.plusMillis(millis);
} else { } else {
// unknown case // unknown case
@@ -79,6 +88,8 @@ public class FeedRefreshIntervalCalculator {
} }
private Instant constrainToBounds(Instant instant) { private Instant constrainToBounds(Instant instant) {
return ObjectUtils.max(ObjectUtils.min(instant, instantSource.instant().plus(maxInterval)), instantSource.instant().plus(interval)); return ObjectUtils.max(
ObjectUtils.min(instant, instantSource.instant().plus(maxInterval)),
instantSource.instant().plus(interval));
} }
} }

View File

@@ -1,20 +1,5 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.Digests; import com.commafeed.backend.Digests;
@@ -29,12 +14,21 @@ import com.commafeed.backend.model.Models;
import com.commafeed.backend.service.FeedEntryService; import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedService; import com.commafeed.backend.service.FeedService;
import com.google.common.util.concurrent.Striped; import com.google.common.util.concurrent.Striped;
import jakarta.inject.Singleton;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
/** /** Updates the feed in the database and inserts new entries */
* Updates the feed in the database and inserts new entries
*/
@Slf4j @Slf4j
@Singleton @Singleton
public class FeedRefreshUpdater { public class FeedRefreshUpdater {
@@ -49,7 +43,11 @@ public class FeedRefreshUpdater {
private final Meter feedUpdated; private final Meter feedUpdated;
private final Meter entryInserted; private final Meter entryInserted;
public FeedRefreshUpdater(UnitOfWork unitOfWork, FeedService feedService, FeedEntryService feedEntryService, MetricRegistry metrics, public FeedRefreshUpdater(
UnitOfWork unitOfWork,
FeedService feedService,
FeedEntryService feedEntryService,
MetricRegistry metrics,
FeedSubscriptionDAO feedSubscriptionDAO) { FeedSubscriptionDAO feedSubscriptionDAO) {
this.unitOfWork = unitOfWork; this.unitOfWork = unitOfWork;
this.feedService = feedService; this.feedService = feedService;
@@ -62,7 +60,8 @@ public class FeedRefreshUpdater {
entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted")); entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
} }
private AddEntryResult addEntry(final Feed feed, final Entry entry, final List<FeedSubscription> subscriptions) { private AddEntryResult addEntry(
final Feed feed, final Entry entry, final List<FeedSubscription> subscriptions) {
boolean processed = false; boolean processed = false;
FeedEntry insertedEntry = null; FeedEntry insertedEntry = null;
Set<FeedSubscription> subscriptionsForWhichEntryIsUnread = new HashSet<>(); Set<FeedSubscription> subscriptionsForWhichEntryIsUnread = new HashSet<>();
@@ -87,7 +86,9 @@ public class FeedRefreshUpdater {
locked2 = lock2.tryLock(1, TimeUnit.MINUTES); locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
if (locked1 && locked2) { if (locked1 && locked2) {
processed = true; processed = true;
insertedEntry = unitOfWork.call(() -> { insertedEntry =
unitOfWork.call(
() -> {
if (feedEntryService.find(feed, entry) != null) { if (feedEntryService.find(feed, entry) != null) {
// entry already exists, nothing to do // entry already exists, nothing to do
return null; return null;
@@ -96,7 +97,8 @@ public class FeedRefreshUpdater {
FeedEntry feedEntry = feedEntryService.create(feed, entry); FeedEntry feedEntry = feedEntryService.create(feed, entry);
entryInserted.mark(); entryInserted.mark();
for (FeedSubscription sub : subscriptions) { for (FeedSubscription sub : subscriptions) {
boolean unread = feedEntryService.applyFilter(sub, feedEntry); boolean unread =
feedEntryService.applyFilter(sub, feedEntry);
if (unread) { if (unread) {
subscriptionsForWhichEntryIsUnread.add(sub); subscriptionsForWhichEntryIsUnread.add(sub);
} }
@@ -107,7 +109,11 @@ public class FeedRefreshUpdater {
log.error("lock timeout for {} - {}", feed.getUrl(), key1); log.error("lock timeout for {} - {}", feed.getUrl(), key1);
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.error("interrupted while waiting for lock for {} : {}", feed.getUrl(), e.getMessage(), e); log.error(
"interrupted while waiting for lock for {} : {}",
feed.getUrl(),
e.getMessage(),
e);
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} finally { } finally {
if (locked1) { if (locked1) {
@@ -123,11 +129,13 @@ public class FeedRefreshUpdater {
public FeedRefreshUpdaterResult update(Feed feed, List<Entry> entries) { public FeedRefreshUpdaterResult update(Feed feed, List<Entry> entries) {
boolean processed = true; boolean processed = true;
long inserted = 0; long inserted = 0;
Map<FeedSubscription, List<FeedEntry>> insertedUnreadEntriesBySubscription = new HashMap<>(); Map<FeedSubscription, List<FeedEntry>> insertedUnreadEntriesBySubscription =
new HashMap<>();
if (!entries.isEmpty()) { if (!entries.isEmpty()) {
List<FeedSubscription> subscriptions = null; List<FeedSubscription> subscriptions = null;
List<Entry> newEntries = unitOfWork.call(() -> feedEntryService.removeExistingEntries(feed, entries)); List<Entry> newEntries =
unitOfWork.call(() -> feedEntryService.removeExistingEntries(feed, entries));
for (Entry entry : newEntries) { for (Entry entry : newEntries) {
if (subscriptions == null) { if (subscriptions == null) {
subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed)); subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed));
@@ -135,9 +143,12 @@ public class FeedRefreshUpdater {
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions); AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
processed &= addEntryResult.processed; processed &= addEntryResult.processed;
inserted += addEntryResult.insertedEntry != null ? 1 : 0; inserted += addEntryResult.insertedEntry != null ? 1 : 0;
addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> { addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(
sub -> {
if (addEntryResult.insertedEntry != null) { if (addEntryResult.insertedEntry != null) {
insertedUnreadEntriesBySubscription.computeIfAbsent(sub, k -> new ArrayList<>()).add(addEntryResult.insertedEntry); insertedUnreadEntriesBySubscription
.computeIfAbsent(sub, k -> new ArrayList<>())
.add(addEntryResult.insertedEntry);
} }
}); });
} }
@@ -163,8 +174,11 @@ public class FeedRefreshUpdater {
return new FeedRefreshUpdaterResult(insertedUnreadEntriesBySubscription); return new FeedRefreshUpdaterResult(insertedUnreadEntriesBySubscription);
} }
private record AddEntryResult(boolean processed, FeedEntry insertedEntry, Set<FeedSubscription> subscriptionsForWhichEntryIsUnread) {} private record AddEntryResult(
boolean processed,
public record FeedRefreshUpdaterResult(Map<FeedSubscription, List<FeedEntry>> insertedUnreadEntriesBySubscription) {} FeedEntry insertedEntry,
Set<FeedSubscription> subscriptionsForWhichEntryIsUnread) {}
public record FeedRefreshUpdaterResult(
Map<FeedSubscription, List<FeedEntry>> insertedUnreadEntriesBySubscription) {}
} }

View File

@@ -1,15 +1,5 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.Strings;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
@@ -18,11 +8,18 @@ import com.commafeed.backend.HttpGetter.TooManyRequestsException;
import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult; import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult;
import com.commafeed.backend.feed.parser.FeedParserResult.Entry; import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import jakarta.inject.Singleton;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.Strings;
/** /**
* Calls {@link FeedFetcher} and updates the Feed object, but does not update the database, ({@link FeedRefreshUpdater} does that) * Calls {@link FeedFetcher} and updates the Feed object, but does not update the database, ({@link
* FeedRefreshUpdater} does that)
*/ */
@Slf4j @Slf4j
@Singleton @Singleton
@@ -33,20 +30,28 @@ public class FeedRefreshWorker {
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
private final Meter feedFetched; private final Meter feedFetched;
public FeedRefreshWorker(FeedRefreshIntervalCalculator refreshIntervalCalculator, FeedFetcher fetcher, CommaFeedConfiguration config, public FeedRefreshWorker(
FeedRefreshIntervalCalculator refreshIntervalCalculator,
FeedFetcher fetcher,
CommaFeedConfiguration config,
MetricRegistry metrics) { MetricRegistry metrics) {
this.refreshIntervalCalculator = refreshIntervalCalculator; this.refreshIntervalCalculator = refreshIntervalCalculator;
this.fetcher = fetcher; this.fetcher = fetcher;
this.config = config; this.config = config;
this.feedFetched = metrics.meter(MetricRegistry.name(getClass(), "feedFetched")); this.feedFetched = metrics.meter(MetricRegistry.name(getClass(), "feedFetched"));
} }
public FeedRefreshWorkerResult update(Feed feed) { public FeedRefreshWorkerResult update(Feed feed) {
try { try {
String url = Optional.ofNullable(feed.getUrlAfterRedirect()).orElse(feed.getUrl()); String url = Optional.ofNullable(feed.getUrlAfterRedirect()).orElse(feed.getUrl());
FeedFetcherResult result = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(), FeedFetcherResult result =
feed.getLastPublishedDate(), feed.getLastContentHash()); fetcher.fetch(
url,
false,
feed.getLastModifiedHeader(),
feed.getEtagHeader(),
feed.getLastPublishedDate(),
feed.getLastContentHash());
// stops here if NotModifiedException or any other exception is thrown // stops here if NotModifiedException or any other exception is thrown
List<Entry> entries = result.feed().entries(); List<Entry> entries = result.feed().entries();
@@ -59,7 +64,10 @@ public class FeedRefreshWorker {
Duration entriesMaxAge = config.database().cleanup().entriesMaxAge(); Duration entriesMaxAge = config.database().cleanup().entriesMaxAge();
if (!entriesMaxAge.isZero()) { if (!entriesMaxAge.isZero()) {
Instant threshold = Instant.now().minus(entriesMaxAge); Instant threshold = Instant.now().minus(entriesMaxAge);
entries = entries.stream().filter(entry -> entry.published().isAfter(threshold)).toList(); entries =
entries.stream()
.filter(entry -> entry.published().isAfter(threshold))
.toList();
} }
String urlAfterRedirect = result.urlAfterRedirect(); String urlAfterRedirect = result.urlAfterRedirect();
@@ -79,8 +87,11 @@ public class FeedRefreshWorker {
feed.setErrorCount(0); feed.setErrorCount(0);
feed.setMessage(null); feed.setMessage(null);
feed.setDisabledUntil(refreshIntervalCalculator.onFetchSuccess(result.feed().lastPublishedDate(), feed.setDisabledUntil(
result.feed().averageEntryInterval(), result.validFor())); refreshIntervalCalculator.onFetchSuccess(
result.feed().lastPublishedDate(),
result.feed().averageEntryInterval(),
result.validFor()));
return new FeedRefreshWorkerResult(feed, entries); return new FeedRefreshWorkerResult(feed, entries);
} catch (NotModifiedException e) { } catch (NotModifiedException e) {
@@ -88,7 +99,9 @@ public class FeedRefreshWorker {
feed.setErrorCount(0); feed.setErrorCount(0);
feed.setMessage(e.getMessage()); feed.setMessage(e.getMessage());
feed.setDisabledUntil(refreshIntervalCalculator.onFeedNotModified(feed.getLastPublishedDate(), feed.getAverageEntryInterval())); feed.setDisabledUntil(
refreshIntervalCalculator.onFeedNotModified(
feed.getLastPublishedDate(), feed.getAverageEntryInterval()));
if (e.getNewLastModifiedHeader() != null) { if (e.getNewLastModifiedHeader() != null) {
feed.setLastModifiedHeader(e.getNewLastModifiedHeader()); feed.setLastModifiedHeader(e.getNewLastModifiedHeader());
@@ -104,7 +117,9 @@ public class FeedRefreshWorker {
feed.setErrorCount(feed.getErrorCount() + 1); feed.setErrorCount(feed.getErrorCount() + 1);
feed.setMessage("Server indicated that we are sending too many requests"); feed.setMessage("Server indicated that we are sending too many requests");
feed.setDisabledUntil(refreshIntervalCalculator.onTooManyRequests(e.getRetryAfter(), feed.getErrorCount())); feed.setDisabledUntil(
refreshIntervalCalculator.onTooManyRequests(
e.getRetryAfter(), feed.getErrorCount()));
return new FeedRefreshWorkerResult(feed, Collections.emptyList()); return new FeedRefreshWorkerResult(feed, Collections.emptyList());
} catch (Exception e) { } catch (Exception e) {
@@ -121,5 +136,4 @@ public class FeedRefreshWorker {
} }
public record FeedRefreshWorkerResult(Feed feed, List<Entry> entries) {} public record FeedRefreshWorkerResult(Feed feed, List<Entry> entries) {}
} }

View File

@@ -1,9 +1,5 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.util.List;
import jakarta.inject.Singleton;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.dao.UserSettingsDAO;
@@ -13,7 +9,8 @@ import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.service.PushNotificationService; import com.commafeed.backend.service.PushNotificationService;
import com.commafeed.frontend.ws.WebSocketMessageBuilder; import com.commafeed.frontend.ws.WebSocketMessageBuilder;
import com.commafeed.frontend.ws.WebSocketSessions; import com.commafeed.frontend.ws.WebSocketSessions;
import jakarta.inject.Singleton;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -30,21 +27,25 @@ public class FeedUpdateNotifier {
public void notifyOverWebsocket(FeedSubscription sub, List<FeedEntry> entries) { public void notifyOverWebsocket(FeedSubscription sub, List<FeedEntry> entries) {
if (!entries.isEmpty()) { if (!entries.isEmpty()) {
webSocketSessions.sendMessage(sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub, entries.size())); webSocketSessions.sendMessage(
sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub, entries.size()));
} }
} }
public void sendPushNotifications(FeedSubscription sub, List<FeedEntry> entries) { public void sendPushNotifications(FeedSubscription sub, List<FeedEntry> entries) {
if (!config.pushNotifications().enabled() || !sub.isPushNotificationsEnabled() || entries.isEmpty()) { if (!config.pushNotifications().enabled()
|| !sub.isPushNotificationsEnabled()
|| entries.isEmpty()) {
return; return;
} }
UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(sub.getUser())); UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(sub.getUser()));
if (settings != null && settings.getPushNotifications() != null && settings.getPushNotifications().getType() != null) { if (settings != null
&& settings.getPushNotifications() != null
&& settings.getPushNotifications().getType() != null) {
for (FeedEntry entry : entries) { for (FeedEntry entry : entries) {
pushNotificationService.notify(settings.getPushNotifications(), sub, entry); pushNotificationService.notify(settings.getPushNotifications(), sub, entry);
} }
} }
} }
} }

View File

@@ -1,14 +1,5 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.util.Collections;
import java.util.Date;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import com.commafeed.backend.feed.parser.TextDirectionDetector; import com.commafeed.backend.feed.parser.TextDirectionDetector;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.frontend.model.Entry; import com.commafeed.frontend.model.Entry;
@@ -16,14 +7,17 @@ import com.rometools.rome.feed.synd.SyndContentImpl;
import com.rometools.rome.feed.synd.SyndEnclosureImpl; import com.rometools.rome.feed.synd.SyndEnclosureImpl;
import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.SyndEntryImpl; import com.rometools.rome.feed.synd.SyndEntryImpl;
import java.util.Collections;
import java.util.Date;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
/** /** Utility methods related to feed handling */
* Utility methods related to feed handling
*
*/
@UtilityClass @UtilityClass
@Slf4j @Slf4j
public class FeedUtils { public class FeedUtils {
@@ -43,7 +37,8 @@ public class FeedUtils {
return false; return false;
} }
return TextDirectionDetector.detect(stripped) == TextDirectionDetector.Direction.RIGHT_TO_LEFT; return TextDirectionDetector.detect(stripped)
== TextDirectionDetector.Direction.RIGHT_TO_LEFT;
} }
public static String getFaviconUrl(FeedSubscription subscription) { public static String getFaviconUrl(FeedSubscription subscription) {
@@ -98,5 +93,4 @@ public class FeedUtils {
e.setPublishedDate(entry.getDate() == null ? null : Date.from(entry.getDate())); e.setPublishedDate(entry.getDate() == null ? null : Date.from(entry.getDate()));
return e; return e;
} }
} }

View File

@@ -1,19 +1,15 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import com.google.common.primitives.Bytes;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang3.RandomUtils;
import com.google.common.primitives.Bytes;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.RandomUtils;
@UtilityClass @UtilityClass
public class ImageProxyUrl { public class ImageProxyUrl {
@@ -66,5 +62,4 @@ public class ImageProxyUrl {
throw new IllegalStateException("Failed to decode URL", e); throw new IllegalStateException("Failed to decode URL", e);
} }
} }
} }

View File

@@ -1,22 +1,18 @@
package com.commafeed.backend.feed.parser; package com.commafeed.backend.feed.parser;
import java.nio.charset.Charset;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Strings;
import com.ibm.icu.text.CharsetDetector; import com.ibm.icu.text.CharsetDetector;
import com.ibm.icu.text.CharsetMatch; import com.ibm.icu.text.CharsetMatch;
import jakarta.inject.Singleton;
import java.nio.charset.Charset;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Strings;
@Singleton @Singleton
public class EncodingDetector { public class EncodingDetector {
/** /**
* Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the characters used in the * Detect feed encoding by using the declared encoding in the xml processing instruction and by
* feed * detecting the characters used in the feed
*
*/ */
public Charset getEncoding(byte[] bytes) { public Charset getEncoding(byte[] bytes) {
String extracted = extractDeclaredEncoding(bytes); String extracted = extractDeclaredEncoding(bytes);
@@ -30,9 +26,7 @@ public class EncodingDetector {
return detectEncoding(bytes); return detectEncoding(bytes);
} }
/** /** Extract the declared encoding from the xml */
* Extract the declared encoding from the xml
*/
public String extractDeclaredEncoding(byte[] bytes) { public String extractDeclaredEncoding(byte[] bytes) {
int index = ArrayUtils.indexOf(bytes, (byte) '>'); int index = ArrayUtils.indexOf(bytes, (byte) '>');
if (index == -1) { if (index == -1) {
@@ -49,9 +43,7 @@ public class EncodingDetector {
return encoding; return encoding;
} }
/** /** Detect encoding by analyzing characters in the array */
* Detect encoding by analyzing characters in the array
*/
private Charset detectEncoding(byte[] bytes) { private Charset detectEncoding(byte[] bytes) {
String encoding = "UTF-8"; String encoding = "UTF-8";
@@ -66,5 +58,4 @@ public class EncodingDetector {
} }
return Charset.forName(encoding); return Charset.forName(encoding);
} }
} }

View File

@@ -1,25 +1,5 @@
package com.commafeed.backend.feed.parser; package com.commafeed.backend.feed.parser;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemProperties;
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.xml.sax.InputSource;
import com.commafeed.backend.Urls; import com.commafeed.backend.Urls;
import com.commafeed.backend.feed.parser.FeedParserResult.Content; import com.commafeed.backend.feed.parser.FeedParserResult.Content;
import com.commafeed.backend.feed.parser.FeedParserResult.Enclosure; import com.commafeed.backend.feed.parser.FeedParserResult.Enclosure;
@@ -38,19 +18,36 @@ import com.rometools.rome.feed.synd.SyndFeed;
import com.rometools.rome.feed.synd.SyndLink; import com.rometools.rome.feed.synd.SyndLink;
import com.rometools.rome.feed.synd.SyndLinkImpl; import com.rometools.rome.feed.synd.SyndLinkImpl;
import com.rometools.rome.io.SyndFeedInput; import com.rometools.rome.io.SyndFeedInput;
import jakarta.inject.Singleton;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemProperties;
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.xml.sax.InputSource;
/** /** Parses raw xml into a FeedParserResult object */
* Parses raw xml into a FeedParserResult object
*/
@Singleton @Singleton
public class FeedParser { public class FeedParser {
private static final Namespace ATOM_10_NS = Namespace.getNamespace("http://www.w3.org/2005/Atom"); private static final Namespace ATOM_10_NS =
Namespace.getNamespace("http://www.w3.org/2005/Atom");
private static final Instant START = Instant.ofEpochMilli(86400000); private static final Instant START = Instant.ofEpochMilli(86400000);
private static final Instant END = Instant.ofEpochMilli(1000L * Integer.MAX_VALUE - 86400000); private static final Instant END = Instant.ofEpochMilli(1000L * Integer.MAX_VALUE - 86400000);
private static final Comparator<Entry> ENTRY_COMPARATOR = Comparator.comparing(Entry::published).reversed(); private static final Comparator<Entry> ENTRY_COMPARATOR =
Comparator.comparing(Entry::published).reversed();
private final EncodingDetector encodingDetector; private final EncodingDetector encodingDetector;
private final XMLCleaner xmlCleaner; private final XMLCleaner xmlCleaner;
@@ -85,22 +82,29 @@ public class FeedParser {
List<Entry> entries = buildEntries(feed, feedUrl); List<Entry> entries = buildEntries(feed, feedUrl);
Instant lastEntryDate = entries.stream().findFirst().map(Entry::published).orElse(null); Instant lastEntryDate = entries.stream().findFirst().map(Entry::published).orElse(null);
Instant lastPublishedDate = toValidInstant(feed.getPublishedDate(), false); Instant lastPublishedDate = toValidInstant(feed.getPublishedDate(), false);
if (lastPublishedDate == null || lastEntryDate != null && lastPublishedDate.isBefore(lastEntryDate)) { if (lastPublishedDate == null
|| lastEntryDate != null && lastPublishedDate.isBefore(lastEntryDate)) {
lastPublishedDate = lastEntryDate; lastPublishedDate = lastEntryDate;
} }
Long averageEntryInterval = averageTimeBetweenEntries(entries); Long averageEntryInterval = averageTimeBetweenEntries(entries);
return new FeedParserResult(title, link, iconUrl, lastPublishedDate, averageEntryInterval, lastEntryDate, entries); return new FeedParserResult(
title,
link,
iconUrl,
lastPublishedDate,
averageEntryInterval,
lastEntryDate,
entries);
} catch (FeedParsingException e) { } catch (FeedParsingException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
throw new FeedParsingException(String.format("Could not parse feed from %s : %s", feedUrl, e.getMessage()), e); throw new FeedParsingException(
String.format("Could not parse feed from %s : %s", feedUrl, e.getMessage()), e);
} }
} }
/** /** Adds atom links for rss feeds */
* Adds atom links for rss feeds
*/
private void handleForeignMarkup(SyndFeed feed) { private void handleForeignMarkup(SyndFeed feed) {
List<Element> foreignMarkup = feed.getForeignMarkup(); List<Element> foreignMarkup = feed.getForeignMarkup();
if (foreignMarkup == null) { if (foreignMarkup == null) {
@@ -149,8 +153,11 @@ public class FeedParser {
String title = getTitle(item); String title = getTitle(item);
String content = getContent(item); String content = getContent(item);
String author = StringUtils.trimToNull(item.getAuthor()); String author = StringUtils.trimToNull(item.getAuthor());
String categories = StringUtils String categories =
.trimToNull(item.getCategories().stream().map(SyndCategory::getName).collect(Collectors.joining(", "))); StringUtils.trimToNull(
item.getCategories().stream()
.map(SyndCategory::getName)
.collect(Collectors.joining(", ")));
Enclosure enclosure = buildEnclosure(item); Enclosure enclosure = buildEnclosure(item);
Media media = buildMedia(item); Media media = buildMedia(item);
@@ -208,7 +215,10 @@ public class FeedParser {
if (item.getContents().isEmpty()) { if (item.getContents().isEmpty()) {
content = item.getDescription() == null ? null : item.getDescription().getValue(); content = item.getDescription() == null ? null : item.getDescription().getValue();
} else { } else {
content = item.getContents().stream().map(SyndContent::getValue).collect(Collectors.joining(System.lineSeparator())); content =
item.getContents().stream()
.map(SyndContent::getValue)
.collect(Collectors.joining(System.lineSeparator()));
} }
return StringUtils.trimToNull(content); return StringUtils.trimToNull(content);
} }
@@ -274,7 +284,10 @@ public class FeedParser {
SummaryStatistics stats = new SummaryStatistics(); SummaryStatistics stats = new SummaryStatistics();
for (int i = 0; i < entries.size() - 1; i++) { for (int i = 0; i < entries.size() - 1; i++) {
long diff = Math.abs(entries.get(i).published().toEpochMilli() - entries.get(i + 1).published().toEpochMilli()); long diff =
Math.abs(
entries.get(i).published().toEpochMilli()
- entries.get(i + 1).published().toEpochMilli());
stats.addValue(diff); stats.addValue(diff);
} }
return (long) stats.getMean(); return (long) stats.getMean();
@@ -291,5 +304,4 @@ public class FeedParser {
super(message, cause); super(message, cause);
} }
} }
} }

View File

@@ -3,13 +3,29 @@ package com.commafeed.backend.feed.parser;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
public record FeedParserResult(String title, String link, String iconUrl, Instant lastPublishedDate, Long averageEntryInterval, public record FeedParserResult(
Instant lastEntryDate, List<Entry> entries) { String title,
String link,
String iconUrl,
Instant lastPublishedDate,
Long averageEntryInterval,
Instant lastEntryDate,
List<Entry> entries) {
public record Entry(String guid, String url, Instant published, Content content) {} public record Entry(String guid, String url, Instant published, Content content) {}
public record Content(String title, String content, String author, String categories, Enclosure enclosure, Media media) {} public record Content(
String title,
String content,
String author,
String categories,
Enclosure enclosure,
Media media) {}
public record Enclosure(String url, String type) {} public record Enclosure(String url, String type) {}
public record Media(String description, String thumbnailUrl, Integer thumbnailWidth, Integer thumbnailHeight) {} public record Media(
String description,
String thumbnailUrl,
Integer thumbnailWidth,
Integer thumbnailHeight) {}
} }

View File

@@ -4,7 +4,6 @@ import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
@UtilityClass @UtilityClass

View File

@@ -2,7 +2,6 @@ package com.commafeed.backend.feed.parser;
import java.text.Bidi; import java.text.Bidi;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.math.NumberUtils;
public class TextDirectionDetector { public class TextDirectionDetector {
@@ -13,7 +12,8 @@ public class TextDirectionDetector {
private static final double RTL_THRESHOLD = 0.4D; private static final double RTL_THRESHOLD = 0.4D;
public enum Direction { public enum Direction {
LEFT_TO_RIGHT, RIGHT_TO_LEFT LEFT_TO_RIGHT,
RIGHT_TO_LEFT
} }
public static Direction detect(String input) { public static Direction detect(String input) {
@@ -52,5 +52,4 @@ public class TextDirectionDetector {
double ratio = (double) rtl / total; double ratio = (double) rtl / total;
return ratio > RTL_THRESHOLD ? Direction.RIGHT_TO_LEFT : Direction.LEFT_TO_RIGHT; return ratio > RTL_THRESHOLD ? Direction.RIGHT_TO_LEFT : Direction.LEFT_TO_RIGHT;
} }
} }

View File

@@ -1,10 +1,8 @@
package com.commafeed.backend.feed.parser; package com.commafeed.backend.feed.parser;
import jakarta.inject.Singleton;
import java.util.Collection; import java.util.Collection;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import jakarta.inject.Singleton;
import org.ahocorasick.trie.Emit; import org.ahocorasick.trie.Emit;
import org.ahocorasick.trie.Trie; import org.ahocorasick.trie.Trie;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -13,9 +11,11 @@ import org.jdom2.Verifier;
@Singleton @Singleton
public class XMLCleaner { public class XMLCleaner {
private static final Pattern DOCTYPE_PATTERN = Pattern.compile("<!DOCTYPE[^>]*>", Pattern.CASE_INSENSITIVE); private static final Pattern DOCTYPE_PATTERN =
Pattern.compile("<!DOCTYPE[^>]*>", Pattern.CASE_INSENSITIVE);
private final Trie trie = Trie.builder().ignoreOverlaps().addKeywords(HtmlEntities.HTML_ENTITIES).build(); private final Trie trie =
Trie.builder().ignoreOverlaps().addKeywords(HtmlEntities.HTML_ENTITIES).build();
public String clean(String xml) { public String clean(String xml) {
xml = removeCharactersBeforeFirstXmlTag(xml); xml = removeCharactersBeforeFirstXmlTag(xml);
@@ -78,5 +78,4 @@ public class XMLCleaner {
return DOCTYPE_PATTERN.matcher(xml).replaceAll(""); return DOCTYPE_PATTERN.matcher(xml).replaceAll("");
} }
} }

View File

@@ -1,20 +1,15 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.io.Serializable;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass; import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.TableGenerator; import jakarta.persistence.TableGenerator;
import java.io.Serializable;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
/** /** Abstract model for all entities, defining id and table generator */
* Abstract model for all entities, defining id and table generator
*
*/
@SuppressWarnings("serial") @SuppressWarnings("serial")
@MappedSuperclass @MappedSuperclass
@Getter @Getter

View File

@@ -1,17 +1,14 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.sql.Types;
import java.time.Instant;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Lob; import jakarta.persistence.Lob;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.sql.Types;
import org.hibernate.annotations.JdbcTypeCode; import java.time.Instant;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.hibernate.annotations.JdbcTypeCode;
@Entity @Entity
@Table(name = "FEEDS") @Table(name = "FEEDS")
@@ -20,17 +17,13 @@ import lombok.Setter;
@Setter @Setter
public class Feed extends AbstractModel { public class Feed extends AbstractModel {
/** /** The url of the feed */
* The url of the feed
*/
@Lob @Lob
@Column(length = Integer.MAX_VALUE, nullable = false) @Column(length = Integer.MAX_VALUE, nullable = false)
@JdbcTypeCode(Types.LONGVARCHAR) @JdbcTypeCode(Types.LONGVARCHAR)
private String url; private String url;
/** /** cache the url after potential http 30x redirects */
* cache the url after potential http 30x redirects
*/
@Column(name = "url_after_redirect", length = 2048, nullable = false) @Column(name = "url_after_redirect", length = 2048, nullable = false)
private String urlAfterRedirect; private String urlAfterRedirect;
@@ -40,9 +33,7 @@ public class Feed extends AbstractModel {
@Column(length = 40, nullable = false) @Column(length = 40, nullable = false)
private String normalizedUrlHash; private String normalizedUrlHash;
/** /** The url of the website, extracted from the feed */
* The url of the website, extracted from the feed
*/
@Lob @Lob
@Column(length = Integer.MAX_VALUE) @Column(length = Integer.MAX_VALUE)
@JdbcTypeCode(Types.LONGVARCHAR) @JdbcTypeCode(Types.LONGVARCHAR)
@@ -53,64 +44,39 @@ public class Feed extends AbstractModel {
@JdbcTypeCode(Types.LONGVARCHAR) @JdbcTypeCode(Types.LONGVARCHAR)
private String iconUrl; private String iconUrl;
/** /** Last time we tried to fetch the feed */
* Last time we tried to fetch the feed @Column private Instant lastUpdated;
*/
@Column
private Instant lastUpdated;
/** /** Last publishedDate value in the feed */
* Last publishedDate value in the feed @Column private Instant lastPublishedDate;
*/
@Column
private Instant lastPublishedDate;
/** /** date of the last entry of the feed */
* date of the last entry of the feed @Column private Instant lastEntryDate;
*/
@Column
private Instant lastEntryDate;
/** /** error message while retrieving the feed */
* error message while retrieving the feed
*/
@Lob @Lob
@Column(length = Integer.MAX_VALUE) @Column(length = Integer.MAX_VALUE)
@JdbcTypeCode(Types.LONGVARCHAR) @JdbcTypeCode(Types.LONGVARCHAR)
private String message; private String message;
/** /** times we failed to retrieve the feed */
* times we failed to retrieve the feed
*/
private int errorCount; private int errorCount;
/** /** feed refresh is disabled until this date */
* feed refresh is disabled until this date @Column private Instant disabledUntil;
*/
@Column
private Instant disabledUntil;
/** /** http header returned by the feed */
* http header returned by the feed
*/
@Column(length = 64) @Column(length = 64)
private String lastModifiedHeader; private String lastModifiedHeader;
/** /** http header returned by the feed */
* http header returned by the feed
*/
@Column(length = 255) @Column(length = 255)
private String etagHeader; private String etagHeader;
/** /** average time between entries in the feed */
* average time between entries in the feed
*/
private Long averageEntryInterval; private Long averageEntryInterval;
/** /** last hash of the content of the feed xml */
* last hash of the content of the feed xml
*/
@Column(length = 40) @Column(length = 40)
private String lastContentHash; private String lastContentHash;
} }

View File

@@ -6,7 +6,6 @@ import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -30,5 +29,4 @@ public class FeedCategory extends AbstractModel {
private boolean collapsed; private boolean collapsed;
private int position; private int position;
} }

View File

@@ -1,8 +1,5 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.time.Instant;
import java.util.Set;
import jakarta.persistence.CascadeType; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@@ -11,7 +8,8 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.Instant;
import java.util.Set;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -38,16 +36,10 @@ public class FeedEntry extends AbstractModel {
@Column(length = 2048) @Column(length = 2048)
private String url; private String url;
/** /** the moment the entry was inserted in the database */
* the moment the entry was inserted in the database @Column private Instant inserted;
*/
@Column
private Instant inserted;
/** /** the moment the entry was published in the feed */
* the moment the entry was published in the feed
*
*/
@Column(name = "updated") @Column(name = "updated")
private Instant published; private Instant published;
@@ -56,5 +48,4 @@ public class FeedEntry extends AbstractModel {
@OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE) @OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE)
private Set<FeedEntryTag> tags; private Set<FeedEntryTag> tags;
} }

View File

@@ -1,21 +1,17 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.sql.Types; import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EnumType; import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated; import jakarta.persistence.Enumerated;
import jakarta.persistence.Lob; import jakarta.persistence.Lob;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.sql.Types;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.hibernate.annotations.JdbcTypeCode;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.hibernate.annotations.JdbcTypeCode;
@Entity @Entity
@Table(name = "FEEDENTRYCONTENTS") @Table(name = "FEEDENTRYCONTENTS")
@@ -81,7 +77,8 @@ public class FeedEntryContent extends AbstractModel {
return false; return false;
} }
return new EqualsBuilder().append(title, c.title) return new EqualsBuilder()
.append(title, c.title)
.append(content, c.content) .append(content, c.content)
.append(author, c.author) .append(author, c.author)
.append(categories, c.categories) .append(categories, c.categories)

View File

@@ -1,9 +1,5 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
@@ -11,7 +7,9 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.Transient; import jakarta.persistence.Transient;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -32,31 +30,24 @@ public class FeedEntryStatus extends AbstractModel {
@Column(name = "read_status") @Column(name = "read_status")
private boolean read; private boolean read;
private boolean starred; private boolean starred;
@Transient @Transient private boolean markable;
private boolean markable;
@Transient @Transient private List<FeedEntryTag> tags = new ArrayList<>();
private List<FeedEntryTag> tags = new ArrayList<>();
/**
* Denormalization starts here
*/
/** Denormalization starts here */
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false) @JoinColumn(nullable = false)
private User user; private User user;
@Column @Column private Instant entryInserted;
private Instant entryInserted;
@Column(name = "entryUpdated") @Column(name = "entryUpdated")
private Instant entryPublished; private Instant entryPublished;
public FeedEntryStatus() { public FeedEntryStatus() {}
}
public FeedEntryStatus(User user, FeedSubscription subscription, FeedEntry entry) { public FeedEntryStatus(User user, FeedSubscription subscription, FeedEntry entry) {
this.user = user; this.user = user;
@@ -65,5 +56,4 @@ public class FeedEntryStatus extends AbstractModel {
this.entryInserted = entry.getInserted(); this.entryInserted = entry.getInserted();
this.entryPublished = entry.getPublished(); this.entryPublished = entry.getPublished();
} }
} }

View File

@@ -6,7 +6,6 @@ import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -28,13 +27,11 @@ public class FeedEntryTag extends AbstractModel {
@Column(name = "name", length = 40) @Column(name = "name", length = 40)
private String name; private String name;
public FeedEntryTag() { public FeedEntryTag() {}
}
public FeedEntryTag(User user, FeedEntry entry, String name) { public FeedEntryTag(User user, FeedEntry entry, String name) {
this.name = name; this.name = name;
this.entry = entry; this.entry = entry;
this.user = user; this.user = user;
} }
} }

View File

@@ -1,7 +1,5 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.util.Set;
import jakarta.persistence.CascadeType; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@@ -10,7 +8,7 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.util.Set;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -51,5 +49,4 @@ public class FeedSubscription extends AbstractModel {
@Column(name = "auto_mark_as_read_after_days") @Column(name = "auto_mark_as_read_after_days")
private Integer autoMarkAsReadAfterDays; private Integer autoMarkAsReadAfterDays;
} }

View File

@@ -2,33 +2,28 @@ package com.commafeed.backend.model;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import lombok.experimental.UtilityClass;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.hibernate.HibernateException; import org.hibernate.HibernateException;
import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer; import org.hibernate.proxy.LazyInitializer;
import lombok.experimental.UtilityClass;
@UtilityClass @UtilityClass
public class Models { public class Models {
public static final Instant MINIMUM_INSTANT = Instant.EPOCH public static final Instant MINIMUM_INSTANT =
Instant.EPOCH
// mariadb timestamp range starts at 1970-01-01 00:00:01 // mariadb timestamp range starts at 1970-01-01 00:00:01
.plusSeconds(1) .plusSeconds(1)
// make sure the timestamp fits for all timezones // make sure the timestamp fits for all timezones
.plus(Duration.ofHours(24)); .plus(Duration.ofHours(24));
/** /** initialize a proxy */
* initialize a proxy
*/
public static void initialize(Object proxy) throws HibernateException { public static void initialize(Object proxy) throws HibernateException {
Hibernate.initialize(proxy); Hibernate.initialize(proxy);
} }
/** /** extract the id from the proxy without initializing it */
* extract the id from the proxy without initializing it
*/
public static Long getId(AbstractModel model) { public static Long getId(AbstractModel model) {
if (model instanceof HibernateProxy proxy) { if (model instanceof HibernateProxy proxy) {
LazyInitializer lazyInitializer = proxy.getHibernateLazyInitializer(); LazyInitializer lazyInitializer = proxy.getHibernateLazyInitializer();

View File

@@ -1,17 +1,14 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.sql.Types;
import java.time.Instant;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Lob; import jakarta.persistence.Lob;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.sql.Types;
import org.hibernate.annotations.JdbcTypeCode; import java.time.Instant;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.hibernate.annotations.JdbcTypeCode;
@Entity @Entity
@Table(name = "USERS") @Table(name = "USERS")
@@ -42,18 +39,14 @@ public class User extends AbstractModel {
@Column(nullable = false) @Column(nullable = false)
private boolean disabled; private boolean disabled;
@Column @Column private Instant lastLogin;
private Instant lastLogin;
@Column @Column private Instant created;
private Instant created;
@Column(length = 40) @Column(length = 40)
private String recoverPasswordToken; private String recoverPasswordToken;
@Column @Column private Instant recoverPasswordTokenDate;
private Instant recoverPasswordTokenDate;
@Column @Column private Instant lastForceRefresh;
private Instant lastForceRefresh;
} }

View File

@@ -8,7 +8,6 @@ import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne; import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -20,7 +19,8 @@ import lombok.Setter;
public class UserRole extends AbstractModel { public class UserRole extends AbstractModel {
public enum Role { public enum Role {
USER, ADMIN USER,
ADMIN
} }
@OneToOne(fetch = FetchType.LAZY) @OneToOne(fetch = FetchType.LAZY)
@@ -31,13 +31,10 @@ public class UserRole extends AbstractModel {
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private Role role; private Role role;
public UserRole() { public UserRole() {}
}
public UserRole(User user, Role role) { public UserRole(User user, Role role) {
this.user = user; this.user = user;
this.role = role; this.role = role;
} }
} }

View File

@@ -1,8 +1,6 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.io.Serializable; import com.fasterxml.jackson.annotation.JsonProperty;
import java.sql.Types;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Embeddable; import jakarta.persistence.Embeddable;
import jakarta.persistence.Embedded; import jakarta.persistence.Embedded;
@@ -14,13 +12,11 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob; import jakarta.persistence.Lob;
import jakarta.persistence.OneToOne; import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.io.Serializable;
import org.hibernate.annotations.JdbcTypeCode; import java.sql.Types;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.hibernate.annotations.JdbcTypeCode;
@Entity @Entity
@Table(name = "USERSETTINGS") @Table(name = "USERSETTINGS")
@@ -179,5 +175,4 @@ public class UserSettings extends AbstractModel {
@Column(name = "push_notification_topic", length = 256) @Column(name = "push_notification_topic", length = 256)
private String topic; private String topic;
} }
} }

View File

@@ -1,13 +1,5 @@
package com.commafeed.backend.opml; package com.commafeed.backend.opml;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.ObjectUtils;
import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
@@ -16,17 +8,21 @@ import com.commafeed.backend.model.User;
import com.rometools.opml.feed.opml.Attribute; import com.rometools.opml.feed.opml.Attribute;
import com.rometools.opml.feed.opml.Opml; import com.rometools.opml.feed.opml.Opml;
import com.rometools.opml.feed.opml.Outline; import com.rometools.opml.feed.opml.Outline;
import jakarta.inject.Singleton;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.ObjectUtils;
@RequiredArgsConstructor @RequiredArgsConstructor
@Singleton @Singleton
public class OPMLExporter { public class OPMLExporter {
private static final Comparator<FeedCategory> CATEGORY_COMPARATOR = Comparator private static final Comparator<FeedCategory> CATEGORY_COMPARATOR =
.comparingInt(e -> ObjectUtils.firstNonNull(e.getPosition(), 0)); Comparator.comparingInt(e -> ObjectUtils.firstNonNull(e.getPosition(), 0));
private static final Comparator<FeedSubscription> SUBSCRIPTION_COMPARATOR = Comparator private static final Comparator<FeedSubscription> SUBSCRIPTION_COMPARATOR =
.comparingInt(e -> ObjectUtils.firstNonNull(e.getPosition(), 0)); Comparator.comparingInt(e -> ObjectUtils.firstNonNull(e.getPosition(), 0));
private final FeedCategoryDAO feedCategoryDAO; private final FeedCategoryDAO feedCategoryDAO;
private final FeedSubscriptionDAO feedSubscriptionDAO; private final FeedSubscriptionDAO feedSubscriptionDAO;
@@ -49,27 +45,36 @@ public class OPMLExporter {
} }
// export root subscriptions // export root subscriptions
for (FeedSubscription sub : subscriptions.stream().filter(s -> s.getCategory() == null).toList()) { for (FeedSubscription sub :
subscriptions.stream().filter(s -> s.getCategory() == null).toList()) {
opml.getOutlines().add(buildSubscriptionOutline(sub)); opml.getOutlines().add(buildSubscriptionOutline(sub));
} }
return opml; return opml;
} }
private Outline buildCategoryOutline(FeedCategory cat, List<FeedCategory> categories, List<FeedSubscription> subscriptions) { private Outline buildCategoryOutline(
FeedCategory cat, List<FeedCategory> categories, List<FeedSubscription> subscriptions) {
Outline outline = new Outline(); Outline outline = new Outline();
outline.setText(cat.getName()); outline.setText(cat.getName());
outline.setTitle(cat.getName()); outline.setTitle(cat.getName());
for (FeedCategory child : categories.stream() for (FeedCategory child :
.filter(c -> c.getParent() != null && c.getParent().getId().equals(cat.getId())) categories.stream()
.filter(
c ->
c.getParent() != null
&& c.getParent().getId().equals(cat.getId()))
.toList()) { .toList()) {
outline.getChildren().add(buildCategoryOutline(child, categories, subscriptions)); outline.getChildren().add(buildCategoryOutline(child, categories, subscriptions));
} }
for (FeedSubscription sub : subscriptions.stream() for (FeedSubscription sub :
.filter(s -> s.getCategory() != null && s.getCategory().getId().equals(cat.getId())) subscriptions.stream()
.filter(
s ->
s.getCategory() != null
&& s.getCategory().getId().equals(cat.getId()))
.toList()) { .toList()) {
outline.getChildren().add(buildSubscriptionOutline(sub)); outline.getChildren().add(buildSubscriptionOutline(sub));
} }

View File

@@ -1,13 +1,5 @@
package com.commafeed.backend.opml; package com.commafeed.backend.opml;
import java.io.StringReader;
import java.util.List;
import jakarta.inject.Singleton;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.feed.parser.XMLCleaner; import com.commafeed.backend.feed.parser.XMLCleaner;
@@ -18,9 +10,13 @@ import com.rometools.opml.feed.opml.Opml;
import com.rometools.opml.feed.opml.Outline; import com.rometools.opml.feed.opml.Outline;
import com.rometools.rome.io.FeedException; import com.rometools.rome.io.FeedException;
import com.rometools.rome.io.WireFeedInput; import com.rometools.rome.io.WireFeedInput;
import jakarta.inject.Singleton;
import java.io.StringReader;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -79,7 +75,8 @@ public class OPMLImporter {
} }
// make sure we continue with the import process even if a feed failed // make sure we continue with the import process even if a feed failed
try { try {
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position); feedSubscriptionService.subscribe(
user, outline.getXmlUrl(), name, parent, position);
} catch (Exception e) { } catch (Exception e) {
log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage()); log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage());
} }

View File

@@ -1,20 +1,14 @@
package com.commafeed.backend.rome; package com.commafeed.backend.rome;
import java.util.Locale;
import org.jdom2.Document;
import org.jdom2.Element;
import com.rometools.opml.io.impl.OPML10Parser; import com.rometools.opml.io.impl.OPML10Parser;
import com.rometools.rome.feed.WireFeed; import com.rometools.rome.feed.WireFeed;
import com.rometools.rome.io.FeedException; import com.rometools.rome.io.FeedException;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import java.util.Locale;
import org.jdom2.Document;
import org.jdom2.Element;
/** /** Support for OPML 1.1 parsing */
* Support for OPML 1.1 parsing
*
*/
@RegisterForReflection @RegisterForReflection
public class OPML11Parser extends OPML10Parser { public class OPML11Parser extends OPML10Parser {
@@ -27,11 +21,11 @@ public class OPML11Parser extends OPML10Parser {
Element e = document.getRootElement(); Element e = document.getRootElement();
return e.getName().equals("opml"); return e.getName().equals("opml");
} }
@Override @Override
public WireFeed parse(Document document, boolean validate, Locale locale) throws IllegalArgumentException, FeedException { public WireFeed parse(Document document, boolean validate, Locale locale)
throws IllegalArgumentException, FeedException {
document.getRootElement().getChildren().add(new Element("head")); document.getRootElement().getChildren().add(new Element("head"));
return super.parse(document, validate, locale); return super.parse(document, validate, locale);
} }

View File

@@ -1,12 +1,14 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import com.steadystate.css.parser.CSSOMParser;
import com.steadystate.css.parser.SACParserCSS21;
import jakarta.inject.Singleton;
import java.io.StringReader; import java.io.StringReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.inject.Singleton; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
@@ -21,21 +23,17 @@ import org.w3c.css.sac.ErrorHandler;
import org.w3c.css.sac.InputSource; import org.w3c.css.sac.InputSource;
import org.w3c.dom.css.CSSStyleDeclaration; import org.w3c.dom.css.CSSStyleDeclaration;
import com.steadystate.css.parser.CSSOMParser;
import com.steadystate.css.parser.SACParserCSS21;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
@Singleton @Singleton
public class FeedEntryContentCleaningService { public class FeedEntryContentCleaningService {
private static final Safelist HTML_WHITELIST = buildWhiteList(); private static final Safelist HTML_WHITELIST = buildWhiteList();
private static final List<String> ALLOWED_IFRAME_CSS_RULES = Arrays.asList("height", "width", "border"); private static final List<String> ALLOWED_IFRAME_CSS_RULES =
private static final List<String> ALLOWED_IMG_CSS_RULES = Arrays.asList("display", "width", "height"); Arrays.asList("height", "width", "border");
private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] { '(', ')' }; private static final List<String> ALLOWED_IMG_CSS_RULES =
Arrays.asList("display", "width", "height");
private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] {'(', ')'};
public String clean(String content, String baseUri, boolean keepTextOnly) { public String clean(String content, String baseUri, boolean keepTextOnly) {
if (StringUtils.isNotBlank(content)) { if (StringUtils.isNotBlank(content)) {
@@ -57,7 +55,8 @@ public class FeedEntryContentCleaningService {
e.attr("style", escaped); e.attr("style", escaped);
} }
clean.outputSettings(new OutputSettings().escapeMode(EscapeMode.base).prettyPrint(false)); clean.outputSettings(
new OutputSettings().escapeMode(EscapeMode.base).prettyPrint(false));
Element body = clean.body(); Element body = clean.body();
if (keepTextOnly) { if (keepTextOnly) {
content = body.text(); content = body.text();
@@ -70,9 +69,49 @@ public class FeedEntryContentCleaningService {
private static Safelist buildWhiteList() { private static Safelist buildWhiteList() {
Safelist whitelist = new Safelist(); Safelist whitelist = new Safelist();
whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em", "h1", whitelist.addTags(
"h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong", "sub", "sup", "a",
"table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul"); "b",
"blockquote",
"br",
"caption",
"cite",
"code",
"col",
"colgroup",
"dd",
"div",
"dl",
"dt",
"em",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"i",
"iframe",
"img",
"li",
"ol",
"p",
"pre",
"q",
"small",
"strike",
"strong",
"sub",
"sup",
"table",
"tbody",
"td",
"tfoot",
"th",
"thead",
"tr",
"u",
"ul");
whitelist.addAttributes("div", "dir"); whitelist.addAttributes("div", "dir");
whitelist.addAttributes("pre", "dir"); whitelist.addAttributes("pre", "dir");
@@ -83,13 +122,24 @@ public class FeedEntryContentCleaningService {
whitelist.addAttributes("blockquote", "cite"); whitelist.addAttributes("blockquote", "cite");
whitelist.addAttributes("col", "span", "width"); whitelist.addAttributes("col", "span", "width");
whitelist.addAttributes("colgroup", "span", "width"); whitelist.addAttributes("colgroup", "span", "width");
whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style"); whitelist.addAttributes(
"iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style"); whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style");
whitelist.addAttributes("ol", "start", "type"); whitelist.addAttributes("ol", "start", "type");
whitelist.addAttributes("q", "cite"); whitelist.addAttributes("q", "cite");
whitelist.addAttributes("table", "border", "bordercolor", "summary", "width"); whitelist.addAttributes("table", "border", "bordercolor", "summary", "width");
whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width"); whitelist.addAttributes(
whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width"); "td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
whitelist.addAttributes(
"th",
"border",
"bordercolor",
"abbr",
"axis",
"colspan",
"rowspan",
"scope",
"width");
whitelist.addAttributes("ul", "type"); whitelist.addAttributes("ul", "type");
whitelist.addProtocols("a", "href", "ftp", "http", "https", "magnet", "mailto"); whitelist.addProtocols("a", "href", "ftp", "http", "https", "magnet", "mailto");
@@ -106,7 +156,8 @@ public class FeedEntryContentCleaningService {
String rule = ""; String rule = "";
try { try {
List<String> rules = new ArrayList<>(); List<String> rules = new ArrayList<>();
CSSStyleDeclaration decl = buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig))); CSSStyleDeclaration decl =
buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig)));
for (int i = 0; i < decl.getLength(); i++) { for (int i = 0; i < decl.getLength(); i++) {
String property = decl.item(i); String property = decl.item(i);
@@ -115,7 +166,8 @@ public class FeedEntryContentCleaningService {
continue; continue;
} }
if (ALLOWED_IFRAME_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) { if (ALLOWED_IFRAME_CSS_RULES.contains(property)
&& StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) {
rules.add(property + ":" + decl.getPropertyValue(property) + ";"); rules.add(property + ":" + decl.getPropertyValue(property) + ";");
} }
} }
@@ -130,7 +182,8 @@ public class FeedEntryContentCleaningService {
String rule = ""; String rule = "";
try { try {
List<String> rules = new ArrayList<>(); List<String> rules = new ArrayList<>();
CSSStyleDeclaration decl = buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig))); CSSStyleDeclaration decl =
buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig)));
for (int i = 0; i < decl.getLength(); i++) { for (int i = 0; i < decl.getLength(); i++) {
String property = decl.item(i); String property = decl.item(i);
@@ -139,7 +192,8 @@ public class FeedEntryContentCleaningService {
continue; continue;
} }
if (ALLOWED_IMG_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) { if (ALLOWED_IMG_CSS_RULES.contains(property)
&& StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) {
rules.add(property + ":" + decl.getPropertyValue(property) + ";"); rules.add(property + ":" + decl.getPropertyValue(property) + ";");
} }
} }
@@ -153,10 +207,12 @@ public class FeedEntryContentCleaningService {
private CSSOMParser buildCssParser() { private CSSOMParser buildCssParser() {
CSSOMParser parser = new CSSOMParser(new SACParserCSS21()); CSSOMParser parser = new CSSOMParser(new SACParserCSS21());
parser.setErrorHandler(new ErrorHandler() { parser.setErrorHandler(
new ErrorHandler() {
@Override @Override
public void warning(CSSParseException exception) throws CSSException { public void warning(CSSParseException exception) throws CSSException {
log.debug("warning while parsing css: {}", exception.getMessage(), exception); log.debug(
"warning while parsing css: {}", exception.getMessage(), exception);
} }
@Override @Override
@@ -166,7 +222,10 @@ public class FeedEntryContentCleaningService {
@Override @Override
public void fatalError(CSSParseException exception) throws CSSException { public void fatalError(CSSParseException exception) throws CSSException {
log.debug("fatal error while parsing css: {}", exception.getMessage(), exception); log.debug(
"fatal error while parsing css: {}",
exception.getMessage(),
exception);
} }
}); });

View File

@@ -1,11 +1,5 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.util.Optional;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import com.commafeed.backend.Digests; import com.commafeed.backend.Digests;
import com.commafeed.backend.dao.FeedEntryContentDAO; import com.commafeed.backend.dao.FeedEntryContentDAO;
import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.FeedUtils;
@@ -13,8 +7,10 @@ import com.commafeed.backend.feed.parser.FeedParserResult.Content;
import com.commafeed.backend.feed.parser.FeedParserResult.Enclosure; import com.commafeed.backend.feed.parser.FeedParserResult.Enclosure;
import com.commafeed.backend.feed.parser.FeedParserResult.Media; import com.commafeed.backend.feed.parser.FeedParserResult.Media;
import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.FeedEntryContent;
import jakarta.inject.Singleton;
import java.util.Optional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
@RequiredArgsConstructor @RequiredArgsConstructor
@Singleton @Singleton
@@ -23,12 +19,12 @@ public class FeedEntryContentService {
private final FeedEntryContentDAO feedEntryContentDAO; private final FeedEntryContentDAO feedEntryContentDAO;
private final FeedEntryContentCleaningService cleaningService; private final FeedEntryContentCleaningService cleaningService;
/** /** this is NOT thread-safe */
* this is NOT thread-safe
*/
public FeedEntryContent findOrCreate(Content content, String baseUrl) { public FeedEntryContent findOrCreate(Content content, String baseUrl) {
FeedEntryContent entryContent = buildContent(content, baseUrl); FeedEntryContent entryContent = buildContent(content, baseUrl);
Optional<FeedEntryContent> existing = feedEntryContentDAO.findExisting(entryContent.getContentHash(), entryContent.getTitleHash()) Optional<FeedEntryContent> existing =
feedEntryContentDAO
.findExisting(entryContent.getContentHash(), entryContent.getTitleHash())
.stream() .stream()
.filter(entryContent::equivalentTo) .filter(entryContent::equivalentTo)
.findFirst(); .findFirst();
@@ -44,12 +40,16 @@ public class FeedEntryContentService {
FeedEntryContent entryContent = new FeedEntryContent(); FeedEntryContent entryContent = new FeedEntryContent();
entryContent.setTitleHash(Digests.sha1Hex(StringUtils.trimToEmpty(content.title()))); entryContent.setTitleHash(Digests.sha1Hex(StringUtils.trimToEmpty(content.title())));
entryContent.setContentHash(Digests.sha1Hex(StringUtils.trimToEmpty(content.content()))); entryContent.setContentHash(Digests.sha1Hex(StringUtils.trimToEmpty(content.content())));
entryContent.setTitle(FeedUtils.truncate(cleaningService.clean(content.title(), baseUrl, true), 2048)); entryContent.setTitle(
FeedUtils.truncate(cleaningService.clean(content.title(), baseUrl, true), 2048));
entryContent.setContent(cleaningService.clean(content.content(), baseUrl, false)); entryContent.setContent(cleaningService.clean(content.content(), baseUrl, false));
entryContent.setAuthor(FeedUtils.truncate(cleaningService.clean(content.author(), baseUrl, true), 128)); entryContent.setAuthor(
FeedUtils.truncate(cleaningService.clean(content.author(), baseUrl, true), 128));
entryContent.setCategories(FeedUtils.truncate(content.categories(), 4096)); entryContent.setCategories(FeedUtils.truncate(content.categories(), 4096));
entryContent.setDirection( entryContent.setDirection(
FeedUtils.isRTL(content.title(), content.content()) ? FeedEntryContent.Direction.RTL : FeedEntryContent.Direction.LTR); FeedUtils.isRTL(content.title(), content.content())
? FeedEntryContent.Direction.RTL
: FeedEntryContent.Direction.LTR);
Enclosure enclosure = content.enclosure(); Enclosure enclosure = content.enclosure();
if (enclosure != null) { if (enclosure != null) {
@@ -59,7 +59,8 @@ public class FeedEntryContentService {
Media media = content.media(); Media media = content.media();
if (media != null) { if (media != null) {
entryContent.setMediaDescription(cleaningService.clean(media.description(), baseUrl, false)); entryContent.setMediaDescription(
cleaningService.clean(media.description(), baseUrl, false));
entryContent.setMediaThumbnailUrl(FeedUtils.truncate(media.thumbnailUrl(), 2048)); entryContent.setMediaThumbnailUrl(FeedUtils.truncate(media.thumbnailUrl(), 2048));
entryContent.setMediaThumbnailWidth(media.thumbnailWidth()); entryContent.setMediaThumbnailWidth(media.thumbnailWidth());
entryContent.setMediaThumbnailHeight(media.thumbnailHeight()); entryContent.setMediaThumbnailHeight(media.thumbnailHeight());
@@ -67,5 +68,4 @@ public class FeedEntryContentService {
return entryContent; return entryContent;
} }
} }

View File

@@ -1,22 +1,7 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelValidationException; import dev.cel.common.CelValidationException;
import dev.cel.common.types.SimpleType; import dev.cel.common.types.SimpleType;
@@ -25,13 +10,25 @@ import dev.cel.compiler.CelCompilerFactory;
import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntime;
import dev.cel.runtime.CelRuntimeFactory; import dev.cel.runtime.CelRuntimeFactory;
import jakarta.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
@RequiredArgsConstructor @RequiredArgsConstructor
@Singleton @Singleton
public class FeedEntryFilteringService { public class FeedEntryFilteringService {
private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder() private static final CelCompiler CEL_COMPILER =
CelCompilerFactory.standardCelCompilerBuilder()
.addVar("title", SimpleType.STRING) .addVar("title", SimpleType.STRING)
.addVar("titleLower", SimpleType.STRING) .addVar("titleLower", SimpleType.STRING)
.addVar("author", SimpleType.STRING) .addVar("author", SimpleType.STRING)
@@ -43,21 +40,33 @@ public class FeedEntryFilteringService {
.addVar("categories", SimpleType.STRING) .addVar("categories", SimpleType.STRING)
.addVar("categoriesLower", SimpleType.STRING) .addVar("categoriesLower", SimpleType.STRING)
.build(); .build();
private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder().build(); private static final CelRuntime CEL_RUNTIME =
CelRuntimeFactory.standardCelRuntimeBuilder().build();
private final ExecutorService executor = Executors.newCachedThreadPool(); private final ExecutorService executor = Executors.newCachedThreadPool();
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
public boolean filterMatchesEntry(String filter, FeedEntry entry) throws FeedEntryFilterException { public boolean filterMatchesEntry(String filter, FeedEntry entry)
throws FeedEntryFilterException {
if (StringUtils.isBlank(filter)) { if (StringUtils.isBlank(filter)) {
return true; return true;
} }
String title = entry.getContent().getTitle() == null ? "" : Jsoup.parse(entry.getContent().getTitle()).text(); String title =
String author = entry.getContent().getAuthor() == null ? "" : entry.getContent().getAuthor(); entry.getContent().getTitle() == null
String content = entry.getContent().getContent() == null ? "" : Jsoup.parse(entry.getContent().getContent()).text(); ? ""
: Jsoup.parse(entry.getContent().getTitle()).text();
String author =
entry.getContent().getAuthor() == null ? "" : entry.getContent().getAuthor();
String content =
entry.getContent().getContent() == null
? ""
: Jsoup.parse(entry.getContent().getContent()).text();
String url = entry.getUrl() == null ? "" : entry.getUrl(); String url = entry.getUrl() == null ? "" : entry.getUrl();
String categories = entry.getContent().getCategories() == null ? "" : entry.getContent().getCategories(); String categories =
entry.getContent().getCategories() == null
? ""
: entry.getContent().getCategories();
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
data.put("title", title); data.put("title", title);
@@ -78,12 +87,17 @@ public class FeedEntryFilteringService {
Future<Object> future = executor.submit(() -> evaluateCelExpression(filter, data)); Future<Object> future = executor.submit(() -> evaluateCelExpression(filter, data));
Object result; Object result;
try { try {
result = future.get(config.feedRefresh().filteringExpressionEvaluationTimeout().toMillis(), TimeUnit.MILLISECONDS); result =
future.get(
config.feedRefresh().filteringExpressionEvaluationTimeout().toMillis(),
TimeUnit.MILLISECONDS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new FeedEntryFilterException("interrupted while evaluating expression " + filter, e); throw new FeedEntryFilterException(
"interrupted while evaluating expression " + filter, e);
} catch (ExecutionException e) { } catch (ExecutionException e) {
throw new FeedEntryFilterException("Exception while evaluating expression " + filter, e); throw new FeedEntryFilterException(
"Exception while evaluating expression " + filter, e);
} catch (TimeoutException e) { } catch (TimeoutException e) {
throw new FeedEntryFilterException("Took too long evaluating expression " + filter, e); throw new FeedEntryFilterException("Took too long evaluating expression " + filter, e);
} }

View File

@@ -1,12 +1,5 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.inject.Singleton;
import com.commafeed.backend.Digests; import com.commafeed.backend.Digests;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO;
@@ -20,7 +13,11 @@ import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException; import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException;
import jakarta.inject.Singleton;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -55,9 +52,12 @@ public class FeedEntryService {
} }
public List<Entry> removeExistingEntries(Feed feed, List<Entry> entries) { public List<Entry> removeExistingEntries(Feed feed, List<Entry> entries) {
Set<String> guidHashes = entries.stream().map(e -> Digests.sha1Hex(e.guid())).collect(Collectors.toSet()); Set<String> guidHashes =
entries.stream().map(e -> Digests.sha1Hex(e.guid())).collect(Collectors.toSet());
Set<String> existingGuidHashes = feedEntryDAO.findExistingGuidHashes(guidHashes, feed); Set<String> existingGuidHashes = feedEntryDAO.findExistingGuidHashes(guidHashes, feed);
return entries.stream().filter(e -> !existingGuidHashes.contains(Digests.sha1Hex(e.guid()))).toList(); return entries.stream()
.filter(e -> !existingGuidHashes.contains(Digests.sha1Hex(e.guid())))
.toList();
} }
public boolean applyFilter(FeedSubscription sub, FeedEntry entry) { public boolean applyFilter(FeedSubscription sub, FeedEntry entry) {
@@ -112,28 +112,58 @@ public class FeedEntryService {
feedEntryStatusDAO.merge(status); feedEntryStatusDAO.merge(status);
} }
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Instant olderThan, Instant insertedBefore, public void markSubscriptionEntries(
User user,
List<FeedSubscription> subscriptions,
Instant olderThan,
Instant insertedBefore,
List<FeedEntryKeyword> keywords) { List<FeedEntryKeyword> keywords) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null, List<FeedEntryStatus> statuses =
false, null, null, null); feedEntryStatusDAO.findBySubscriptions(
user,
subscriptions,
true,
keywords,
null,
-1,
-1,
null,
false,
null,
null,
null);
markList(statuses, olderThan, insertedBefore); markList(statuses, olderThan, insertedBefore);
} }
public void markStarredEntries(User user, Instant olderThan, Instant insertedBefore) { public void markStarredEntries(User user, Instant olderThan, Instant insertedBefore) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findStarred(user, null, null, -1, -1, null, false); List<FeedEntryStatus> statuses =
feedEntryStatusDAO.findStarred(user, null, null, -1, -1, null, false);
markList(statuses, olderThan, insertedBefore); markList(statuses, olderThan, insertedBefore);
} }
private void markList(List<FeedEntryStatus> statuses, Instant olderThan, Instant insertedBefore) { private void markList(
List<FeedEntryStatus> statusesToMark = statuses.stream().filter(FeedEntryStatus::isMarkable).filter(s -> { List<FeedEntryStatus> statuses, Instant olderThan, Instant insertedBefore) {
List<FeedEntryStatus> statusesToMark =
statuses.stream()
.filter(FeedEntryStatus::isMarkable)
.filter(
s -> {
Instant entryDate = s.getEntry().getPublished(); Instant entryDate = s.getEntry().getPublished();
return olderThan == null || entryDate == null || entryDate.isBefore(olderThan); return olderThan == null
}).filter(s -> { || entryDate == null
|| entryDate.isBefore(olderThan);
})
.filter(
s -> {
Instant insertedDate = s.getEntry().getInserted(); Instant insertedDate = s.getEntry().getInserted();
return insertedBefore == null || insertedDate == null || insertedDate.isBefore(insertedBefore); return insertedBefore == null
}).toList(); || insertedDate == null
|| insertedDate.isBefore(insertedBefore);
})
.toList();
statusesToMark.forEach(s -> { statusesToMark.forEach(
s -> {
s.setRead(true); s.setRead(true);
feedEntryStatusDAO.merge(s); feedEntryStatusDAO.merge(s);
}); });

View File

@@ -1,17 +1,14 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.inject.Singleton;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryTagDAO; import com.commafeed.backend.dao.FeedEntryTagDAO;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -28,16 +25,18 @@ public class FeedEntryTagService {
} }
List<FeedEntryTag> existingTags = feedEntryTagDAO.findByEntry(user, entry); List<FeedEntryTag> existingTags = feedEntryTagDAO.findByEntry(user, entry);
Set<String> existingTagNames = existingTags.stream().map(FeedEntryTag::getName).collect(Collectors.toSet()); Set<String> existingTagNames =
existingTags.stream().map(FeedEntryTag::getName).collect(Collectors.toSet());
List<FeedEntryTag> addList = tagNames.stream() List<FeedEntryTag> addList =
tagNames.stream()
.filter(name -> !existingTagNames.contains(name)) .filter(name -> !existingTagNames.contains(name))
.map(name -> new FeedEntryTag(user, entry, name)) .map(name -> new FeedEntryTag(user, entry, name))
.toList(); .toList();
List<FeedEntryTag> removeList = existingTags.stream().filter(tag -> !tagNames.contains(tag.getName())).toList(); List<FeedEntryTag> removeList =
existingTags.stream().filter(tag -> !tagNames.contains(tag.getName())).toList();
addList.forEach(feedEntryTagDAO::persist); addList.forEach(feedEntryTagDAO::persist);
feedEntryTagDAO.delete(removeList); feedEntryTagDAO.delete(removeList);
} }
} }

View File

@@ -1,28 +1,25 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import jakarta.inject.Singleton;
import jakarta.ws.rs.core.MediaType;
import org.apache.commons.lang3.ArrayUtils;
import com.commafeed.backend.favicon.Favicon; import com.commafeed.backend.favicon.Favicon;
import com.commafeed.backend.favicon.FaviconFetcher; import com.commafeed.backend.favicon.FaviconFetcher;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.google.common.io.Resources; import com.google.common.io.Resources;
import io.quarkus.arc.All; import io.quarkus.arc.All;
import jakarta.inject.Singleton;
import jakarta.ws.rs.core.MediaType;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
@Singleton @Singleton
@Slf4j @Slf4j
public class FeedFaviconService { public class FeedFaviconService {
private static final Set<MediaType> ICON_MIMETYPE_BLACKLIST = Set.of(MediaType.APPLICATION_XML_TYPE, MediaType.TEXT_HTML_TYPE); private static final Set<MediaType> ICON_MIMETYPE_BLACKLIST =
Set.of(MediaType.APPLICATION_XML_TYPE, MediaType.TEXT_HTML_TYPE);
private static final long MIN_ICON_LENGTH = 100; private static final long MIN_ICON_LENGTH = 100;
private static final long MAX_ICON_LENGTH = 100000; private static final long MAX_ICON_LENGTH = 100000;
@@ -31,8 +28,12 @@ public class FeedFaviconService {
public FeedFaviconService(@All List<FaviconFetcher> faviconFetchers) throws IOException { public FeedFaviconService(@All List<FaviconFetcher> faviconFetchers) throws IOException {
this.faviconFetchers = faviconFetchers; this.faviconFetchers = faviconFetchers;
this.defaultFavicon = new Favicon( this.defaultFavicon =
Resources.toByteArray(Objects.requireNonNull(getClass().getResource("/images/default_favicon.gif"))), "image/gif"); new Favicon(
Resources.toByteArray(
Objects.requireNonNull(
getClass().getResource("/images/default_favicon.gif"))),
"image/gif");
} }
public Favicon fetchFavicon(Feed feed) { public Favicon fetchFavicon(Feed feed) {

View File

@@ -1,16 +1,13 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.time.Instant;
import jakarta.inject.Singleton;
import com.commafeed.backend.Digests; import com.commafeed.backend.Digests;
import com.commafeed.backend.Urls; import com.commafeed.backend.Urls;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.Models; import com.commafeed.backend.model.Models;
import jakarta.inject.Singleton;
import java.time.Instant;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@Singleton @Singleton
@@ -42,5 +39,4 @@ public class FeedService {
feed.setEtagHeader(FeedUtils.truncate(feed.getEtagHeader(), 255)); feed.setEtagHeader(FeedUtils.truncate(feed.getEtagHeader(), 255));
feedDAO.merge(feed); feedDAO.merge(feed);
} }
} }

View File

@@ -1,12 +1,5 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import jakarta.inject.Singleton;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.Urls; import com.commafeed.backend.Urls;
import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO;
@@ -18,7 +11,11 @@ import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.UnreadCount; import com.commafeed.frontend.model.UnreadCount;
import jakarta.inject.Singleton;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@@ -31,8 +28,12 @@ public class FeedSubscriptionService {
private final FeedRefreshEngine feedRefreshEngine; private final FeedRefreshEngine feedRefreshEngine;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
public FeedSubscriptionService(FeedEntryStatusDAO feedEntryStatusDAO, FeedSubscriptionDAO feedSubscriptionDAO, FeedService feedService, public FeedSubscriptionService(
FeedRefreshEngine feedRefreshEngine, CommaFeedConfiguration config) { FeedEntryStatusDAO feedEntryStatusDAO,
FeedSubscriptionDAO feedSubscriptionDAO,
FeedService feedService,
FeedRefreshEngine feedRefreshEngine,
CommaFeedConfiguration config) {
this.feedEntryStatusDAO = feedEntryStatusDAO; this.feedEntryStatusDAO = feedEntryStatusDAO;
this.feedSubscriptionDAO = feedSubscriptionDAO; this.feedSubscriptionDAO = feedSubscriptionDAO;
this.feedService = feedService; this.feedService = feedService;
@@ -40,19 +41,26 @@ public class FeedSubscriptionService {
this.config = config; this.config = config;
// automatically refresh new feeds after they are subscribed to // automatically refresh new feeds after they are subscribed to
// we need to use this hook because the feed needs to have been persisted before being processed by the feed engine // we need to use this hook because the feed needs to have been persisted before being
feedSubscriptionDAO.onPostCommitInsert(sub -> { // processed
// by the feed engine
feedSubscriptionDAO.onPostCommitInsert(
sub -> {
Feed feed = sub.getFeed(); Feed feed = sub.getFeed();
if (feed.getDisabledUntil() == null || feed.getDisabledUntil().isBefore(Instant.now())) { if (feed.getDisabledUntil() == null
|| feed.getDisabledUntil().isBefore(Instant.now())) {
feedRefreshEngine.refreshImmediately(feed); feedRefreshEngine.refreshImmediately(feed);
} }
}); });
} }
public long subscribe(User user, String url, String title, FeedCategory category, int position) { public long subscribe(
User user, String url, String title, FeedCategory category, int position) {
Integer maxFeedsPerUser = config.database().cleanup().maxFeedsPerUser(); Integer maxFeedsPerUser = config.database().cleanup().maxFeedsPerUser();
if (maxFeedsPerUser > 0 && feedSubscriptionDAO.count(user) >= maxFeedsPerUser) { if (maxFeedsPerUser > 0 && feedSubscriptionDAO.count(user) >= maxFeedsPerUser) {
String message = String.format("You cannot subscribe to more feeds on this CommaFeed instance (max %s feeds per user)", String message =
String.format(
"You cannot subscribe to more feeds on this CommaFeed instance (max %s feeds per user)",
maxFeedsPerUser); maxFeedsPerUser);
throw new FeedSubscriptionException(message); throw new FeedSubscriptionException(message);
} }
@@ -88,7 +96,10 @@ public class FeedSubscriptionService {
public void refreshAll(User user) throws ForceFeedRefreshTooSoonException { public void refreshAll(User user) throws ForceFeedRefreshTooSoonException {
Instant lastForceRefresh = user.getLastForceRefresh(); Instant lastForceRefresh = user.getLastForceRefresh();
if (lastForceRefresh != null && lastForceRefresh.plus(config.feedRefresh().forceRefreshCooldownDuration()).isAfter(Instant.now())) { if (lastForceRefresh != null
&& lastForceRefresh
.plus(config.feedRefresh().forceRefreshCooldownDuration())
.isAfter(Instant.now())) {
throw new ForceFeedRefreshTooSoonException(); throw new ForceFeedRefreshTooSoonException();
} }
@@ -102,9 +113,10 @@ public class FeedSubscriptionService {
} }
public Map<Long, UnreadCount> getUnreadCount(User user) { public Map<Long, UnreadCount> getUnreadCount(User user) {
return feedSubscriptionDAO.findAll(user) return feedSubscriptionDAO.findAll(user).stream()
.stream() .collect(
.collect(Collectors.toMap(FeedSubscription::getId, feedEntryStatusDAO::getUnreadCount)); Collectors.toMap(
FeedSubscription::getId, feedEntryStatusDAO::getUnreadCount));
} }
@SuppressWarnings("serial") @SuppressWarnings("serial")
@@ -120,5 +132,4 @@ public class FeedSubscriptionService {
super(); super();
} }
} }
} }

View File

@@ -1,11 +1,9 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import jakarta.inject.Singleton;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import io.quarkus.mailer.Mail; import io.quarkus.mailer.Mail;
import io.quarkus.mailer.Mailer; import io.quarkus.mailer.Mailer;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor

View File

@@ -1,20 +1,16 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import jakarta.inject.Singleton;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.spec.KeySpec; import java.security.spec.KeySpec;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory; import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.PBEKeySpec;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
// taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html // taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html
@Slf4j @Slf4j
@@ -89,5 +85,4 @@ public class PasswordEncryptionService {
} }
return salt; return salt;
} }
} }

View File

@@ -1,12 +1,20 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpClientFactory;
import com.commafeed.backend.Urls;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserSettings.PushNotificationUserSettings;
import io.vertx.core.json.JsonObject;
import jakarta.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import lombok.extern.slf4j.Slf4j;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.config.RequestConfig;
@@ -18,18 +26,6 @@ import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicNameValuePair; import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.util.Timeout; import org.apache.hc.core5.util.Timeout;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpClientFactory;
import com.commafeed.backend.Urls;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserSettings.PushNotificationUserSettings;
import io.vertx.core.json.JsonObject;
import lombok.extern.slf4j.Slf4j;
@Singleton @Singleton
@Slf4j @Slf4j
public class PushNotificationService { public class PushNotificationService {
@@ -38,18 +34,25 @@ public class PushNotificationService {
private final Meter meter; private final Meter meter;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
public PushNotificationService(HttpClientFactory httpClientFactory, MetricRegistry metrics, CommaFeedConfiguration config) { public PushNotificationService(
HttpClientFactory httpClientFactory,
MetricRegistry metrics,
CommaFeedConfiguration config) {
this.httpClient = httpClientFactory.newClient(config.pushNotifications().threads()); this.httpClient = httpClientFactory.newClient(config.pushNotifications().threads());
this.meter = metrics.meter(MetricRegistry.name(getClass(), "notify")); this.meter = metrics.meter(MetricRegistry.name(getClass(), "notify"));
this.config = config; this.config = config;
} }
public void notify(PushNotificationUserSettings settings, FeedSubscription subscription, FeedEntry entry) { public void notify(
PushNotificationUserSettings settings, FeedSubscription subscription, FeedEntry entry) {
if (!config.pushNotifications().enabled() || settings.getType() == null) { if (!config.pushNotifications().enabled() || settings.getType() == null) {
return; return;
} }
log.debug("sending {} push notification for entry {} in feed {}", settings.getType(), entry.getId(), log.debug(
"sending {} push notification for entry {} in feed {}",
settings.getType(),
entry.getId(),
subscription.getFeed().getId()); subscription.getFeed().getId());
String entryTitle = entry.getContent() != null ? entry.getContent().getTitle() : null; String entryTitle = entry.getContent() != null ? entry.getContent().getTitle() : null;
String entryUrl = entry.getUrl(); String entryUrl = entry.getUrl();
@@ -64,7 +67,9 @@ public class PushNotificationService {
case NTFY -> sendNtfy(settings, feedTitle, entryTitle, entryUrl); case NTFY -> sendNtfy(settings, feedTitle, entryTitle, entryUrl);
case GOTIFY -> sendGotify(settings, feedTitle, entryTitle, entryUrl); case GOTIFY -> sendGotify(settings, feedTitle, entryTitle, entryUrl);
case PUSHOVER -> sendPushover(settings, feedTitle, entryTitle, entryUrl); case PUSHOVER -> sendPushover(settings, feedTitle, entryTitle, entryUrl);
default -> throw new IllegalStateException("unsupported notification type: " + settings.getType()); default ->
throw new IllegalStateException(
"unsupported notification type: " + settings.getType());
} }
} catch (IOException e) { } catch (IOException e) {
throw new PushNotificationException("Failed to send external notification", e); throw new PushNotificationException("Failed to send external notification", e);
@@ -73,7 +78,12 @@ public class PushNotificationService {
meter.mark(); meter.mark();
} }
private void sendNtfy(PushNotificationUserSettings settings, String feedTitle, String entryTitle, String entryUrl) throws IOException { private void sendNtfy(
PushNotificationUserSettings settings,
String feedTitle,
String entryTitle,
String entryUrl)
throws IOException {
String serverUrl = Urls.removeTrailingSlash(settings.getServerUrl()); String serverUrl = Urls.removeTrailingSlash(settings.getServerUrl());
String topic = settings.getTopic(); String topic = settings.getTopic();
@@ -83,7 +93,10 @@ public class PushNotificationService {
} }
HttpPost request = new HttpPost(serverUrl + "/" + topic); HttpPost request = new HttpPost(serverUrl + "/" + topic);
request.setConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build()); request.setConfig(
RequestConfig.custom()
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
.build());
request.addHeader("Title", feedTitle); request.addHeader("Title", feedTitle);
request.setEntity(new StringEntity(entryTitle, StandardCharsets.UTF_8)); request.setEntity(new StringEntity(entryTitle, StandardCharsets.UTF_8));
@@ -95,15 +108,22 @@ public class PushNotificationService {
request.addHeader("Authorization", "Bearer " + settings.getUserSecret()); request.addHeader("Authorization", "Bearer " + settings.getUserSecret());
} }
httpClient.execute(request, response -> { httpClient.execute(
request,
response -> {
if (response.getCode() >= 400) { if (response.getCode() >= 400) {
throw new PushNotificationException("ntfy notification failed with status " + response.getCode()); throw new PushNotificationException(
"ntfy notification failed with status " + response.getCode());
} }
return null; return null;
}); });
} }
private void sendGotify(PushNotificationUserSettings settings, String feedTitle, String entryTitle, String entryUrl) private void sendGotify(
PushNotificationUserSettings settings,
String feedTitle,
String entryTitle,
String entryUrl)
throws IOException { throws IOException {
String serverUrl = Urls.removeTrailingSlash(settings.getServerUrl()); String serverUrl = Urls.removeTrailingSlash(settings.getServerUrl());
String token = settings.getUserSecret(); String token = settings.getUserSecret();
@@ -118,24 +138,39 @@ public class PushNotificationService {
json.put("message", entryTitle); json.put("message", entryTitle);
json.put("priority", 5); json.put("priority", 5);
if (StringUtils.isNotBlank(entryUrl)) { if (StringUtils.isNotBlank(entryUrl)) {
json.put("extras", json.put(
new JsonObject().put("client::notification", new JsonObject().put("click", new JsonObject().put("url", entryUrl)))); "extras",
new JsonObject()
.put(
"client::notification",
new JsonObject()
.put("click", new JsonObject().put("url", entryUrl))));
} }
HttpPost request = new HttpPost(serverUrl + "/message"); HttpPost request = new HttpPost(serverUrl + "/message");
request.setConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build()); request.setConfig(
RequestConfig.custom()
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
.build());
request.addHeader("X-Gotify-Key", token); request.addHeader("X-Gotify-Key", token);
request.setEntity(new StringEntity(json.toString(), ContentType.APPLICATION_JSON)); request.setEntity(new StringEntity(json.toString(), ContentType.APPLICATION_JSON));
httpClient.execute(request, response -> { httpClient.execute(
request,
response -> {
if (response.getCode() >= 400) { if (response.getCode() >= 400) {
throw new PushNotificationException("gotify notification failed with status " + response.getCode()); throw new PushNotificationException(
"gotify notification failed with status " + response.getCode());
} }
return null; return null;
}); });
} }
private void sendPushover(PushNotificationUserSettings settings, String feedTitle, String entryTitle, String entryUrl) private void sendPushover(
PushNotificationUserSettings settings,
String feedTitle,
String entryTitle,
String entryUrl)
throws IOException { throws IOException {
String token = settings.getUserSecret(); String token = settings.getUserSecret();
String userKey = settings.getUserId(); String userKey = settings.getUserId();
@@ -155,12 +190,18 @@ public class PushNotificationService {
} }
HttpPost request = new HttpPost("https://api.pushover.net/1/messages.json"); HttpPost request = new HttpPost("https://api.pushover.net/1/messages.json");
request.setConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build()); request.setConfig(
RequestConfig.custom()
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
.build());
request.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8)); request.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
httpClient.execute(request, response -> { httpClient.execute(
request,
response -> {
if (response.getCode() >= 400) { if (response.getCode() >= 400) {
throw new PushNotificationException("pushover notification failed with status " + response.getCode()); throw new PushNotificationException(
"pushover notification failed with status " + response.getCode());
} }
return null; return null;
}); });

View File

@@ -1,16 +1,5 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import jakarta.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedConstants; import com.commafeed.CommaFeedConstants;
import com.commafeed.backend.Digests; import com.commafeed.backend.Digests;
@@ -24,8 +13,15 @@ import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.service.internal.PostLoginActivities; import com.commafeed.backend.service.internal.PostLoginActivities;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import jakarta.inject.Singleton;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
@RequiredArgsConstructor @RequiredArgsConstructor
@Singleton @Singleton
@@ -42,9 +38,7 @@ public class UserService {
private final PostLoginActivities postLoginActivities; private final PostLoginActivities postLoginActivities;
/** /** try to log in with given credentials */
* try to log in with given credentials
*/
public Optional<User> login(String nameOrEmail, String password) { public Optional<User> login(String nameOrEmail, String password) {
if (nameOrEmail == null || password == null) { if (nameOrEmail == null || password == null) {
return Optional.empty(); return Optional.empty();
@@ -55,7 +49,8 @@ public class UserService {
user = userDAO.findByEmail(nameOrEmail); user = userDAO.findByEmail(nameOrEmail);
} }
if (user != null && !user.isDisabled()) { if (user != null && !user.isDisabled()) {
boolean authenticated = encryptionService.authenticate(password, user.getPassword(), user.getSalt()); boolean authenticated =
encryptionService.authenticate(password, user.getPassword(), user.getSalt());
if (authenticated) { if (authenticated) {
performPostLoginActivities(user); performPostLoginActivities(user);
return Optional.of(user); return Optional.of(user);
@@ -64,9 +59,7 @@ public class UserService {
return Optional.empty(); return Optional.empty();
} }
/** /** try to log in with given api key */
* try to log in with given api key
*/
public Optional<User> login(String apiKey) { public Optional<User> login(String apiKey) {
if (apiKey == null) { if (apiKey == null) {
return Optional.empty(); return Optional.empty();
@@ -80,9 +73,7 @@ public class UserService {
return Optional.empty(); return Optional.empty();
} }
/** /** try to log in with given fever api key */
* try to log in with given fever api key
*/
public Optional<User> login(long userId, String feverApiKey) { public Optional<User> login(long userId, String feverApiKey) {
if (feverApiKey == null) { if (feverApiKey == null) {
return Optional.empty(); return Optional.empty();
@@ -102,9 +93,7 @@ public class UserService {
return Optional.of(user); return Optional.of(user);
} }
/** /** should triggers after successful login */
* should triggers after successful login
*/
public void performPostLoginActivities(User user) { public void performPostLoginActivities(User user) {
postLoginActivities.executeFor(user); postLoginActivities.executeFor(user);
} }
@@ -113,10 +102,17 @@ public class UserService {
return register(name, password, email, roles, false); return register(name, password, email, roles, false);
} }
public User register(String name, String password, String email, Collection<Role> roles, boolean forceRegistration) { public User register(
String name,
String password,
String email,
Collection<Role> roles,
boolean forceRegistration) {
if (!forceRegistration) { if (!forceRegistration) {
Preconditions.checkState(config.users().allowRegistrations(), "Registrations are closed on this CommaFeed instance"); Preconditions.checkState(
config.users().allowRegistrations(),
"Registrations are closed on this CommaFeed instance");
} }
Preconditions.checkArgument(userDAO.findByName(name) == null, "Name already taken"); Preconditions.checkArgument(userDAO.findByName(name) == null, "Name already taken");
@@ -139,7 +135,12 @@ public class UserService {
} }
public void createDemoUser() { public void createDemoUser() {
register(CommaFeedConstants.USERNAME_DEMO, "demo", "demo@commafeed.com", Collections.singletonList(Role.USER), true); register(
CommaFeedConstants.USERNAME_DEMO,
"demo",
"demo@commafeed.com",
Collections.singletonList(Role.USER),
true);
} }
public void unregister(User user) { public void unregister(User user) {
@@ -151,7 +152,9 @@ public class UserService {
} }
public String generateApiKey(User user) { public String generateApiKey(User user) {
byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID().toString(), user.getSalt()); byte[] key =
encryptionService.getEncryptedPassword(
UUID.randomUUID().toString(), user.getSalt());
return Digests.sha1Hex(key); return Digests.sha1Hex(key);
} }

View File

@@ -1,10 +1,5 @@
package com.commafeed.backend.service.db; package com.commafeed.backend.service.db;
import java.time.Instant;
import java.util.List;
import jakarta.inject.Singleton;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
@@ -16,13 +11,12 @@ import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.AbstractModel; import com.commafeed.backend.model.AbstractModel;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import jakarta.inject.Singleton;
import java.time.Instant;
import java.util.List;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /** Contains utility methods for cleaning the database */
* Contains utility methods for cleaning the database
*
*/
@Slf4j @Slf4j
@Singleton @Singleton
public class DatabaseCleaningService { public class DatabaseCleaningService {
@@ -36,8 +30,14 @@ public class DatabaseCleaningService {
private final boolean keepStarredEntries; private final boolean keepStarredEntries;
private final Meter entriesDeletedMeter; private final Meter entriesDeletedMeter;
public DatabaseCleaningService(CommaFeedConfiguration config, UnitOfWork unitOfWork, FeedDAO feedDAO, FeedEntryDAO feedEntryDAO, public DatabaseCleaningService(
FeedEntryContentDAO feedEntryContentDAO, FeedEntryStatusDAO feedEntryStatusDAO, MetricRegistry metrics) { CommaFeedConfiguration config,
UnitOfWork unitOfWork,
FeedDAO feedDAO,
FeedEntryDAO feedEntryDAO,
FeedEntryContentDAO feedEntryContentDAO,
FeedEntryStatusDAO feedEntryStatusDAO,
MetricRegistry metrics) {
this.unitOfWork = unitOfWork; this.unitOfWork = unitOfWork;
this.feedDAO = feedDAO; this.feedDAO = feedDAO;
this.feedEntryDAO = feedEntryDAO; this.feedEntryDAO = feedEntryDAO;
@@ -68,13 +68,21 @@ public class DatabaseCleaningService {
return; return;
} }
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize)); entriesDeleted =
unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize));
entriesDeletedMeter.mark(entriesDeleted); entriesDeletedMeter.mark(entriesDeleted);
entriesTotal += entriesDeleted; entriesTotal += entriesDeleted;
log.debug("removed {} entries for feeds without subscriptions", entriesTotal); log.debug("removed {} entries for feeds without subscriptions", entriesTotal);
} while (entriesDeleted > 0); } while (entriesDeleted > 0);
} }
deleted = unitOfWork.call(() -> feedDAO.delete(feedDAO.findByIds(feeds.stream().map(AbstractModel::getId).toList()))); deleted =
unitOfWork.call(
() ->
feedDAO.delete(
feedDAO.findByIds(
feeds.stream()
.map(AbstractModel::getId)
.toList())));
total += deleted; total += deleted;
log.debug("removed {} feeds without subscriptions", total); log.debug("removed {} feeds without subscriptions", total);
} while (deleted != 0); } while (deleted != 0);
@@ -107,8 +115,11 @@ public class DatabaseCleaningService {
return; return;
} }
List<FeedCapacity> feeds = unitOfWork List<FeedCapacity> feeds =
.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize, keepStarredEntries)); unitOfWork.call(
() ->
feedEntryDAO.findFeedsExceedingCapacity(
maxFeedCapacity, batchSize, keepStarredEntries));
if (feeds.isEmpty()) { if (feeds.isEmpty()) {
break; break;
} }
@@ -118,12 +129,19 @@ public class DatabaseCleaningService {
int deleted; int deleted;
do { do {
if (Thread.currentThread().isInterrupted()) { if (Thread.currentThread().isInterrupted()) {
log.info("interrupted, stopping cleanup of entries exceeding feed capacity"); log.info(
"interrupted, stopping cleanup of entries exceeding feed capacity");
return; return;
} }
final long rem = remaining; final long rem = remaining;
deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.id(), Math.min(batchSize, rem), keepStarredEntries)); deleted =
unitOfWork.call(
() ->
feedEntryDAO.deleteOldEntries(
feed.id(),
Math.min(batchSize, rem),
keepStarredEntries));
entriesDeletedMeter.mark(deleted); entriesDeletedMeter.mark(deleted);
total += deleted; total += deleted;
remaining -= deleted; remaining -= deleted;
@@ -144,7 +162,11 @@ public class DatabaseCleaningService {
return; return;
} }
deleted = unitOfWork.call(() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize, keepStarredEntries)); deleted =
unitOfWork.call(
() ->
feedEntryDAO.deleteEntriesOlderThan(
olderThan, batchSize, keepStarredEntries));
entriesDeletedMeter.mark(deleted); entriesDeletedMeter.mark(deleted);
total += deleted; total += deleted;
log.debug("removed {} old entries", total); log.debug("removed {} old entries", total);
@@ -162,7 +184,9 @@ public class DatabaseCleaningService {
return; return;
} }
deleted = unitOfWork.call(() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize)); deleted =
unitOfWork.call(
() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize));
total += deleted; total += deleted;
log.debug("removed {} old read statuses", total); log.debug("removed {} old read statuses", total);
} while (deleted != 0); } while (deleted != 0);

View File

@@ -1,17 +1,14 @@
package com.commafeed.backend.service.db; package com.commafeed.backend.service.db;
import jakarta.inject.Singleton;
import org.kohsuke.MetaInfServices;
import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserDAO;
import jakarta.inject.Singleton;
import liquibase.database.Database; import liquibase.database.Database;
import liquibase.database.core.PostgresDatabase; import liquibase.database.core.PostgresDatabase;
import liquibase.structure.DatabaseObject; import liquibase.structure.DatabaseObject;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.kohsuke.MetaInfServices;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -26,12 +23,14 @@ public class DatabaseStartupService {
} }
/** /**
* Register a postgresql database in liquibase that doesn't escape columns, so that we can use lower case columns * Register a postgresql database in liquibase that doesn't escape columns, so that we can use
* lower case columns
*/ */
@MetaInfServices(Database.class) @MetaInfServices(Database.class)
public static class LowerCaseColumnsPostgresDatabase extends PostgresDatabase { public static class LowerCaseColumnsPostgresDatabase extends PostgresDatabase {
@Override @Override
public String escapeObjectName(String objectName, Class<? extends DatabaseObject> objectType) { public String escapeObjectName(
String objectName, Class<? extends DatabaseObject> objectType) {
return objectName; return objectName;
} }
@@ -40,5 +39,4 @@ public class DatabaseStartupService {
return super.getPriority() + 1; return super.getPriority() + 1;
} }
} }
} }

View File

@@ -1,14 +1,11 @@
package com.commafeed.backend.service.internal; package com.commafeed.backend.service.internal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import jakarta.inject.Singleton;
import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import jakarta.inject.Singleton;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -19,7 +16,9 @@ public class PostLoginActivities {
private final UnitOfWork unitOfWork; private final UnitOfWork unitOfWork;
public void executeFor(User user) { public void executeFor(User user) {
// only update lastLogin every once in a while in order to avoid invalidating the cache every time someone logs in // only update lastLogin every once in a while in order to avoid invalidating the cache
// every
// time someone logs in
Instant now = Instant.now(); Instant now = Instant.now();
Instant lastLogin = user.getLastLogin(); Instant lastLogin = user.getLastLogin();
if (lastLogin == null || ChronoUnit.MINUTES.between(lastLogin, now) >= 30) { if (lastLogin == null || ChronoUnit.MINUTES.between(lastLogin, now) >= 30) {

View File

@@ -1,11 +1,8 @@
package com.commafeed.backend.task; package com.commafeed.backend.task;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Singleton;
import com.commafeed.backend.service.db.DatabaseCleaningService; import com.commafeed.backend.service.db.DatabaseCleaningService;
import jakarta.inject.Singleton;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -33,5 +30,4 @@ public class AutoMarkAsReadTask extends ScheduledTask {
public TimeUnit getTimeUnit() { public TimeUnit getTimeUnit() {
return TimeUnit.MINUTES; return TimeUnit.MINUTES;
} }
} }

View File

@@ -1,16 +1,13 @@
package com.commafeed.backend.task; package com.commafeed.backend.task;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Singleton;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedConstants; import com.commafeed.CommaFeedConstants;
import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.service.UserService; import com.commafeed.backend.service.UserService;
import jakarta.inject.Singleton;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -31,7 +28,8 @@ public class DemoAccountCleanupTask extends ScheduledTask {
} }
log.info("recreating demo user account"); log.info("recreating demo user account");
unitOfWork.run(() -> { unitOfWork.run(
() -> {
User demoUser = userDAO.findByName(CommaFeedConstants.USERNAME_DEMO); User demoUser = userDAO.findByName(CommaFeedConstants.USERNAME_DEMO);
if (demoUser == null) { if (demoUser == null) {
return; return;
@@ -40,7 +38,6 @@ public class DemoAccountCleanupTask extends ScheduledTask {
userService.unregister(demoUser); userService.unregister(demoUser);
userService.createDemoUser(); userService.createDemoUser();
}); });
} }
@Override @Override
@@ -57,5 +54,4 @@ public class DemoAccountCleanupTask extends ScheduledTask {
protected TimeUnit getTimeUnit() { protected TimeUnit getTimeUnit() {
return TimeUnit.MINUTES; return TimeUnit.MINUTES;
} }
} }

View File

@@ -1,12 +1,9 @@
package com.commafeed.backend.task; package com.commafeed.backend.task;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Singleton;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.service.db.DatabaseCleaningService; import com.commafeed.backend.service.db.DatabaseCleaningService;
import jakarta.inject.Singleton;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -38,5 +35,4 @@ public class EntriesExceedingFeedCapacityCleanupTask extends ScheduledTask {
public TimeUnit getTimeUnit() { public TimeUnit getTimeUnit() {
return TimeUnit.MINUTES; return TimeUnit.MINUTES;
} }
} }

View File

@@ -1,14 +1,11 @@
package com.commafeed.backend.task; package com.commafeed.backend.task;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Singleton;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.service.db.DatabaseCleaningService; import com.commafeed.backend.service.db.DatabaseCleaningService;
import jakarta.inject.Singleton;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -41,5 +38,4 @@ public class OldEntriesCleanupTask extends ScheduledTask {
public TimeUnit getTimeUnit() { public TimeUnit getTimeUnit() {
return TimeUnit.MINUTES; return TimeUnit.MINUTES;
} }
} }

View File

@@ -1,13 +1,10 @@
package com.commafeed.backend.task; package com.commafeed.backend.task;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Singleton;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.service.db.DatabaseCleaningService; import com.commafeed.backend.service.db.DatabaseCleaningService;
import jakarta.inject.Singleton;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -39,5 +36,4 @@ public class OldStatusesCleanupTask extends ScheduledTask {
public TimeUnit getTimeUnit() { public TimeUnit getTimeUnit() {
return TimeUnit.MINUTES; return TimeUnit.MINUTES;
} }
} }

View File

@@ -1,11 +1,8 @@
package com.commafeed.backend.task; package com.commafeed.backend.task;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Singleton;
import com.commafeed.backend.service.db.DatabaseCleaningService; import com.commafeed.backend.service.db.DatabaseCleaningService;
import jakarta.inject.Singleton;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -33,5 +30,4 @@ public class OrphanedContentsCleanupTask extends ScheduledTask {
public TimeUnit getTimeUnit() { public TimeUnit getTimeUnit() {
return TimeUnit.MINUTES; return TimeUnit.MINUTES;
} }
} }

View File

@@ -1,11 +1,8 @@
package com.commafeed.backend.task; package com.commafeed.backend.task;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Singleton;
import com.commafeed.backend.service.db.DatabaseCleaningService; import com.commafeed.backend.service.db.DatabaseCleaningService;
import jakarta.inject.Singleton;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -33,5 +30,4 @@ public class OrphanedFeedsCleanupTask extends ScheduledTask {
public TimeUnit getTimeUnit() { public TimeUnit getTimeUnit() {
return TimeUnit.MINUTES; return TimeUnit.MINUTES;
} }
} }

View File

@@ -2,7 +2,6 @@ package com.commafeed.backend.task;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@@ -16,15 +15,21 @@ public abstract class ScheduledTask {
protected abstract TimeUnit getTimeUnit(); protected abstract TimeUnit getTimeUnit();
public void register(ScheduledExecutorService executor) { public void register(ScheduledExecutorService executor) {
Runnable runnable = () -> { Runnable runnable =
() -> {
try { try {
ScheduledTask.this.run(); ScheduledTask.this.run();
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
} }
}; };
log.debug("registering task {} for execution every {} {}, starting in {} {}", getClass().getSimpleName(), getPeriod(), log.debug(
getTimeUnit(), getInitialDelay(), getTimeUnit()); "registering task {} for execution every {} {}, starting in {} {}",
getClass().getSimpleName(),
getPeriod(),
getTimeUnit(),
getInitialDelay(),
getTimeUnit());
executor.scheduleWithFixedDelay(runnable, getInitialDelay(), getPeriod(), getTimeUnit()); executor.scheduleWithFixedDelay(runnable, getInitialDelay(), getPeriod(), getTimeUnit());
} }
} }

View File

@@ -1,15 +1,12 @@
package com.commafeed.backend.task; package com.commafeed.backend.task;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import jakarta.inject.Singleton;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import io.quarkus.arc.All; import io.quarkus.arc.All;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j

View File

@@ -2,11 +2,8 @@ package com.commafeed.backend.urlprovider;
import java.util.List; import java.util.List;
/** /** Tries to find a feed url given the url and page content */
* Tries to find a feed url given the url and page content
*/
public interface FeedURLProvider { public interface FeedURLProvider {
List<String> get(String url, String urlContent); List<String> get(String url, String urlContent);
} }

View File

@@ -1,10 +1,8 @@
package com.commafeed.backend.urlprovider; package com.commafeed.backend.urlprovider;
import jakarta.inject.Singleton;
import java.util.List; import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
import jakarta.inject.Singleton;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
@@ -17,9 +15,10 @@ public class InPageReferenceFeedURLProvider implements FeedURLProvider {
if (!"html".equals(doc.children().getFirst().tagName())) { if (!"html".equals(doc.children().getFirst().tagName())) {
return List.of(); return List.of();
} }
return Stream.concat(doc.select("link[type=application/atom+xml]").stream(), doc.select("link[type=application/rss+xml]").stream()) return Stream.concat(
doc.select("link[type=application/atom+xml]").stream(),
doc.select("link[type=application/rss+xml]").stream())
.map(node -> node.attr("abs:href")) .map(node -> node.attr("abs:href"))
.toList(); .toList();
} }
} }

View File

@@ -1,22 +1,21 @@
package com.commafeed.backend.urlprovider; package com.commafeed.backend.urlprovider;
import java.util.List;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
import java.util.List;
import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.Strings;
/** /**
* Workaround for Youtube channels * Workaround for Youtube channels
* *
* converts the channel URL https://www.youtube.com/channel/CHANNEL_ID to the valid feed URL * <p>converts the channel URL https://www.youtube.com/channel/CHANNEL_ID to the valid feed URL
* https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID * https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID
*/ */
@Singleton @Singleton
public class YoutubeFeedURLProvider implements FeedURLProvider { public class YoutubeFeedURLProvider implements FeedURLProvider {
private static final String PREFIX = "https://www.youtube.com/channel/"; private static final String PREFIX = "https://www.youtube.com/channel/";
private static final String REPLACEMENT_PREFIX = "https://www.youtube.com/feeds/videos.xml?channel_id="; private static final String REPLACEMENT_PREFIX =
"https://www.youtube.com/feeds/videos.xml?channel_id=";
@Override @Override
public List<String> get(String url, String urlContent) { public List<String> get(String url, String urlContent) {
@@ -26,5 +25,4 @@ public class YoutubeFeedURLProvider implements FeedURLProvider {
return List.of(REPLACEMENT_PREFIX + url.substring(PREFIX.length())); return List.of(REPLACEMENT_PREFIX + url.substring(PREFIX.length()));
} }
} }

View File

@@ -1,13 +1,11 @@
package com.commafeed.frontend.model; package com.commafeed.frontend.model;
import io.quarkus.runtime.annotations.RegisterForReflection;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.Data; import lombok.Data;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Schema(description = "Entry details") @Schema(description = "Entry details")

View File

@@ -1,13 +1,11 @@
package com.commafeed.frontend.model; package com.commafeed.frontend.model;
import io.quarkus.runtime.annotations.RegisterForReflection;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.Data; import lombok.Data;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Schema(description = "List of entries with some metadata") @Schema(description = "List of entries with some metadata")
@@ -24,7 +22,9 @@ public class Entries implements Serializable {
@Schema(description = "times the server tried to refresh the feed and failed", required = true) @Schema(description = "times the server tried to refresh the feed and failed", required = true)
private int errorCount; private int errorCount;
@Schema(description = "URL of the website, extracted from the feed, only filled if querying for feed entries, not category entries") @Schema(
description =
"URL of the website, extracted from the feed, only filled if querying for feed entries, not category entries")
private String feedLink; private String feedLink;
@Schema(description = "list generation timestamp", required = true) @Schema(description = "list generation timestamp", required = true)
@@ -43,8 +43,8 @@ public class Entries implements Serializable {
private List<Entry> entries = new ArrayList<>(); private List<Entry> entries = new ArrayList<>();
@Schema( @Schema(
description = "if true, the unread flag was ignored in the request, all entries are returned regardless of their read status", description =
"if true, the unread flag was ignored in the request, all entries are returned regardless of their read status",
required = true) required = true)
private boolean ignoredReadStatus; private boolean ignoredReadStatus;
} }

View File

@@ -1,22 +1,19 @@
package com.commafeed.frontend.model; package com.commafeed.frontend.model;
import java.io.Serializable;
import java.time.Instant;
import java.util.List;
import org.apache.commons.lang3.Strings;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import java.io.Serializable;
import java.time.Instant;
import java.util.List;
import lombok.Data; import lombok.Data;
import org.apache.commons.lang3.Strings;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Schema(description = "Entry details") @Schema(description = "Entry details")
@@ -66,7 +63,10 @@ public class Entry implements Serializable {
@Schema(description = "entry publication date", type = SchemaType.INTEGER, required = true) @Schema(description = "entry publication date", type = SchemaType.INTEGER, required = true)
private Instant date; private Instant date;
@Schema(description = "entry insertion date in the database", type = SchemaType.INTEGER, required = true) @Schema(
description = "entry insertion date in the database",
type = SchemaType.INTEGER,
required = true)
private Instant insertedDate; private Instant insertedDate;
@Schema(description = "feed id", required = true) @Schema(description = "feed id", required = true)
@@ -93,7 +93,9 @@ public class Entry implements Serializable {
@Schema(description = "starred status", required = true) @Schema(description = "starred status", required = true)
private boolean starred; private boolean starred;
@Schema(description = "whether the entry is still markable (old entry statuses are discarded)", required = true) @Schema(
description = "whether the entry is still markable (old entry statuses are discarded)",
required = true)
private boolean markable; private boolean markable;
@Schema(description = "tags", required = true) @Schema(description = "tags", required = true)
@@ -124,16 +126,23 @@ public class Entry implements Serializable {
if (content != null) { if (content != null) {
entry.setRtl(content.getDirection() == FeedEntryContent.Direction.RTL); entry.setRtl(content.getDirection() == FeedEntryContent.Direction.RTL);
entry.setTitle(content.getTitle()); entry.setTitle(content.getTitle());
entry.setContent(proxyImages ? FeedUtils.proxyImages(content.getContent()) : content.getContent()); entry.setContent(
proxyImages
? FeedUtils.proxyImages(content.getContent())
: content.getContent());
entry.setAuthor(content.getAuthor()); entry.setAuthor(content.getAuthor());
entry.setEnclosureType(content.getEnclosureType()); entry.setEnclosureType(content.getEnclosureType());
entry.setEnclosureUrl(proxyImages && Strings.CS.contains(content.getEnclosureType(), "image") entry.setEnclosureUrl(
proxyImages && Strings.CS.contains(content.getEnclosureType(), "image")
? FeedUtils.proxyImage(content.getEnclosureUrl()) ? FeedUtils.proxyImage(content.getEnclosureUrl())
: content.getEnclosureUrl()); : content.getEnclosureUrl());
entry.setMediaDescription(content.getMediaDescription()); entry.setMediaDescription(content.getMediaDescription());
entry.setMediaThumbnailUrl(proxyImages ? FeedUtils.proxyImage(content.getMediaThumbnailUrl()) : content.getMediaThumbnailUrl()); entry.setMediaThumbnailUrl(
proxyImages
? FeedUtils.proxyImage(content.getMediaThumbnailUrl())
: content.getMediaThumbnailUrl());
entry.setMediaThumbnailWidth(content.getMediaThumbnailWidth()); entry.setMediaThumbnailWidth(content.getMediaThumbnailWidth());
entry.setMediaThumbnailHeight(content.getMediaThumbnailHeight()); entry.setMediaThumbnailHeight(content.getMediaThumbnailHeight());
@@ -142,5 +151,4 @@ public class Entry implements Serializable {
return entry; return entry;
} }
} }

View File

@@ -1,11 +1,9 @@
package com.commafeed.frontend.model; package com.commafeed.frontend.model;
import java.io.Serializable;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import java.io.Serializable;
import lombok.Data; import lombok.Data;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Schema(description = "Feed details") @Schema(description = "Feed details")
@@ -18,5 +16,4 @@ public class FeedInfo implements Serializable {
@Schema(description = "title", required = true) @Schema(description = "title", required = true)
private String title; private String title;
} }

View File

@@ -1,11 +1,9 @@
package com.commafeed.frontend.model; package com.commafeed.frontend.model;
import java.io.Serializable;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import java.io.Serializable;
import lombok.Data; import lombok.Data;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Schema(description = "Server infos") @Schema(description = "Server infos")
@@ -13,8 +11,7 @@ import lombok.Data;
@RegisterForReflection @RegisterForReflection
public class ServerInfo implements Serializable { public class ServerInfo implements Serializable {
@Schema @Schema private String announcement;
private String announcement;
@Schema(required = true) @Schema(required = true)
private String version; private String version;
@@ -54,5 +51,4 @@ public class ServerInfo implements Serializable {
@Schema(required = true) @Schema(required = true)
private boolean pushNotificationsEnabled; private boolean pushNotificationsEnabled;
} }

View File

@@ -1,17 +1,14 @@
package com.commafeed.frontend.model; package com.commafeed.frontend.model;
import java.io.Serializable;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import com.commafeed.backend.model.UserSettings.IconDisplayMode; import com.commafeed.backend.model.UserSettings.IconDisplayMode;
import com.commafeed.backend.model.UserSettings.PushNotificationType; import com.commafeed.backend.model.UserSettings.PushNotificationType;
import com.commafeed.backend.model.UserSettings.ReadingMode; import com.commafeed.backend.model.UserSettings.ReadingMode;
import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.model.UserSettings.ScrollMode; import com.commafeed.backend.model.UserSettings.ScrollMode;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import java.io.Serializable;
import lombok.Data; import lombok.Data;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Schema(description = "User settings") @Schema(description = "User settings")
@@ -28,10 +25,14 @@ public class Settings implements Serializable {
@Schema(description = "user reads entries in ascending or descending order", required = true) @Schema(description = "user reads entries in ascending or descending order", required = true)
private ReadingOrder readingOrder; private ReadingOrder readingOrder;
@Schema(description = "user wants category and feeds with no unread entries shown", required = true) @Schema(
description = "user wants category and feeds with no unread entries shown",
required = true)
private boolean showRead; private boolean showRead;
@Schema(description = "In expanded view, scroll through entries mark them as read", required = true) @Schema(
description = "In expanded view, scroll through entries mark them as read",
required = true)
private boolean scrollMarks; private boolean scrollMarks;
@Schema(description = "user's custom css for the website") @Schema(description = "user's custom css for the website")
@@ -40,31 +41,42 @@ public class Settings implements Serializable {
@Schema(description = "user's custom js for the website") @Schema(description = "user's custom js for the website")
private String customJs; private String customJs;
@Schema(description = "user's preferred scroll speed when navigating between entries", required = true) @Schema(
description = "user's preferred scroll speed when navigating between entries",
required = true)
private int scrollSpeed; private int scrollSpeed;
@Schema(description = "whether to scroll to the selected entry", required = true) @Schema(description = "whether to scroll to the selected entry", required = true)
private ScrollMode scrollMode; private ScrollMode scrollMode;
@Schema(description = "number of entries to keep above the selected entry when scrolling", required = true) @Schema(
description = "number of entries to keep above the selected entry when scrolling",
required = true)
private int entriesToKeepOnTopWhenScrolling; private int entriesToKeepOnTopWhenScrolling;
@Schema(description = "whether to show the star icon in the header of entries", required = true) @Schema(description = "whether to show the star icon in the header of entries", required = true)
private IconDisplayMode starIconDisplayMode; private IconDisplayMode starIconDisplayMode;
@Schema(description = "whether to show the external link icon in the header of entries", required = true) @Schema(
description = "whether to show the external link icon in the header of entries",
required = true)
private IconDisplayMode externalLinkIconDisplayMode; private IconDisplayMode externalLinkIconDisplayMode;
@Schema(description = "ask for confirmation when marking all entries as read", required = true) @Schema(description = "ask for confirmation when marking all entries as read", required = true)
private boolean markAllAsReadConfirmation; private boolean markAllAsReadConfirmation;
@Schema(description = "navigate to the next unread category or feed after marking all entries as read", required = true) @Schema(
description =
"navigate to the next unread category or feed after marking all entries as read",
required = true)
private boolean markAllAsReadNavigateToNextUnread; private boolean markAllAsReadNavigateToNextUnread;
@Schema(description = "show commafeed's own context menu on right click", required = true) @Schema(description = "show commafeed's own context menu on right click", required = true)
private boolean customContextMenu; private boolean customContextMenu;
@Schema(description = "on mobile, show action buttons at the bottom of the screen", required = true) @Schema(
description = "on mobile, show action buttons at the bottom of the screen",
required = true)
private boolean mobileFooter; private boolean mobileFooter;
@Schema(description = "show unread count in the title", required = true) @Schema(description = "show unread count in the title", required = true)

View File

@@ -1,18 +1,15 @@
package com.commafeed.frontend.model; package com.commafeed.frontend.model;
import java.io.Serializable;
import java.time.Instant;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import java.io.Serializable;
import java.time.Instant;
import lombok.Data; import lombok.Data;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Schema(description = "User information") @Schema(description = "User information")
@@ -35,7 +32,10 @@ public class Subscription implements Serializable {
@Schema(description = "last time the feed was refreshed", type = SchemaType.INTEGER) @Schema(description = "last time the feed was refreshed", type = SchemaType.INTEGER)
private Instant lastRefresh; private Instant lastRefresh;
@Schema(description = "next time the feed refresh is planned, null if refresh is already queued", type = SchemaType.INTEGER) @Schema(
description =
"next time the feed refresh is planned, null if refresh is already queued",
type = SchemaType.INTEGER)
private Instant nextRefresh; private Instant nextRefresh;
@Schema(description = "this subscription's feed url", required = true) @Schema(description = "this subscription's feed url", required = true)
@@ -59,16 +59,22 @@ public class Subscription implements Serializable {
@Schema(description = "date of the newest item", type = SchemaType.INTEGER) @Schema(description = "date of the newest item", type = SchemaType.INTEGER)
private Instant newestItemTime; private Instant newestItemTime;
@Schema(description = "CEL string evaluated on new entries to mark them as read if they do not match") @Schema(
description =
"CEL string evaluated on new entries to mark them as read if they do not match")
private String filter; private String filter;
@Schema(description = "JEXL legacy filter") @Schema(description = "JEXL legacy filter")
private String filterLegacy; private String filterLegacy;
@Schema(description = "whether to send push notifications for new entries of this feed", required = true) @Schema(
description = "whether to send push notifications for new entries of this feed",
required = true)
private boolean pushNotificationsEnabled; private boolean pushNotificationsEnabled;
@Schema(description = "automatically mark entries as read after this many days (null to disable)") @Schema(
description =
"automatically mark entries as read after this many days (null to disable)")
private Integer autoMarkAsReadAfterDays; private Integer autoMarkAsReadAfterDays;
public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) { public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) {
@@ -85,7 +91,9 @@ public class Subscription implements Serializable {
sub.setIconUrl(FeedUtils.getFaviconUrl(subscription)); sub.setIconUrl(FeedUtils.getFaviconUrl(subscription));
sub.setLastRefresh(feed.getLastUpdated()); sub.setLastRefresh(feed.getLastUpdated());
sub.setNextRefresh( sub.setNextRefresh(
(feed.getDisabledUntil() != null && feed.getDisabledUntil().isBefore(Instant.now())) ? null : feed.getDisabledUntil()); (feed.getDisabledUntil() != null && feed.getDisabledUntil().isBefore(Instant.now()))
? null
: feed.getDisabledUntil());
sub.setUnread(unreadCount.getUnreadCount()); sub.setUnread(unreadCount.getUnreadCount());
sub.setNewestItemTime(unreadCount.getNewestItemTime()); sub.setNewestItemTime(unreadCount.getNewestItemTime());
sub.setCategoryId(category == null ? null : String.valueOf(category.getId())); sub.setCategoryId(category == null ? null : String.valueOf(category.getId()));
@@ -95,5 +103,4 @@ public class Subscription implements Serializable {
sub.setAutoMarkAsReadAfterDays(subscription.getAutoMarkAsReadAfterDays()); sub.setAutoMarkAsReadAfterDays(subscription.getAutoMarkAsReadAfterDays());
return sub; return sub;
} }
} }

View File

@@ -1,36 +1,30 @@
package com.commafeed.frontend.model; package com.commafeed.frontend.model;
import io.quarkus.runtime.annotations.RegisterForReflection;
import java.io.Serializable; import java.io.Serializable;
import java.time.Instant; import java.time.Instant;
import lombok.Data;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.media.Schema;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.Data;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@Schema(description = "Unread count") @Schema(description = "Unread count")
@Data @Data
@RegisterForReflection @RegisterForReflection
public class UnreadCount implements Serializable { public class UnreadCount implements Serializable {
@Schema @Schema private long feedId;
private long feedId;
@Schema @Schema private long unreadCount;
private long unreadCount;
@Schema(type = SchemaType.INTEGER) @Schema(type = SchemaType.INTEGER)
private Instant newestItemTime; private Instant newestItemTime;
public UnreadCount() { public UnreadCount() {}
}
public UnreadCount(long feedId, long unreadCount, Instant newestItemTime) { public UnreadCount(long feedId, long unreadCount, Instant newestItemTime) {
this.feedId = feedId; this.feedId = feedId;
this.unreadCount = unreadCount; this.unreadCount = unreadCount;
this.newestItemTime = newestItemTime; this.newestItemTime = newestItemTime;
} }
} }

Some files were not shown because too many files have changed in this diff Show More