mirror of
https://github.com/Athou/commafeed.git
synced 2026-10-10 06:15:12 +00:00
use google java formatter to avoid downloading eclipse during build
This commit is contained in:
@@ -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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 {}
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,4 @@ import com.commafeed.backend.model.Feed;
|
|||||||
public interface FaviconFetcher {
|
public interface FaviconFetcher {
|
||||||
|
|
||||||
Favicon fetch(Feed feed);
|
Favicon fetch(Feed feed);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user