Merge pull request #461 from Athou/statuses-revamp

Statuses revamp
This commit is contained in:
Athou
2013-07-26 06:23:06 -07:00
101 changed files with 1773 additions and 1703 deletions

View File

@@ -16,6 +16,12 @@ Deploy on your own server (using TomEE, a lightweight JavaEE6 container based on
[Safari extension](https://github.com/Athou/commafeed-safari) [Safari extension](https://github.com/Athou/commafeed-safari)
Warning - updating from version 1.0.0
-------------------------------------
If you're updating from version 1.0.0, feed history will be deleted. See why [here](https://www.commafeed.com/announcement/20130725.html).
The last commit with no data loss has been [tagged](https://github.com/Athou/commafeed/tree/1.0.0).
Deployment on OpenShift Deployment on OpenShift
----------------------- -----------------------

View File

@@ -0,0 +1,283 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<profiles version="12">
<profile kind="CodeFormatterProfile" name="Eclipse [built-in] 140 chars" version="12">
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
<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_type_parameters" value="do not 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_comma_in_type_arguments" value="insert"/>
<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_colon_in_case" 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.comment.new_lines_at_block_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
<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_after_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
<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.use_on_off_tags" value="false"/>
<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.insert_new_line_before_else_in_if_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" 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_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
<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_after_binary_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" 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_between_empty_parens_in_enum_constant" value="do not insert"/>
<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.insert_new_line_after_annotation_on_local_variable" value="insert"/>
<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_while" value="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_type_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
<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_comma_in_superinterfaces" 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_before_binary_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
<setting id="org.eclipse.jdt.core.compiler.source" value="1.5"/>
<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_after_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
<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.insert_space_after_comma_in_enum_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="true"/>
<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_arguments_in_explicit_constructor_call" value="16"/>
<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.blank_lines_before_member_type" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" 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.insert_space_after_opening_paren_in_for" value="do not 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.alignment_for_selector_in_method_invocation" value="16"/>
<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_after_unary_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" 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.insert_space_before_closing_paren_in_switch" value="do not insert"/>
<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_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="140"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" 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_after_opening_paren_in_parenthesized_expression" 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_opening_brace_in_constructor_declaration" value="insert"/>
<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_after_annotation_on_method" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
<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.enabling_tag" value="@formatter:on"/>
<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_superclass_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="0"/>
<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="tab"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" 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.indent_statements_compare_to_body" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
<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_colon_in_for" value="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.alignment_for_parameters_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
<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_new_line_in_empty_method_body" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/>
<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.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="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.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
<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.insert_space_before_closing_paren_in_if" 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_after_comma_in_method_declaration_throws" 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_opening_paren_in_annotation" 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.insert_space_after_question_in_wildcard" value="do not insert"/>
<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.comment.preserve_white_space_between_code_and_line_comments" value="false"/>
<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_before_comma_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
<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.brace_position_for_method_declaration" 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_array_initializer" 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.format_line_comment_starting_on_first_column" value="true"/>
<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_new_line_after_annotation_on_field" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
<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.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" 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_after_comma_in_superinterfaces" 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.tabulation.size" value="4"/>
<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.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
<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_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" 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_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
<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_comma_in_constructor_declaration_throws" value="do not insert"/>
<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_throws_clause_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" 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_closing_paren_in_cast" value="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.brace_position_for_block_in_case" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
<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.insert_space_after_opening_paren_in_catch" value="do not insert"/>
<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.insert_space_before_opening_paren_in_method_invocation" value="do not 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.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.5"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
<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.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
<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.insert_space_before_unary_operator" value="do not insert"/>
<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_space_before_opening_brace_in_anonymous_type_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.insert_new_line_in_empty_enum_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
<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.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
<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.insert_space_after_colon_in_for" value="insert"/>
<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.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="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.compiler.codegen.inlineJsrBytecode" value="enabled"/>
<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/>
<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.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not 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_new_line_before_while_in_do_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="140"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="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_comma_in_enum_constant_arguments" 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.brace_position_for_constructor_declaration" value="end_of_line"/>
<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.insert_space_before_colon_in_conditional" value="insert"/>
<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_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
<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.indent_statements_compare_to_block" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" 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.alignment_for_compact_if" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
<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_colon_in_default" value="do not insert"/>
<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_between_empty_parens_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
<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_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="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_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<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_before_postfix_operator" value="do not 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_paren_in_synchronized" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.5"/>
<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="false"/>
<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.alignment_for_enum_constants" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
<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.insert_space_before_opening_paren_in_parenthesized_expression" value="do not 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_before_comma_in_multiple_local_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" 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_after_opening_paren_in_method_invocation" 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.brace_position_for_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" 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_after_opening_paren_in_method_declaration" value="do not insert"/>
<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_paren_in_catch" value="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_after_comma_in_multiple_field_declarations" value="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.insert_space_after_comma_in_parameterized_type_reference" value="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.comment.new_lines_at_javadoc_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" 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.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" 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_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
</profile>
</profiles>

View File

@@ -4,7 +4,7 @@
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>1.0.0</version> <version>1.2.0</version>
<packaging>war</packaging> <packaging>war</packaging>
<name>CommaFeed</name> <name>CommaFeed</name>

View File

@@ -1,6 +1,7 @@
package com.commafeed.backend; package com.commafeed.backend;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -10,7 +11,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryContentDAO;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
@@ -29,6 +32,12 @@ public class DatabaseCleaner {
@Inject @Inject
FeedSubscriptionDAO feedSubscriptionDAO; FeedSubscriptionDAO feedSubscriptionDAO;
@Inject
FeedEntryContentDAO feedEntryContentDAO;
@Inject
FeedEntryStatusDAO feedEntryStatusDAO;
@Inject @Inject
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@@ -45,16 +54,16 @@ public class DatabaseCleaner {
return total; return total;
} }
public long cleanEntriesWithoutFeeds() { public long cleanContentsWithoutEntries() {
long total = 0; long total = 0;
int deleted = -1; int deleted = -1;
do { do {
deleted = feedEntryDAO.deleteWithoutFeeds(100); deleted = feedEntryContentDAO.deleteWithoutEntries(10);
total += deleted; total += deleted;
log.info("removed {} entries without feeds", total); log.info("removed {} feeds without subscriptions", total);
} while (deleted != 0); } while (deleted != 0);
log.info("cleanup done: {} entries without feeds deleted", total); log.info("cleanup done: {} feeds without subscriptions deleted", total);
return total; return total;
} }
@@ -88,4 +97,10 @@ public class DatabaseCleaner {
} }
feedDAO.saveOrUpdate(into); feedDAO.saveOrUpdate(into);
} }
public void cleanStatusesOlderThan(Date olderThan) {
log.info("cleaning old read statuses");
int deleted = feedEntryStatusDAO.deleteOldStatuses(olderThan);
log.info("cleaned {} read statuses", deleted);
}
} }

View File

@@ -33,18 +33,14 @@ public class DatabaseUpdater {
try { try {
Thread currentThread = Thread.currentThread(); Thread currentThread = Thread.currentThread();
ClassLoader classLoader = currentThread.getContextClassLoader(); ClassLoader classLoader = currentThread.getContextClassLoader();
ResourceAccessor accessor = new ClassLoaderResourceAccessor( ResourceAccessor accessor = new ClassLoaderResourceAccessor(classLoader);
classLoader);
context = new InitialContext(); context = new InitialContext();
DataSource dataSource = (DataSource) context DataSource dataSource = (DataSource) context.lookup(datasourceName);
.lookup(datasourceName);
connection = dataSource.getConnection(); connection = dataSource.getConnection();
JdbcConnection jdbcConnection = new JdbcConnection(connection); JdbcConnection jdbcConnection = new JdbcConnection(connection);
Database database = DatabaseFactory.getInstance() Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(jdbcConnection);
.findCorrectDatabaseImplementation(
jdbcConnection);
if (database instanceof PostgresDatabase) { if (database instanceof PostgresDatabase) {
database = new PostgresDatabase() { database = new PostgresDatabase() {
@@ -56,9 +52,7 @@ public class DatabaseUpdater {
database.setConnection(jdbcConnection); database.setConnection(jdbcConnection);
} }
Liquibase liq = new Liquibase( Liquibase liq = new Liquibase("changelogs/db.changelog-master.xml", accessor, database);
"changelogs/db.changelog-master.xml", accessor,
database);
liq.update("prod"); liq.update("prod");
} finally { } finally {
if (context != null) { if (context != null) {

View File

@@ -1,62 +1,44 @@
package com.commafeed.backend; package com.commafeed.backend;
import java.util.Collection; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.TreeSet;
import org.apache.commons.collections.CollectionUtils; public class FixedSizeSortedSet<E> {
import com.google.common.collect.Lists; private List<E> inner;
public class FixedSizeSortedSet<E> extends TreeSet<E> {
private static final long serialVersionUID = 1L;
private final Comparator<? super E> comparator; private final Comparator<? super E> comparator;
private final int maxSize; private final int capacity;
public FixedSizeSortedSet(int maxSize, Comparator<? super E> comparator) { public FixedSizeSortedSet(int capacity, Comparator<? super E> comparator) {
super(comparator); this.inner = new ArrayList<E>(Math.max(0, capacity));
this.maxSize = maxSize; this.capacity = capacity < 0 ? Integer.MAX_VALUE : capacity;
this.comparator = comparator; this.comparator = comparator;
} }
@Override public void add(E e) {
public boolean add(E e) { int position = Math.abs(Collections.binarySearch(inner, e, comparator) + 1);
if (isFull()) { if (isFull()) {
E last = last(); if (position < inner.size()) {
int comparison = comparator.compare(e, last); inner.remove(inner.size() - 1);
if (comparison < 0) { inner.add(position, e);
remove(last);
return super.add(e);
} else {
return false;
} }
} else { } else {
return super.add(e); inner.add(position, e);
} }
} }
@Override public E last() {
public boolean addAll(Collection<? extends E> c) { return inner.get(inner.size() - 1);
if (CollectionUtils.isEmpty(c)) {
return false;
}
boolean success = true;
for (E e : c) {
success &= add(e);
}
return success;
} }
public boolean isFull() { public boolean isFull() {
return size() == maxSize; return inner.size() == capacity;
} }
@SuppressWarnings("unchecked")
public List<E> asList() { public List<E> asList() {
return (List<E>) Lists.newArrayList(toArray()); return inner;
} }
} }

View File

@@ -56,9 +56,7 @@ public class HttpGetter {
static { static {
try { try {
SSL_CONTEXT = SSLContext.getInstance("TLS"); SSL_CONTEXT = SSLContext.getInstance("TLS");
SSL_CONTEXT.init(new KeyManager[0], SSL_CONTEXT.init(new KeyManager[0], new TrustManager[] { new DefaultTrustManager() }, new SecureRandom());
new TrustManager[] { new DefaultTrustManager() },
new SecureRandom());
} catch (Exception e) { } catch (Exception e) {
log.error("Could not configure ssl context"); log.error("Could not configure ssl context");
} }
@@ -66,8 +64,7 @@ public class HttpGetter {
private static final X509HostnameVerifier VERIFIER = new DefaultHostnameVerifier(); private static final X509HostnameVerifier VERIFIER = new DefaultHostnameVerifier();
public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, IOException, NotModifiedException {
IOException, NotModifiedException {
return getBinary(url, null, null, timeout); return getBinary(url, null, null, timeout);
} }
@@ -85,8 +82,8 @@ public class HttpGetter {
* @throws NotModifiedException * @throws NotModifiedException
* if the url hasn't changed since we asked for it last time * if the url hasn't changed since we asked for it last time
*/ */
public HttpResult getBinary(String url, String lastModified, String eTag, int timeout) public HttpResult getBinary(String url, String lastModified, String eTag, int timeout) throws ClientProtocolException, IOException,
throws ClientProtocolException, IOException, NotModifiedException { NotModifiedException {
HttpResult result = null; HttpResult result = null;
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
@@ -112,8 +109,7 @@ public class HttpGetter {
if (code == HttpStatus.SC_NOT_MODIFIED) { if (code == HttpStatus.SC_NOT_MODIFIED) {
throw new NotModifiedException("304 http code"); throw new NotModifiedException("304 http code");
} else if (code >= 300) { } else if (code >= 300) {
throw new HttpResponseException(code, throw new HttpResponseException(code, "Server returned HTTP error code " + code);
"Server returned HTTP error code " + code);
} }
} catch (HttpResponseException e) { } catch (HttpResponseException e) {
@@ -123,14 +119,11 @@ public class HttpGetter {
throw e; throw e;
} }
} }
Header lastModifiedHeader = response Header lastModifiedHeader = response.getFirstHeader(HttpHeaders.LAST_MODIFIED);
.getFirstHeader(HttpHeaders.LAST_MODIFIED);
Header eTagHeader = response.getFirstHeader(HttpHeaders.ETAG); Header eTagHeader = response.getFirstHeader(HttpHeaders.ETAG);
String lastModifiedResponse = lastModifiedHeader == null ? null String lastModifiedResponse = lastModifiedHeader == null ? null : StringUtils.trimToNull(lastModifiedHeader.getValue());
: StringUtils.trimToNull(lastModifiedHeader.getValue()); if (lastModified != null && StringUtils.equals(lastModified, lastModifiedResponse)) {
if (lastModified != null
&& StringUtils.equals(lastModified, lastModifiedResponse)) {
throw new NotModifiedException("lastModifiedHeader is the same"); throw new NotModifiedException("lastModifiedHeader is the same");
} }
@@ -147,10 +140,8 @@ public class HttpGetter {
long duration = System.currentTimeMillis() - start; long duration = System.currentTimeMillis() - start;
Header contentType = entity.getContentType(); Header contentType = entity.getContentType();
result = new HttpResult(content, contentType == null ? null result = new HttpResult(content, contentType == null ? null : contentType.getValue(), lastModifiedHeader == null ? null
: contentType.getValue(), lastModifiedHeader == null ? null : lastModifiedHeader.getValue(), eTagHeader == null ? null : eTagHeader.getValue(), duration);
: lastModifiedHeader.getValue(), eTagHeader == null ? null
: eTagHeader.getValue(), duration);
} finally { } finally {
client.getConnectionManager().shutdown(); client.getConnectionManager().shutdown();
} }
@@ -165,8 +156,7 @@ public class HttpGetter {
private String eTag; private String eTag;
private long duration; private long duration;
public HttpResult(byte[] content, String contentType, public HttpResult(byte[] content, String contentType, String lastModifiedSince, String eTag, long duration) {
String lastModifiedSince, String eTag, long duration) {
this.content = content; this.content = content;
this.contentType = contentType; this.contentType = contentType;
this.lastModifiedSince = lastModifiedSince; this.lastModifiedSince = lastModifiedSince;
@@ -209,8 +199,7 @@ public class HttpGetter {
HttpProtocolParams.setContentCharset(params, UTF8); HttpProtocolParams.setContentCharset(params, UTF8);
HttpConnectionParams.setConnectionTimeout(params, timeout); HttpConnectionParams.setConnectionTimeout(params, timeout);
HttpConnectionParams.setSoTimeout(params, timeout); HttpConnectionParams.setSoTimeout(params, timeout);
client.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, client.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
false));
return new DecompressingHttpClient(client); return new DecompressingHttpClient(client);
} }
@@ -225,13 +214,11 @@ public class HttpGetter {
private static class DefaultTrustManager implements X509TrustManager { private static class DefaultTrustManager implements X509TrustManager {
@Override @Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
throws CertificateException {
} }
@Override @Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
throws CertificateException {
} }
@Override @Override
@@ -240,21 +227,18 @@ public class HttpGetter {
} }
} }
private static class DefaultHostnameVerifier implements private static class DefaultHostnameVerifier implements X509HostnameVerifier {
X509HostnameVerifier {
@Override @Override
public void verify(String string, SSLSocket ssls) throws IOException { public void verify(String string, SSLSocket ssls) throws IOException {
} }
@Override @Override
public void verify(String string, X509Certificate xc) public void verify(String string, X509Certificate xc) throws SSLException {
throws SSLException {
} }
@Override @Override
public void verify(String string, String[] strings, String[] strings1) public void verify(String string, String[] strings, String[] strings1) throws SSLException {
throws SSLException {
} }
@Override @Override

View File

@@ -55,13 +55,11 @@ public class MetricsBean {
} }
public void entryUpdated(int statusesCount) { public void entryInserted() {
thisHour.entriesInserted++; thisHour.entriesInserted++;
thisMinute.entriesInserted++; thisMinute.entriesInserted++;
thisHour.statusesInserted += statusesCount;
thisMinute.statusesInserted += statusesCount;
} }
public void entryCacheHit() { public void entryCacheHit() {
@@ -107,7 +105,6 @@ public class MetricsBean {
private int feedsRefreshed; private int feedsRefreshed;
private int feedsUpdated; private int feedsUpdated;
private int entriesInserted; private int entriesInserted;
private int statusesInserted;
private int threadWaited; private int threadWaited;
private int pushNotificationsReceived; private int pushNotificationsReceived;
private int pushFeedsQueued; private int pushFeedsQueued;
@@ -138,14 +135,6 @@ public class MetricsBean {
this.entriesInserted = entriesInserted; this.entriesInserted = entriesInserted;
} }
public int getStatusesInserted() {
return statusesInserted;
}
public void setStatusesInserted(int statusesInserted) {
this.statusesInserted = statusesInserted;
}
public int getThreadWaited() { public int getThreadWaited() {
return threadWaited; return threadWaited;
} }

View File

@@ -0,0 +1,37 @@
package com.commafeed.backend;
import java.util.Date;
import javax.ejb.Schedule;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.services.ApplicationSettingsService;
@Stateless
public class ScheduledTasks {
protected final static Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
@Inject
ApplicationSettingsService applicationSettingsService;
@Inject
DatabaseCleaner cleaner;
@PersistenceContext
EntityManager em;
// every day at midnight
@Schedule(hour = "0", persistent = false)
private void cleanupOldStatuses() {
Date threshold = applicationSettingsService.get().getUnreadThreshold();
if (threshold != null) {
cleaner.cleanStatusesOlderThan(threshold);
}
}
}

View File

@@ -17,7 +17,7 @@ import org.apache.commons.io.IOUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.ApplicationSettingsDAO;
import com.commafeed.backend.feeds.FeedRefreshTaskGiver; import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
import com.commafeed.backend.model.ApplicationSettings; import com.commafeed.backend.model.ApplicationSettings;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
@@ -38,7 +38,7 @@ public class StartupBean {
DatabaseUpdater databaseUpdater; DatabaseUpdater databaseUpdater;
@Inject @Inject
UserDAO userDAO; ApplicationSettingsDAO applicationSettingsDAO;
@Inject @Inject
UserService userService; UserService userService;
@@ -58,7 +58,7 @@ public class StartupBean {
startupTime = System.currentTimeMillis(); startupTime = System.currentTimeMillis();
databaseUpdater.update(); databaseUpdater.update();
if (userDAO.getCount() == 0) { if (applicationSettingsDAO.getCount() == 0) {
initialData(); initialData();
} }
applicationSettingsService.applyLogLevel(); applicationSettingsService.applyLogLevel();
@@ -79,8 +79,7 @@ public class StartupBean {
IOUtils.closeQuietly(is); IOUtils.closeQuietly(is);
} }
for (Object key : props.keySet()) { for (Object key : props.keySet()) {
supportedLanguages.put(key.toString(), supportedLanguages.put(key.toString(), props.getProperty(key.toString()));
props.getProperty(key.toString()));
} }
} }
@@ -92,11 +91,8 @@ public class StartupBean {
applicationSettingsService.save(settings); applicationSettingsService.save(settings);
try { try {
userService.register(USERNAME_ADMIN, "admin", userService.register(USERNAME_ADMIN, "admin", "admin@commafeed.com", Arrays.asList(Role.ADMIN, Role.USER), true);
"admin@commafeed.com", userService.register(USERNAME_DEMO, "demo", "demo@commafeed.com", Arrays.asList(Role.USER), true);
Arrays.asList(Role.ADMIN, Role.USER), true);
userService.register(USERNAME_DEMO, "demo", "demo@commafeed.com",
Arrays.asList(Role.USER), true);
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
} }

View File

@@ -1,34 +1,38 @@
package com.commafeed.backend.cache; package com.commafeed.backend.cache;
import java.util.List; import java.util.List;
import java.util.Map;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
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.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.Category; import com.commafeed.frontend.model.Category;
public abstract class CacheService { public abstract class CacheService {
// feed entries for faster refresh
public abstract List<String> getLastEntries(Feed feed); public abstract List<String> getLastEntries(Feed feed);
public abstract void setLastEntries(Feed feed, List<String> entries); public abstract void setLastEntries(Feed feed, List<String> entries);
public String buildUniqueEntryKey(Feed feed, FeedEntry entry) { public String buildUniqueEntryKey(Feed feed, FeedEntry entry) {
return DigestUtils.sha1Hex(entry.getGuid() + return DigestUtils.sha1Hex(entry.getGuid() + entry.getUrl());
entry.getUrl());
} }
public abstract Category getRootCategory(User user); // user categories
public abstract Category getUserRootCategory(User user);
public abstract void setRootCategory(User user, Category category); public abstract void setUserRootCategory(User user, Category category);
public abstract Map<Long, Long> getUnreadCounts(User user);
public abstract void setUnreadCounts(User user, Map<Long, Long> map); public abstract void invalidateUserRootCategory(User... users);
public abstract void invalidateUserData(User... users); // unread count
public abstract Long getUnreadCount(FeedSubscription sub);
public abstract void setUnreadCount(FeedSubscription sub, Long count);
public abstract void invalidateUnreadCount(FeedSubscription... subs);
} }

View File

@@ -2,12 +2,12 @@ package com.commafeed.backend.cache;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative; import javax.enterprise.inject.Alternative;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.Category; import com.commafeed.frontend.model.Category;
@@ -25,27 +25,32 @@ public class NoopCacheService extends CacheService {
} }
@Override @Override
public Category getRootCategory(User user) { public Long getUnreadCount(FeedSubscription sub) {
return null; return null;
} }
@Override @Override
public void setRootCategory(User user, Category category) { public void setUnreadCount(FeedSubscription sub, Long count) {
} }
@Override @Override
public Map<Long, Long> getUnreadCounts(User user) { public void invalidateUnreadCount(FeedSubscription... subs) {
}
@Override
public Category getUserRootCategory(User user) {
return null; return null;
} }
@Override @Override
public void setUnreadCounts(User user, Map<Long, Long> map) { public void setUserRootCategory(User user, Category category) {
} }
@Override @Override
public void invalidateUserData(User... users) { public void invalidateUserRootCategory(User... users) {
} }

View File

@@ -1,7 +1,6 @@
package com.commafeed.backend.cache; package com.commafeed.backend.cache;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -17,23 +16,22 @@ import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Pipeline; import redis.clients.jedis.Pipeline;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.Models; import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.Category; import com.commafeed.frontend.model.Category;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.MapType;
import com.google.api.client.util.Lists; import com.google.api.client.util.Lists;
@Alternative @Alternative
@ApplicationScoped @ApplicationScoped
public class RedisCacheService extends CacheService { public class RedisCacheService extends CacheService {
private static final Logger log = LoggerFactory private static final Logger log = LoggerFactory.getLogger(RedisCacheService.class);
.getLogger(RedisCacheService.class); private static ObjectMapper mapper = new ObjectMapper();
private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost"); private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
private ObjectMapper mapper = new ObjectMapper();
@Override @Override
public List<String> getLastEntries(Feed feed) { public List<String> getLastEntries(Feed feed) {
@@ -70,11 +68,11 @@ public class RedisCacheService extends CacheService {
} }
@Override @Override
public Category getRootCategory(User user) { public Category getUserRootCategory(User user) {
Category cat = null; Category cat = null;
Jedis jedis = pool.getResource(); Jedis jedis = pool.getResource();
try { try {
String key = buildRedisRootCategoryKey(user); String key = buildRedisUserRootCategoryKey(user);
String json = jedis.get(key); String json = jedis.get(key);
if (json != null) { if (json != null) {
cat = mapper.readValue(json, Category.class); cat = mapper.readValue(json, Category.class);
@@ -88,10 +86,10 @@ public class RedisCacheService extends CacheService {
} }
@Override @Override
public void setRootCategory(User user, Category category) { public void setUserRootCategory(User user, Category category) {
Jedis jedis = pool.getResource(); Jedis jedis = pool.getResource();
try { try {
String key = buildRedisRootCategoryKey(user); String key = buildRedisUserRootCategoryKey(user);
Pipeline pipe = jedis.pipelined(); Pipeline pipe = jedis.pipelined();
pipe.del(key); pipe.del(key);
@@ -106,37 +104,35 @@ public class RedisCacheService extends CacheService {
} }
@Override @Override
public Map<Long, Long> getUnreadCounts(User user) { public Long getUnreadCount(FeedSubscription sub) {
Map<Long, Long> map = null; Long count = null;
Jedis jedis = pool.getResource(); Jedis jedis = pool.getResource();
try { try {
String key = buildRedisUnreadCountKey(user); String key = buildRedisUnreadCountKey(sub);
String json = jedis.get(key); String countString = jedis.get(key);
if (json != null) { if (countString != null) {
MapType type = mapper.getTypeFactory().constructMapType( count = Long.valueOf(countString);
Map.class, Long.class, Long.class);
map = mapper.readValue(json, type);
} }
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
} finally { } finally {
pool.returnResource(jedis); pool.returnResource(jedis);
} }
return map; return count;
} }
@Override @Override
public void setUnreadCounts(User user, Map<Long, Long> map) { public void setUnreadCount(FeedSubscription sub, Long count) {
Jedis jedis = pool.getResource(); Jedis jedis = pool.getResource();
try { try {
String key = buildRedisUnreadCountKey(user); String key = buildRedisUnreadCountKey(sub);
Pipeline pipe = jedis.pipelined(); Pipeline pipe = jedis.pipelined();
pipe.del(key); pipe.del(key);
pipe.set(key, mapper.writeValueAsString(map)); pipe.set(key, String.valueOf(count));
pipe.expire(key, (int) TimeUnit.MINUTES.toSeconds(30)); pipe.expire(key, (int) TimeUnit.MINUTES.toSeconds(30));
pipe.sync(); pipe.sync();
} catch (JsonProcessingException e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
} finally { } finally {
pool.returnResource(jedis); pool.returnResource(jedis);
@@ -144,15 +140,13 @@ public class RedisCacheService extends CacheService {
} }
@Override @Override
public void invalidateUserData(User... users) { public void invalidateUserRootCategory(User... users) {
Jedis jedis = pool.getResource(); Jedis jedis = pool.getResource();
try { try {
Pipeline pipe = jedis.pipelined(); Pipeline pipe = jedis.pipelined();
if (users != null) { if (users != null) {
for (User user : users) { for (User user : users) {
String key = buildRedisRootCategoryKey(user); String key = buildRedisUserRootCategoryKey(user);
pipe.del(key);
key = buildRedisUnreadCountKey(user);
pipe.del(key); pipe.del(key);
} }
} }
@@ -162,16 +156,33 @@ public class RedisCacheService extends CacheService {
} }
} }
private String buildRedisRootCategoryKey(User user) { @Override
return "root_cat:" + Models.getId(user); public void invalidateUnreadCount(FeedSubscription... subs) {
} Jedis jedis = pool.getResource();
try {
private String buildRedisUnreadCountKey(User user) { Pipeline pipe = jedis.pipelined();
return "unread_count:" + Models.getId(user); if (subs != null) {
for (FeedSubscription sub : subs) {
String key = buildRedisUnreadCountKey(sub);
pipe.del(key);
}
}
pipe.sync();
} finally {
pool.returnResource(jedis);
}
} }
private String buildRedisEntryKey(Feed feed) { private String buildRedisEntryKey(Feed feed) {
return "feed:" + feed.getId(); return "f:" + Models.getId(feed);
}
private String buildRedisUserRootCategoryKey(User user) {
return "c:" + Models.getId(user);
}
private String buildRedisUnreadCountKey(FeedSubscription sub) {
return "u:" + Models.getId(sub);
} }
} }

View File

@@ -26,8 +26,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
CriteriaQuery<FeedCategory> query = builder.createQuery(getType()); CriteriaQuery<FeedCategory> query = builder.createQuery(getType());
Root<FeedCategory> root = query.from(getType()); Root<FeedCategory> root = query.from(getType());
Join<FeedCategory, User> userJoin = (Join<FeedCategory, User>) root Join<FeedCategory, User> userJoin = (Join<FeedCategory, User>) root.fetch(FeedCategory_.user);
.fetch(FeedCategory_.user);
query.where(builder.equal(userJoin.get(User_.id), user.getId())); query.where(builder.equal(userJoin.get(User_.id), user.getId()));
@@ -38,14 +37,12 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
CriteriaQuery<FeedCategory> query = builder.createQuery(getType()); CriteriaQuery<FeedCategory> query = builder.createQuery(getType());
Root<FeedCategory> root = query.from(getType()); Root<FeedCategory> root = query.from(getType());
Predicate p1 = builder.equal( Predicate p1 = builder.equal(root.get(FeedCategory_.user).get(User_.id), user.getId());
root.get(FeedCategory_.user).get(User_.id), user.getId());
Predicate p2 = builder.equal(root.get(FeedCategory_.id), id); Predicate p2 = builder.equal(root.get(FeedCategory_.id), id);
query.where(p1, p2); query.where(p1, p2);
return Iterables.getFirst(cache(em.createQuery(query)).getResultList(), return Iterables.getFirst(cache(em.createQuery(query)).getResultList(), null);
null);
} }
public FeedCategory findByName(User user, String name, FeedCategory parent) { public FeedCategory findByName(User user, String name, FeedCategory parent) {
@@ -60,8 +57,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
if (parent == null) { if (parent == null) {
predicates.add(builder.isNull(root.get(FeedCategory_.parent))); predicates.add(builder.isNull(root.get(FeedCategory_.parent)));
} else { } else {
predicates predicates.add(builder.equal(root.get(FeedCategory_.parent), parent));
.add(builder.equal(root.get(FeedCategory_.parent), parent));
} }
query.where(predicates.toArray(new Predicate[0])); query.where(predicates.toArray(new Predicate[0]));
@@ -85,8 +81,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
if (parent == null) { if (parent == null) {
predicates.add(builder.isNull(root.get(FeedCategory_.parent))); predicates.add(builder.isNull(root.get(FeedCategory_.parent)));
} else { } else {
predicates predicates.add(builder.equal(root.get(FeedCategory_.parent), parent));
.add(builder.equal(root.get(FeedCategory_.parent), parent));
} }
query.where(predicates.toArray(new Predicate[0])); query.where(predicates.toArray(new Predicate[0]));
@@ -94,8 +89,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
return em.createQuery(query).getResultList(); return em.createQuery(query).getResultList();
} }
public List<FeedCategory> findAllChildrenCategories(User user, public List<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) {
FeedCategory parent) {
List<FeedCategory> list = Lists.newArrayList(); List<FeedCategory> list = Lists.newArrayList();
List<FeedCategory> all = findAll(user); List<FeedCategory> all = findAll(user);
for (FeedCategory cat : all) { for (FeedCategory cat : all) {

View File

@@ -39,23 +39,17 @@ public class FeedDAO extends GenericDAO<Feed> {
public List<Feed> feeds; public List<Feed> feeds;
} }
private List<Predicate> getUpdatablePredicates(Root<Feed> root, private List<Predicate> getUpdatablePredicates(Root<Feed> root, Date threshold) {
Date threshold) {
Predicate hasSubscriptions = builder.isNotEmpty(root Predicate hasSubscriptions = builder.isNotEmpty(root.get(Feed_.subscriptions));
.get(Feed_.subscriptions));
Predicate neverUpdated = builder.isNull(root.get(Feed_.lastUpdated)); Predicate neverUpdated = builder.isNull(root.get(Feed_.lastUpdated));
Predicate updatedBeforeThreshold = builder.lessThan( Predicate updatedBeforeThreshold = builder.lessThan(root.get(Feed_.lastUpdated), threshold);
root.get(Feed_.lastUpdated), threshold);
Predicate disabledDateIsNull = builder.isNull(root Predicate disabledDateIsNull = builder.isNull(root.get(Feed_.disabledUntil));
.get(Feed_.disabledUntil)); Predicate disabledDateIsInPast = builder.lessThan(root.get(Feed_.disabledUntil), new Date());
Predicate disabledDateIsInPast = builder.lessThan(
root.get(Feed_.disabledUntil), new Date());
return Lists.newArrayList(hasSubscriptions, return Lists.newArrayList(hasSubscriptions, builder.or(neverUpdated, updatedBeforeThreshold),
builder.or(neverUpdated, updatedBeforeThreshold),
builder.or(disabledDateIsNull, disabledDateIsInPast)); builder.or(disabledDateIsNull, disabledDateIsInPast));
} }
@@ -64,8 +58,7 @@ public class FeedDAO extends GenericDAO<Feed> {
Root<Feed> root = query.from(getType()); Root<Feed> root = query.from(getType());
query.select(builder.count(root)); query.select(builder.count(root));
query.where(getUpdatablePredicates(root, threshold).toArray( query.where(getUpdatablePredicates(root, threshold).toArray(new Predicate[0]));
new Predicate[0]));
TypedQuery<Long> q = em.createQuery(query); TypedQuery<Long> q = em.createQuery(query);
return q.getSingleResult(); return q.getSingleResult();
@@ -75,8 +68,7 @@ public class FeedDAO extends GenericDAO<Feed> {
CriteriaQuery<Feed> query = builder.createQuery(getType()); CriteriaQuery<Feed> query = builder.createQuery(getType());
Root<Feed> root = query.from(getType()); Root<Feed> root = query.from(getType());
query.where(getUpdatablePredicates(root, threshold).toArray( query.where(getUpdatablePredicates(root, threshold).toArray(new Predicate[0]));
new Predicate[0]));
query.orderBy(builder.asc(root.get(Feed_.lastUpdated))); query.orderBy(builder.asc(root.get(Feed_.lastUpdated)));
@@ -94,11 +86,9 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
String normalized = FeedUtils.normalizeURL(url); String normalized = FeedUtils.normalizeURL(url);
feeds = findByField(Feed_.normalizedUrlHash, feeds = findByField(Feed_.normalizedUrlHash, DigestUtils.sha1Hex(normalized));
DigestUtils.sha1Hex(normalized));
feed = Iterables.getFirst(feeds, null); feed = Iterables.getFirst(feeds, null);
if (feed != null if (feed != null && StringUtils.equals(normalized, feed.getNormalizedUrl())) {
&& StringUtils.equals(normalized, feed.getNormalizedUrl())) {
return feed; return feed;
} }
@@ -110,8 +100,7 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
public void deleteRelationships(Feed feed) { public void deleteRelationships(Feed feed) {
Query relationshipDeleteQuery = em Query relationshipDeleteQuery = em.createNamedQuery("Feed.deleteEntryRelationships");
.createNamedQuery("Feed.deleteEntryRelationships");
relationshipDeleteQuery.setParameter("feedId", feed.getId()); relationshipDeleteQuery.setParameter("feedId", feed.getId());
relationshipDeleteQuery.executeUpdate(); relationshipDeleteQuery.executeUpdate();
} }
@@ -120,8 +109,7 @@ public class FeedDAO extends GenericDAO<Feed> {
CriteriaQuery<Feed> query = builder.createQuery(getType()); CriteriaQuery<Feed> query = builder.createQuery(getType());
Root<Feed> root = query.from(getType()); Root<Feed> root = query.from(getType());
SetJoin<Feed, FeedSubscription> join = root.join(Feed_.subscriptions, SetJoin<Feed, FeedSubscription> join = root.join(Feed_.subscriptions, JoinType.LEFT);
JoinType.LEFT);
query.where(builder.isNull(join.get(FeedSubscription_.id))); query.where(builder.isNull(join.get(FeedSubscription_.id)));
TypedQuery<Feed> q = em.createQuery(query); TypedQuery<Feed> q = em.createQuery(query);
q.setMaxResults(max); q.setMaxResults(max);
@@ -138,8 +126,7 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
public static enum DuplicateMode { public static enum DuplicateMode {
NORMALIZED_URL(Feed_.normalizedUrlHash), LAST_CONTENT( NORMALIZED_URL(Feed_.normalizedUrlHash), LAST_CONTENT(Feed_.lastContentHash), PUSH_TOPIC(Feed_.pushTopicHash);
Feed_.lastContentHash), PUSH_TOPIC(Feed_.pushTopicHash);
private SingularAttribute<Feed, String> path; private SingularAttribute<Feed, String> path;
private DuplicateMode(SingularAttribute<Feed, String> path) { private DuplicateMode(SingularAttribute<Feed, String> path) {
@@ -151,8 +138,7 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
} }
public List<FeedCount> findDuplicates(DuplicateMode mode, int offset, public List<FeedCount> findDuplicates(DuplicateMode mode, int offset, int limit, long minCount) {
int limit, long minCount) {
CriteriaQuery<String> query = builder.createQuery(String.class); CriteriaQuery<String> query = builder.createQuery(String.class);
Root<Feed> root = query.from(getType()); Root<Feed> root = query.from(getType());

View File

@@ -0,0 +1,48 @@
package com.commafeed.backend.dao;
import java.util.List;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryContent_;
import com.commafeed.backend.model.FeedEntry_;
import com.google.common.collect.Iterables;
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
public FeedEntryContent findExisting(String contentHash, String titleHash) {
CriteriaQuery<FeedEntryContent> query = builder.createQuery(getType());
Root<FeedEntryContent> root = query.from(getType());
Predicate p1 = builder.equal(root.get(FeedEntryContent_.contentHash), contentHash);
Predicate p2 = builder.equal(root.get(FeedEntryContent_.titleHash), titleHash);
query.where(p1, p2);
TypedQuery<FeedEntryContent> q = em.createQuery(query);
return Iterables.getFirst(q.getResultList(), null);
}
public int deleteWithoutEntries(int max) {
CriteriaQuery<FeedEntryContent> query = builder.createQuery(getType());
Root<FeedEntryContent> root = query.from(getType());
Join<FeedEntryContent, FeedEntry> join = root.join(FeedEntryContent_.entries, JoinType.LEFT);
query.where(builder.isNull(join.get(FeedEntry_.id)));
TypedQuery<FeedEntryContent> q = em.createQuery(query);
q.setMaxResults(max);
List<FeedEntryContent> list = q.getResultList();
int deleted = list.size();
return deleted;
}
}

View File

@@ -4,77 +4,38 @@ import java.util.Date;
import java.util.List; import java.util.List;
import javax.ejb.Stateless; import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.JoinType; import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import javax.persistence.criteria.SetJoin;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntry_; import com.commafeed.backend.model.FeedEntry_;
import com.commafeed.backend.model.FeedFeedEntry; import com.commafeed.backend.model.Feed_;
import com.commafeed.backend.model.FeedFeedEntry_;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@Stateless @Stateless
public class FeedEntryDAO extends GenericDAO<FeedEntry> { public class FeedEntryDAO extends GenericDAO<FeedEntry> {
@Inject protected static final Logger log = LoggerFactory.getLogger(FeedEntryDAO.class);
ApplicationSettingsService applicationSettingsService;
protected static final Logger log = LoggerFactory public FeedEntry findExisting(String guid, String url, Long feedId) {
.getLogger(FeedEntryDAO.class);
public static class EntryWithFeed {
public FeedEntry entry;
public FeedFeedEntry ffe;
public EntryWithFeed(FeedEntry entry, FeedFeedEntry ffe) {
this.entry = entry;
this.ffe = ffe;
}
}
public EntryWithFeed findExisting(String guid, String url, Long feedId) {
TypedQuery<EntryWithFeed> q = em.createNamedQuery(
"EntryStatus.existing", EntryWithFeed.class);
q.setParameter("guidHash", DigestUtils.sha1Hex(guid));
q.setParameter("url", url);
q.setParameter("feedId", feedId);
EntryWithFeed result = null;
List<EntryWithFeed> list = q.getResultList();
for (EntryWithFeed ewf : list) {
if (ewf.entry != null && ewf.ffe != null) {
result = ewf;
break;
}
}
if (result == null) {
result = Iterables.getFirst(list, null);
}
return result;
}
public List<FeedEntry> findByFeed(Feed feed, int offset, int limit) {
CriteriaQuery<FeedEntry> query = builder.createQuery(getType()); CriteriaQuery<FeedEntry> query = builder.createQuery(getType());
Root<FeedEntry> root = query.from(getType()); Root<FeedEntry> root = query.from(getType());
SetJoin<FeedEntry, FeedFeedEntry> feedsJoin = root.join(FeedEntry_.feedRelationships);
query.where(builder.equal(feedsJoin.get(FeedFeedEntry_.feed), feed)); Predicate p1 = builder.equal(root.get(FeedEntry_.guidHash), DigestUtils.sha1Hex(guid));
query.orderBy(builder.desc(feedsJoin.get(FeedFeedEntry_.entryUpdated))); Predicate p2 = builder.equal(root.get(FeedEntry_.url), url);
TypedQuery<FeedEntry> q = em.createQuery(query); Predicate p3 = builder.equal(root.get(FeedEntry_.feed).get(Feed_.id), feedId);
limit(q, offset, limit);
setTimeout(q, applicationSettingsService.get().getQueryTimeout()); query.where(p1, p2, p3);
return q.getResultList();
List<FeedEntry> list = em.createQuery(query).getResultList();
return Iterables.getFirst(list, null);
} }
public int delete(Date olderThan, int max) { public int delete(Date olderThan, int max) {
@@ -90,20 +51,4 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
delete(list); delete(list);
return deleted; return deleted;
} }
public int deleteWithoutFeeds(int max) {
CriteriaQuery<FeedEntry> query = builder.createQuery(getType());
Root<FeedEntry> root = query.from(getType());
SetJoin<FeedEntry, FeedFeedEntry> join = root.join(FeedEntry_.feedRelationships,
JoinType.LEFT);
query.where(builder.isNull(join.get(FeedFeedEntry_.feed)));
TypedQuery<FeedEntry> q = em.createQuery(query);
q.setMaxResults(max);
List<FeedEntry> list = q.getResultList();
int deleted = list.size();
delete(list);
return deleted;
}
} }

View File

@@ -10,25 +10,30 @@ import javax.inject.Inject;
import javax.persistence.Query; import javax.persistence.Query;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Path; import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.hibernate.Criteria;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.Disjunction;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.sql.JoinType;
import org.hibernate.transform.Transformers;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.commafeed.backend.FixedSizeSortedSet; import com.commafeed.backend.FixedSizeSortedSet;
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.FeedEntryContent_;
import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedEntryStatus_; import com.commafeed.backend.model.FeedEntryStatus_;
import com.commafeed.backend.model.FeedEntry_; import com.commafeed.backend.model.FeedEntry_;
import com.commafeed.backend.model.FeedFeedEntry;
import com.commafeed.backend.model.FeedFeedEntry_;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.Models; import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
@@ -36,27 +41,14 @@ import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@Stateless @Stateless
public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> { public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
protected static Logger log = LoggerFactory protected static Logger log = LoggerFactory.getLogger(FeedEntryStatusDAO.class);
.getLogger(FeedEntryStatusDAO.class);
private static final Comparator<FeedEntry> ENTRY_COMPARATOR_DESC = new Comparator<FeedEntry>() { private static final String ALIAS_STATUS = "status";
@Override private static final String ALIAS_ENTRY = "entry";
public int compare(FeedEntry o1, FeedEntry o2) {
return ObjectUtils.compare(o2.getUpdated(), o1.getUpdated());
};
};
private static final Comparator<FeedEntry> ENTRY_COMPARATOR_ASC = new Comparator<FeedEntry>() {
@Override
public int compare(FeedEntry o1, FeedEntry o2) {
return ObjectUtils.compare(o1.getUpdated(), o2.getUpdated());
};
};
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_DESC = new Comparator<FeedEntryStatus>() { private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_DESC = new Comparator<FeedEntryStatus>() {
@Override @Override
@@ -81,22 +73,30 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
Root<FeedEntryStatus> root = query.from(getType()); Root<FeedEntryStatus> root = query.from(getType());
Predicate p1 = builder.equal(root.get(FeedEntryStatus_.entry), entry); Predicate p1 = builder.equal(root.get(FeedEntryStatus_.entry), entry);
Predicate p2 = builder.equal(root.get(FeedEntryStatus_.subscription), Predicate p2 = builder.equal(root.get(FeedEntryStatus_.subscription), sub);
sub);
query.where(p1, p2); query.where(p1, p2);
List<FeedEntryStatus> statuses = em.createQuery(query).getResultList(); List<FeedEntryStatus> statuses = em.createQuery(query).getResultList();
FeedEntryStatus status = Iterables.getFirst(statuses, null); FeedEntryStatus status = Iterables.getFirst(statuses, null);
return handleStatus(status, sub, entry);
}
private FeedEntryStatus handleStatus(FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
if (status == null) { if (status == null) {
Date unreadThreshold = applicationSettingsService.get().getUnreadThreshold();
boolean read = unreadThreshold == null ? false : entry.getUpdated().before(unreadThreshold);
status = new FeedEntryStatus(sub.getUser(), sub, entry); status = new FeedEntryStatus(sub.getUser(), sub, entry);
status.setRead(true); status.setRead(read);
status.setMarkable(!read);
} else {
status.setMarkable(true);
} }
return status; return status;
} }
public List<FeedEntryStatus> findStarred(User user, Date newerThan, public List<FeedEntryStatus> findStarred(User user, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent) {
int offset, int limit, ReadingOrder order, boolean includeContent) {
CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType()); CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType());
Root<FeedEntryStatus> root = query.from(getType()); Root<FeedEntryStatus> root = query.from(getType());
@@ -108,8 +108,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
query.where(predicates.toArray(new Predicate[0])); query.where(predicates.toArray(new Predicate[0]));
if (newerThan != null) { if (newerThan != null) {
predicates.add(builder.greaterThanOrEqualTo( predicates.add(builder.greaterThanOrEqualTo(root.get(FeedEntryStatus_.entryInserted), newerThan));
root.get(FeedEntryStatus_.entryInserted), newerThan));
} }
orderStatusesBy(query, root, order); orderStatusesBy(query, root, order);
@@ -117,200 +116,129 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
TypedQuery<FeedEntryStatus> q = em.createQuery(query); TypedQuery<FeedEntryStatus> q = em.createQuery(query);
limit(q, offset, limit); limit(q, offset, limit);
setTimeout(q); setTimeout(q);
return lazyLoadContent(includeContent, q.getResultList()); List<FeedEntryStatus> statuses = q.getResultList();
for (FeedEntryStatus status : statuses) {
status = handleStatus(status, status.getSubscription(), status.getEntry());
}
return lazyLoadContent(includeContent, statuses);
} }
public List<FeedEntryStatus> findBySubscriptions( private Criteria buildSearchCriteria(FeedSubscription sub, boolean unreadOnly, String keywords, Date newerThan, int offset, int limit,
List<FeedSubscription> subscriptions, String keywords, ReadingOrder order, boolean includeContent, Date last) {
Date newerThan, int offset, int limit, ReadingOrder order, Criteria criteria = getSession().createCriteria(FeedEntry.class, ALIAS_ENTRY);
boolean includeContent) {
int capacity = offset + limit; criteria.add(Restrictions.eq(FeedEntry_.feed.getName(), sub.getFeed()));
Comparator<FeedEntry> comparator = order == ReadingOrder.desc ? ENTRY_COMPARATOR_DESC
: ENTRY_COMPARATOR_ASC;
FixedSizeSortedSet<FeedEntry> set = new FixedSizeSortedSet<FeedEntry>(
capacity < 0 ? Integer.MAX_VALUE : capacity, comparator);
for (FeedSubscription sub : subscriptions) {
CriteriaQuery<FeedEntry> query = builder
.createQuery(FeedEntry.class);
Root<FeedEntry> root = query.from(FeedEntry.class);
Join<FeedEntry, FeedFeedEntry> ffeJoin = root
.join(FeedEntry_.feedRelationships);
List<Predicate> predicates = Lists.newArrayList(); if (keywords != null) {
predicates.add(builder.equal(ffeJoin.get(FeedFeedEntry_.feed), Criteria contentJoin = criteria.createCriteria(FeedEntry_.content.getName(), "content", JoinType.INNER_JOIN);
sub.getFeed()));
if (newerThan != null) { for (String keyword : keywords.split(" ")) {
predicates.add(builder.greaterThanOrEqualTo( Disjunction or = Restrictions.disjunction();
root.get(FeedEntry_.inserted), newerThan)); or.add(Restrictions.ilike(FeedEntryContent_.content.getName(), keyword, MatchMode.ANYWHERE));
or.add(Restrictions.ilike(FeedEntryContent_.title.getName(), keyword, MatchMode.ANYWHERE));
contentJoin.add(or);
} }
if (keywords != null) {
Join<FeedEntry, FeedEntryContent> contentJoin = root
.join(FeedEntry_.content);
String joinedKeywords = StringUtils.join(keywords.toLowerCase()
.split(" "), "%");
joinedKeywords = "%" + joinedKeywords + "%";
Predicate content = builder.like(builder.lower(contentJoin
.get(FeedEntryContent_.content)), joinedKeywords);
Predicate title = builder
.like(builder.lower(contentJoin
.get(FeedEntryContent_.title)), joinedKeywords);
predicates.add(builder.or(content, title));
}
if (order != null && !set.isEmpty() && set.isFull()) {
Predicate filter = null;
FeedEntry last = set.last();
if (order == ReadingOrder.desc) {
filter = builder.greaterThan(
ffeJoin.get(FeedFeedEntry_.entryUpdated),
last.getUpdated());
} else {
filter = builder.lessThan(
ffeJoin.get(FeedFeedEntry_.entryUpdated),
last.getUpdated());
}
predicates.add(filter);
}
query.where(predicates.toArray(new Predicate[0]));
orderEntriesBy(query, ffeJoin, order);
TypedQuery<FeedEntry> q = em.createQuery(query);
limit(q, 0, capacity);
setTimeout(q);
List<FeedEntry> list = q.getResultList();
for (FeedEntry entry : list) {
entry.setSubscription(sub);
}
set.addAll(list);
} }
Criteria statusJoin = criteria.createCriteria(FeedEntry_.statuses.getName(), ALIAS_STATUS, JoinType.LEFT_OUTER_JOIN,
Restrictions.eq(FeedEntryStatus_.subscription.getName(), sub));
List<FeedEntry> entries = set.asList(); if (unreadOnly) {
int size = entries.size();
if (size < offset) {
return Lists.newArrayList();
}
entries = entries.subList(Math.max(offset, 0), size); Disjunction or = Restrictions.disjunction();
or.add(Restrictions.isNull(FeedEntryStatus_.read.getName()));
or.add(Restrictions.eq(FeedEntryStatus_.read.getName(), false));
statusJoin.add(or);
List<FeedEntryStatus> results = Lists.newArrayList(); Date unreadThreshold = applicationSettingsService.get().getUnreadThreshold();
for (FeedEntry entry : entries) { if (unreadThreshold != null) {
FeedSubscription subscription = entry.getSubscription(); criteria.add(Restrictions.ge(FeedEntry_.updated.getName(), unreadThreshold));
results.add(getStatus(subscription, entry));
}
return lazyLoadContent(includeContent, results);
}
public List<FeedEntryStatus> findUnreadBySubscriptions(
List<FeedSubscription> subscriptions, Date newerThan, int offset,
int limit, ReadingOrder order, boolean includeContent) {
int capacity = offset + limit;
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC
: STATUS_COMPARATOR_ASC;
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(
capacity < 0 ? Integer.MAX_VALUE : capacity, comparator);
for (FeedSubscription sub : subscriptions) {
CriteriaQuery<FeedEntryStatus> query = builder
.createQuery(getType());
Root<FeedEntryStatus> root = query.from(getType());
List<Predicate> predicates = Lists.newArrayList();
predicates.add(builder.equal(
root.get(FeedEntryStatus_.subscription), sub));
predicates.add(builder.isFalse(root.get(FeedEntryStatus_.read)));
if (newerThan != null) {
predicates.add(builder.greaterThanOrEqualTo(
root.get(FeedEntryStatus_.entryInserted), newerThan));
} }
if (order != null && !set.isEmpty() && set.isFull()) {
Predicate filter = null;
FeedEntryStatus last = set.last();
if (order == ReadingOrder.desc) {
filter = builder.greaterThan(
root.get(FeedEntryStatus_.entryUpdated),
last.getEntryUpdated());
} else {
filter = builder.lessThan(
root.get(FeedEntryStatus_.entryUpdated),
last.getEntryUpdated());
}
predicates.add(filter);
}
query.where(predicates.toArray(new Predicate[0]));
orderStatusesBy(query, root, order);
TypedQuery<FeedEntryStatus> q = em.createQuery(query);
limit(q, -1, limit);
setTimeout(q);
List<FeedEntryStatus> list = q.getResultList();
set.addAll(list);
} }
List<FeedEntryStatus> entries = set.asList();
int size = entries.size();
if (size < offset) {
return Lists.newArrayList();
}
entries = entries.subList(Math.max(offset, 0), size);
return lazyLoadContent(includeContent, entries);
}
public List<FeedEntryStatus> findAllUnread(User user, Date newerThan,
int offset, int limit, ReadingOrder order, boolean includeContent) {
CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType());
Root<FeedEntryStatus> root = query.from(getType());
List<Predicate> predicates = Lists.newArrayList();
predicates.add(builder.equal(root.get(FeedEntryStatus_.user), user));
predicates.add(builder.isFalse(root.get(FeedEntryStatus_.read)));
if (newerThan != null) { if (newerThan != null) {
predicates.add(builder.greaterThanOrEqualTo( criteria.add(Restrictions.ge(FeedEntry_.inserted.getName(), newerThan));
root.get(FeedEntryStatus_.entryInserted), newerThan));
} }
query.where(predicates.toArray(new Predicate[0])); if (last != null) {
orderStatusesBy(query, root, order); if (order == ReadingOrder.desc) {
criteria.add(Restrictions.gt(FeedEntry_.updated.getName(), last));
TypedQuery<FeedEntryStatus> q = em.createQuery(query); } else {
limit(q, offset, limit); criteria.add(Restrictions.lt(FeedEntry_.updated.getName(), last));
setTimeout(q); }
return lazyLoadContent(includeContent, q.getResultList());
}
/**
* Map between subscriptionId and unread count
*/
@SuppressWarnings("rawtypes")
public Map<Long, Long> getUnreadCount(User user) {
Map<Long, Long> map = Maps.newHashMap();
Query query = em.createNamedQuery("EntryStatus.unreadCounts");
query.setParameter("user", user);
setTimeout(query);
List resultList = query.getResultList();
for (Object o : resultList) {
Object[] array = (Object[]) o;
map.put((Long) array[0], (Long) array[1]);
} }
return map;
if (order != null) {
Order o = null;
if (order == ReadingOrder.asc) {
o = Order.asc(FeedEntry_.updated.getName());
} else {
o = Order.desc(FeedEntry_.updated.getName());
}
criteria.addOrder(o);
}
if (offset > -1) {
criteria.setFirstResult(offset);
}
if (limit > -1) {
criteria.setMaxResults(limit);
}
int timeout = applicationSettingsService.get().getQueryTimeout();
if (timeout > 0) {
criteria.setTimeout(timeout);
}
return criteria;
} }
private List<FeedEntryStatus> lazyLoadContent(boolean includeContent, @SuppressWarnings("unchecked")
List<FeedEntryStatus> results) { public List<FeedEntryStatus> findBySubscriptions(List<FeedSubscription> subscriptions, boolean unreadOnly, String keywords,
Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent) {
int capacity = offset + limit;
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(capacity, comparator);
for (FeedSubscription sub : subscriptions) {
Date last = (order != null && set.isFull()) ? set.last().getEntryUpdated() : null;
Criteria criteria = buildSearchCriteria(sub, unreadOnly, keywords, newerThan, -1, capacity, order, includeContent, last);
criteria.setResultTransformer(CriteriaSpecification.ALIAS_TO_ENTITY_MAP);
List<Map<String, Object>> list = criteria.list();
for (Map<String, Object> map : list) {
FeedEntryStatus status = (FeedEntryStatus) map.get(ALIAS_STATUS);
FeedEntry entry = (FeedEntry) map.get(ALIAS_ENTRY);
entry.setSubscription(sub);
status = handleStatus(status, sub, entry);
status.setEntry(entry);
set.add(status);
}
}
List<FeedEntryStatus> statuses = set.asList();
int size = statuses.size();
if (size < offset) {
return Lists.newArrayList();
}
statuses = statuses.subList(Math.max(offset, 0), size);
return lazyLoadContent(includeContent, statuses);
}
@SuppressWarnings("unchecked")
public Long getUnreadCount(FeedSubscription subscription) {
Long count = null;
Criteria criteria = buildSearchCriteria(subscription, true, null, null, -1, -1, null, false, null);
ProjectionList projection = Projections.projectionList();
projection.add(Projections.rowCount(), "count");
criteria.setProjection(projection);
criteria.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
List<Map<String, Long>> list = criteria.list();
for (Map<String, Long> row : list) {
count = row.get("count");
}
return count;
}
private List<FeedEntryStatus> lazyLoadContent(boolean includeContent, List<FeedEntryStatus> results) {
if (includeContent) { if (includeContent) {
for (FeedEntryStatus status : results) { for (FeedEntryStatus status : results) {
Models.initialize(status.getSubscription().getFeed()); Models.initialize(status.getSubscription().getFeed());
@@ -320,18 +248,11 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
return results; return results;
} }
private void orderEntriesBy(CriteriaQuery<?> query, private void orderStatusesBy(CriteriaQuery<?> query, Path<FeedEntryStatus> statusJoin, ReadingOrder order) {
Path<FeedFeedEntry> ffeJoin, ReadingOrder order) {
orderBy(query, ffeJoin.get(FeedFeedEntry_.entryUpdated), order);
}
private void orderStatusesBy(CriteriaQuery<?> query,
Path<FeedEntryStatus> statusJoin, ReadingOrder order) {
orderBy(query, statusJoin.get(FeedEntryStatus_.entryUpdated), order); orderBy(query, statusJoin.get(FeedEntryStatus_.entryUpdated), order);
} }
private void orderBy(CriteriaQuery<?> query, Path<Date> date, private void orderBy(CriteriaQuery<?> query, Path<Date> date, ReadingOrder order) {
ReadingOrder order) {
if (order != null) { if (order != null) {
if (order == ReadingOrder.asc) { if (order == ReadingOrder.asc) {
query.orderBy(builder.asc(date)); query.orderBy(builder.asc(date));
@@ -345,43 +266,10 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
setTimeout(query, applicationSettingsService.get().getQueryTimeout()); setTimeout(query, applicationSettingsService.get().getQueryTimeout());
} }
public void markAllEntries(User user, Date olderThan) { public int deleteOldStatuses(Date olderThan) {
List<FeedEntryStatus> statuses = findAllUnread(user, null, -1, -1, Query query = em.createNamedQuery("Statuses.deleteOld");
null, false); query.setParameter("date", olderThan);
markList(statuses, olderThan); return query.executeUpdate();
}
public void markSubscriptionEntries(List<FeedSubscription> subscriptions,
Date olderThan) {
List<FeedEntryStatus> statuses = findUnreadBySubscriptions(
subscriptions, null, -1, -1, null, false);
markList(statuses, olderThan);
}
public void markStarredEntries(User user, Date olderThan) {
List<FeedEntryStatus> statuses = findStarred(user, null, -1, -1, null,
false);
markList(statuses, olderThan);
}
private void markList(List<FeedEntryStatus> statuses, Date olderThan) {
List<FeedEntryStatus> list = Lists.newArrayList();
for (FeedEntryStatus status : statuses) {
if (!status.isRead()) {
Date inserted = status.getEntry().getInserted();
if (olderThan == null || inserted == null
|| olderThan.after(inserted)) {
if (status.isStarred()) {
status.setRead(true);
list.add(status);
} else {
delete(status);
}
}
}
}
saveOrUpdate(list);
} }
} }

View File

@@ -28,8 +28,7 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
CriteriaQuery<FeedSubscription> query = builder.createQuery(getType()); CriteriaQuery<FeedSubscription> query = builder.createQuery(getType());
Root<FeedSubscription> root = query.from(getType()); Root<FeedSubscription> root = query.from(getType());
Predicate p1 = builder.equal( Predicate p1 = builder.equal(root.get(FeedSubscription_.user).get(User_.id), user.getId());
root.get(FeedSubscription_.user).get(User_.id), user.getId());
Predicate p2 = builder.equal(root.get(FeedSubscription_.id), id); Predicate p2 = builder.equal(root.get(FeedSubscription_.id), id);
root.fetch(FeedSubscription_.feed, JoinType.LEFT); root.fetch(FeedSubscription_.feed, JoinType.LEFT);
@@ -37,8 +36,7 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
query.where(p1, p2); query.where(p1, p2);
FeedSubscription sub = Iterables.getFirst(cache(em.createQuery(query)) FeedSubscription sub = Iterables.getFirst(cache(em.createQuery(query)).getResultList(), null);
.getResultList(), null);
initRelations(sub); initRelations(sub);
return sub; return sub;
} }
@@ -47,10 +45,8 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
CriteriaQuery<FeedSubscription> query = builder.createQuery(getType()); CriteriaQuery<FeedSubscription> query = builder.createQuery(getType());
Root<FeedSubscription> root = query.from(getType()); Root<FeedSubscription> root = query.from(getType());
query.where(builder.equal(root.get(FeedSubscription_.feed) query.where(builder.equal(root.get(FeedSubscription_.feed).get(Feed_.id), feed.getId()));
.get(Feed_.id), feed.getId())); List<FeedSubscription> list = cache(em.createQuery(query)).getResultList();
List<FeedSubscription> list = cache(em.createQuery(query))
.getResultList();
initRelations(list); initRelations(list);
return list; return list;
} }
@@ -60,18 +56,15 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
CriteriaQuery<FeedSubscription> query = builder.createQuery(getType()); CriteriaQuery<FeedSubscription> query = builder.createQuery(getType());
Root<FeedSubscription> root = query.from(getType()); Root<FeedSubscription> root = query.from(getType());
Predicate p1 = builder.equal( Predicate p1 = builder.equal(root.get(FeedSubscription_.user).get(User_.id), user.getId());
root.get(FeedSubscription_.user).get(User_.id), user.getId()); Predicate p2 = builder.equal(root.get(FeedSubscription_.feed).get(Feed_.id), feed.getId());
Predicate p2 = builder.equal(
root.get(FeedSubscription_.feed).get(Feed_.id), feed.getId());
root.fetch(FeedSubscription_.feed, JoinType.LEFT); root.fetch(FeedSubscription_.feed, JoinType.LEFT);
root.fetch(FeedSubscription_.category, JoinType.LEFT); root.fetch(FeedSubscription_.category, JoinType.LEFT);
query.where(p1, p2); query.where(p1, p2);
FeedSubscription sub = Iterables.getFirst(cache(em.createQuery(query)) FeedSubscription sub = Iterables.getFirst(cache(em.createQuery(query)).getResultList(), null);
.getResultList(), null);
initRelations(sub); initRelations(sub);
return sub; return sub;
} }
@@ -84,57 +77,46 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
root.fetch(FeedSubscription_.feed, JoinType.LEFT); root.fetch(FeedSubscription_.feed, JoinType.LEFT);
root.fetch(FeedSubscription_.category, JoinType.LEFT); root.fetch(FeedSubscription_.category, JoinType.LEFT);
query.where(builder.equal(root.get(FeedSubscription_.user) query.where(builder.equal(root.get(FeedSubscription_.user).get(User_.id), user.getId()));
.get(User_.id), user.getId()));
List<FeedSubscription> list = cache(em.createQuery(query)) List<FeedSubscription> list = cache(em.createQuery(query)).getResultList();
.getResultList();
initRelations(list); initRelations(list);
return list; return list;
} }
public List<FeedSubscription> findByCategory(User user, public List<FeedSubscription> findByCategory(User user, FeedCategory category) {
FeedCategory category) {
CriteriaQuery<FeedSubscription> query = builder.createQuery(getType()); CriteriaQuery<FeedSubscription> query = builder.createQuery(getType());
Root<FeedSubscription> root = query.from(getType()); Root<FeedSubscription> root = query.from(getType());
Predicate p1 = builder.equal( Predicate p1 = builder.equal(root.get(FeedSubscription_.user).get(User_.id), user.getId());
root.get(FeedSubscription_.user).get(User_.id), user.getId());
Predicate p2 = null; Predicate p2 = null;
if (category == null) { if (category == null) {
p2 = builder.isNull( p2 = builder.isNull(root.get(FeedSubscription_.category));
root.get(FeedSubscription_.category));
} else { } else {
p2 = builder.equal( p2 = builder.equal(root.get(FeedSubscription_.category).get(FeedCategory_.id), category.getId());
root.get(FeedSubscription_.category).get(FeedCategory_.id),
category.getId());
} }
query.where(p1, p2); query.where(p1, p2);
List<FeedSubscription> list = cache(em.createQuery(query)) List<FeedSubscription> list = cache(em.createQuery(query)).getResultList();
.getResultList();
initRelations(list); initRelations(list);
return list; return list;
} }
public List<FeedSubscription> findByCategories(User user,
List<FeedCategory> categories) {
List<Long> categoryIds = Lists.transform(categories, public List<FeedSubscription> findByCategories(User user, List<FeedCategory> categories) {
new Function<FeedCategory, Long>() {
@Override List<Long> categoryIds = Lists.transform(categories, new Function<FeedCategory, Long>() {
public Long apply(FeedCategory input) { @Override
return input.getId(); public Long apply(FeedCategory input) {
} return input.getId();
}); }
});
List<FeedSubscription> subscriptions = Lists.newArrayList(); List<FeedSubscription> subscriptions = Lists.newArrayList();
for (FeedSubscription sub : findAll(user)) { for (FeedSubscription sub : findAll(user)) {
if (sub.getCategory() != null if (sub.getCategory() != null && categoryIds.contains(sub.getCategory().getId())) {
&& categoryIds.contains(sub.getCategory().getId())) {
subscriptions.add(sub); subscriptions.add(sub);
} }
} }

View File

@@ -35,8 +35,13 @@ public abstract class GenericDAO<T extends AbstractModel> {
builder = em.getCriteriaBuilder(); builder = em.getCriteriaBuilder();
} }
public void saveOrUpdate(Collection<? extends AbstractModel> models) { public Session getSession() {
Session session = em.unwrap(Session.class); Session session = em.unwrap(Session.class);
return session;
}
public void saveOrUpdate(Collection<? extends AbstractModel> models) {
Session session = getSession();
int i = 1; int i = 1;
for (AbstractModel model : models) { for (AbstractModel model : models) {
session.saveOrUpdate(model); session.saveOrUpdate(model);
@@ -91,8 +96,7 @@ public abstract class GenericDAO<T extends AbstractModel> {
return q.getResultList(); return q.getResultList();
} }
public List<T> findAll(int startIndex, int count, String orderBy, public List<T> findAll(int startIndex, int count, String orderBy, boolean asc) {
boolean asc) {
CriteriaQuery<T> query = builder.createQuery(getType()); CriteriaQuery<T> query = builder.createQuery(getType());
Root<T> root = query.from(getType()); Root<T> root = query.from(getType());
@@ -121,8 +125,7 @@ public abstract class GenericDAO<T extends AbstractModel> {
return findByField(field, value, false); return findByField(field, value, false);
} }
protected <V> List<T> findByField(Attribute<T, V> field, V value, protected <V> List<T> findByField(Attribute<T, V> field, V value, boolean cache) {
boolean cache) {
CriteriaQuery<T> query = builder.createQuery(getType()); CriteriaQuery<T> query = builder.createQuery(getType());
Root<T> root = query.from(getType()); Root<T> root = query.from(getType());
@@ -152,7 +155,7 @@ public abstract class GenericDAO<T extends AbstractModel> {
query.unwrap(Query.class).setCacheable(true); query.unwrap(Query.class).setCacheable(true);
return query; return query;
} }
protected void setTimeout(javax.persistence.Query query, int queryTimeout) { protected void setTimeout(javax.persistence.Query query, int queryTimeout) {
if (queryTimeout > 0) { if (queryTimeout > 0) {
query.setHint("javax.persistence.query.timeout", queryTimeout); query.setHint("javax.persistence.query.timeout", queryTimeout);

View File

@@ -18,11 +18,10 @@ public class UserDAO extends GenericDAO<User> {
CriteriaQuery<User> query = builder.createQuery(getType()); CriteriaQuery<User> query = builder.createQuery(getType());
Root<User> root = query.from(getType()); Root<User> root = query.from(getType());
query.where(builder.equal(builder.lower(root.get(User_.name)), query.where(builder.equal(builder.lower(root.get(User_.name)), name.toLowerCase()));
name.toLowerCase()));
TypedQuery<User> q = em.createQuery(query); TypedQuery<User> q = em.createQuery(query);
cache(q); cache(q);
User user = null; User user = null;
try { try {
user = q.getSingleResult(); user = q.getSingleResult();
@@ -38,7 +37,7 @@ public class UserDAO extends GenericDAO<User> {
query.where(builder.equal(root.get(User_.apiKey), key)); query.where(builder.equal(root.get(User_.apiKey), key));
TypedQuery<User> q = em.createQuery(query); TypedQuery<User> q = em.createQuery(query);
cache(q); cache(q);
User user = null; User user = null;
try { try {
user = q.getSingleResult(); user = q.getSingleResult();

View File

@@ -33,8 +33,7 @@ public class UserRoleDAO extends GenericDAO<UserRole> {
CriteriaQuery<UserRole> query = builder.createQuery(getType()); CriteriaQuery<UserRole> query = builder.createQuery(getType());
Root<UserRole> root = query.from(getType()); Root<UserRole> root = query.from(getType());
query.where(builder.equal(root.get(UserRole_.user).get(User_.id), query.where(builder.equal(root.get(UserRole_.user).get(User_.id), user.getId()));
user.getId()));
return cache(em.createQuery(query)).getResultList(); return cache(em.createQuery(query)).getResultList();
} }

View File

@@ -18,8 +18,7 @@ public class UserSettingsDAO extends GenericDAO<UserSettings> {
CriteriaQuery<UserSettings> query = builder.createQuery(getType()); CriteriaQuery<UserSettings> query = builder.createQuery(getType());
Root<UserSettings> root = query.from(getType()); Root<UserSettings> root = query.from(getType());
query.where(builder.equal(root.get(UserSettings_.user).get(User_.id), query.where(builder.equal(root.get(UserSettings_.user).get(User_.id), user.getId()));
user.getId()));
UserSettings settings = null; UserSettings settings = null;
try { try {

View File

@@ -27,12 +27,9 @@ public class FaviconFetcher {
private static long MAX_ICON_LENGTH = 20000; private static long MAX_ICON_LENGTH = 20000;
private static int TIMEOUT = 4000; private static int TIMEOUT = 4000;
protected static List<String> ICON_MIMETYPES = Arrays.asList( protected static List<String> ICON_MIMETYPES = Arrays.asList("image/x-icon", "image/vnd.microsoft.icon", "image/ico", "image/icon",
"image/x-icon", "image/vnd.microsoft.icon", "image/ico", "text/ico", "application/ico", "image/x-ms-bmp", "image/x-bmp", "image/gif", "image/png", "image/jpeg");
"image/icon", "text/ico", "application/ico", "image/x-ms-bmp", private static List<String> ICON_MIMETYPE_BLACKLIST = Arrays.asList("application/xml", "text/html");
"image/x-bmp", "image/gif", "image/png", "image/jpeg");
private static List<String> ICON_MIMETYPE_BLACKLIST = Arrays.asList(
"application/xml", "text/html");
@Inject @Inject
HttpGetter getter; HttpGetter getter;
@@ -101,14 +98,12 @@ public class FaviconFetcher {
} }
if (length < MIN_ICON_LENGTH) { if (length < MIN_ICON_LENGTH) {
log.debug("Length {} below MIN_ICON_LENGTH {}", length, log.debug("Length {} below MIN_ICON_LENGTH {}", length, MIN_ICON_LENGTH);
MIN_ICON_LENGTH);
return false; return false;
} }
if (length > MAX_ICON_LENGTH) { if (length > MAX_ICON_LENGTH) {
log.debug("Length {} greater than MAX_ICON_LENGTH {}", length, log.debug("Length {} greater than MAX_ICON_LENGTH {}", length, MAX_ICON_LENGTH);
MAX_ICON_LENGTH);
return false; return false;
} }
@@ -126,8 +121,7 @@ public class FaviconFetcher {
return null; return null;
} }
Elements icons = doc Elements icons = doc.select("link[rel~=(?i)^(shortcut|icon|shortcut icon)$]");
.select("link[rel~=(?i)^(shortcut|icon|shortcut icon)$]");
if (icons.isEmpty()) { if (icons.isEmpty()) {
log.debug("No icon found in page {}", url); log.debug("No icon found in page {}", url);

View File

@@ -30,18 +30,15 @@ public class FeedFetcher {
@Inject @Inject
HttpGetter getter; HttpGetter getter;
public FetchedFeed fetch(String feedUrl, boolean extractFeedUrlFromHtml, public FetchedFeed fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag, Date lastPublishedDate,
String lastModified, String eTag, Date lastPublishedDate, String lastContentHash) throws FeedException, ClientProtocolException, IOException, NotModifiedException {
String lastContentHash) throws FeedException,
ClientProtocolException, IOException, NotModifiedException {
log.debug("Fetching feed {}", feedUrl); log.debug("Fetching feed {}", feedUrl);
FetchedFeed fetchedFeed = null; FetchedFeed fetchedFeed = null;
int timeout = 20000; int timeout = 20000;
HttpResult result = getter.getBinary(feedUrl, lastModified, eTag, timeout); HttpResult result = getter.getBinary(feedUrl, lastModified, eTag, timeout);
if (extractFeedUrlFromHtml) { if (extractFeedUrlFromHtml) {
String extractedUrl = extractFeedUrl( String extractedUrl = extractFeedUrl(StringUtils.newStringUtf8(result.getContent()), feedUrl);
StringUtils.newStringUtf8(result.getContent()), feedUrl);
if (org.apache.commons.lang.StringUtils.isNotBlank(extractedUrl)) { if (org.apache.commons.lang.StringUtils.isNotBlank(extractedUrl)) {
result = getter.getBinary(extractedUrl, lastModified, eTag, timeout); result = getter.getBinary(extractedUrl, lastModified, eTag, timeout);
feedUrl = extractedUrl; feedUrl = extractedUrl;
@@ -54,18 +51,15 @@ public class FeedFetcher {
} }
String hash = DigestUtils.sha1Hex(content); String hash = DigestUtils.sha1Hex(content);
if (lastContentHash != null && hash != null if (lastContentHash != null && hash != null && lastContentHash.equals(hash)) {
&& lastContentHash.equals(hash)) {
log.debug("content hash not modified: {}", feedUrl); log.debug("content hash not modified: {}", feedUrl);
throw new NotModifiedException("content hash not modified"); throw new NotModifiedException("content hash not modified");
} }
fetchedFeed = parser.parse(feedUrl, content); fetchedFeed = parser.parse(feedUrl, content);
if (lastPublishedDate != null if (lastPublishedDate != null && fetchedFeed.getFeed().getLastPublishedDate() != null
&& fetchedFeed.getFeed().getLastPublishedDate() != null && lastPublishedDate.getTime() == fetchedFeed.getFeed().getLastPublishedDate().getTime()) {
&& lastPublishedDate.getTime() == fetchedFeed.getFeed()
.getLastPublishedDate().getTime()) {
log.debug("publishedDate not modified: {}", feedUrl); log.debug("publishedDate not modified: {}", feedUrl);
throw new NotModifiedException("publishedDate not modified"); throw new NotModifiedException("publishedDate not modified");
} }

View File

@@ -5,7 +5,6 @@ import java.text.DateFormat;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.SystemUtils; import org.apache.commons.lang.SystemUtils;
import org.jdom.Element; import org.jdom.Element;
@@ -34,12 +33,10 @@ public class FeedParser {
private static Logger log = LoggerFactory.getLogger(FeedParser.class); private static Logger log = LoggerFactory.getLogger(FeedParser.class);
private static final String ATOM_10_URI = "http://www.w3.org/2005/Atom"; private static final String ATOM_10_URI = "http://www.w3.org/2005/Atom";
private static final Namespace ATOM_10_NS = Namespace private static final Namespace ATOM_10_NS = Namespace.getNamespace(ATOM_10_URI);
.getNamespace(ATOM_10_URI);
private static final Date START = new Date(86400000); private static final Date START = new Date(86400000);
private static final Date END = new Date( private static final Date END = new Date(1000l * Integer.MAX_VALUE - 86400000);
1000l * Integer.MAX_VALUE - 86400000);
private static final Function<SyndContent, String> CONTENT_TO_STRING = new Function<SyndContent, String>() { private static final Function<SyndContent, String> CONTENT_TO_STRING = new Function<SyndContent, String>() {
public String apply(SyndContent content) { public String apply(SyndContent content) {
@@ -56,11 +53,9 @@ public class FeedParser {
try { try {
String encoding = FeedUtils.guessEncoding(xml); String encoding = FeedUtils.guessEncoding(xml);
String xmlString = FeedUtils.trimInvalidXmlCharacters(new String( String xmlString = FeedUtils.trimInvalidXmlCharacters(new String(xml, encoding));
xml, encoding));
if (xmlString == null) { if (xmlString == null) {
throw new FeedException("Input string is null for url " throw new FeedException("Input string is null for url " + feedUrl);
+ feedUrl);
} }
InputSource source = new InputSource(new StringReader(xmlString)); InputSource source = new InputSource(new StringReader(xmlString));
SyndFeed rss = new SyndFeedInput().build(source); SyndFeed rss = new SyndFeedInput().build(source);
@@ -89,21 +84,16 @@ public class FeedParser {
continue; continue;
} }
entry.setGuid(FeedUtils.truncate(guid, 2048)); entry.setGuid(FeedUtils.truncate(guid, 2048));
entry.setGuidHash(DigestUtils.sha1Hex(guid)); entry.setUrl(FeedUtils.truncate(FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink()), 2048));
entry.setUrl(FeedUtils.truncate(
FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink()),
2048));
entry.setUpdated(validateDate(getEntryUpdateDate(item), true)); entry.setUpdated(validateDate(getEntryUpdateDate(item), true));
entry.setAuthor(item.getAuthor());
FeedEntryContent content = new FeedEntryContent(); FeedEntryContent content = new FeedEntryContent();
content.setContent(getContent(item)); content.setContent(getContent(item));
content.setTitle(getTitle(item)); content.setTitle(getTitle(item));
SyndEnclosure enclosure = (SyndEnclosure) Iterables.getFirst( content.setAuthor(StringUtils.trimToNull(item.getAuthor()));
item.getEnclosures(), null); SyndEnclosure enclosure = (SyndEnclosure) Iterables.getFirst(item.getEnclosures(), null);
if (enclosure != null) { if (enclosure != null) {
content.setEnclosureUrl(FeedUtils.truncate( content.setEnclosureUrl(FeedUtils.truncate(enclosure.getUrl(), 2048));
enclosure.getUrl(), 2048));
content.setEnclosureType(enclosure.getType()); content.setEnclosureType(enclosure.getType());
} }
entry.setContent(content); entry.setContent(content);
@@ -113,21 +103,17 @@ public class FeedParser {
Date lastEntryDate = null; Date lastEntryDate = null;
Date publishedDate = validateDate(rss.getPublishedDate(), false); Date publishedDate = validateDate(rss.getPublishedDate(), false);
if (!entries.isEmpty()) { if (!entries.isEmpty()) {
List<Long> sortedTimestamps = FeedUtils List<Long> sortedTimestamps = FeedUtils.getSortedTimestamps(entries);
.getSortedTimestamps(entries);
Long timestamp = sortedTimestamps.get(0); Long timestamp = sortedTimestamps.get(0);
lastEntryDate = new Date(timestamp); lastEntryDate = new Date(timestamp);
publishedDate = getFeedPublishedDate(publishedDate, entries); publishedDate = getFeedPublishedDate(publishedDate, entries);
} }
feed.setLastPublishedDate(validateDate(publishedDate, true)); feed.setLastPublishedDate(validateDate(publishedDate, true));
feed.setAverageEntryInterval(FeedUtils feed.setAverageEntryInterval(FeedUtils.averageTimeBetweenEntries(entries));
.averageTimeBetweenEntries(entries));
feed.setLastEntryDate(lastEntryDate); feed.setLastEntryDate(lastEntryDate);
} catch (Exception e) { } catch (Exception e) {
throw new FeedException(String.format( throw new FeedException(String.format("Could not parse feed from %s : %s", feedUrl, e.getMessage()), e);
"Could not parse feed from %s : %s", feedUrl,
e.getMessage()), e);
} }
return fetchedFeed; return fetchedFeed;
} }
@@ -146,8 +132,7 @@ public class FeedParser {
for (Object object : elements) { for (Object object : elements) {
if (object instanceof Element) { if (object instanceof Element) {
Element element = (Element) object; Element element = (Element) object;
if ("link".equals(element.getName()) if ("link".equals(element.getName()) && ATOM_10_NS.equals(element.getNamespace())) {
&& ATOM_10_NS.equals(element.getNamespace())) {
SyndLink link = new SyndLinkImpl(); SyndLink link = new SyndLinkImpl();
link.setRel(element.getAttributeValue("rel")); link.setRel(element.getAttributeValue("rel"));
link.setHref(element.getAttributeValue("href")); link.setHref(element.getAttributeValue("href"));
@@ -158,8 +143,7 @@ public class FeedParser {
} }
} }
private Date getFeedPublishedDate(Date publishedDate, private Date getFeedPublishedDate(Date publishedDate, List<FeedEntry> entries) {
List<FeedEntry> entries) {
for (FeedEntry entry : entries) { for (FeedEntry entry : entries) {
if (publishedDate == null || entry.getUpdated().getTime() > publishedDate.getTime()) { if (publishedDate == null || entry.getUpdated().getTime() > publishedDate.getTime()) {
@@ -199,14 +183,11 @@ public class FeedParser {
private String getContent(SyndEntry item) { private String getContent(SyndEntry item) {
String content = null; String content = null;
if (item.getContents().isEmpty()) { if (item.getContents().isEmpty()) {
content = item.getDescription() == null ? null : item content = item.getDescription() == null ? null : item.getDescription().getValue();
.getDescription().getValue();
} else { } else {
content = StringUtils.join(Collections2.transform( content = StringUtils.join(Collections2.transform(item.getContents(), CONTENT_TO_STRING), SystemUtils.LINE_SEPARATOR);
item.getContents(), CONTENT_TO_STRING),
SystemUtils.LINE_SEPARATOR);
} }
return content; return StringUtils.trimToNull(content);
} }
private String getTitle(SyndEntry item) { private String getTitle(SyndEntry item) {
@@ -219,15 +200,14 @@ public class FeedParser {
title = "(no title)"; title = "(no title)";
} }
} }
return title; return StringUtils.trimToNull(title);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private String findHub(SyndFeed feed) { private String findHub(SyndFeed feed) {
for (SyndLink l : (List<SyndLink>) feed.getLinks()) { for (SyndLink l : (List<SyndLink>) feed.getLinks()) {
if ("hub".equalsIgnoreCase(l.getRel())) { if ("hub".equalsIgnoreCase(l.getRel())) {
log.debug("found hub {} for feed {}", l.getHref(), log.debug("found hub {} for feed {}", l.getHref(), feed.getLink());
feed.getLink());
return l.getHref(); return l.getHref();
} }
} }
@@ -238,8 +218,7 @@ public class FeedParser {
private String findSelf(SyndFeed feed) { private String findSelf(SyndFeed feed) {
for (SyndLink l : (List<SyndLink>) feed.getLinks()) { for (SyndLink l : (List<SyndLink>) feed.getLinks()) {
if ("self".equalsIgnoreCase(l.getRel())) { if ("self".equalsIgnoreCase(l.getRel())) {
log.debug("found self {} for feed {}", l.getHref(), log.debug("found self {} for feed {}", l.getHref(), feed.getLink());
feed.getLink());
return l.getHref(); return l.getHref();
} }
} }

View File

@@ -12,32 +12,28 @@ import org.slf4j.LoggerFactory;
public class FeedRefreshExecutor { public class FeedRefreshExecutor {
private static Logger log = LoggerFactory private static Logger log = LoggerFactory.getLogger(FeedRefreshExecutor.class);
.getLogger(FeedRefreshExecutor.class);
private String poolName; private String poolName;
private ThreadPoolExecutor pool; private ThreadPoolExecutor pool;
private LinkedBlockingDeque<Runnable> queue; private LinkedBlockingDeque<Runnable> queue;
public FeedRefreshExecutor(final String poolName, int threads, public FeedRefreshExecutor(final String poolName, int threads, int queueCapacity) {
int queueCapacity) {
log.info("Creating pool {} with {} threads", poolName, threads); log.info("Creating pool {} with {} threads", poolName, threads);
this.poolName = poolName; this.poolName = poolName;
pool = new ThreadPoolExecutor(threads, threads, 0, pool = new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS, queue = new LinkedBlockingDeque<Runnable>(queueCapacity) {
TimeUnit.MILLISECONDS, private static final long serialVersionUID = 1L;
queue = new LinkedBlockingDeque<Runnable>(queueCapacity) {
private static final long serialVersionUID = 1L;
@Override @Override
public boolean offer(Runnable r) { public boolean offer(Runnable r) {
Task task = (Task) r; Task task = (Task) r;
if (task.isUrgent()) { if (task.isUrgent()) {
return offerFirst(r); return offerFirst(r);
} else { } else {
return offerLast(r); return offerLast(r);
} }
} }
}); });
pool.setRejectedExecutionHandler(new RejectedExecutionHandler() { pool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override @Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
@@ -50,8 +46,7 @@ public class FeedRefreshExecutor {
queue.put(r); queue.put(r);
} }
} catch (InterruptedException e1) { } catch (InterruptedException e1) {
log.error(poolName log.error(poolName + " interrupted while waiting for queue.", e1);
+ " interrupted while waiting for queue.", e1);
} }
} }
}); });
@@ -80,13 +75,11 @@ public class FeedRefreshExecutor {
try { try {
Thread.sleep(100); Thread.sleep(100);
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.error( log.error("{} interrupted while waiting for threads to finish.", poolName);
"{} interrupted while waiting for threads to finish.",
poolName);
} }
} }
} }
private static class NamedThreadFactory implements ThreadFactory { private static class NamedThreadFactory implements ThreadFactory {
private final ThreadGroup group; private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1); private final AtomicInteger threadNumber = new AtomicInteger(1);
@@ -94,15 +87,12 @@ public class FeedRefreshExecutor {
private NamedThreadFactory(String poolName) { private NamedThreadFactory(String poolName) {
SecurityManager s = System.getSecurityManager(); SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
Thread.currentThread().getThreadGroup();
namePrefix = poolName + "-thread-"; namePrefix = poolName + "-thread-";
} }
public Thread newThread(Runnable r) { public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon()) if (t.isDaemon())
t.setDaemon(false); t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY) if (t.getPriority() != Thread.NORM_PRIORITY)

View File

@@ -52,8 +52,7 @@ public class FeedRefreshTaskGiver {
@PostConstruct @PostConstruct
public void init() { public void init() {
backgroundThreads = applicationSettingsService.get() backgroundThreads = applicationSettingsService.get().getBackgroundThreads();
.getBackgroundThreads();
executor = Executors.newFixedThreadPool(1); executor = Executors.newFixedThreadPool(1);
} }
@@ -125,8 +124,7 @@ public class FeedRefreshTaskGiver {
public void add(Feed feed) { public void add(Feed feed) {
Date threshold = getThreshold(); Date threshold = getThreshold();
if (feed.getLastUpdated() == null if (feed.getLastUpdated() == null || feed.getLastUpdated().before(threshold)) {
|| feed.getLastUpdated().before(threshold)) {
addQueue.add(feed); addQueue.add(feed);
} }
} }

View File

@@ -1,7 +1,9 @@
package com.commafeed.backend.feeds; package com.commafeed.backend.feeds;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
@@ -11,6 +13,7 @@ import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject; import javax.inject.Inject;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -25,6 +28,7 @@ import com.commafeed.backend.feeds.FeedRefreshExecutor.Task;
import com.commafeed.backend.model.ApplicationSettings; import com.commafeed.backend.model.ApplicationSettings;
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.FeedEntryContent;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.pubsubhubbub.SubscriptionHandler; import com.commafeed.backend.pubsubhubbub.SubscriptionHandler;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
@@ -35,8 +39,7 @@ import com.google.common.util.concurrent.Striped;
@ApplicationScoped @ApplicationScoped
public class FeedRefreshUpdater { public class FeedRefreshUpdater {
protected static Logger log = LoggerFactory protected static Logger log = LoggerFactory.getLogger(FeedRefreshUpdater.class);
.getLogger(FeedRefreshUpdater.class);
@Inject @Inject
FeedUpdateService feedUpdateService; FeedUpdateService feedUpdateService;
@@ -109,10 +112,9 @@ public class FeedRefreshUpdater {
if (!lastEntries.contains(cacheKey)) { if (!lastEntries.contains(cacheKey)) {
log.debug("cache miss for {}", entry.getUrl()); log.debug("cache miss for {}", entry.getUrl());
if (subscriptions == null) { if (subscriptions == null) {
subscriptions = feedSubscriptionDAO subscriptions = feedSubscriptionDAO.findByFeed(feed);
.findByFeed(feed);
} }
ok &= updateEntry(feed, entry, subscriptions); ok &= addEntry(feed, entry, subscriptions);
metricsBean.entryCacheMiss(); metricsBean.entryCacheMiss();
} else { } else {
log.debug("cache hit for {}", entry.getUrl()); log.debug("cache hit for {}", entry.getUrl());
@@ -139,27 +141,40 @@ public class FeedRefreshUpdater {
} }
} }
private boolean updateEntry(final Feed feed, final FeedEntry entry, private boolean addEntry(final Feed feed, final FeedEntry entry, final List<FeedSubscription> subscriptions) {
final List<FeedSubscription> subscriptions) {
boolean success = false; boolean success = false;
String key = StringUtils.trimToEmpty(entry.getGuid() + entry.getUrl()); // lock on feed, make sure we are not updating the same feed twice at
Lock lock = locks.get(key); // the same time
boolean locked = false; String key1 = StringUtils.trimToEmpty("" + feed.getId());
// lock on content, make sure we are not updating the same entry
// twice at the same time
FeedEntryContent content = entry.getContent();
String key2 = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getContent() + content.getTitle()));
Iterator<Lock> iterator = locks.bulkGet(Arrays.asList(key1, key2)).iterator();
Lock lock1 = iterator.next();
Lock lock2 = iterator.next();
boolean locked1 = false;
boolean locked2 = false;
try { try {
locked = lock.tryLock(1, TimeUnit.MINUTES); locked1 = lock1.tryLock(1, TimeUnit.MINUTES);
if (locked) { locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
if (locked1 && locked2) {
feedUpdateService.updateEntry(feed, entry, subscriptions); feedUpdateService.updateEntry(feed, entry, subscriptions);
success = true; success = true;
} else { } else {
log.error("lock timeout for " + feed.getUrl() + " - " + key); log.error("lock timeout for " + feed.getUrl() + " - " + key1);
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.error("interrupted while waiting for lock for " + feed.getUrl() log.error("interrupted while waiting for lock for " + feed.getUrl() + " : " + e.getMessage(), e);
+ " : " + e.getMessage(), e);
} finally { } finally {
if (locked) { if (locked1) {
lock.unlock(); lock1.unlock();
}
if (locked2) {
lock2.unlock();
} }
} }
return success; return success;

View File

@@ -25,8 +25,7 @@ import com.sun.syndication.io.FeedException;
@ApplicationScoped @ApplicationScoped
public class FeedRefreshWorker { public class FeedRefreshWorker {
private static Logger log = LoggerFactory private static Logger log = LoggerFactory.getLogger(FeedRefreshWorker.class);
.getLogger(FeedRefreshWorker.class);
@Inject @Inject
FeedRefreshUpdater feedRefreshUpdater; FeedRefreshUpdater feedRefreshUpdater;
@@ -52,8 +51,7 @@ public class FeedRefreshWorker {
private void init() { private void init() {
ApplicationSettings settings = applicationSettingsService.get(); ApplicationSettings settings = applicationSettingsService.get();
int threads = settings.getBackgroundThreads(); int threads = settings.getBackgroundThreads();
pool = new FeedRefreshExecutor("feed-refresh-worker", threads, pool = new FeedRefreshExecutor("feed-refresh-worker", threads, 20 * threads);
20 * threads);
} }
@PreDestroy @PreDestroy
@@ -95,8 +93,7 @@ public class FeedRefreshWorker {
private void update(Feed feed) { private void update(Feed feed) {
Date now = new Date(); Date now = new Date();
try { try {
FetchedFeed fetchedFeed = fetcher.fetch(feed.getUrl(), false, FetchedFeed fetchedFeed = fetcher.fetch(feed.getUrl(), false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
feed.getLastModifiedHeader(), feed.getEtagHeader(),
feed.getLastPublishedDate(), feed.getLastContentHash()); feed.getLastPublishedDate(), feed.getLastContentHash());
// stops here if NotModifiedException or any other exception is // stops here if NotModifiedException or any other exception is
// thrown // thrown
@@ -104,21 +101,17 @@ public class FeedRefreshWorker {
Date disabledUntil = null; Date disabledUntil = null;
if (applicationSettingsService.get().isHeavyLoad()) { if (applicationSettingsService.get().isHeavyLoad()) {
disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed.getFeed().getLastEntryDate(), fetchedFeed.getFeed()
.getFeed().getLastEntryDate(), fetchedFeed.getFeed()
.getAverageEntryInterval()); .getAverageEntryInterval());
} }
feed.setLastUpdateSuccess(now); feed.setLastUpdateSuccess(now);
feed.setLink(fetchedFeed.getFeed().getLink()); feed.setLink(fetchedFeed.getFeed().getLink());
feed.setLastModifiedHeader(fetchedFeed.getFeed() feed.setLastModifiedHeader(fetchedFeed.getFeed().getLastModifiedHeader());
.getLastModifiedHeader());
feed.setEtagHeader(fetchedFeed.getFeed().getEtagHeader()); feed.setEtagHeader(fetchedFeed.getFeed().getEtagHeader());
feed.setLastContentHash(fetchedFeed.getFeed().getLastContentHash()); feed.setLastContentHash(fetchedFeed.getFeed().getLastContentHash());
feed.setLastPublishedDate(fetchedFeed.getFeed() feed.setLastPublishedDate(fetchedFeed.getFeed().getLastPublishedDate());
.getLastPublishedDate()); feed.setAverageEntryInterval(fetchedFeed.getFeed().getAverageEntryInterval());
feed.setAverageEntryInterval(fetchedFeed.getFeed()
.getAverageEntryInterval());
feed.setLastEntryDate(fetchedFeed.getFeed().getLastEntryDate()); feed.setLastEntryDate(fetchedFeed.getFeed().getLastEntryDate());
feed.setErrorCount(0); feed.setErrorCount(0);
@@ -129,14 +122,11 @@ public class FeedRefreshWorker {
feedRefreshUpdater.updateFeed(feed, entries); feedRefreshUpdater.updateFeed(feed, entries);
} catch (NotModifiedException e) { } catch (NotModifiedException e) {
log.debug("Feed not modified : {} - {}", feed.getUrl(), log.debug("Feed not modified : {} - {}", feed.getUrl(), e.getMessage());
e.getMessage());
Date disabledUntil = null; Date disabledUntil = null;
if (applicationSettingsService.get().isHeavyLoad()) { if (applicationSettingsService.get().isHeavyLoad()) {
disabledUntil = FeedUtils disabledUntil = FeedUtils.buildDisabledUntil(feed.getLastEntryDate(), feed.getAverageEntryInterval());
.buildDisabledUntil(feed.getLastEntryDate(),
feed.getAverageEntryInterval());
} }
feed.setErrorCount(0); feed.setErrorCount(0);
feed.setMessage(null); feed.setMessage(null);
@@ -144,8 +134,7 @@ public class FeedRefreshWorker {
taskGiver.giveBack(feed); taskGiver.giveBack(feed);
} catch (Exception e) { } catch (Exception e) {
String message = "Unable to refresh feed " + feed.getUrl() + " : " String message = "Unable to refresh feed " + feed.getUrl() + " : " + e.getMessage();
+ e.getMessage();
if (e instanceof FeedException) { if (e instanceof FeedException) {
log.debug(e.getClass().getName() + " " + message, e); log.debug(e.getClass().getName() + " " + message, e);
} else { } else {
@@ -154,8 +143,7 @@ public class FeedRefreshWorker {
feed.setErrorCount(feed.getErrorCount() + 1); feed.setErrorCount(feed.getErrorCount() + 1);
feed.setMessage(message); feed.setMessage(message);
feed.setDisabledUntil(FeedUtils.buildDisabledUntil(feed feed.setDisabledUntil(FeedUtils.buildDisabledUntil(feed.getErrorCount()));
.getErrorCount()));
taskGiver.giveBack(feed); taskGiver.giveBack(feed);
} }

View File

@@ -41,10 +41,8 @@ public class FeedUtils {
protected static Logger log = LoggerFactory.getLogger(FeedUtils.class); protected static Logger log = LoggerFactory.getLogger(FeedUtils.class);
private static final String ESCAPED_QUESTION_MARK = Pattern.quote("?"); private static final String ESCAPED_QUESTION_MARK = Pattern.quote("?");
private static final List<String> ALLOWED_IFRAME_CSS_RULES = Arrays.asList( private static final List<String> ALLOWED_IFRAME_CSS_RULES = Arrays.asList("height", "width", "border");
"height", "width", "border"); private static final char[] DISALLOWED_IFRAME_CSS_RULE_CHARACTERS = new char[] { '(', ')' };
private static final char[] DISALLOWED_IFRAME_CSS_RULE_CHARACTERS = new char[] {
'(', ')' };
public static String truncate(String string, int length) { public static String truncate(String string, int length) {
if (string != null) { if (string != null) {
@@ -54,8 +52,8 @@ public class FeedUtils {
} }
/** /**
* Detect feed encoding by using the declared encoding in the xml processing * Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the characters used in the
* instruction and by detecting the characters used in the feed * feed
* *
*/ */
public static String guessEncoding(byte[] bytes) { public static String guessEncoding(byte[] bytes) {
@@ -87,8 +85,7 @@ public class FeedUtils {
} }
/** /**
* Normalize the url. The resulting url is not meant to be fetched but * 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
* rather used as a mean to identify a feed and avoid duplicates
*/ */
public static String normalizeURL(String url) { public static String normalizeURL(String url) {
if (url == null) { if (url == null) {
@@ -113,13 +110,11 @@ public class FeedUtils {
normalized = normalized.replace("//www.", "//"); normalized = normalized.replace("//www.", "//");
// feedproxy redirects to feedburner // feedproxy redirects to feedburner
normalized = normalized.replace("feedproxy.google.com", normalized = normalized.replace("feedproxy.google.com", "feeds.feedburner.com");
"feeds.feedburner.com");
// feedburner feeds have a special treatment // feedburner feeds have a special treatment
if (normalized.split(ESCAPED_QUESTION_MARK)[0].contains("feedburner.com")) { if (normalized.split(ESCAPED_QUESTION_MARK)[0].contains("feedburner.com")) {
normalized = normalized.replace("feeds2.feedburner.com", normalized = normalized.replace("feeds2.feedburner.com", "feeds.feedburner.com");
"feeds.feedburner.com");
normalized = normalized.split(ESCAPED_QUESTION_MARK)[0]; normalized = normalized.split(ESCAPED_QUESTION_MARK)[0];
normalized = StringUtils.removeEnd(normalized, "/"); normalized = StringUtils.removeEnd(normalized, "/");
} }
@@ -146,17 +141,13 @@ public class FeedUtils {
return encoding; return encoding;
} }
public static String handleContent(String content, String baseUri, public static String handleContent(String content, String baseUri, boolean keepTextOnly) {
boolean keepTextOnly) {
if (StringUtils.isNotBlank(content)) { if (StringUtils.isNotBlank(content)) {
baseUri = StringUtils.trimToEmpty(baseUri); baseUri = StringUtils.trimToEmpty(baseUri);
Whitelist whitelist = new Whitelist(); Whitelist whitelist = new Whitelist();
whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em",
"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",
"h1", "h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul");
"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");
@@ -167,22 +158,16 @@ public class FeedUtils {
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", whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
"allowfullscreen", "frameborder", "style"); whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width");
whitelist.addAttributes("img", "align", "alt", "height", "src",
"title", "width");
whitelist.addAttributes("ol", "start", "type"); whitelist.addAttributes("ol", "start", "type");
whitelist.addAttributes("q", "cite"); whitelist.addAttributes("q", "cite");
whitelist.addAttributes("table", "border", "bordercolor", whitelist.addAttributes("table", "border", "bordercolor", "summary", "width");
"summary", "width"); whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
whitelist.addAttributes("td", "border", "bordercolor", "abbr", whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
"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", whitelist.addProtocols("a", "href", "ftp", "http", "https", "mailto");
"mailto");
whitelist.addProtocols("blockquote", "cite", "http", "https"); whitelist.addProtocols("blockquote", "cite", "http", "https");
whitelist.addProtocols("img", "src", "http", "https"); whitelist.addProtocols("img", "src", "http", "https");
whitelist.addProtocols("q", "cite", "http", "https"); whitelist.addProtocols("q", "cite", "http", "https");
@@ -199,8 +184,7 @@ public class FeedUtils {
e.attr("style", escaped); e.attr("style", escaped);
} }
clean.outputSettings(new OutputSettings().escapeMode( clean.outputSettings(new OutputSettings().escapeMode(EscapeMode.base).prettyPrint(false));
EscapeMode.base).prettyPrint(false));
Element body = clean.body(); Element body = clean.body();
if (keepTextOnly) { if (keepTextOnly) {
content = body.text(); content = body.text();
@@ -215,9 +199,7 @@ public class FeedUtils {
List<String> rules = Lists.newArrayList(); List<String> rules = Lists.newArrayList();
CSSOMParser parser = new CSSOMParser(); CSSOMParser parser = new CSSOMParser();
try { try {
CSSStyleDeclaration decl = parser CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
.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);
@@ -226,11 +208,8 @@ public class FeedUtils {
continue; continue;
} }
if (ALLOWED_IFRAME_CSS_RULES.contains(property) if (ALLOWED_IFRAME_CSS_RULES.contains(property) && StringUtils.containsNone(value, DISALLOWED_IFRAME_CSS_RULE_CHARACTERS)) {
&& StringUtils.containsNone(value, rules.add(property + ":" + decl.getPropertyValue(property) + ";");
DISALLOWED_IFRAME_CSS_RULE_CHARACTERS)) {
rules.add(property + ":" + decl.getPropertyValue(property)
+ ";");
} }
} }
} catch (IOException e) { } catch (IOException e) {
@@ -278,8 +257,7 @@ public class FeedUtils {
} }
if (c >= 32 || c == 9 || c == 10 || c == 13) { if (c >= 32 || c == 9 || c == 10 || c == 13) {
if (!Character.isHighSurrogate(c) if (!Character.isHighSurrogate(c) && !Character.isLowSurrogate(c)) {
&& !Character.isLowSurrogate(c)) {
sb.append(c); sb.append(c);
} }
} }
@@ -306,8 +284,7 @@ public class FeedUtils {
/** /**
* When the feed was refreshed successfully * When the feed was refreshed successfully
*/ */
public static Date buildDisabledUntil(Date publishedDate, public static Date buildDisabledUntil(Date publishedDate, Long averageEntryInterval) {
Long averageEntryInterval) {
Date now = new Date(); Date now = new Date();
if (publishedDate == null) { if (publishedDate == null) {
@@ -325,8 +302,7 @@ public class FeedUtils {
} else if (averageEntryInterval != null) { } else if (averageEntryInterval != null) {
// use average time between entries to decide when to refresh next // use average time between entries to decide when to refresh next
int factor = 2; int factor = 2;
return new Date(Math.min(DateUtils.addHours(now, 6).getTime(), return new Date(Math.min(DateUtils.addHours(now, 6).getTime(), now.getTime() + averageEntryInterval / factor));
now.getTime() + averageEntryInterval / factor));
} else { } else {
// unknown case, recheck in 24 hours // unknown case, recheck in 24 hours
return DateUtils.addHours(now, 24); return DateUtils.addHours(now, 24);
@@ -378,14 +354,11 @@ public class FeedUtils {
return baseUrl + url; return baseUrl + url;
} }
public static String getFaviconUrl(FeedSubscription subscription, public static String getFaviconUrl(FeedSubscription subscription, String publicUrl) {
String publicUrl) { return removeTrailingSlash(publicUrl) + "/rest/feed/favicon/" + subscription.getId();
return removeTrailingSlash(publicUrl) + "/rest/feed/favicon/"
+ subscription.getId();
} }
public static String proxyImages(String content, String publicUrl, public static String proxyImages(String content, String publicUrl, boolean proxyImages) {
boolean proxyImages) {
if (!proxyImages) { if (!proxyImages) {
return content; return content;
} }
@@ -398,8 +371,7 @@ public class FeedUtils {
for (Element element : elements) { for (Element element : elements) {
String href = element.attr("src"); String href = element.attr("src");
if (href != null) { if (href != null) {
String proxy = removeTrailingSlash(publicUrl) String proxy = removeTrailingSlash(publicUrl) + "/rest/server/proxy?u=" + imageProxyEncoder(href);
+ "/rest/server/proxy?u=" + imageProxyEncoder(href);
element.attr("src", proxy); element.attr("src", proxy);
} }
} }

View File

@@ -28,13 +28,11 @@ public class OPMLExporter {
public Opml export(User user) { public Opml export(User user) {
Opml opml = new Opml(); Opml opml = new Opml();
opml.setFeedType("opml_1.1"); opml.setFeedType("opml_1.1");
opml.setTitle(String.format("%s subscriptions in CommaFeed", opml.setTitle(String.format("%s subscriptions in CommaFeed", user.getName()));
user.getName()));
opml.setCreated(new Date()); opml.setCreated(new Date());
List<FeedCategory> categories = feedCategoryDAO.findAll(user); List<FeedCategory> categories = feedCategoryDAO.findAll(user);
List<FeedSubscription> subscriptions = feedSubscriptionDAO List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
.findAll(user);
for (FeedCategory cat : categories) { for (FeedCategory cat : categories) {
opml.getOutlines().add(buildCategoryOutline(cat, subscriptions)); opml.getOutlines().add(buildCategoryOutline(cat, subscriptions));
@@ -50,20 +48,17 @@ public class OPMLExporter {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Outline buildCategoryOutline(FeedCategory cat, private Outline buildCategoryOutline(FeedCategory cat, List<FeedSubscription> subscriptions) {
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 : cat.getChildren()) { for (FeedCategory child : cat.getChildren()) {
outline.getChildren().add( outline.getChildren().add(buildCategoryOutline(child, subscriptions));
buildCategoryOutline(child, subscriptions));
} }
for (FeedSubscription sub : subscriptions) { for (FeedSubscription sub : subscriptions) {
if (sub.getCategory() != null if (sub.getCategory() != null && sub.getCategory().getId().equals(cat.getId())) {
&& sub.getCategory().getId().equals(cat.getId())) {
outline.getChildren().add(buildSubscriptionOutline(sub)); outline.getChildren().add(buildSubscriptionOutline(sub));
} }
} }
@@ -76,11 +71,9 @@ public class OPMLExporter {
outline.setText(sub.getTitle()); outline.setText(sub.getTitle());
outline.setTitle(sub.getTitle()); outline.setTitle(sub.getTitle());
outline.setType("rss"); outline.setType("rss");
outline.getAttributes().add( outline.getAttributes().add(new Attribute("xmlUrl", sub.getFeed().getUrl()));
new Attribute("xmlUrl", sub.getFeed().getUrl()));
if (sub.getFeed().getLink() != null) { if (sub.getFeed().getLink() != null) {
outline.getAttributes().add( outline.getAttributes().add(new Attribute("htmlUrl", sub.getFeed().getLink()));
new Attribute("htmlUrl", sub.getFeed().getLink()));
} }
return outline; return outline;
} }

View File

@@ -63,13 +63,12 @@ public class OPMLImporter {
if (name == null) { if (name == null) {
name = FeedUtils.truncate(outline.getTitle(), 128); name = FeedUtils.truncate(outline.getTitle(), 128);
} }
FeedCategory category = feedCategoryDAO.findByName(user, name, FeedCategory category = feedCategoryDAO.findByName(user, name, parent);
parent);
if (category == null) { if (category == null) {
if (StringUtils.isBlank(name)) { if (StringUtils.isBlank(name)) {
name = "Unnamed category"; name = "Unnamed category";
} }
category = new FeedCategory(); category = new FeedCategory();
category.setName(name); category.setName(name);
category.setParent(parent); category.setParent(parent);
@@ -91,15 +90,13 @@ public class OPMLImporter {
} }
// make sure we continue with the import process even a feed failed // make sure we continue with the import process even a feed failed
try { try {
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent);
name, parent);
} catch (FeedSubscriptionException e) { } catch (FeedSubscriptionException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
log.error("error while importing {}: {}", outline.getXmlUrl(), log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage());
e.getMessage());
} }
} }
cache.invalidateUserData(user); cache.invalidateUserRootCategory(user);
} }
} }

View File

@@ -14,7 +14,12 @@ public abstract class AbstractModel implements Serializable {
@Id @Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "gen") @GeneratedValue(strategy = GenerationType.TABLE, generator = "gen")
@TableGenerator(name = "gen", table = "hibernate_sequences", pkColumnName = "sequence_name", valueColumnName = "sequence_next_hi_value", allocationSize = 1000) @TableGenerator(
name = "gen",
table = "hibernate_sequences",
pkColumnName = "sequence_name",
valueColumnName = "sequence_next_hi_value",
allocationSize = 1000)
private Long id; private Long id;
public Long getId() { public Long getId() {

View File

@@ -1,5 +1,7 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.util.Date;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Table; import javax.persistence.Table;
@@ -7,8 +9,11 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.log4j.Level; import org.apache.log4j.Level;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity @Entity
@Table(name = "APPLICATIONSETTINGS") @Table(name = "APPLICATIONSETTINGS")
@SuppressWarnings("serial") @SuppressWarnings("serial")
@@ -35,10 +40,19 @@ public class ApplicationSettings extends AbstractModel {
private boolean imageProxyEnabled; private boolean imageProxyEnabled;
private int queryTimeout; private int queryTimeout;
private boolean crawlingPaused; private boolean crawlingPaused;
private int keepStatusDays = 0;
@Column(length = 255) @Column(length = 255)
private String announcement; private String announcement;
@JsonIgnore
public Date getUnreadThreshold() {
int keepStatusDays = getKeepStatusDays();
return keepStatusDays > 0 ? DateUtils.addDays(new Date(), -1 * keepStatusDays) : null;
}
/* getters and setters below */
public String getPublicUrl() { public String getPublicUrl() {
return publicUrl; return publicUrl;
} }
@@ -123,8 +137,7 @@ public class ApplicationSettings extends AbstractModel {
return googleAnalyticsTrackingCode; return googleAnalyticsTrackingCode;
} }
public void setGoogleAnalyticsTrackingCode( public void setGoogleAnalyticsTrackingCode(String googleAnalyticsTrackingCode) {
String googleAnalyticsTrackingCode) {
this.googleAnalyticsTrackingCode = googleAnalyticsTrackingCode; this.googleAnalyticsTrackingCode = googleAnalyticsTrackingCode;
} }
@@ -200,4 +213,12 @@ public class ApplicationSettings extends AbstractModel {
this.crawlingPaused = crawlingPaused; this.crawlingPaused = crawlingPaused;
} }
public int getKeepStatusDays() {
return keepStatusDays;
}
public void setKeepStatusDays(int keepStatusDays) {
this.keepStatusDays = keepStatusDays;
}
} }

View File

@@ -4,6 +4,7 @@ import java.util.Date;
import java.util.Set; import java.util.Set;
import javax.persistence.Cacheable; import javax.persistence.Cacheable;
import javax.persistence.CascadeType;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
@@ -107,8 +108,8 @@ public class Feed extends AbstractModel {
@Column(length = 40) @Column(length = 40)
private String lastContentHash; private String lastContentHash;
@OneToMany(mappedBy = "feed") @OneToMany(mappedBy = "feed", cascade = CascadeType.REMOVE)
private Set<FeedFeedEntry> entryRelationships; private Set<FeedEntry> entries;
@OneToMany(mappedBy = "feed") @OneToMany(mappedBy = "feed")
private Set<FeedSubscription> subscriptions; private Set<FeedSubscription> subscriptions;
@@ -135,8 +136,7 @@ public class Feed extends AbstractModel {
private Date pushLastPing; private Date pushLastPing;
/** /**
* Denotes a feed that needs to be refreshed before others. Currently used * Denotes a feed that needs to be refreshed before others. Currently used when a feed is queued manually for refresh. Not persisted.
* when a feed is queued manually for refresh. Not persisted.
*/ */
@Transient @Transient
private boolean urgent; private boolean urgent;
@@ -325,12 +325,12 @@ public class Feed extends AbstractModel {
this.normalizedUrlHash = normalizedUrlHash; this.normalizedUrlHash = normalizedUrlHash;
} }
public Set<FeedFeedEntry> getEntryRelationships() { public Set<FeedEntry> getEntries() {
return entryRelationships; return entries;
} }
public void setEntryRelationships(Set<FeedFeedEntry> entryRelationships) { public void setEntries(Set<FeedEntry> entries) {
this.entryRelationships = entryRelationships; this.entries = entries;
} }
} }

View File

@@ -9,6 +9,7 @@ import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType; import javax.persistence.FetchType;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
import javax.persistence.OneToOne; import javax.persistence.OneToOne;
import javax.persistence.Table; import javax.persistence.Table;
@@ -32,8 +33,8 @@ public class FeedEntry extends AbstractModel {
@Column(length = 40, nullable = false) @Column(length = 40, nullable = false)
private String guidHash; private String guidHash;
@OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE) @ManyToOne(fetch = FetchType.LAZY)
private Set<FeedFeedEntry> feedRelationships; private Feed feed;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false) @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false)
@JoinColumn(nullable = false, updatable = false) @JoinColumn(nullable = false, updatable = false)
@@ -42,9 +43,6 @@ public class FeedEntry extends AbstractModel {
@Column(length = 2048) @Column(length = 2048)
private String url; private String url;
@Column(name = "author", length = 128)
private String author;
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date inserted; private Date inserted;
@@ -116,20 +114,12 @@ public class FeedEntry extends AbstractModel {
this.guidHash = guidHash; this.guidHash = guidHash;
} }
public String getAuthor() { public Feed getFeed() {
return author; return feed;
} }
public void setAuthor(String author) { public void setFeed(Feed feed) {
this.author = author; this.feed = feed;
}
public Set<FeedFeedEntry> getFeedRelationships() {
return feedRelationships;
}
public void setFeedRelationships(Set<FeedFeedEntry> feedRelationships) {
this.feedRelationships = feedRelationships;
} }
public FeedSubscription getSubscription() { public FeedSubscription getSubscription() {

View File

@@ -1,9 +1,12 @@
package com.commafeed.backend.model; package com.commafeed.backend.model;
import java.util.Set;
import javax.persistence.Cacheable; import javax.persistence.Cacheable;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Lob; import javax.persistence.Lob;
import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
@@ -18,17 +21,29 @@ public class FeedEntryContent extends AbstractModel {
@Column(length = 2048) @Column(length = 2048)
private String title; private String title;
@Column(length = 40)
private String titleHash;
@Lob @Lob
@Column(length = Integer.MAX_VALUE) @Column(length = Integer.MAX_VALUE)
private String content; private String content;
@Column(length = 40)
private String contentHash;
@Column(name = "author", length = 128)
private String author;
@Column(length = 2048) @Column(length = 2048)
private String enclosureUrl; private String enclosureUrl;
@Column(length = 255) @Column(length = 255)
private String enclosureType; private String enclosureType;
@OneToMany(mappedBy = "content")
private Set<FeedEntry> entries;
public String getContent() { public String getContent() {
return content; return content;
} }
@@ -61,4 +76,36 @@ public class FeedEntryContent extends AbstractModel {
this.title = title; this.title = title;
} }
public String getContentHash() {
return contentHash;
}
public void setContentHash(String contentHash) {
this.contentHash = contentHash;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Set<FeedEntry> getEntries() {
return entries;
}
public void setEntries(Set<FeedEntry> entries) {
this.entries = entries;
}
public String getTitleHash() {
return titleHash;
}
public void setTitleHash(String titleHash) {
this.titleHash = titleHash;
}
} }

View File

@@ -11,6 +11,7 @@ import javax.persistence.ManyToOne;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
import javax.persistence.Transient;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
@@ -34,6 +35,9 @@ public class FeedEntryStatus extends AbstractModel {
private boolean read; private boolean read;
private boolean starred; private boolean starred;
@Transient
private boolean markable;
/** /**
* Denormalization starts here * Denormalization starts here
*/ */
@@ -116,4 +120,12 @@ public class FeedEntryStatus extends AbstractModel {
this.user = user; this.user = user;
} }
public boolean isMarkable() {
return markable;
}
public void setMarkable(boolean markable) {
this.markable = markable;
}
} }

View File

@@ -1,73 +0,0 @@
package com.commafeed.backend.model;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Cacheable;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Table(name = "FEED_FEEDENTRIES")
@SuppressWarnings("serial")
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
public class FeedFeedEntry implements Serializable {
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "FEED_ID")
private Feed feed;
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "FEEDENTRY_ID")
private FeedEntry entry;
@Temporal(TemporalType.TIMESTAMP)
private Date entryUpdated;
public FeedFeedEntry() {
}
public FeedFeedEntry(Feed feed, FeedEntry entry) {
this.feed = feed;
this.entry = entry;
this.entryUpdated = entry.getUpdated();
}
public Feed getFeed() {
return feed;
}
public void setFeed(Feed feed) {
this.feed = feed;
}
public FeedEntry getEntry() {
return entry;
}
public void setEntry(FeedEntry entry) {
this.entry = entry;
}
public Date getEntryUpdated() {
return entryUpdated;
}
public void setEntryUpdated(Date entryUpdated) {
this.entryUpdated = entryUpdated;
}
}

View File

@@ -55,8 +55,7 @@ public class User extends AbstractModel {
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date recoverPasswordTokenDate; private Date recoverPasswordTokenDate;
@OneToMany(mappedBy = "user", cascade = { CascadeType.PERSIST, @OneToMany(mappedBy = "user", cascade = { CascadeType.PERSIST, CascadeType.REMOVE })
CascadeType.REMOVE })
private Set<UserRole> roles = Sets.newHashSet(); private Set<UserRole> roles = Sets.newHashSet();
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)

View File

@@ -26,8 +26,7 @@ import com.google.common.collect.Lists;
public class SubscriptionHandler { public class SubscriptionHandler {
private static Logger log = LoggerFactory private static Logger log = LoggerFactory.getLogger(SubscriptionHandler.class);
.getLogger(SubscriptionHandler.class);
@Inject @Inject
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@@ -47,16 +46,13 @@ public class SubscriptionHandler {
String hub = feed.getPushHub(); String hub = feed.getPushHub();
String topic = feed.getPushTopic(); String topic = feed.getPushTopic();
String publicUrl = FeedUtils String publicUrl = FeedUtils.removeTrailingSlash(applicationSettingsService.get().getPublicUrl());
.removeTrailingSlash(applicationSettingsService.get()
.getPublicUrl());
log.debug("sending new pubsub subscription to {} for {}", hub, topic); log.debug("sending new pubsub subscription to {} for {}", hub, topic);
HttpPost post = new HttpPost(hub); HttpPost post = new HttpPost(hub);
List<NameValuePair> nvp = Lists.newArrayList(); List<NameValuePair> nvp = Lists.newArrayList();
nvp.add(new BasicNameValuePair("hub.callback", publicUrl nvp.add(new BasicNameValuePair("hub.callback", publicUrl + "/rest/push/callback"));
+ "/rest/push/callback"));
nvp.add(new BasicNameValuePair("hub.topic", topic)); nvp.add(new BasicNameValuePair("hub.topic", topic));
nvp.add(new BasicNameValuePair("hub.mode", "subscribe")); nvp.add(new BasicNameValuePair("hub.mode", "subscribe"));
nvp.add(new BasicNameValuePair("hub.verify", "async")); nvp.add(new BasicNameValuePair("hub.verify", "async"));
@@ -65,8 +61,7 @@ public class SubscriptionHandler {
nvp.add(new BasicNameValuePair("hub.lease_seconds", "")); nvp.add(new BasicNameValuePair("hub.lease_seconds", ""));
post.setHeader(HttpHeaders.USER_AGENT, "CommaFeed"); post.setHeader(HttpHeaders.USER_AGENT, "CommaFeed");
post.setHeader(HttpHeaders.CONTENT_TYPE, post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
MediaType.APPLICATION_FORM_URLENCODED);
HttpClient client = HttpGetter.newClient(20000); HttpClient client = HttpGetter.newClient(20000);
try { try {
@@ -77,23 +72,19 @@ public class SubscriptionHandler {
if (code != 204 && code != 202 && code != 200) { if (code != 204 && code != 202 && code != 200) {
String message = EntityUtils.toString(response.getEntity()); String message = EntityUtils.toString(response.getEntity());
String pushpressError = " is value is not allowed. You may only subscribe to"; String pushpressError = " is value is not allowed. You may only subscribe to";
if (code == 400 if (code == 400 && StringUtils.contains(message, pushpressError)) {
&& StringUtils.contains(message, pushpressError)) {
String[] tokens = message.split(" "); String[] tokens = message.split(" ");
feed.setPushTopic(tokens[tokens.length - 1]); feed.setPushTopic(tokens[tokens.length - 1]);
taskGiver.giveBack(feed); taskGiver.giveBack(feed);
log.debug("handled pushpress subfeed {} : {}", topic, log.debug("handled pushpress subfeed {} : {}", topic, feed.getPushTopic());
feed.getPushTopic());
} else { } else {
throw new Exception("Unexpected response code: " + code throw new Exception("Unexpected response code: " + code + " " + response.getStatusLine().getReasonPhrase() + " - "
+ " " + response.getStatusLine().getReasonPhrase() + message);
+ " - " + message);
} }
} }
log.debug("subscribed to {} for {}", hub, topic); log.debug("subscribed to {} for {}", hub, topic);
} catch (Exception e) { } catch (Exception e) {
log.error("Could not subscribe to {} for {} : " + e.getMessage(), log.error("Could not subscribe to {} for {} : " + e.getMessage(), hub, topic);
hub, topic);
} finally { } finally {
client.getConnectionManager().shutdown(); client.getConnectionManager().shutdown();
} }

View File

@@ -4,8 +4,7 @@ import org.jdom.Element;
import com.sun.syndication.feed.opml.Opml; import com.sun.syndication.feed.opml.Opml;
public class OPML11Generator extends public class OPML11Generator extends com.sun.syndication.io.impl.OPML10Generator {
com.sun.syndication.io.impl.OPML10Generator {
public OPML11Generator() { public OPML11Generator() {
super("opml_1.1"); super("opml_1.1");

View File

@@ -6,20 +6,17 @@ import org.jdom.Element;
import com.sun.syndication.io.impl.OPML10Parser; import com.sun.syndication.io.impl.OPML10Parser;
public class OPML11Parser extends OPML10Parser { public class OPML11Parser extends OPML10Parser {
public OPML11Parser() { public OPML11Parser() {
super("opml_1.1"); super("opml_1.1");
} }
@Override @Override
public boolean isMyType(Document document) { public boolean isMyType(Document document) {
Element e = document.getRootElement(); Element e = document.getRootElement();
if (e.getName().equals("opml") if (e.getName().equals("opml") && (e.getChild("head") == null || e.getChild("head").getChild("docs") == null)
&& (e.getChild("head") == null || e.getChild("head").getChild( && (e.getAttributeValue("version") == null || e.getAttributeValue("version").equals("1.1"))) {
"docs") == null)
&& (e.getAttributeValue("version") == null || e
.getAttributeValue("version").equals("1.1"))) {
return true; return true;
} }

View File

@@ -26,8 +26,7 @@ public class RSSRDF10Parser extends RSS10Parser {
Element rssRoot = document.getRootElement(); Element rssRoot = document.getRootElement();
Namespace defaultNS = rssRoot.getNamespace(); Namespace defaultNS = rssRoot.getNamespace();
List additionalNSs = Lists.newArrayList(rssRoot List additionalNSs = Lists.newArrayList(rssRoot.getAdditionalNamespaces());
.getAdditionalNamespaces());
List<Element> children = rssRoot.getChildren(); List<Element> children = rssRoot.getChildren();
if (CollectionUtils.isNotEmpty(children)) { if (CollectionUtils.isNotEmpty(children)) {
Element child = children.get(0); Element child = children.get(0);

View File

@@ -29,8 +29,7 @@ public class ApplicationSettingsService {
public ApplicationSettings get() { public ApplicationSettings get() {
if (settings == null) { if (settings == null) {
settings = Iterables.getFirst(applicationSettingsDAO.findAll(), settings = Iterables.getFirst(applicationSettingsDAO.findAll(), null);
null);
} }
return settings; return settings;
} }

View File

@@ -0,0 +1,37 @@
package com.commafeed.backend.services;
import javax.inject.Inject;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.dao.FeedEntryContentDAO;
import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.model.FeedEntryContent;
public class FeedEntryContentService {
@Inject
FeedEntryContentDAO feedEntryContentDAO;
/**
* this is NOT thread-safe
*/
public FeedEntryContent findOrCreate(FeedEntryContent content, String baseUrl) {
String contentHash = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getContent()));
String titleHash = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getTitle()));
FeedEntryContent existing = feedEntryContentDAO.findExisting(contentHash, titleHash);
if (existing == null) {
content.setContentHash(contentHash);
content.setTitleHash(titleHash);
content.setAuthor(FeedUtils.truncate(FeedUtils.handleContent(content.getAuthor(), baseUrl, true), 128));
content.setTitle(FeedUtils.truncate(FeedUtils.handleContent(content.getTitle(), baseUrl, true), 2048));
content.setContent(FeedUtils.handleContent(content.getContent(), baseUrl, false));
existing = content;
feedEntryContentDAO.saveOrUpdate(existing);
}
return existing;
}
}

View File

@@ -1,8 +1,12 @@
package com.commafeed.backend.services; package com.commafeed.backend.services;
import java.util.Date;
import java.util.List;
import javax.ejb.Stateless; import javax.ejb.Stateless;
import javax.inject.Inject; import javax.inject.Inject;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO;
@@ -10,6 +14,7 @@ import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryStatus; 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.google.common.collect.Lists;
@Stateless @Stateless
public class FeedEntryService { public class FeedEntryService {
@@ -19,14 +24,15 @@ public class FeedEntryService {
@Inject @Inject
FeedSubscriptionDAO feedSubscriptionDAO; FeedSubscriptionDAO feedSubscriptionDAO;
@Inject @Inject
FeedEntryDAO feedEntryDAO; FeedEntryDAO feedEntryDAO;
public void markEntry(User user, Long entryId, Long subscriptionId, @Inject
boolean read) { CacheService cache;
FeedSubscription sub = feedSubscriptionDAO.findById(user,
subscriptionId); public void markEntry(User user, Long entryId, Long subscriptionId, boolean read) {
FeedSubscription sub = feedSubscriptionDAO.findById(user, subscriptionId);
if (sub == null) { if (sub == null) {
return; return;
} }
@@ -37,32 +43,17 @@ public class FeedEntryService {
} }
FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry); FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry);
if (status.isMarkable()) {
if (read) { status.setRead(read);
if (status.getId() != null) {
if (status.isStarred()) {
status.setRead(true);
feedEntryStatusDAO.saveOrUpdate(status);
} else {
feedEntryStatusDAO.delete(status);
}
}
} else {
if (status.getId() == null) {
status = new FeedEntryStatus(user, sub, entry);
status.setSubscription(sub);
}
status.setRead(false);
feedEntryStatusDAO.saveOrUpdate(status); feedEntryStatusDAO.saveOrUpdate(status);
cache.invalidateUnreadCount(sub);
cache.invalidateUserRootCategory(user);
} }
} }
public void starEntry(User user, Long entryId, Long subscriptionId, public void starEntry(User user, Long entryId, Long subscriptionId, boolean starred) {
boolean starred) {
FeedSubscription sub = feedSubscriptionDAO.findById(user, FeedSubscription sub = feedSubscriptionDAO.findById(user, subscriptionId);
subscriptionId);
if (sub == null) { if (sub == null) {
return; return;
} }
@@ -73,24 +64,33 @@ public class FeedEntryService {
} }
FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry); FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry);
status.setStarred(starred);
feedEntryStatusDAO.saveOrUpdate(status);
}
if (!starred) { public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan) {
if (status.getId() != null) { List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(subscriptions, true, null, null, -1, -1, null, false);
if (!status.isRead()) { markList(statuses, olderThan);
status.setStarred(false); cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
feedEntryStatusDAO.saveOrUpdate(status); cache.invalidateUserRootCategory(user);
} else { }
feedEntryStatusDAO.delete(status);
public void markStarredEntries(User user, Date olderThan) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findStarred(user, null, -1, -1, null, false);
markList(statuses, olderThan);
}
private void markList(List<FeedEntryStatus> statuses, Date olderThan) {
List<FeedEntryStatus> list = Lists.newArrayList();
for (FeedEntryStatus status : statuses) {
if (!status.isRead()) {
Date inserted = status.getEntry().getInserted();
if (olderThan == null || inserted == null || olderThan.after(inserted)) {
status.setRead(true);
list.add(status);
} }
} }
} else {
if (status.getId() == null) {
status = new FeedEntryStatus(user, sub, entry);
status.setSubscription(sub);
status.setRead(true);
}
status.setStarred(true);
feedEntryStatusDAO.saveOrUpdate(status);
} }
feedEntryStatusDAO.saveOrUpdate(list);
} }
} }

View File

@@ -18,17 +18,14 @@ import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
import com.commafeed.backend.feeds.FeedUtils; import com.commafeed.backend.feeds.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.FeedEntry;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.Models; import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.google.api.client.util.Lists; import com.google.api.client.util.Maps;
public class FeedSubscriptionService { public class FeedSubscriptionService {
private static Logger log = LoggerFactory private static Logger log = LoggerFactory.getLogger(FeedSubscriptionService.class);
.getLogger(FeedSubscriptionService.class);
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApplicationException @ApplicationException
@@ -59,63 +56,62 @@ public class FeedSubscriptionService {
@Inject @Inject
CacheService cache; CacheService cache;
public Feed subscribe(User user, String url, String title, public Feed subscribe(User user, String url, String title, FeedCategory category) {
FeedCategory category) {
final String pubUrl = applicationSettingsService.get().getPublicUrl(); final String pubUrl = applicationSettingsService.get().getPublicUrl();
if (StringUtils.isBlank(pubUrl)) { if (StringUtils.isBlank(pubUrl)) {
throw new FeedSubscriptionException( throw new FeedSubscriptionException("Public URL of this CommaFeed instance is not set");
"Public URL of this CommaFeed instance is not set");
} }
if (url.startsWith(pubUrl)) { if (url.startsWith(pubUrl)) {
throw new FeedSubscriptionException( throw new FeedSubscriptionException("Could not subscribe to a feed from this CommaFeed instance");
"Could not subscribe to a feed from this CommaFeed instance");
} }
Feed feed = feedService.findOrCreate(url); Feed feed = feedService.findOrCreate(url);
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, feed); FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, feed);
boolean newSubscription = false;
if (sub == null) { if (sub == null) {
sub = new FeedSubscription(); sub = new FeedSubscription();
sub.setFeed(feed); sub.setFeed(feed);
sub.setUser(user); sub.setUser(user);
newSubscription = true;
} }
sub.setCategory(category); sub.setCategory(category);
sub.setPosition(0); sub.setPosition(0);
sub.setTitle(FeedUtils.truncate(title, 128)); sub.setTitle(FeedUtils.truncate(title, 128));
feedSubscriptionDAO.saveOrUpdate(sub); feedSubscriptionDAO.saveOrUpdate(sub);
if (newSubscription) {
try {
List<FeedEntryStatus> statuses = Lists.newArrayList();
List<FeedEntry> allEntries = feedEntryDAO.findByFeed(feed, 0,
10);
for (FeedEntry entry : allEntries) {
FeedEntryStatus status = new FeedEntryStatus(user, sub, entry);
status.setRead(false);
status.setSubscription(sub);
statuses.add(status);
}
feedEntryStatusDAO.saveOrUpdate(statuses);
} catch (Exception e) {
log.error(
"could not fetch initial statuses when importing {} : {}",
feed.getUrl(), e.getMessage());
}
}
taskGiver.add(feed); taskGiver.add(feed);
cache.invalidateUserRootCategory(user);
return feed; return feed;
} }
public boolean unsubscribe(User user, Long subId) {
FeedSubscription sub = feedSubscriptionDAO.findById(user, subId);
if (sub != null) {
feedSubscriptionDAO.delete(sub);
cache.invalidateUserRootCategory(user);
return true;
} else {
return false;
}
}
public Long getUnreadCount(FeedSubscription sub) {
Long count = cache.getUnreadCount(sub);
if (count == null) {
log.debug("unread count cache miss for {}", Models.getId(sub));
count = feedEntryStatusDAO.getUnreadCount(sub);
cache.setUnreadCount(sub, count);
}
return count;
}
public Map<Long, Long> getUnreadCount(User user) { public Map<Long, Long> getUnreadCount(User user) {
Map<Long, Long> map = cache.getUnreadCounts(user); Map<Long, Long> map = Maps.newHashMap();
if (map == null) { List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
log.debug("unread count cache miss for {}", Models.getId(user)); for (FeedSubscription sub : subs) {
map = feedEntryStatusDAO.getUnreadCount(user); map.put(sub.getId(), getUnreadCount(sub));
cache.setUnreadCounts(user, map);
} }
return map; return map;
} }
} }

View File

@@ -8,25 +8,23 @@ import javax.inject.Inject;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext; import javax.persistence.PersistenceContext;
import org.apache.commons.codec.digest.DigestUtils;
import com.commafeed.backend.MetricsBean; import com.commafeed.backend.MetricsBean;
import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryDAO.EntryWithFeed;
import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.feeds.FeedUtils;
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.FeedEntryContent; import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedFeedEntry;
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.google.common.collect.Lists; import com.google.common.collect.Lists;
@Stateless @Stateless
public class FeedUpdateService { public class FeedUpdateService {
@PersistenceContext @PersistenceContext
protected EntityManager em; protected EntityManager em;
@@ -45,47 +43,33 @@ public class FeedUpdateService {
@Inject @Inject
CacheService cache; CacheService cache;
public void updateEntry(Feed feed, FeedEntry entry, @Inject
List<FeedSubscription> subscriptions) { FeedEntryContentService feedEntryContentService;
EntryWithFeed existing = feedEntryDAO.findExisting(entry.getGuid(), /**
entry.getUrl(), feed.getId()); * this is NOT thread-safe
*/
public void updateEntry(Feed feed, FeedEntry entry, List<FeedSubscription> subscriptions) {
FeedEntry update = null; FeedEntry existing = feedEntryDAO.findExisting(entry.getGuid(), entry.getUrl(), feed.getId());
FeedFeedEntry ffe = null; if (existing != null) {
if (existing == null) { return;
entry.setAuthor(FeedUtils.truncate(FeedUtils.handleContent(
entry.getAuthor(), feed.getLink(), true), 128));
FeedEntryContent content = entry.getContent();
content.setTitle(FeedUtils.truncate(FeedUtils.handleContent(
content.getTitle(), feed.getLink(), true), 2048));
content.setContent(FeedUtils.handleContent(content.getContent(),
feed.getLink(), false));
entry.setInserted(new Date());
ffe = new FeedFeedEntry(feed, entry);
update = entry;
} else if (existing.ffe == null) {
ffe = new FeedFeedEntry(feed, existing.entry);
update = existing.entry;
} }
if (update != null) { FeedEntryContent content = feedEntryContentService.findOrCreate(entry.getContent(), feed.getLink());
List<FeedEntryStatus> statusUpdateList = Lists.newArrayList(); entry.setGuidHash(DigestUtils.sha1Hex(entry.getGuid()));
List<User> users = Lists.newArrayList(); entry.setContent(content);
for (FeedSubscription sub : subscriptions) { entry.setInserted(new Date());
User user = sub.getUser(); entry.setFeed(feed);
FeedEntryStatus status = new FeedEntryStatus(user, sub, update);
status.setSubscription(sub); List<User> users = Lists.newArrayList();
statusUpdateList.add(status); for (FeedSubscription sub : subscriptions) {
users.add(user); User user = sub.getUser();
} users.add(user);
cache.invalidateUserData(users.toArray(new User[0]));
feedEntryDAO.saveOrUpdate(update);
feedEntryStatusDAO.saveOrUpdate(statusUpdateList);
em.persist(ffe);
metricsBean.entryUpdated(statusUpdateList.size());
} }
feedEntryDAO.saveOrUpdate(entry);
metricsBean.entryInserted();
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(users.toArray(new User[0]));
} }
} }

View File

@@ -26,8 +26,7 @@ public class MailService implements Serializable {
@Inject @Inject
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
public void sendMail(User user, String subject, String content) public void sendMail(User user, String subject, String content) throws Exception {
throws Exception {
ApplicationSettings settings = applicationSettingsService.get(); ApplicationSettings settings = applicationSettingsService.get();
@@ -50,8 +49,7 @@ public class MailService implements Serializable {
Message message = new MimeMessage(session); Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(username, "CommaFeed")); message.setFrom(new InternetAddress(username, "CommaFeed"));
message.setRecipients(Message.RecipientType.TO, message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(dest));
InternetAddress.parse(dest));
message.setSubject("CommaFeed - " + subject); message.setSubject("CommaFeed - " + subject);
message.setContent(content, "text/html; charset=utf-8"); message.setContent(content, "text/html; charset=utf-8");

View File

@@ -53,8 +53,7 @@ public class PasswordEncryptionService implements Serializable {
// http://blog.crackpassword.com/2010/09/smartphone-forensics-cracking-blackberry-backup-passwords/ // http://blog.crackpassword.com/2010/09/smartphone-forensics-cracking-blackberry-backup-passwords/
int iterations = 20000; int iterations = 20000;
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, derivedKeyLength);
derivedKeyLength);
byte[] bytes = null; byte[] bytes = null;
try { try {

View File

@@ -48,15 +48,13 @@ public class UserService {
User user = userDAO.findByName(name); User user = userDAO.findByName(name);
if (user != null && !user.isDisabled()) { if (user != null && !user.isDisabled()) {
boolean authenticated = encryptionService.authenticate(password, boolean authenticated = encryptionService.authenticate(password, user.getPassword(), user.getSalt());
user.getPassword(), user.getSalt());
if (authenticated) { if (authenticated) {
Date lastLogin = user.getLastLogin(); Date lastLogin = user.getLastLogin();
Date now = new Date(); Date now = new Date();
// only update lastLogin field every hour in order to not // only update lastLogin field every hour in order to not
// invalidate the cache everytime someone logs in // invalidate the cache everytime someone logs in
if (lastLogin == null if (lastLogin == null || lastLogin.before(DateUtils.addHours(now, -1))) {
|| lastLogin.before(DateUtils.addHours(now, -1))) {
user.setLastLogin(now); user.setLastLogin(now);
userDAO.saveOrUpdate(user); userDAO.saveOrUpdate(user);
} }
@@ -66,39 +64,30 @@ public class UserService {
return null; return null;
} }
public User register(String name, String password, String email, public User register(String name, String password, String email, Collection<Role> roles) {
Collection<Role> roles) {
return register(name, password, email, roles, false); return register(name, password, email, roles, false);
} }
public User register(String name, String password, String email, public User register(String name, String password, String email, Collection<Role> roles, boolean forceRegistration) {
Collection<Role> roles, boolean forceRegistration) {
Preconditions.checkNotNull(name); Preconditions.checkNotNull(name);
Preconditions.checkArgument(StringUtils.length(name) <= 32, Preconditions.checkArgument(StringUtils.length(name) <= 32, "Name too long (32 characters maximum)");
"Name too long (32 characters maximum)");
Preconditions.checkNotNull(password); Preconditions.checkNotNull(password);
if (!forceRegistration) { if (!forceRegistration) {
Preconditions.checkState(applicationSettingsService.get() Preconditions.checkState(applicationSettingsService.get().isAllowRegistrations(),
.isAllowRegistrations(),
"Registrations are closed on this CommaFeed instance"); "Registrations are closed on this CommaFeed instance");
Preconditions.checkNotNull(email); Preconditions.checkNotNull(email);
Preconditions.checkArgument(StringUtils.length(name) >= 3, Preconditions.checkArgument(StringUtils.length(name) >= 3, "Name too short (3 characters minimum)");
"Name too short (3 characters minimum)"); Preconditions
Preconditions.checkArgument( .checkArgument(forceRegistration || StringUtils.length(password) >= 6, "Password too short (6 characters maximum)");
forceRegistration || StringUtils.length(password) >= 6, Preconditions.checkArgument(StringUtils.contains(email, "@"), "Invalid email address");
"Password too short (6 characters maximum)");
Preconditions.checkArgument(StringUtils.contains(email, "@"),
"Invalid email address");
} }
Preconditions.checkArgument(userDAO.findByName(name) == null, Preconditions.checkArgument(userDAO.findByName(name) == null, "Name already taken");
"Name already taken");
if (StringUtils.isNotBlank(email)) { if (StringUtils.isNotBlank(email)) {
Preconditions.checkArgument(userDAO.findByEmail(email) == null, Preconditions.checkArgument(userDAO.findByEmail(email) == null, "Email already taken");
"Email already taken");
} }
User user = new User(); User user = new User();
@@ -122,8 +111,7 @@ public class UserService {
} }
public String generateApiKey(User user) { public String generateApiKey(User user) {
byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID() byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID().toString(), user.getSalt());
.toString(), user.getSalt());
return DigestUtils.sha1Hex(key); return DigestUtils.sha1Hex(key);
} }
} }

View File

@@ -49,15 +49,12 @@ import com.commafeed.frontend.utils.exception.DisplayExceptionPage;
public class CommaFeedApplication extends AuthenticatedWebApplication { public class CommaFeedApplication extends AuthenticatedWebApplication {
private static Logger log = LoggerFactory private static Logger log = LoggerFactory.getLogger(CommaFeedApplication.class);
.getLogger(CommaFeedApplication.class);
public CommaFeedApplication() { public CommaFeedApplication() {
super(); super();
String prod = ResourceBundle.getBundle("application").getString( String prod = ResourceBundle.getBundle("application").getString("production");
"production"); setConfigurationType(Boolean.valueOf(prod) ? RuntimeConfigurationType.DEPLOYMENT : RuntimeConfigurationType.DEVELOPMENT);
setConfigurationType(Boolean.valueOf(prod) ? RuntimeConfigurationType.DEPLOYMENT
: RuntimeConfigurationType.DEVELOPMENT);
} }
@Override @Override
@@ -66,16 +63,16 @@ public class CommaFeedApplication extends AuthenticatedWebApplication {
mountPage("welcome", WelcomePage.class); mountPage("welcome", WelcomePage.class);
mountPage("demo", DemoLoginPage.class); mountPage("demo", DemoLoginPage.class);
mountPage("recover", PasswordRecoveryPage.class); mountPage("recover", PasswordRecoveryPage.class);
mountPage("recover2", PasswordRecoveryCallbackPage.class); mountPage("recover2", PasswordRecoveryCallbackPage.class);
mountPage("logout", LogoutPage.class); mountPage("logout", LogoutPage.class);
mountPage("error", DisplayExceptionPage.class); mountPage("error", DisplayExceptionPage.class);
// mountPage("google/import/redirect", GoogleImportRedirectPage.class); // mountPage("google/import/redirect", GoogleImportRedirectPage.class);
// mountPage(GoogleImportCallbackPage.PAGE_PATH, // mountPage(GoogleImportCallbackPage.PAGE_PATH,
// GoogleImportCallbackPage.class); // GoogleImportCallbackPage.class);
mountPage("next", NextUnreadRedirectPage.class); mountPage("next", NextUnreadRedirectPage.class);
@@ -89,8 +86,7 @@ public class CommaFeedApplication extends AuthenticatedWebApplication {
setHeaderResponseDecorator(new IHeaderResponseDecorator() { setHeaderResponseDecorator(new IHeaderResponseDecorator() {
@Override @Override
public IHeaderResponse decorate(IHeaderResponse response) { public IHeaderResponse decorate(IHeaderResponse response) {
return new JavaScriptFilteredIntoFooterHeaderResponse(response, return new JavaScriptFilteredIntoFooterHeaderResponse(response, "footer-container");
"footer-container");
} }
}); });
@@ -100,61 +96,52 @@ public class CommaFeedApplication extends AuthenticatedWebApplication {
AjaxRequestTarget target = cycle.find(AjaxRequestTarget.class); AjaxRequestTarget target = cycle.find(AjaxRequestTarget.class);
// redirect to the error page if ajax request, render error on // redirect to the error page if ajax request, render error on
// current page otherwise // current page otherwise
RedirectPolicy policy = target == null ? RedirectPolicy.NEVER_REDIRECT RedirectPolicy policy = target == null ? RedirectPolicy.NEVER_REDIRECT : RedirectPolicy.AUTO_REDIRECT;
: RedirectPolicy.AUTO_REDIRECT; return new RenderPageRequestHandler(new PageProvider(new DisplayExceptionPage(ex)), policy);
return new RenderPageRequestHandler(new PageProvider(
new DisplayExceptionPage(ex)), policy);
} }
}); });
} }
private void setupSecurity() { private void setupSecurity() {
getSecuritySettings().setAuthenticationStrategy( getSecuritySettings().setAuthenticationStrategy(new DefaultAuthenticationStrategy("LoggedIn") {
new DefaultAuthenticationStrategy("LoggedIn") {
private CookieUtils cookieUtils = null; private CookieUtils cookieUtils = null;
@Override @Override
protected CookieUtils getCookieUtils() { protected CookieUtils getCookieUtils() {
if (cookieUtils == null) { if (cookieUtils == null) {
cookieUtils = new CookieUtils() { cookieUtils = new CookieUtils() {
@Override @Override
protected void initializeCookie(Cookie cookie) { protected void initializeCookie(Cookie cookie) {
super.initializeCookie(cookie); super.initializeCookie(cookie);
cookie.setHttpOnly(true); cookie.setHttpOnly(true);
}
};
} }
return cookieUtils; };
} }
}); return cookieUtils;
getSecuritySettings().setAuthorizationStrategy( }
new IAuthorizationStrategy() { });
getSecuritySettings().setAuthorizationStrategy(new IAuthorizationStrategy() {
@Override @Override
public <T extends IRequestableComponent> boolean isInstantiationAuthorized( public <T extends IRequestableComponent> boolean isInstantiationAuthorized(Class<T> componentClass) {
Class<T> componentClass) { boolean authorized = true;
boolean authorized = true;
boolean restricted = componentClass boolean restricted = componentClass.isAnnotationPresent(SecurityCheck.class);
.isAnnotationPresent(SecurityCheck.class); if (restricted) {
if (restricted) { SecurityCheck annotation = componentClass.getAnnotation(SecurityCheck.class);
SecurityCheck annotation = componentClass Roles roles = CommaFeedSession.get().getRoles();
.getAnnotation(SecurityCheck.class); authorized = roles.hasAnyRole(new Roles(annotation.value().name()));
Roles roles = CommaFeedSession.get().getRoles(); }
authorized = roles.hasAnyRole(new Roles(annotation return authorized;
.value().name())); }
}
return authorized;
}
@Override @Override
public boolean isActionAuthorized(Component component, public boolean isActionAuthorized(Component component, Action action) {
Action action) { return true;
return true; }
} });
});
} }
@Override @Override
@@ -164,10 +151,8 @@ public class CommaFeedApplication extends AuthenticatedWebApplication {
protected void setupInjection() { protected void setupInjection() {
try { try {
BeanManager beanManager = (BeanManager) new InitialContext() BeanManager beanManager = (BeanManager) new InitialContext().lookup("java:comp/BeanManager");
.lookup("java:comp/BeanManager"); new CdiConfiguration(beanManager).setPropagation(ConversationPropagation.NONE).configure(this);
new CdiConfiguration(beanManager).setPropagation(
ConversationPropagation.NONE).configure(this);
} catch (NamingException e) { } catch (NamingException e) {
log.warn("Could not locate bean manager. CDI is disabled."); log.warn("Could not locate bean manager. CDI is disabled.");
} }

View File

@@ -24,8 +24,7 @@ import com.wordnik.swagger.annotations.ApiProperty;
@ApiClass("Entry details") @ApiClass("Entry details")
public class Entry implements Serializable { public class Entry implements Serializable {
public static Entry build(FeedEntryStatus status, String publicUrl, public static Entry build(FeedEntryStatus status, String publicUrl, boolean proxyImages) {
boolean proxyImages) {
Entry entry = new Entry(); Entry entry = new Entry();
FeedEntry feedEntry = status.getEntry(); FeedEntry feedEntry = status.getEntry();
@@ -33,14 +32,14 @@ public class Entry implements Serializable {
entry.setRead(status.isRead()); entry.setRead(status.isRead());
entry.setStarred(status.isStarred()); entry.setStarred(status.isStarred());
entry.setMarkable(status.isMarkable());
entry.setId(String.valueOf(feedEntry.getId())); entry.setId(String.valueOf(feedEntry.getId()));
entry.setGuid(feedEntry.getGuid()); entry.setGuid(feedEntry.getGuid());
entry.setTitle(feedEntry.getContent().getTitle()); entry.setTitle(feedEntry.getContent().getTitle());
entry.setContent(FeedUtils.proxyImages(feedEntry.getContent() entry.setContent(FeedUtils.proxyImages(feedEntry.getContent().getContent(), publicUrl, proxyImages));
.getContent(), publicUrl, proxyImages));
entry.setRtl(FeedUtils.isRTL(feedEntry)); entry.setRtl(FeedUtils.isRTL(feedEntry));
entry.setAuthor(feedEntry.getAuthor()); entry.setAuthor(feedEntry.getContent().getAuthor());
entry.setEnclosureUrl(feedEntry.getContent().getEnclosureUrl()); entry.setEnclosureUrl(feedEntry.getContent().getEnclosureUrl());
entry.setEnclosureType(feedEntry.getContent().getEnclosureType()); entry.setEnclosureType(feedEntry.getContent().getEnclosureType());
entry.setDate(feedEntry.getUpdated()); entry.setDate(feedEntry.getUpdated());
@@ -124,6 +123,9 @@ public class Entry implements Serializable {
@ApiProperty("starred status") @ApiProperty("starred status")
private boolean starred; private boolean starred;
@ApiProperty("wether the entry is still markable (old entry statuses are discarded)")
private boolean markable;
public String getId() { public String getId() {
return id; return id;
} }
@@ -268,4 +270,12 @@ public class Entry implements Serializable {
this.insertedDate = insertedDate; this.insertedDate = insertedDate;
} }
public boolean isMarkable() {
return markable;
}
public void setMarkable(boolean markable) {
this.markable = markable;
}
} }

View File

@@ -20,8 +20,7 @@ import com.wordnik.swagger.annotations.ApiProperty;
@ApiClass("User information") @ApiClass("User information")
public class Subscription implements Serializable { public class Subscription implements Serializable {
public static Subscription build(FeedSubscription subscription, public static Subscription build(FeedSubscription subscription, String publicUrl, long unreadCount) {
String publicUrl, long unreadCount) {
Date now = new Date(); Date now = new Date();
FeedCategory category = subscription.getCategory(); FeedCategory category = subscription.getCategory();
Feed feed = subscription.getFeed(); Feed feed = subscription.getFeed();
@@ -35,12 +34,9 @@ public class Subscription implements Serializable {
sub.setFeedLink(feed.getLink()); sub.setFeedLink(feed.getLink());
sub.setIconUrl(FeedUtils.getFaviconUrl(subscription, publicUrl)); sub.setIconUrl(FeedUtils.getFaviconUrl(subscription, publicUrl));
sub.setLastRefresh(feed.getLastUpdated()); sub.setLastRefresh(feed.getLastUpdated());
sub.setNextRefresh((feed.getDisabledUntil() != null && feed sub.setNextRefresh((feed.getDisabledUntil() != null && feed.getDisabledUntil().before(now)) ? null : feed.getDisabledUntil());
.getDisabledUntil().before(now)) ? null : feed
.getDisabledUntil());
sub.setUnread(unreadCount); sub.setUnread(unreadCount);
sub.setCategoryId(category == null ? null : String.valueOf(category sub.setCategoryId(category == null ? null : String.valueOf(category.getId()));
.getId()));
return sub; return sub;
} }

View File

@@ -14,7 +14,7 @@ import com.wordnik.swagger.annotations.ApiProperty;
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Feed information request") @ApiClass("Feed information request")
public class FeedInfoRequest implements Serializable { public class FeedInfoRequest implements Serializable {
@ApiProperty(value = "feed url", required = true) @ApiProperty(value = "feed url", required = true)
private String url; private String url;
@@ -25,7 +25,5 @@ public class FeedInfoRequest implements Serializable {
public void setUrl(String url) { public void setUrl(String url) {
this.url = url; this.url = url;
} }
} }

View File

@@ -24,7 +24,9 @@ public class MarkRequest implements Serializable {
@ApiProperty(value = "mark as read or unread") @ApiProperty(value = "mark as read or unread")
private boolean read; private boolean read;
@ApiProperty(value = "only entries older than this, pass the timestamp you got from the entry list to prevent marking an entry that was not retrieved", required = false) @ApiProperty(
value = "only entries older than this, pass the timestamp you got from the entry list to prevent marking an entry that was not retrieved",
required = false)
private Long olderThan; private Long olderThan;
public String getId() { public String getId() {

View File

@@ -79,15 +79,12 @@ public abstract class BasePage extends WebPage {
if (user != null) { if (user != null) {
UserSettings settings = userSettingsDAO.findByUser(user); UserSettings settings = userSettingsDAO.findByUser(user);
if (settings != null) { if (settings != null) {
lang = settings.getLanguage() == null ? "en" : settings lang = settings.getLanguage() == null ? "en" : settings.getLanguage();
.getLanguage(); theme = settings.getTheme() == null ? "default" : settings.getTheme();
theme = settings.getTheme() == null ? "default" : settings
.getTheme();
} }
} }
add(new TransparentWebMarkupContainer("html").setMarkupId( add(new TransparentWebMarkupContainer("html").setMarkupId("theme-" + theme).add(new AttributeModifier("lang", lang)));
"theme-" + theme).add(new AttributeModifier("lang", lang)));
settings = applicationSettingsService.get(); settings = applicationSettingsService.get();
add(new HeaderResponseContainer("footer-container", "footer-container")); add(new HeaderResponseContainer("footer-container", "footer-container"));
@@ -107,8 +104,7 @@ public abstract class BasePage extends WebPage {
if (getApplication().getConfigurationType() == RuntimeConfigurationType.DEPLOYMENT) { if (getApplication().getConfigurationType() == RuntimeConfigurationType.DEPLOYMENT) {
long startupTime = startupBean.getStartupTime(); long startupTime = startupBean.getStartupTime();
String suffix = "?" + startupTime; String suffix = "?" + startupTime;
response.render(JavaScriptHeaderItem.forUrl("static/all.js" response.render(JavaScriptHeaderItem.forUrl("static/all.js" + suffix));
+ suffix));
response.render(CssHeaderItem.forUrl("static/all.css" + suffix)); response.render(CssHeaderItem.forUrl("static/all.css" + suffix));
} else { } else {
response.render(JavaScriptHeaderItem.forUrl("wro/lib.js")); response.render(JavaScriptHeaderItem.forUrl("wro/lib.js"));

View File

@@ -16,8 +16,7 @@ public class DemoLoginPage extends WebPage {
UserService userService; UserService userService;
public DemoLoginPage() { public DemoLoginPage() {
CommaFeedSession.get().authenticate(StartupBean.USERNAME_DEMO, CommaFeedSession.get().authenticate(StartupBean.USERNAME_DEMO, StartupBean.USERNAME_DEMO);
StartupBean.USERNAME_DEMO);
setResponsePage(getApplication().getHomePage()); setResponsePage(getApplication().getHomePage());
} }
} }

View File

@@ -55,8 +55,7 @@ public class GoogleImportCallbackPage extends WebPage {
if (request.getQueryString() != null) { if (request.getQueryString() != null) {
urlBuffer.append('?').append(request.getQueryString()); urlBuffer.append('?').append(request.getQueryString());
} }
AuthorizationCodeResponseUrl responseUrl = new AuthorizationCodeResponseUrl( AuthorizationCodeResponseUrl responseUrl = new AuthorizationCodeResponseUrl(urlBuffer.toString());
urlBuffer.toString());
String code = responseUrl.getCode(); String code = responseUrl.getCode();
if (responseUrl.getError() != null) { if (responseUrl.getError() != null) {
@@ -73,8 +72,8 @@ public class GoogleImportCallbackPage extends WebPage {
HttpTransport httpTransport = new NetHttpTransport(); HttpTransport httpTransport = new NetHttpTransport();
JacksonFactory jsonFactory = new JacksonFactory(); JacksonFactory jsonFactory = new JacksonFactory();
AuthorizationCodeTokenRequest tokenRequest = new AuthorizationCodeTokenRequest( AuthorizationCodeTokenRequest tokenRequest = new AuthorizationCodeTokenRequest(httpTransport, jsonFactory, new GenericUrl(
httpTransport, jsonFactory, new GenericUrl(TOKEN_URL), code); TOKEN_URL), code);
tokenRequest.setRedirectUri(redirectUri); tokenRequest.setRedirectUri(redirectUri);
tokenRequest.put("client_id", clientId); tokenRequest.put("client_id", clientId);
tokenRequest.put("client_secret", clientSecret); tokenRequest.put("client_secret", clientSecret);
@@ -87,16 +86,13 @@ public class GoogleImportCallbackPage extends WebPage {
TokenResponse tokenResponse = tokenRequest.execute(); TokenResponse tokenResponse = tokenRequest.execute();
String accessToken = tokenResponse.getAccessToken(); String accessToken = tokenResponse.getAccessToken();
HttpRequest httpRequest = httpTransport.createRequestFactory() HttpRequest httpRequest = httpTransport.createRequestFactory().buildGetRequest(new GenericUrl(EXPORT_URL));
.buildGetRequest(new GenericUrl(EXPORT_URL)); BearerToken.authorizationHeaderAccessMethod().intercept(httpRequest, accessToken);
BearerToken.authorizationHeaderAccessMethod().intercept(
httpRequest, accessToken);
String opml = httpRequest.execute().parseAsString(); String opml = httpRequest.execute().parseAsString();
User user = CommaFeedSession.get().getUser(); User user = CommaFeedSession.get().getUser();
if (user != null) { if (user != null) {
if (StartupBean.USERNAME_DEMO.equals(user.getName())) { if (StartupBean.USERNAME_DEMO.equals(user.getName())) {
throw new DisplayException( throw new DisplayException("Import is disabled for the demo account");
"Import is disabled for the demo account");
} }
importer.importOpml(CommaFeedSession.get().getUser(), opml); importer.importOpml(CommaFeedSession.get().getUser(), opml);
} }

View File

@@ -15,8 +15,7 @@ import com.commafeed.backend.services.ApplicationSettingsService;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class GoogleImportRedirectPage extends WebPage { public class GoogleImportRedirectPage extends WebPage {
private static Logger log = Logger private static Logger log = Logger.getLogger(GoogleImportRedirectPage.class);
.getLogger(GoogleImportRedirectPage.class);
private static final String SCOPE = "https://www.google.com/reader/subscriptions/export email profile"; private static final String SCOPE = "https://www.google.com/reader/subscriptions/export email profile";
private static final String AUTH_URL = "https://accounts.google.com/o/oauth2/auth"; private static final String AUTH_URL = "https://accounts.google.com/o/oauth2/auth";

View File

@@ -18,16 +18,12 @@ public class HomePage extends BasePage {
public void renderHead(IHeaderResponse response) { public void renderHead(IHeaderResponse response) {
super.renderHead(response); super.renderHead(response);
response.render(CssHeaderItem.forReference( response.render(CssHeaderItem.forReference(new UserCustomCssReference() {
new UserCustomCssReference() { @Override
@Override protected String getCss() {
protected String getCss() { UserSettings settings = userSettingsDAO.findByUser(CommaFeedSession.get().getUser());
UserSettings settings = userSettingsDAO return settings == null ? null : settings.getCustomCss();
.findByUser(CommaFeedSession.get().getUser()); }
return settings == null ? null : settings }, new PageParameters().add("_t", System.currentTimeMillis()), null));
.getCustomCss();
}
}, new PageParameters().add("_t", System.currentTimeMillis()),
null));
} }
} }

View File

@@ -52,20 +52,15 @@ public class NextUnreadRedirectPage extends WebPage {
} }
List<FeedEntryStatus> statuses = null; List<FeedEntryStatus> statuses = null;
if (StringUtils.isBlank(categoryId) if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
|| CategoryREST.ALL.equals(categoryId)) { List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
statuses = feedEntryStatusDAO.findAllUnread(user, null, 0, 1, statuses = feedEntryStatusDAO.findBySubscriptions(subs, true, null, null, 0, 1, order, true);
order, true);
} else { } else {
FeedCategory category = feedCategoryDAO.findById(user, FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
Long.valueOf(categoryId));
if (category != null) { if (category != null) {
List<FeedCategory> children = feedCategoryDAO List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user, category);
.findAllChildrenCategories(user, category); List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, children);
List<FeedSubscription> subscriptions = feedSubscriptionDAO statuses = feedEntryStatusDAO.findBySubscriptions(subscriptions, true, null, null, 0, 1, order, true);
.findByCategories(user, children);
statuses = feedEntryStatusDAO.findUnreadBySubscriptions(
subscriptions, null, 0, 1, order, true);
} }
} }

View File

@@ -40,12 +40,10 @@ public class PasswordRecoveryCallbackPage extends BasePage {
if (user == null) { if (user == null) {
throw new DisplayException("email not found"); throw new DisplayException("email not found");
} }
if (user.getRecoverPasswordToken() == null if (user.getRecoverPasswordToken() == null || !user.getRecoverPasswordToken().equals(token)) {
|| !user.getRecoverPasswordToken().equals(token)) {
throw new DisplayException("invalid token"); throw new DisplayException("invalid token");
} }
if (user.getRecoverPasswordTokenDate().before( if (user.getRecoverPasswordTokenDate().before(DateUtils.addDays(new Date(), -2))) {
DateUtils.addDays(new Date(), -2))) {
throw new DisplayException("token expired"); throw new DisplayException("token expired");
} }
@@ -57,8 +55,7 @@ public class PasswordRecoveryCallbackPage extends BasePage {
protected void onSubmit() { protected void onSubmit() {
String passwd = password.getObject(); String passwd = password.getObject();
if (StringUtils.equals(passwd, confirm.getObject())) { if (StringUtils.equals(passwd, confirm.getObject())) {
byte[] password = encryptionService.getEncryptedPassword( byte[] password = encryptionService.getEncryptedPassword(passwd, user.getSalt());
passwd, user.getSalt());
user.setPassword(password); user.setPassword(password);
user.setApiKey(userService.generateApiKey(user)); user.setApiKey(userService.generateApiKey(user));
user.setRecoverPasswordToken(null); user.setRecoverPasswordToken(null);
@@ -71,10 +68,8 @@ public class PasswordRecoveryCallbackPage extends BasePage {
} }
}; };
add(form); add(form);
form.add(new PasswordTextField("password", password).setResetPassword( form.add(new PasswordTextField("password", password).setResetPassword(true).add(StringValidator.minimumLength(6)));
true).add(StringValidator.minimumLength(6))); form.add(new PasswordTextField("confirm", confirm).setResetPassword(true).add(StringValidator.minimumLength(6)));
form.add(new PasswordTextField("confirm", confirm).setResetPassword(
true).add(StringValidator.minimumLength(6)));
form.add(new BookmarkablePageLink<Void>("cancel", HomePage.class)); form.add(new BookmarkablePageLink<Void>("cancel", HomePage.class));

View File

@@ -21,8 +21,7 @@ import com.commafeed.frontend.pages.components.BootstrapFeedbackPanel;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class PasswordRecoveryPage extends BasePage { public class PasswordRecoveryPage extends BasePage {
private static Logger log = LoggerFactory private static Logger log = LoggerFactory.getLogger(PasswordRecoveryPage.class);
.getLogger(PasswordRecoveryPage.class);
public PasswordRecoveryPage() { public PasswordRecoveryPage() {
@@ -37,12 +36,10 @@ public class PasswordRecoveryPage extends BasePage {
error("Email not found."); error("Email not found.");
} else { } else {
try { try {
user.setRecoverPasswordToken(DigestUtils.sha1Hex(UUID user.setRecoverPasswordToken(DigestUtils.sha1Hex(UUID.randomUUID().toString()));
.randomUUID().toString()));
user.setRecoverPasswordTokenDate(new Date()); user.setRecoverPasswordTokenDate(new Date());
userDAO.saveOrUpdate(user); userDAO.saveOrUpdate(user);
mailService.sendMail(user, "Password recovery", mailService.sendMail(user, "Password recovery", buildEmailContent(user));
buildEmailContent(user));
info("Email sent."); info("Email sent.");
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
@@ -67,9 +64,7 @@ public class PasswordRecoveryPage extends BasePage {
private String buildEmailContent(User user) throws Exception { private String buildEmailContent(User user) throws Exception {
String publicUrl = FeedUtils String publicUrl = FeedUtils.removeTrailingSlash(applicationSettingsService.get().getPublicUrl());
.removeTrailingSlash(applicationSettingsService.get()
.getPublicUrl());
publicUrl += "/recover2"; publicUrl += "/recover2";
return String return String
@@ -78,11 +73,7 @@ public class PasswordRecoveryPage extends BasePage {
} }
private String callbackUrl(User user, String publicUrl) throws Exception { private String callbackUrl(User user, String publicUrl) throws Exception {
return new URIBuilder(publicUrl) return new URIBuilder(publicUrl).addParameter(PasswordRecoveryCallbackPage.PARAM_EMAIL, user.getEmail())
.addParameter(PasswordRecoveryCallbackPage.PARAM_EMAIL, .addParameter(PasswordRecoveryCallbackPage.PARAM_TOKEN, user.getRecoverPasswordToken()).build().toURL().toString();
user.getEmail())
.addParameter(PasswordRecoveryCallbackPage.PARAM_TOKEN,
user.getRecoverPasswordToken()).build().toURL()
.toString();
} }
} }

View File

@@ -53,8 +53,7 @@ public class TestRssPage extends WebPage {
} catch (InterruptedException e) { } catch (InterruptedException e) {
// do nothing // do nothing
} }
getRequestCycle().scheduleRequestHandlerAfterCurrent( getRequestCycle().scheduleRequestHandlerAfterCurrent(new TextRequestHandler("text/xml", "UTF-8", writer.toString()));
new TextRequestHandler("text/xml", "UTF-8", writer.toString()));
} }
} }

View File

@@ -15,16 +15,14 @@ public class WelcomePage extends BasePage {
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
public WelcomePage() { public WelcomePage() {
add(new BookmarkablePageLink<Void>("logo-link", getApplication() add(new BookmarkablePageLink<Void>("logo-link", getApplication().getHomePage()));
.getHomePage()));
add(new BookmarkablePageLink<Void>("demo-login", DemoLoginPage.class)); add(new BookmarkablePageLink<Void>("demo-login", DemoLoginPage.class));
add(new LoginPanel("login")); add(new LoginPanel("login"));
add(new RegisterPanel("register") { add(new RegisterPanel("register") {
@Override @Override
protected void onConfigure() { protected void onConfigure() {
super.onConfigure(); super.onConfigure();
setVisibilityAllowed(applicationSettingsService.get() setVisibilityAllowed(applicationSettingsService.get().isAllowRegistrations());
.isAllowRegistrations());
} }
}); });
} }

View File

@@ -19,11 +19,9 @@ public class LoginPanel extends SignInPanel {
public LoginPanel(String id) { public LoginPanel(String id) {
super(id); super(id);
replace(new BootstrapFeedbackPanel("feedback", replace(new BootstrapFeedbackPanel("feedback", new ContainerFeedbackMessageFilter(this)));
new ContainerFeedbackMessageFilter(this)));
Form<?> form = (Form<?>) get("signInForm"); Form<?> form = (Form<?>) get("signInForm");
form.add(new BookmarkablePageLink<Void>("recover", form.add(new BookmarkablePageLink<Void>("recover", PasswordRecoveryPage.class) {
PasswordRecoveryPage.class){
@Override @Override
protected void onConfigure() { protected void onConfigure() {
super.onConfigure(); super.onConfigure();

View File

@@ -45,62 +45,51 @@ public class RegisterPanel extends Panel {
IModel<RegistrationRequest> model = Model.of(new RegistrationRequest()); IModel<RegistrationRequest> model = Model.of(new RegistrationRequest());
Form<RegistrationRequest> form = new StatelessForm<RegistrationRequest>( Form<RegistrationRequest> form = new StatelessForm<RegistrationRequest>("form", model) {
"form", model) {
@Override @Override
protected void onSubmit() { protected void onSubmit() {
if (applicationSettingsService.get().isAllowRegistrations()) { if (applicationSettingsService.get().isAllowRegistrations()) {
RegistrationRequest req = getModelObject(); RegistrationRequest req = getModelObject();
userService.register(req.getName(), req.getPassword(), userService.register(req.getName(), req.getPassword(), req.getEmail(), Arrays.asList(Role.USER));
req.getEmail(), Arrays.asList(Role.USER));
IAuthenticationStrategy strategy = getApplication() IAuthenticationStrategy strategy = getApplication().getSecuritySettings().getAuthenticationStrategy();
.getSecuritySettings().getAuthenticationStrategy();
strategy.save(req.getName(), req.getPassword()); strategy.save(req.getName(), req.getPassword());
CommaFeedSession.get().signIn(req.getName(), CommaFeedSession.get().signIn(req.getName(), req.getPassword());
req.getPassword());
} }
setResponsePage(getApplication().getHomePage()); setResponsePage(getApplication().getHomePage());
} }
}; };
add(form); add(form);
add(new BootstrapFeedbackPanel("feedback", add(new BootstrapFeedbackPanel("feedback", new ContainerFeedbackMessageFilter(form)));
new ContainerFeedbackMessageFilter(form)));
RegistrationRequest p = MF.p(RegistrationRequest.class); RegistrationRequest p = MF.p(RegistrationRequest.class);
form.add(new RequiredTextField<String>("name", MF.m(model, p.getName())) form.add(new RequiredTextField<String>("name", MF.m(model, p.getName())).add(StringValidator.lengthBetween(3, 32)).add(
.add(StringValidator.lengthBetween(3, 32)).add( new IValidator<String>() {
new IValidator<String>() { @Override
@Override public void validate(IValidatable<String> validatable) {
public void validate( String name = validatable.getValue();
IValidatable<String> validatable) { User user = userDAO.findByName(name);
String name = validatable.getValue(); if (user != null) {
User user = userDAO.findByName(name); validatable.error(new ValidationError("Name is already taken."));
if (user != null) { }
validatable.error(new ValidationError( }
"Name is already taken.")); }));
} form.add(new PasswordTextField("password", MF.m(model, p.getPassword())).setResetPassword(false).add(
} StringValidator.minimumLength(6)));
})); form.add(new RequiredTextField<String>("email", MF.m(model, p.getEmail())) {
form.add(new PasswordTextField("password", MF.m(model, p.getPassword()))
.setResetPassword(false).add(StringValidator.minimumLength(6)));
form.add(new RequiredTextField<String>("email", MF.m(model,
p.getEmail())) {
@Override @Override
protected String getInputType() { protected String getInputType() {
return "email"; return "email";
} }
}.add(RfcCompliantEmailAddressValidator.getInstance()).add( }.add(RfcCompliantEmailAddressValidator.getInstance()).add(new IValidator<String>() {
new IValidator<String>() { @Override
@Override public void validate(IValidatable<String> validatable) {
public void validate(IValidatable<String> validatable) { String email = validatable.getValue();
String email = validatable.getValue(); User user = userDAO.findByEmail(email);
User user = userDAO.findByEmail(email); if (user != null) {
if (user != null) { validatable.error(new ValidationError("Email is already taken."));
validatable.error(new ValidationError( }
"Email is already taken.")); }
} }));
}
}));
} }
} }

View File

@@ -12,8 +12,7 @@ import ro.isdc.wro.model.resource.processor.impl.css.CssImportPreProcessor;
public class SassImportProcessor extends CssImportPreProcessor { public class SassImportProcessor extends CssImportPreProcessor {
@Override @Override
protected String doTransform(String cssContent, List<Resource> foundImports) protected String doTransform(String cssContent, List<Resource> foundImports) throws IOException {
throws IOException {
for (Resource resource : foundImports) { for (Resource resource : foundImports) {
String uri = resource.getUri(); String uri = resource.getUri();
int lastSlash = uri.lastIndexOf('/'); int lastSlash = uri.lastIndexOf('/');

View File

@@ -15,10 +15,8 @@ import ro.isdc.wro.model.resource.SupportedResourceType;
public class SassOnlyProcessor extends RubySassCssProcessor { public class SassOnlyProcessor extends RubySassCssProcessor {
@Override @Override
public void process(Resource resource, Reader reader, Writer writer) public void process(Resource resource, Reader reader, Writer writer) throws IOException {
throws IOException { if (resource.getUri().endsWith(".sass") || resource.getUri().endsWith(".scss")) {
if (resource.getUri().endsWith(".sass")
|| resource.getUri().endsWith(".scss")) {
super.process(resource, reader, writer); super.process(resource, reader, writer);
} else { } else {
writer.write(IOUtils.toString(reader)); writer.write(IOUtils.toString(reader));

View File

@@ -17,8 +17,7 @@ public class TimestampProcessor implements ResourcePreProcessor {
private static final String NOW = "" + System.currentTimeMillis(); private static final String NOW = "" + System.currentTimeMillis();
@Override @Override
public void process(Resource resource, Reader reader, Writer writer) public void process(Resource resource, Reader reader, Writer writer) throws IOException {
throws IOException {
String content = IOUtils.toString(reader); String content = IOUtils.toString(reader);
content = content.replace("${timestamp}", NOW); content = content.replace("${timestamp}", NOW);
writer.write(content); writer.write(content);

View File

@@ -24,10 +24,8 @@ public abstract class UserCustomCssReference extends ResourceReference {
resourceResponse.setTextEncoding("UTF-8"); resourceResponse.setTextEncoding("UTF-8");
resourceResponse.setWriteCallback(new WriteCallback() { resourceResponse.setWriteCallback(new WriteCallback() {
@Override @Override
public void writeData(Attributes attributes) public void writeData(Attributes attributes) throws IOException {
throws IOException { attributes.getResponse().write(StringUtils.trimToEmpty(getCss()));
attributes.getResponse().write(
StringUtils.trimToEmpty(getCss()));
} }
}); });
return resourceResponse; return resourceResponse;

View File

@@ -10,7 +10,7 @@ import com.google.api.client.util.Maps;
/** /**
* Build-time solution * Build-time solution
* *
*/ */
public class WroAdditionalProvider implements ProcessorProvider { public class WroAdditionalProvider implements ProcessorProvider {

View File

@@ -7,7 +7,7 @@ import ro.isdc.wro.model.resource.processor.ResourcePreProcessor;
/** /**
* Runtime solution * Runtime solution
* *
*/ */
public class WroManagerFactory extends ConfigurableWroManagerFactory { public class WroManagerFactory extends ConfigurableWroManagerFactory {

View File

@@ -1,6 +1,5 @@
package com.commafeed.frontend.rest; package com.commafeed.frontend.rest;
public class Enums { public class Enums {
public enum Type { public enum Type {

View File

@@ -17,18 +17,14 @@ import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
public class JsonProvider extends JacksonJsonProvider { public class JsonProvider extends JacksonJsonProvider {
@Override @Override
public void writeTo(Object value, Class<?> type, Type genericType, public void writeTo(Object value, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException {
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException {
httpHeaders.putSingle(HttpHeaders.CONTENT_TYPE, mediaType.toString() httpHeaders.putSingle(HttpHeaders.CONTENT_TYPE, mediaType.toString() + ";charset=UTF-8");
+ ";charset=UTF-8");
httpHeaders.putSingle(HttpHeaders.CACHE_CONTROL, "no-cache"); httpHeaders.putSingle(HttpHeaders.CACHE_CONTROL, "no-cache");
httpHeaders.putSingle(HttpHeaders.PRAGMA, "no-cache"); httpHeaders.putSingle(HttpHeaders.PRAGMA, "no-cache");
super.writeTo(value, type, genericType, annotations, mediaType, super.writeTo(value, type, genericType, annotations, mediaType, httpHeaders, entityStream);
httpHeaders, entityStream);
} }
} }

View File

@@ -51,8 +51,7 @@ public abstract class AbstractREST {
ServletWebResponse swresp = new ServletWebResponse(swreq, response); ServletWebResponse swresp = new ServletWebResponse(swreq, response);
RequestCycle cycle = app.createRequestCycle(swreq, swresp); RequestCycle cycle = app.createRequestCycle(swreq, swresp);
ThreadContext.setRequestCycle(cycle); ThreadContext.setRequestCycle(cycle);
CommaFeedSession session = (CommaFeedSession) app CommaFeedSession session = (CommaFeedSession) app.fetchCreateAndSetSession(cycle);
.fetchCreateAndSetSession(cycle);
if (session.getUser() == null) { if (session.getUser() == null) {
cookieLogin(app, session); cookieLogin(app, session);
@@ -63,8 +62,7 @@ public abstract class AbstractREST {
} }
private void cookieLogin(CommaFeedApplication app, CommaFeedSession session) { private void cookieLogin(CommaFeedApplication app, CommaFeedSession session) {
IAuthenticationStrategy authenticationStrategy = app IAuthenticationStrategy authenticationStrategy = app.getSecuritySettings().getAuthenticationStrategy();
.getSecuritySettings().getAuthenticationStrategy();
String[] data = authenticationStrategy.load(); String[] data = authenticationStrategy.load();
if (data != null && data.length > 1) { if (data != null && data.length > 1) {
session.signIn(data[0], data[1]); session.signIn(data[0], data[1]);
@@ -98,8 +96,7 @@ public abstract class AbstractREST {
boolean allowed = true; boolean allowed = true;
User user = null; User user = null;
Method method = context.getMethod(); Method method = context.getMethod();
SecurityCheck check = method.isAnnotationPresent(SecurityCheck.class) ? method SecurityCheck check = method.isAnnotationPresent(SecurityCheck.class) ? method.getAnnotation(SecurityCheck.class) : method
.getAnnotation(SecurityCheck.class) : method
.getDeclaringClass().getAnnotation(SecurityCheck.class); .getDeclaringClass().getAnnotation(SecurityCheck.class);
if (check != null) { if (check != null) {
@@ -113,11 +110,9 @@ public abstract class AbstractREST {
} }
if (!allowed) { if (!allowed) {
if (user == null) { if (user == null) {
return Response.status(Status.UNAUTHORIZED) return Response.status(Status.UNAUTHORIZED).entity("You are not authorized to do this.").build();
.entity("You are not authorized to do this.").build();
} else { } else {
return Response.status(Status.FORBIDDEN) return Response.status(Status.FORBIDDEN).entity("You are not authorized to do this.").build();
.entity("You are not authorized to do this.").build();
} }
} }

View File

@@ -33,40 +33,33 @@ public abstract class AbstractResourceREST extends AbstractREST {
@GET @GET
@SecurityCheck(value = Role.NONE) @SecurityCheck(value = Role.NONE)
@ApiOperation(value = "Returns information about API parameters", responseClass = "com.wordnik.swagger.core.Documentation") @ApiOperation(value = "Returns information about API parameters", responseClass = "com.wordnik.swagger.core.Documentation")
public Response getHelp(@Context Application app, public Response getHelp(@Context Application app, @Context HttpHeaders headers, @Context UriInfo uriInfo) {
@Context HttpHeaders headers, @Context UriInfo uriInfo) {
TypeUtil.addAllowablePackage(Entries.class.getPackage().getName()); TypeUtil.addAllowablePackage(Entries.class.getPackage().getName());
TypeUtil.addAllowablePackage(MarkRequest.class.getPackage().getName()); TypeUtil.addAllowablePackage(MarkRequest.class.getPackage().getName());
String apiVersion = ApiDocumentationREST.API_VERSION; String apiVersion = ApiDocumentationREST.API_VERSION;
String swaggerVersion = SwaggerSpec.version(); String swaggerVersion = SwaggerSpec.version();
String basePath = ApiDocumentationREST String basePath = ApiDocumentationREST.getBasePath(applicationSettingsService.get().getPublicUrl());
.getBasePath(applicationSettingsService.get().getPublicUrl());
Class<?> resource = null; Class<?> resource = null;
String path = prependSlash(uriInfo.getPath()); String path = prependSlash(uriInfo.getPath());
for (Class<?> klass : app.getClasses()) { for (Class<?> klass : app.getClasses()) {
Api api = klass.getAnnotation(Api.class); Api api = klass.getAnnotation(Api.class);
if (api != null && api.value() != null if (api != null && api.value() != null && StringUtils.equals(prependSlash(api.value()), path)) {
&& StringUtils.equals(prependSlash(api.value()), path)) {
resource = klass; resource = klass;
break; break;
} }
} }
if (resource == null) { if (resource == null) {
return Response return Response.status(Status.NOT_FOUND).entity("Api annotation not found on class " + getClass().getName()).build();
.status(Status.NOT_FOUND)
.entity("Api annotation not found on class "
+ getClass().getName()).build();
} }
Api api = resource.getAnnotation(Api.class); Api api = resource.getAnnotation(Api.class);
String apiPath = api.value(); String apiPath = api.value();
String apiListingPath = api.value(); String apiListingPath = api.value();
Documentation doc = new HelpApi(null).filterDocs(JaxrsApiReader.read( Documentation doc = new HelpApi(null).filterDocs(JaxrsApiReader.read(resource, apiVersion, swaggerVersion, basePath, apiPath),
resource, apiVersion, swaggerVersion, basePath, apiPath),
headers, uriInfo, apiListingPath, apiPath); headers, uriInfo, apiListingPath, apiPath);
doc.setSwaggerVersion(swaggerVersion); doc.setSwaggerVersion(swaggerVersion);

View File

@@ -102,24 +102,18 @@ public class AdminREST extends AbstractResourceREST {
roles.add(Role.ADMIN); roles.add(Role.ADMIN);
} }
try { try {
userService.register(userModel.getName(), userService.register(userModel.getName(), userModel.getPassword(), userModel.getEmail(), roles, true);
userModel.getPassword(), userModel.getEmail(), roles,
true);
} catch (Exception e) { } catch (Exception e) {
return Response.status(Status.CONFLICT).entity(e.getMessage()) return Response.status(Status.CONFLICT).entity(e.getMessage()).build();
.build();
} }
} else { } else {
User user = userDAO.findById(id); User user = userDAO.findById(id);
if (StartupBean.USERNAME_ADMIN.equals(user.getName()) if (StartupBean.USERNAME_ADMIN.equals(user.getName()) && !userModel.isEnabled()) {
&& !userModel.isEnabled()) { return Response.status(Status.FORBIDDEN).entity("You cannot disable the admin user.").build();
return Response.status(Status.FORBIDDEN)
.entity("You cannot disable the admin user.").build();
} }
user.setName(userModel.getName()); user.setName(userModel.getName());
if (StringUtils.isNotBlank(userModel.getPassword())) { if (StringUtils.isNotBlank(userModel.getPassword())) {
user.setPassword(encryptionService.getEncryptedPassword( user.setPassword(encryptionService.getEncryptedPassword(userModel.getPassword(), user.getSalt()));
userModel.getPassword(), user.getSalt()));
} }
user.setEmail(userModel.getEmail()); user.setEmail(userModel.getEmail());
user.setDisabled(!userModel.isEnabled()); user.setDisabled(!userModel.isEnabled());
@@ -130,10 +124,7 @@ public class AdminREST extends AbstractResourceREST {
userRoleDAO.saveOrUpdate(new UserRole(user, Role.ADMIN)); userRoleDAO.saveOrUpdate(new UserRole(user, Role.ADMIN));
} else if (!userModel.isAdmin() && roles.contains(Role.ADMIN)) { } else if (!userModel.isAdmin() && roles.contains(Role.ADMIN)) {
if (StartupBean.USERNAME_ADMIN.equals(user.getName())) { if (StartupBean.USERNAME_ADMIN.equals(user.getName())) {
return Response return Response.status(Status.FORBIDDEN).entity("You cannot remove the admin role from the admin user.").build();
.status(Status.FORBIDDEN)
.entity("You cannot remove the admin role from the admin user.")
.build();
} }
for (UserRole userRole : userRoleDAO.findAll(user)) { for (UserRole userRole : userRoleDAO.findAll(user)) {
if (userRole.getRole() == Role.ADMIN) { if (userRole.getRole() == Role.ADMIN) {
@@ -150,8 +141,7 @@ public class AdminREST extends AbstractResourceREST {
@Path("/user/get/{id}") @Path("/user/get/{id}")
@GET @GET
@ApiOperation(value = "Get user information", notes = "Get user information", responseClass = "com.commafeed.frontend.model.UserModel") @ApiOperation(value = "Get user information", notes = "Get user information", responseClass = "com.commafeed.frontend.model.UserModel")
public Response getUser( public Response getUser(@ApiParam(value = "user id", required = true) @PathParam("id") Long id) {
@ApiParam(value = "user id", required = true) @PathParam("id") Long id) {
Preconditions.checkNotNull(id); Preconditions.checkNotNull(id);
User user = userDAO.findById(id); User user = userDAO.findById(id);
UserModel userModel = new UserModel(); UserModel userModel = new UserModel();
@@ -205,8 +195,7 @@ public class AdminREST extends AbstractResourceREST {
return Response.status(Status.NOT_FOUND).build(); return Response.status(Status.NOT_FOUND).build();
} }
if (StartupBean.USERNAME_ADMIN.equals(user.getName())) { if (StartupBean.USERNAME_ADMIN.equals(user.getName())) {
return Response.status(Status.FORBIDDEN) return Response.status(Status.FORBIDDEN).entity("You cannot delete the admin user.").build();
.entity("You cannot delete the admin user.").build();
} }
userService.unregister(user); userService.unregister(user);
return Response.ok().build(); return Response.ok().build();
@@ -214,7 +203,10 @@ public class AdminREST extends AbstractResourceREST {
@Path("/settings") @Path("/settings")
@GET @GET
@ApiOperation(value = "Retrieve application settings", notes = "Retrieve application settings", responseClass = "com.commafeed.backend.model.ApplicationSettings") @ApiOperation(
value = "Retrieve application settings",
notes = "Retrieve application settings",
responseClass = "com.commafeed.backend.model.ApplicationSettings")
public Response getSettings() { public Response getSettings() {
return Response.ok(applicationSettingsService.get()).build(); return Response.ok(applicationSettingsService.get()).build();
} }
@@ -222,8 +214,7 @@ public class AdminREST extends AbstractResourceREST {
@Path("/settings") @Path("/settings")
@POST @POST
@ApiOperation(value = "Save application settings", notes = "Save application settings") @ApiOperation(value = "Save application settings", notes = "Save application settings")
public Response saveSettings( public Response saveSettings(@ApiParam(required = true) ApplicationSettings settings) {
@ApiParam(required = true) ApplicationSettings settings) {
Preconditions.checkNotNull(settings); Preconditions.checkNotNull(settings);
applicationSettingsService.save(settings); applicationSettingsService.save(settings);
return Response.ok().build(); return Response.ok().build();
@@ -232,8 +223,7 @@ public class AdminREST extends AbstractResourceREST {
@Path("/metrics") @Path("/metrics")
@GET @GET
@ApiOperation(value = "Retrieve server metrics") @ApiOperation(value = "Retrieve server metrics")
public Response getMetrics( public Response getMetrics(@QueryParam("backlog") @DefaultValue("false") boolean backlog) {
@QueryParam("backlog") @DefaultValue("false") boolean backlog) {
Map<String, Object> map = Maps.newLinkedHashMap(); Map<String, Object> map = Maps.newLinkedHashMap();
map.put("lastMinute", metricsBean.getLastMinute()); map.put("lastMinute", metricsBean.getLastMinute());
map.put("lastHour", metricsBean.getLastHour()); map.put("lastHour", metricsBean.getLastHour());
@@ -254,43 +244,44 @@ public class AdminREST extends AbstractResourceREST {
@ApiOperation(value = "Feeds cleanup", notes = "Delete feeds without subscriptions and entries without feeds") @ApiOperation(value = "Feeds cleanup", notes = "Delete feeds without subscriptions and entries without feeds")
public Response cleanupFeeds() { public Response cleanupFeeds() {
Map<String, Long> map = Maps.newHashMap(); Map<String, Long> map = Maps.newHashMap();
map.put("feeds_without_subscriptions", map.put("feeds_without_subscriptions", cleaner.cleanFeedsWithoutSubscriptions());
cleaner.cleanFeedsWithoutSubscriptions()); return Response.ok(map).build();
map.put("entries_without_feeds", cleaner.cleanEntriesWithoutFeeds()); }
@Path("/cleanup/content")
@GET
@ApiOperation(value = "Content cleanup", notes = "Delete contents without entries")
public Response cleanupContents() {
Map<String, Long> map = Maps.newHashMap();
map.put("contents_without_entries", cleaner.cleanContentsWithoutEntries());
return Response.ok(map).build(); return Response.ok(map).build();
} }
@Path("/cleanup/entries") @Path("/cleanup/entries")
@GET @GET
@ApiOperation(value = "Entries cleanup", notes = "Delete entries older than given date") @ApiOperation(value = "Entries cleanup", notes = "Delete entries older than given date")
public Response cleanupEntries( public Response cleanupEntries(@QueryParam("days") @DefaultValue("30") int days) {
@QueryParam("days") @DefaultValue("30") int days) {
Map<String, Long> map = Maps.newHashMap(); Map<String, Long> map = Maps.newHashMap();
map.put("old entries", map.put("old_entries", cleaner.cleanEntriesOlderThan(days, TimeUnit.DAYS));
cleaner.cleanEntriesOlderThan(days, TimeUnit.DAYS));
return Response.ok(map).build(); return Response.ok(map).build();
} }
@Path("/cleanup/findDuplicateFeeds") @Path("/cleanup/findDuplicateFeeds")
@GET @GET
@ApiOperation(value = "Find duplicate feeds") @ApiOperation(value = "Find duplicate feeds")
public Response findDuplicateFeeds(@QueryParam("mode") DuplicateMode mode, public Response findDuplicateFeeds(@QueryParam("mode") DuplicateMode mode, @QueryParam("page") int page,
@QueryParam("page") int page, @QueryParam("limit") int limit, @QueryParam("limit") int limit, @QueryParam("minCount") long minCount) {
@QueryParam("minCount") long minCount) { List<FeedCount> list = feedDAO.findDuplicates(mode, limit * page, limit, minCount);
List<FeedCount> list = feedDAO.findDuplicates(mode, limit * page,
limit, minCount);
return Response.ok(list).build(); return Response.ok(list).build();
} }
@Path("/cleanup/merge") @Path("/cleanup/merge")
@POST @POST
@ApiOperation(value = "Merge feeds", notes = "Merge feeds together") @ApiOperation(value = "Merge feeds", notes = "Merge feeds together")
public Response mergeFeeds( public Response mergeFeeds(@ApiParam(required = true) FeedMergeRequest request) {
@ApiParam(required = true) FeedMergeRequest request) {
Feed into = feedDAO.findById(request.getIntoFeedId()); Feed into = feedDAO.findById(request.getIntoFeedId());
if (into == null) { if (into == null) {
return Response.status(Status.BAD_REQUEST) return Response.status(Status.BAD_REQUEST).entity("'into feed' not found").build();
.entity("'into feed' not found").build();
} }
List<Feed> feeds = Lists.newArrayList(); List<Feed> feeds = Lists.newArrayList();
@@ -300,8 +291,7 @@ public class AdminREST extends AbstractResourceREST {
} }
if (feeds.isEmpty()) { if (feeds.isEmpty()) {
return Response.status(Status.BAD_REQUEST) return Response.status(Status.BAD_REQUEST).entity("'from feeds' empty").build();
.entity("'from feeds' empty").build();
} }
cleaner.mergeFeeds(into, feeds); cleaner.mergeFeeds(into, feeds);

View File

@@ -40,14 +40,12 @@ public class ApiDocumentationREST extends AbstractREST {
} }
Api api = resource.getAnnotation(Api.class); Api api = resource.getAnnotation(Api.class);
if (api != null) { if (api != null) {
doc.addApi(new DocumentationEndPoint(api.value(), api doc.addApi(new DocumentationEndPoint(api.value(), api.description()));
.description()));
} }
} }
doc.setSwaggerVersion(SwaggerSpec.version()); doc.setSwaggerVersion(SwaggerSpec.version());
doc.setBasePath(getBasePath(applicationSettingsService.get() doc.setBasePath(getBasePath(applicationSettingsService.get().getPublicUrl()));
.getPublicUrl()));
doc.setApiVersion(API_VERSION); doc.setApiVersion(API_VERSION);
return Response.ok().entity(doc).build(); return Response.ok().entity(doc).build();

View File

@@ -33,6 +33,7 @@ import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.services.FeedEntryService;
import com.commafeed.backend.services.FeedSubscriptionService; import com.commafeed.backend.services.FeedSubscriptionService;
import com.commafeed.frontend.SecurityCheck; import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.Category; import com.commafeed.frontend.model.Category;
@@ -68,6 +69,9 @@ public class CategoryREST extends AbstractResourceREST {
@Inject @Inject
FeedEntryStatusDAO feedEntryStatusDAO; FeedEntryStatusDAO feedEntryStatusDAO;
@Inject
FeedEntryService feedEntryService;
@Inject @Inject
FeedCategoryDAO feedCategoryDAO; FeedCategoryDAO feedCategoryDAO;
@@ -82,14 +86,20 @@ public class CategoryREST extends AbstractResourceREST {
@Path("/entries") @Path("/entries")
@GET @GET
@ApiOperation(value = "Get category entries", notes = "Get a list of category entries", responseClass = "com.commafeed.frontend.model.Entries") @ApiOperation(
value = "Get category entries",
notes = "Get a list of category entries",
responseClass = "com.commafeed.frontend.model.Entries")
public Response getCategoryEntries( public Response getCategoryEntries(
@ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id, @ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id, @ApiParam(
@ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @DefaultValue("unread") @QueryParam("readType") ReadType readType, value = "all entries or only unread ones",
@ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan, allowableValues = "all,unread",
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, required = true) @DefaultValue("unread") @QueryParam("readType") ReadType readType, @ApiParam(
@ApiParam(value = "limit for paging, default 20, maximum 50") @DefaultValue("20") @QueryParam("limit") int limit, value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
@ApiParam(value = "date ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order) { @ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(
value = "limit for paging, default 20, maximum 50") @DefaultValue("20") @QueryParam("limit") int limit, @ApiParam(
value = "date ordering",
allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
Preconditions.checkNotNull(readType); Preconditions.checkNotNull(readType);
limit = Math.min(limit, 50); limit = Math.min(limit, 50);
@@ -101,60 +111,38 @@ public class CategoryREST extends AbstractResourceREST {
id = ALL; id = ALL;
} }
Date newerThanDate = newerThan == null ? null : new Date( Date newerThanDate = newerThan == null ? null : new Date(Long.valueOf(newerThan));
Long.valueOf(newerThan));
if (ALL.equals(id)) { if (ALL.equals(id)) {
entries.setName("All"); entries.setName("All");
List<FeedEntryStatus> list = null; List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(getUser());
List<FeedSubscription> subscriptions = feedSubscriptionDAO List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(subscriptions, unreadOnly, null, newerThanDate, offset,
.findAll(getUser()); limit + 1, order, true);
if (unreadOnly) {
list = feedEntryStatusDAO.findAllUnread(getUser(),
newerThanDate, offset, limit + 1, order, true);
} else {
list = feedEntryStatusDAO.findBySubscriptions(subscriptions,
null, newerThanDate, offset, limit + 1, order, true);
}
for (FeedEntryStatus status : list) { for (FeedEntryStatus status : list) {
entries.getEntries().add( entries.getEntries().add(
Entry.build(status, applicationSettingsService.get() Entry.build(status, applicationSettingsService.get().getPublicUrl(), applicationSettingsService.get()
.getPublicUrl(), applicationSettingsService .isImageProxyEnabled()));
.get().isImageProxyEnabled()));
} }
} else if (STARRED.equals(id)) { } else if (STARRED.equals(id)) {
entries.setName("Starred"); entries.setName("Starred");
List<FeedEntryStatus> starred = feedEntryStatusDAO.findStarred( List<FeedEntryStatus> starred = feedEntryStatusDAO.findStarred(getUser(), newerThanDate, offset, limit + 1, order, true);
getUser(), newerThanDate, offset, limit + 1, order, true);
for (FeedEntryStatus status : starred) { for (FeedEntryStatus status : starred) {
entries.getEntries().add( entries.getEntries().add(
Entry.build(status, applicationSettingsService.get() Entry.build(status, applicationSettingsService.get().getPublicUrl(), applicationSettingsService.get()
.getPublicUrl(), applicationSettingsService .isImageProxyEnabled()));
.get().isImageProxyEnabled()));
} }
} else { } else {
FeedCategory parent = feedCategoryDAO.findById(getUser(), FeedCategory parent = feedCategoryDAO.findById(getUser(), Long.valueOf(id));
Long.valueOf(id));
if (parent != null) { if (parent != null) {
List<FeedCategory> categories = feedCategoryDAO List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(getUser(), parent);
.findAllChildrenCategories(getUser(), parent); List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(getUser(), categories);
List<FeedSubscription> subs = feedSubscriptionDAO List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(subs, unreadOnly, null, newerThanDate, offset,
.findByCategories(getUser(), categories); limit + 1, order, true);
List<FeedEntryStatus> list = null;
if (unreadOnly) {
list = feedEntryStatusDAO.findUnreadBySubscriptions(subs,
newerThanDate, offset, limit + 1, order, true);
} else {
list = feedEntryStatusDAO.findBySubscriptions(subs, null,
newerThanDate, offset, limit + 1, order, true);
}
for (FeedEntryStatus status : list) { for (FeedEntryStatus status : list) {
entries.getEntries().add( entries.getEntries().add(
Entry.build(status, applicationSettingsService Entry.build(status, applicationSettingsService.get().getPublicUrl(), applicationSettingsService.get()
.get().getPublicUrl(), .isImageProxyEnabled()));
applicationSettingsService.get()
.isImageProxyEnabled()));
} }
entries.setName(parent.getName()); entries.setName(parent.getName());
} }
@@ -186,8 +174,7 @@ public class CategoryREST extends AbstractResourceREST {
int offset = 0; int offset = 0;
int limit = 20; int limit = 20;
Entries entries = (Entries) getCategoryEntries(id, readType, null, Entries entries = (Entries) getCategoryEntries(id, readType, null, offset, limit, order).getEntity();
offset, limit, order).getEntity();
SyndFeed feed = new SyndFeedImpl(); SyndFeed feed = new SyndFeedImpl();
feed.setFeedType("rss_2.0"); feed.setFeedType("rss_2.0");
@@ -216,36 +203,30 @@ public class CategoryREST extends AbstractResourceREST {
@Path("/mark") @Path("/mark")
@POST @POST
@ApiOperation(value = "Mark category entries", notes = "Mark feed entries of this category as read") @ApiOperation(value = "Mark category entries", notes = "Mark feed entries of this category as read")
public Response markCategoryEntries( public Response markCategoryEntries(@ApiParam(value = "category id, or 'all'", required = true) MarkRequest req) {
@ApiParam(value = "category id, or 'all'", required = true) MarkRequest req) {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId()); Preconditions.checkNotNull(req.getId());
Date olderThan = req.getOlderThan() == null ? null : new Date( Date olderThan = req.getOlderThan() == null ? null : new Date(req.getOlderThan());
req.getOlderThan());
if (ALL.equals(req.getId())) { if (ALL.equals(req.getId())) {
feedEntryStatusDAO.markAllEntries(getUser(), olderThan); List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(getUser());
feedEntryService.markSubscriptionEntries(getUser(), subscriptions, olderThan);
} else if (STARRED.equals(req.getId())) { } else if (STARRED.equals(req.getId())) {
feedEntryStatusDAO.markStarredEntries(getUser(), olderThan); feedEntryService.markStarredEntries(getUser(), olderThan);
} else { } else {
FeedCategory parent = feedCategoryDAO.findById(getUser(), FeedCategory parent = feedCategoryDAO.findById(getUser(), Long.valueOf(req.getId()));
Long.valueOf(req.getId())); List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(getUser(), parent);
List<FeedCategory> categories = feedCategoryDAO List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(getUser(), categories);
.findAllChildrenCategories(getUser(), parent); feedEntryService.markSubscriptionEntries(getUser(), subs, olderThan);
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(
getUser(), categories);
feedEntryStatusDAO.markSubscriptionEntries(subs, olderThan);
} }
cache.invalidateUserData(getUser());
return Response.ok(Status.OK).build(); return Response.ok(Status.OK).build();
} }
@Path("/add") @Path("/add")
@POST @POST
@ApiOperation(value = "Add a category", notes = "Add a new feed category") @ApiOperation(value = "Add a category", notes = "Add a new feed category")
public Response addCategory( public Response addCategory(@ApiParam(required = true) AddCategoryRequest req) {
@ApiParam(required = true) AddCategoryRequest req) {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getName()); Preconditions.checkNotNull(req.getName());
@@ -260,7 +241,7 @@ public class CategoryREST extends AbstractResourceREST {
cat.setParent(parent); cat.setParent(parent);
} }
feedCategoryDAO.saveOrUpdate(cat); feedCategoryDAO.saveOrUpdate(cat);
cache.invalidateUserData(getUser()); cache.invalidateUserRootCategory(getUser());
return Response.ok().build(); return Response.ok().build();
} }
@@ -274,24 +255,21 @@ public class CategoryREST extends AbstractResourceREST {
FeedCategory cat = feedCategoryDAO.findById(getUser(), req.getId()); FeedCategory cat = feedCategoryDAO.findById(getUser(), req.getId());
if (cat != null) { if (cat != null) {
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategory( List<FeedSubscription> subs = feedSubscriptionDAO.findByCategory(getUser(), cat);
getUser(), cat);
for (FeedSubscription sub : subs) { for (FeedSubscription sub : subs) {
sub.setCategory(null); sub.setCategory(null);
} }
feedSubscriptionDAO.saveOrUpdate(subs); feedSubscriptionDAO.saveOrUpdate(subs);
List<FeedCategory> categories = feedCategoryDAO List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(getUser(), cat);
.findAllChildrenCategories(getUser(), cat);
for (FeedCategory child : categories) { for (FeedCategory child : categories) {
if (!child.getId().equals(cat.getId()) if (!child.getId().equals(cat.getId()) && child.getParent().getId().equals(cat.getId())) {
&& child.getParent().getId().equals(cat.getId())) {
child.setParent(null); child.setParent(null);
} }
} }
feedCategoryDAO.saveOrUpdate(categories); feedCategoryDAO.saveOrUpdate(categories);
feedCategoryDAO.delete(cat); feedCategoryDAO.delete(cat);
cache.invalidateUserData(getUser()); cache.invalidateUserRootCategory(getUser());
return Response.ok().build(); return Response.ok().build();
} else { } else {
return Response.status(Status.NOT_FOUND).build(); return Response.status(Status.NOT_FOUND).build();
@@ -301,43 +279,35 @@ public class CategoryREST extends AbstractResourceREST {
@POST @POST
@Path("/modify") @Path("/modify")
@ApiOperation(value = "Rename a category", notes = "Rename an existing feed category") @ApiOperation(value = "Rename a category", notes = "Rename an existing feed category")
public Response modifyCategory( public Response modifyCategory(@ApiParam(required = true) CategoryModificationRequest req) {
@ApiParam(required = true) CategoryModificationRequest req) {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId()); Preconditions.checkNotNull(req.getId());
FeedCategory category = feedCategoryDAO FeedCategory category = feedCategoryDAO.findById(getUser(), req.getId());
.findById(getUser(), req.getId());
if (StringUtils.isNotBlank(req.getName())) { if (StringUtils.isNotBlank(req.getName())) {
category.setName(req.getName()); category.setName(req.getName());
} }
FeedCategory parent = null; FeedCategory parent = null;
if (req.getParentId() != null if (req.getParentId() != null && !CategoryREST.ALL.equals(req.getParentId())
&& !CategoryREST.ALL.equals(req.getParentId()) && !StringUtils.equals(req.getParentId(), String.valueOf(req.getId()))) {
&& !StringUtils.equals(req.getParentId(), parent = feedCategoryDAO.findById(getUser(), Long.valueOf(req.getParentId()));
String.valueOf(req.getId()))) {
parent = feedCategoryDAO.findById(getUser(),
Long.valueOf(req.getParentId()));
} }
category.setParent(parent); category.setParent(parent);
if (req.getPosition() != null) { if (req.getPosition() != null) {
List<FeedCategory> categories = feedCategoryDAO.findByParent( List<FeedCategory> categories = feedCategoryDAO.findByParent(getUser(), parent);
getUser(), parent);
Collections.sort(categories, new Comparator<FeedCategory>() { Collections.sort(categories, new Comparator<FeedCategory>() {
@Override @Override
public int compare(FeedCategory o1, FeedCategory o2) { public int compare(FeedCategory o1, FeedCategory o2) {
return ObjectUtils.compare(o1.getPosition(), return ObjectUtils.compare(o1.getPosition(), o2.getPosition());
o2.getPosition());
} }
}); });
int existingIndex = -1; int existingIndex = -1;
for (int i = 0; i < categories.size(); i++) { for (int i = 0; i < categories.size(); i++) {
if (ObjectUtils.equals(categories.get(i).getId(), if (ObjectUtils.equals(categories.get(i).getId(), category.getId())) {
category.getId())) {
existingIndex = i; existingIndex = i;
} }
} }
@@ -345,8 +315,7 @@ public class CategoryREST extends AbstractResourceREST {
categories.remove(existingIndex); categories.remove(existingIndex);
} }
categories.add(Math.min(req.getPosition(), categories.size()), categories.add(Math.min(req.getPosition(), categories.size()), category);
category);
for (int i = 0; i < categories.size(); i++) { for (int i = 0; i < categories.size(); i++) {
categories.get(i).setPosition(i); categories.get(i).setPosition(i);
} }
@@ -356,7 +325,7 @@ public class CategoryREST extends AbstractResourceREST {
} }
feedCategoryDAO.saveOrUpdate(category); feedCategoryDAO.saveOrUpdate(category);
cache.invalidateUserData(getUser()); cache.invalidateUserRootCategory(getUser());
return Response.ok(Status.OK).build(); return Response.ok(Status.OK).build();
} }
@@ -367,14 +336,13 @@ public class CategoryREST extends AbstractResourceREST {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId()); Preconditions.checkNotNull(req.getId());
FeedCategory category = feedCategoryDAO.findById(getUser(), FeedCategory category = feedCategoryDAO.findById(getUser(), Long.valueOf(req.getId()));
Long.valueOf(req.getId()));
if (category == null) { if (category == null) {
return Response.status(Status.NOT_FOUND).build(); return Response.status(Status.NOT_FOUND).build();
} }
category.setCollapsed(req.isCollapse()); category.setCollapsed(req.isCollapse());
feedCategoryDAO.saveOrUpdate(category); feedCategoryDAO.saveOrUpdate(category);
cache.invalidateUserData(getUser()); cache.invalidateUserRootCategory(getUser());
return Response.ok(Status.OK).build(); return Response.ok(Status.OK).build();
} }
@@ -383,8 +351,7 @@ public class CategoryREST extends AbstractResourceREST {
@ApiOperation(value = "Get unread count for feed subscriptions", responseClass = "List[com.commafeed.frontend.model.UnreadCount]") @ApiOperation(value = "Get unread count for feed subscriptions", responseClass = "List[com.commafeed.frontend.model.UnreadCount]")
public Response getUnreadCount() { public Response getUnreadCount() {
List<UnreadCount> list = Lists.newArrayList(); List<UnreadCount> list = Lists.newArrayList();
Map<Long, Long> unreadCount = feedSubscriptionService Map<Long, Long> unreadCount = feedSubscriptionService.getUnreadCount(getUser());
.getUnreadCount(getUser());
for (Map.Entry<Long, Long> e : unreadCount.entrySet()) { for (Map.Entry<Long, Long> e : unreadCount.entrySet()) {
list.add(new UnreadCount(e.getKey(), e.getValue())); list.add(new UnreadCount(e.getKey(), e.getValue()));
} }
@@ -393,39 +360,37 @@ public class CategoryREST extends AbstractResourceREST {
@GET @GET
@Path("/get") @Path("/get")
@ApiOperation(value = "Get feed categories", notes = "Get all categories and subscriptions of the user", responseClass = "com.commafeed.frontend.model.Category") @ApiOperation(
value = "Get feed categories",
notes = "Get all categories and subscriptions of the user",
responseClass = "com.commafeed.frontend.model.Category")
public Response getSubscriptions() { public Response getSubscriptions() {
User user = getUser(); User user = getUser();
Category root = cache.getRootCategory(user); Category root = cache.getUserRootCategory(user);
if (root == null) { if (root == null) {
log.debug("root category cache miss for {}", user.getName()); log.debug("tree cache miss for {}", user.getId());
List<FeedCategory> categories = feedCategoryDAO.findAll(user); List<FeedCategory> categories = feedCategoryDAO.findAll(user);
List<FeedSubscription> subscriptions = feedSubscriptionDAO List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
.findAll(getUser()); Map<Long, Long> unreadCount = feedSubscriptionService.getUnreadCount(user);
Map<Long, Long> unreadCount = feedSubscriptionService
.getUnreadCount(getUser());
root = buildCategory(null, categories, subscriptions, unreadCount); root = buildCategory(null, categories, subscriptions, unreadCount);
root.setId("all"); root.setId("all");
root.setName("All"); root.setName("All");
cache.setRootCategory(user, root); cache.setUserRootCategory(user, root);
} }
return Response.ok(root).build(); return Response.ok(root).build();
} }
private Category buildCategory(Long id, List<FeedCategory> categories, private Category buildCategory(Long id, List<FeedCategory> categories, List<FeedSubscription> subscriptions, Map<Long, Long> unreadCount) {
List<FeedSubscription> subscriptions, Map<Long, Long> unreadCount) {
Category category = new Category(); Category category = new Category();
category.setId(String.valueOf(id)); category.setId(String.valueOf(id));
category.setExpanded(true); category.setExpanded(true);
for (FeedCategory c : categories) { for (FeedCategory c : categories) {
if ((id == null && c.getParent() == null) if ((id == null && c.getParent() == null) || (c.getParent() != null && ObjectUtils.equals(c.getParent().getId(), id))) {
|| (c.getParent() != null && ObjectUtils.equals(c Category child = buildCategory(c.getId(), categories, subscriptions, unreadCount);
.getParent().getId(), id))) {
Category child = buildCategory(c.getId(), categories,
subscriptions, unreadCount);
child.setId(String.valueOf(c.getId())); child.setId(String.valueOf(c.getId()));
child.setName(c.getName()); child.setName(c.getName());
child.setPosition(c.getPosition()); child.setPosition(c.getPosition());
@@ -445,13 +410,10 @@ public class CategoryREST extends AbstractResourceREST {
for (FeedSubscription subscription : subscriptions) { for (FeedSubscription subscription : subscriptions) {
if ((id == null && subscription.getCategory() == null) if ((id == null && subscription.getCategory() == null)
|| (subscription.getCategory() != null && ObjectUtils || (subscription.getCategory() != null && ObjectUtils.equals(subscription.getCategory().getId(), id))) {
.equals(subscription.getCategory().getId(), id))) {
Long size = unreadCount.get(subscription.getId()); Long size = unreadCount.get(subscription.getId());
long unread = size == null ? 0 : size; long unread = size == null ? 0 : size;
Subscription sub = Subscription Subscription sub = Subscription.build(subscription, applicationSettingsService.get().getPublicUrl(), unread);
.build(subscription, applicationSettingsService.get()
.getPublicUrl(), unread);
category.getFeeds().add(sub); category.getFeeds().add(sub);
} }
} }

View File

@@ -1,5 +1,6 @@
package com.commafeed.frontend.rest.resources; package com.commafeed.frontend.rest.resources;
import java.util.Iterator;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
@@ -12,8 +13,8 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.jsoup.Jsoup;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedEntryStatus;
@@ -26,7 +27,6 @@ import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.frontend.model.request.MultipleMarkRequest; import com.commafeed.frontend.model.request.MultipleMarkRequest;
import com.commafeed.frontend.model.request.StarRequest; import com.commafeed.frontend.model.request.StarRequest;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation; import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam; import com.wordnik.swagger.annotations.ApiParam;
@@ -44,29 +44,22 @@ public class EntryREST extends AbstractResourceREST {
@Inject @Inject
FeedSubscriptionDAO feedSubscriptionDAO; FeedSubscriptionDAO feedSubscriptionDAO;
@Inject
CacheService cache;
@Path("/mark") @Path("/mark")
@POST @POST
@ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread") @ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread")
public Response markFeedEntry( public Response markFeedEntry(@ApiParam(value = "Mark Request", required = true) MarkRequest req) {
@ApiParam(value = "Mark Request", required = true) MarkRequest req) {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId()); Preconditions.checkNotNull(req.getId());
Preconditions.checkNotNull(req.getFeedId()); Preconditions.checkNotNull(req.getFeedId());
feedEntryService.markEntry(getUser(), Long.valueOf(req.getId()), feedEntryService.markEntry(getUser(), Long.valueOf(req.getId()), req.getFeedId(), req.isRead());
req.getFeedId(), req.isRead());
cache.invalidateUserData(getUser());
return Response.ok(Status.OK).build(); return Response.ok(Status.OK).build();
} }
@Path("/markMultiple") @Path("/markMultiple")
@POST @POST
@ApiOperation(value = "Mark multiple feed entries", notes = "Mark feed entries as read/unread") @ApiOperation(value = "Mark multiple feed entries", notes = "Mark feed entries as read/unread")
public Response markFeedEntries( public Response markFeedEntries(@ApiParam(value = "Multiple Mark Request", required = true) MultipleMarkRequest req) {
@ApiParam(value = "Multiple Mark Request", required = true) MultipleMarkRequest req) {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getRequests()); Preconditions.checkNotNull(req.getRequests());
@@ -80,45 +73,76 @@ public class EntryREST extends AbstractResourceREST {
@Path("/star") @Path("/star")
@POST @POST
@ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread") @ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread")
public Response starFeedEntry( public Response starFeedEntry(@ApiParam(value = "Star Request", required = true) StarRequest req) {
@ApiParam(value = "Star Request", required = true) StarRequest req) {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId()); Preconditions.checkNotNull(req.getId());
Preconditions.checkNotNull(req.getFeedId()); Preconditions.checkNotNull(req.getFeedId());
feedEntryService.starEntry(getUser(), Long.valueOf(req.getId()), feedEntryService.starEntry(getUser(), Long.valueOf(req.getId()), req.getFeedId(), req.isStarred());
req.getFeedId(), req.isStarred());
return Response.ok(Status.OK).build(); return Response.ok(Status.OK).build();
} }
@Path("/search") @Path("/search")
@GET @GET
@ApiOperation(value = "Search for entries", notes = "Look through title and content of entries by keywords", responseClass = "com.commafeed.frontend.model.Entries") @ApiOperation(
value = "Search for entries",
notes = "Look through title and content of entries by keywords",
responseClass = "com.commafeed.frontend.model.Entries")
public Response searchEntries( public Response searchEntries(
@ApiParam(value = "keywords separated by spaces, 3 characters minimum", required = true) @QueryParam("keywords") String keywords, @ApiParam(value = "keywords separated by spaces, 3 characters minimum", required = true) @QueryParam("keywords") String keywords,
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(
@ApiParam(value = "limit for paging") @DefaultValue("-1") @QueryParam("limit") int limit) { value = "limit for paging") @DefaultValue("-1") @QueryParam("limit") int limit) {
keywords = StringUtils.trimToEmpty(keywords); keywords = StringUtils.trimToEmpty(keywords);
limit = Math.min(limit, 50); limit = Math.min(limit, 50);
Preconditions.checkArgument(StringUtils.length(keywords) >= 3); Preconditions.checkArgument(StringUtils.length(keywords) >= 3);
Entries entries = new Entries(); Entries entries = new Entries();
List<Entry> list = Lists.newArrayList();
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(getUser()); List<FeedSubscription> subs = feedSubscriptionDAO.findAll(getUser());
List<FeedEntryStatus> entriesStatus = feedEntryStatusDAO List<FeedEntryStatus> entriesStatus = feedEntryStatusDAO.findBySubscriptions(subs, false, keywords, null, offset, limit + 1,
.findBySubscriptions(subs, keywords, null, offset, limit, ReadingOrder.desc, true);
ReadingOrder.desc, true);
for (FeedEntryStatus status : entriesStatus) { for (FeedEntryStatus status : entriesStatus) {
list.add(Entry.build(status, applicationSettingsService.get() entries.getEntries().add(
.getPublicUrl(), applicationSettingsService.get() Entry.build(status, applicationSettingsService.get().getPublicUrl(), applicationSettingsService.get()
.isImageProxyEnabled())); .isImageProxyEnabled()));
} }
boolean hasMore = entries.getEntries().size() > limit;
if (hasMore) {
entries.setHasMore(true);
entries.getEntries().remove(entries.getEntries().size() - 1);
}
removeUnwanted(entries.getEntries(), keywords);
entries.setName("Search for : " + keywords); entries.setName("Search for : " + keywords);
entries.getEntries().addAll(list); entries.setTimestamp(System.currentTimeMillis());
return Response.ok(entries).build(); return Response.ok(entries).build();
} }
private void removeUnwanted(List<Entry> entries, String keywords) {
Iterator<Entry> it = entries.iterator();
while (it.hasNext()) {
Entry entry = it.next();
boolean keep = true;
for (String keyword : keywords.split(" ")) {
String content = Jsoup.parse(entry.getContent()).text();
if (!StringUtils.containsIgnoreCase(content, keyword)) {
keep = false;
break;
}
String title = Jsoup.parse(entry.getTitle()).text();
if (!StringUtils.containsIgnoreCase(title, keyword)) {
keep = false;
break;
}
}
if (!keep) {
it.remove();
}
}
}
} }

View File

@@ -55,6 +55,7 @@ import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.services.FeedEntryService;
import com.commafeed.backend.services.FeedSubscriptionService; import com.commafeed.backend.services.FeedSubscriptionService;
import com.commafeed.frontend.SecurityCheck; import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Entries;
@@ -97,6 +98,9 @@ public class FeedREST extends AbstractResourceREST {
@Inject @Inject
FeedSubscriptionDAO feedSubscriptionDAO; FeedSubscriptionDAO feedSubscriptionDAO;
@Inject
FeedEntryService feedEntryService;
@Inject @Inject
FeedSubscriptionService feedSubscriptionService; FeedSubscriptionService feedSubscriptionService;
@@ -124,13 +128,15 @@ public class FeedREST extends AbstractResourceREST {
@Path("/entries") @Path("/entries")
@GET @GET
@ApiOperation(value = "Get feed entries", notes = "Get a list of feed entries", responseClass = "com.commafeed.frontend.model.Entries") @ApiOperation(value = "Get feed entries", notes = "Get a list of feed entries", responseClass = "com.commafeed.frontend.model.Entries")
public Response getFeedEntries( public Response getFeedEntries(@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id, @ApiParam(
@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id, value = "all entries or only unread ones",
@ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @DefaultValue("unread") @QueryParam("readType") ReadType readType, allowableValues = "all,unread",
@ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan, required = true) @DefaultValue("unread") @QueryParam("readType") ReadType readType, @ApiParam(
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
@ApiParam(value = "limit for paging, default 20, maximum 50") @DefaultValue("20") @QueryParam("limit") int limit, @ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(
@ApiParam(value = "date ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order) { value = "limit for paging, default 20, maximum 50") @DefaultValue("20") @QueryParam("limit") int limit, @ApiParam(
value = "date ordering",
allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
Preconditions.checkNotNull(id); Preconditions.checkNotNull(id);
Preconditions.checkNotNull(readType); Preconditions.checkNotNull(readType);
@@ -141,33 +147,22 @@ public class FeedREST extends AbstractResourceREST {
Entries entries = new Entries(); Entries entries = new Entries();
boolean unreadOnly = readType == ReadType.unread; boolean unreadOnly = readType == ReadType.unread;
Date newerThanDate = newerThan == null ? null : new Date( Date newerThanDate = newerThan == null ? null : new Date(Long.valueOf(newerThan));
Long.valueOf(newerThan));
FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(), FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(), Long.valueOf(id));
Long.valueOf(id));
if (subscription != null) { if (subscription != null) {
entries.setName(subscription.getTitle()); entries.setName(subscription.getTitle());
entries.setMessage(subscription.getFeed().getMessage()); entries.setMessage(subscription.getFeed().getMessage());
entries.setErrorCount(subscription.getFeed().getErrorCount()); entries.setErrorCount(subscription.getFeed().getErrorCount());
entries.setFeedLink(subscription.getFeed().getLink()); entries.setFeedLink(subscription.getFeed().getLink());
List<FeedEntryStatus> list = null; List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(Arrays.asList(subscription), unreadOnly, null,
if (unreadOnly) { newerThanDate, offset, limit + 1, order, true);
list = feedEntryStatusDAO.findUnreadBySubscriptions(
Arrays.asList(subscription), newerThanDate, offset,
limit + 1, order, true);
} else {
list = feedEntryStatusDAO.findBySubscriptions(
Arrays.asList(subscription), null, newerThanDate,
offset, limit + 1, order, true);
}
for (FeedEntryStatus status : list) { for (FeedEntryStatus status : list) {
entries.getEntries().add( entries.getEntries().add(
Entry.build(status, applicationSettingsService.get() Entry.build(status, applicationSettingsService.get().getPublicUrl(), applicationSettingsService.get()
.getPublicUrl(), applicationSettingsService .isImageProxyEnabled()));
.get().isImageProxyEnabled()));
} }
boolean hasMore = entries.getEntries().size() > limit; boolean hasMore = entries.getEntries().size() > limit;
@@ -186,8 +181,7 @@ public class FeedREST extends AbstractResourceREST {
@ApiOperation(value = "Get feed entries as a feed", notes = "Get a feed of feed entries") @ApiOperation(value = "Get feed entries as a feed", notes = "Get a feed of feed entries")
@Produces(MediaType.APPLICATION_XML) @Produces(MediaType.APPLICATION_XML)
@SecurityCheck(value = Role.USER, apiKeyAllowed = true) @SecurityCheck(value = Role.USER, apiKeyAllowed = true)
public Response getFeedEntriesAsFeed( public Response getFeedEntriesAsFeed(@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id) {
@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id) {
Preconditions.checkNotNull(id); Preconditions.checkNotNull(id);
@@ -196,8 +190,7 @@ public class FeedREST extends AbstractResourceREST {
int offset = 0; int offset = 0;
int limit = 20; int limit = 20;
Entries entries = (Entries) getFeedEntries(id, readType, null, offset, Entries entries = (Entries) getFeedEntries(id, readType, null, offset, limit, order).getEntity();
limit, order).getEntity();
SyndFeed feed = new SyndFeedImpl(); SyndFeed feed = new SyndFeedImpl();
feed.setFeedType("rss_2.0"); feed.setFeedType("rss_2.0");
@@ -228,16 +221,13 @@ public class FeedREST extends AbstractResourceREST {
url = StringUtils.trimToEmpty(url); url = StringUtils.trimToEmpty(url);
url = prependHttp(url); url = prependHttp(url);
try { try {
FetchedFeed feed = feedFetcher.fetch(url, true, null, null, null, FetchedFeed feed = feedFetcher.fetch(url, true, null, null, null, null);
null);
info = new FeedInfo(); info = new FeedInfo();
info.setUrl(feed.getFeed().getUrl()); info.setUrl(feed.getFeed().getUrl());
info.setTitle(feed.getTitle()); info.setTitle(feed.getTitle());
} catch (Exception e) { } catch (Exception e) {
throw new WebApplicationException(e, Response throw new WebApplicationException(e, Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build());
.status(Status.INTERNAL_SERVER_ERROR)
.entity(e.getMessage()).build());
} }
return info; return info;
} }
@@ -245,8 +235,7 @@ public class FeedREST extends AbstractResourceREST {
@POST @POST
@Path("/fetch") @Path("/fetch")
@ApiOperation(value = "Fetch a feed", notes = "Fetch a feed by its url", responseClass = "com.commafeed.frontend.model.FeedInfo") @ApiOperation(value = "Fetch a feed", notes = "Fetch a feed by its url", responseClass = "com.commafeed.frontend.model.FeedInfo")
public Response fetchFeed( public Response fetchFeed(@ApiParam(value = "feed url", required = true) FeedInfoRequest req) {
@ApiParam(value = "feed url", required = true) FeedInfoRequest req) {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getUrl()); Preconditions.checkNotNull(req.getUrl());
@@ -254,8 +243,7 @@ public class FeedREST extends AbstractResourceREST {
try { try {
info = fetchFeedInternal(req.getUrl()); info = fetchFeedInternal(req.getUrl());
} catch (Exception e) { } catch (Exception e) {
return Response.status(Status.INTERNAL_SERVER_ERROR) return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
.entity(e.getMessage()).build();
} }
return Response.ok(info).build(); return Response.ok(info).build();
} }
@@ -268,8 +256,7 @@ public class FeedREST extends AbstractResourceREST {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId()); Preconditions.checkNotNull(req.getId());
FeedSubscription sub = feedSubscriptionDAO.findById(getUser(), FeedSubscription sub = feedSubscriptionDAO.findById(getUser(), req.getId());
req.getId());
if (sub != null) { if (sub != null) {
Feed feed = sub.getFeed(); Feed feed = sub.getFeed();
feed.setUrgent(true); feed.setUrgent(true);
@@ -283,54 +270,43 @@ public class FeedREST extends AbstractResourceREST {
@Path("/mark") @Path("/mark")
@POST @POST
@ApiOperation(value = "Mark feed entries", notes = "Mark feed entries as read (unread is not supported)") @ApiOperation(value = "Mark feed entries", notes = "Mark feed entries as read (unread is not supported)")
public Response markFeedEntries( public Response markFeedEntries(@ApiParam(value = "Mark request") MarkRequest req) {
@ApiParam(value = "Mark request") MarkRequest req) {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId()); Preconditions.checkNotNull(req.getId());
Date olderThan = req.getOlderThan() == null ? null : new Date( Date olderThan = req.getOlderThan() == null ? null : new Date(req.getOlderThan());
req.getOlderThan());
FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(), FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(), Long.valueOf(req.getId()));
Long.valueOf(req.getId()));
if (subscription != null) { if (subscription != null) {
feedEntryStatusDAO.markSubscriptionEntries( feedEntryService.markSubscriptionEntries(getUser(), Arrays.asList(subscription), olderThan);
Arrays.asList(subscription), olderThan);
} }
cache.invalidateUserData(getUser());
return Response.ok(Status.OK).build(); return Response.ok(Status.OK).build();
} }
@GET @GET
@Path("/get/{id}") @Path("/get/{id}")
@ApiOperation(value = "", notes = "") @ApiOperation(value = "", notes = "")
public Response get( public Response get(@ApiParam(value = "user id", required = true) @PathParam("id") Long id) {
@ApiParam(value = "user id", required = true) @PathParam("id") Long id) {
Preconditions.checkNotNull(id); Preconditions.checkNotNull(id);
FeedSubscription sub = feedSubscriptionDAO.findById(getUser(), id); FeedSubscription sub = feedSubscriptionDAO.findById(getUser(), id);
if (sub == null) { if (sub == null) {
return Response.status(Status.NOT_FOUND).build(); return Response.status(Status.NOT_FOUND).build();
} }
Long unreadCount = feedSubscriptionService.getUnreadCount(getUser()) Long unreadCount = feedSubscriptionService.getUnreadCount(getUser()).get(id);
.get(id);
if (unreadCount == null) { if (unreadCount == null) {
unreadCount = new Long(0); unreadCount = new Long(0);
} }
return Response.ok( return Response.ok(Subscription.build(sub, applicationSettingsService.get().getPublicUrl(), unreadCount)).build();
Subscription.build(sub, applicationSettingsService.get()
.getPublicUrl(), unreadCount)).build();
} }
@GET @GET
@Path("/favicon/{id}") @Path("/favicon/{id}")
@ApiOperation(value = "Fetch a feed's icon", notes = "Fetch a feed's icon") @ApiOperation(value = "Fetch a feed's icon", notes = "Fetch a feed's icon")
public Response getFavicon( public Response getFavicon(@ApiParam(value = "subscription id") @PathParam("id") Long id) {
@ApiParam(value = "subscription id") @PathParam("id") Long id) {
Preconditions.checkNotNull(id); Preconditions.checkNotNull(id);
FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(), FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(), id);
id);
if (subscription == null) { if (subscription == null) {
return Response.status(Status.NOT_FOUND).build(); return Response.status(Status.NOT_FOUND).build();
} }
@@ -340,11 +316,8 @@ public class FeedREST extends AbstractResourceREST {
ResponseBuilder builder = null; ResponseBuilder builder = null;
if (icon == null) { if (icon == null) {
String baseUrl = FeedUtils String baseUrl = FeedUtils.removeTrailingSlash(applicationSettingsService.get().getPublicUrl());
.removeTrailingSlash(applicationSettingsService.get() builder = Response.status(Status.MOVED_PERMANENTLY).location(URI.create(baseUrl + "/images/default_favicon.gif"));
.getPublicUrl());
builder = Response.status(Status.MOVED_PERMANENTLY).location(
URI.create(baseUrl + "/images/default_favicon.gif"));
} else { } else {
builder = Response.ok(icon, "image/x-icon"); builder = Response.ok(icon, "image/x-icon");
} }
@@ -366,8 +339,7 @@ public class FeedREST extends AbstractResourceREST {
@POST @POST
@Path("/subscribe") @Path("/subscribe")
@ApiOperation(value = "Subscribe to a feed", notes = "Subscribe to a feed") @ApiOperation(value = "Subscribe to a feed", notes = "Subscribe to a feed")
public Response subscribe( public Response subscribe(@ApiParam(value = "subscription request", required = true) SubscribeRequest req) {
@ApiParam(value = "subscription request", required = true) SubscribeRequest req) {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getTitle()); Preconditions.checkNotNull(req.getTitle());
Preconditions.checkNotNull(req.getUrl()); Preconditions.checkNotNull(req.getUrl());
@@ -376,28 +348,21 @@ public class FeedREST extends AbstractResourceREST {
try { try {
url = fetchFeedInternal(url).getUrl(); url = fetchFeedInternal(url).getUrl();
FeedCategory category = CategoryREST.ALL FeedCategory category = CategoryREST.ALL.equals(req.getCategoryId()) ? null : feedCategoryDAO.findById(Long.valueOf(req
.equals(req.getCategoryId()) ? null : feedCategoryDAO .getCategoryId()));
.findById(Long.valueOf(req.getCategoryId()));
FeedInfo info = fetchFeedInternal(url); FeedInfo info = fetchFeedInternal(url);
feedSubscriptionService.subscribe(getUser(), info.getUrl(), feedSubscriptionService.subscribe(getUser(), info.getUrl(), req.getTitle(), category);
req.getTitle(), category);
} catch (Exception e) { } catch (Exception e) {
log.info("Failed to subscribe to URL {}: {}", url, e.getMessage()); log.info("Failed to subscribe to URL {}: {}", url, e.getMessage());
return Response return Response.status(Status.SERVICE_UNAVAILABLE).entity("Failed to subscribe to URL " + url + ": " + e.getMessage()).build();
.status(Status.SERVICE_UNAVAILABLE)
.entity("Failed to subscribe to URL " + url + ": "
+ e.getMessage()).build();
} }
cache.invalidateUserData(getUser());
return Response.ok(Status.OK).build(); return Response.ok(Status.OK).build();
} }
@GET @GET
@Path("/subscribe") @Path("/subscribe")
@ApiOperation(value = "Subscribe to a feed", notes = "Subscribe to a feed") @ApiOperation(value = "Subscribe to a feed", notes = "Subscribe to a feed")
public Response subscribe( public Response subscribe(@ApiParam(value = "feed url", required = true) @QueryParam("url") String url) {
@ApiParam(value = "feed url", required = true) @QueryParam("url") String url) {
try { try {
Preconditions.checkNotNull(url); Preconditions.checkNotNull(url);
@@ -406,14 +371,11 @@ public class FeedREST extends AbstractResourceREST {
url = fetchFeedInternal(url).getUrl(); url = fetchFeedInternal(url).getUrl();
FeedInfo info = fetchFeedInternal(url); FeedInfo info = fetchFeedInternal(url);
feedSubscriptionService.subscribe(getUser(), info.getUrl(), feedSubscriptionService.subscribe(getUser(), info.getUrl(), info.getTitle(), null);
info.getTitle(), null);
} catch (Exception e) { } catch (Exception e) {
log.info("Could not subscribe to url {} : {}", url, e.getMessage()); log.info("Could not subscribe to url {} : {}", url, e.getMessage());
} }
return Response.temporaryRedirect( return Response.temporaryRedirect(URI.create(applicationSettingsService.get().getPublicUrl())).build();
URI.create(applicationSettingsService.get().getPublicUrl()))
.build();
} }
private String prependHttp(String url) { private String prependHttp(String url) {
@@ -430,11 +392,8 @@ public class FeedREST extends AbstractResourceREST {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId()); Preconditions.checkNotNull(req.getId());
FeedSubscription sub = feedSubscriptionDAO.findById(getUser(), boolean deleted = feedSubscriptionService.unsubscribe(getUser(), req.getId());
req.getId()); if (deleted) {
if (sub != null) {
feedSubscriptionDAO.delete(sub);
cache.invalidateUserData(getUser());
return Response.ok(Status.OK).build(); return Response.ok(Status.OK).build();
} else { } else {
return Response.status(Status.NOT_FOUND).build(); return Response.status(Status.NOT_FOUND).build();
@@ -444,41 +403,34 @@ public class FeedREST extends AbstractResourceREST {
@POST @POST
@Path("/modify") @Path("/modify")
@ApiOperation(value = "Modify a subscription", notes = "Modify a feed subscription") @ApiOperation(value = "Modify a subscription", notes = "Modify a feed subscription")
public Response modify( public Response modify(@ApiParam(value = "subscription id", required = true) FeedModificationRequest req) {
@ApiParam(value = "subscription id", required = true) FeedModificationRequest req) {
Preconditions.checkNotNull(req); Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId()); Preconditions.checkNotNull(req.getId());
FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(), FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(), req.getId());
req.getId());
if (StringUtils.isNotBlank(req.getName())) { if (StringUtils.isNotBlank(req.getName())) {
subscription.setTitle(req.getName()); subscription.setTitle(req.getName());
} }
FeedCategory parent = null; FeedCategory parent = null;
if (req.getCategoryId() != null if (req.getCategoryId() != null && !CategoryREST.ALL.equals(req.getCategoryId())) {
&& !CategoryREST.ALL.equals(req.getCategoryId())) { parent = feedCategoryDAO.findById(getUser(), Long.valueOf(req.getCategoryId()));
parent = feedCategoryDAO.findById(getUser(),
Long.valueOf(req.getCategoryId()));
} }
subscription.setCategory(parent); subscription.setCategory(parent);
if (req.getPosition() != null) { if (req.getPosition() != null) {
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategory( List<FeedSubscription> subs = feedSubscriptionDAO.findByCategory(getUser(), parent);
getUser(), parent);
Collections.sort(subs, new Comparator<FeedSubscription>() { Collections.sort(subs, new Comparator<FeedSubscription>() {
@Override @Override
public int compare(FeedSubscription o1, FeedSubscription o2) { public int compare(FeedSubscription o1, FeedSubscription o2) {
return ObjectUtils.compare(o1.getPosition(), return ObjectUtils.compare(o1.getPosition(), o2.getPosition());
o2.getPosition());
} }
}); });
int existingIndex = -1; int existingIndex = -1;
for (int i = 0; i < subs.size(); i++) { for (int i = 0; i < subs.size(); i++) {
if (ObjectUtils.equals(subs.get(i).getId(), if (ObjectUtils.equals(subs.get(i).getId(), subscription.getId())) {
subscription.getId())) {
existingIndex = i; existingIndex = i;
} }
} }
@@ -494,7 +446,7 @@ public class FeedREST extends AbstractResourceREST {
} else { } else {
feedSubscriptionDAO.saveOrUpdate(subscription); feedSubscriptionDAO.saveOrUpdate(subscription);
} }
cache.invalidateUserData(getUser()); cache.invalidateUserRootCategory(getUser());
return Response.ok(Status.OK).build(); return Response.ok(Status.OK).build();
} }
@@ -506,22 +458,19 @@ public class FeedREST extends AbstractResourceREST {
String publicUrl = applicationSettingsService.get().getPublicUrl(); String publicUrl = applicationSettingsService.get().getPublicUrl();
if (StringUtils.isBlank(publicUrl)) { if (StringUtils.isBlank(publicUrl)) {
throw new WebApplicationException(Response throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR)
.status(Status.INTERNAL_SERVER_ERROR)
.entity("Set the public URL in the admin section.").build()); .entity("Set the public URL in the admin section.").build());
} }
if (StartupBean.USERNAME_DEMO.equals(getUser().getName())) { if (StartupBean.USERNAME_DEMO.equals(getUser().getName())) {
return Response.status(Status.FORBIDDEN) return Response.status(Status.FORBIDDEN).entity("Import is disabled for the demo account").build();
.entity("Import is disabled for the demo account").build();
} }
try { try {
FileItemFactory factory = new DiskFileItemFactory(1000000, null); FileItemFactory factory = new DiskFileItemFactory(1000000, null);
ServletFileUpload upload = new ServletFileUpload(factory); ServletFileUpload upload = new ServletFileUpload(factory);
for (FileItem item : upload.parseRequest(request)) { for (FileItem item : upload.parseRequest(request)) {
if ("file".equals(item.getFieldName())) { if ("file".equals(item.getFieldName())) {
String opml = IOUtils.toString(item.getInputStream(), String opml = IOUtils.toString(item.getInputStream(), "UTF-8");
"UTF-8");
if (StringUtils.isNotBlank(opml)) { if (StringUtils.isNotBlank(opml)) {
opmlImporter.importOpml(getUser(), opml); opmlImporter.importOpml(getUser(), opml);
} }
@@ -529,14 +478,9 @@ public class FeedREST extends AbstractResourceREST {
} }
} }
} catch (Exception e) { } catch (Exception e) {
throw new WebApplicationException(Response throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build());
.status(Status.INTERNAL_SERVER_ERROR)
.entity(e.getMessage()).build());
} }
cache.invalidateUserData(getUser()); return Response.temporaryRedirect(URI.create(applicationSettingsService.get().getPublicUrl())).build();
return Response.temporaryRedirect(
URI.create(applicationSettingsService.get().getPublicUrl()))
.build();
} }
@GET @GET
@@ -550,8 +494,7 @@ public class FeedREST extends AbstractResourceREST {
try { try {
opmlString = output.outputString(opml); opmlString = output.outputString(opml);
} catch (Exception e) { } catch (Exception e) {
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e) return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e).build();
.build();
} }
return Response.ok(opmlString).build(); return Response.ok(opmlString).build();
} }

View File

@@ -33,8 +33,7 @@ import com.google.api.client.repackaged.com.google.common.base.Preconditions;
@Path("/push") @Path("/push")
public class PubSubHubbubCallbackREST { public class PubSubHubbubCallbackREST {
private static Logger log = LoggerFactory private static Logger log = LoggerFactory.getLogger(PubSubHubbubCallbackREST.class);
.getLogger(PubSubHubbubCallbackREST.class);
@Context @Context
HttpServletRequest request; HttpServletRequest request;
@@ -57,13 +56,10 @@ public class PubSubHubbubCallbackREST {
@Path("/callback") @Path("/callback")
@GET @GET
@Produces(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN)
public Response verify(@QueryParam("hub.mode") String mode, public Response verify(@QueryParam("hub.mode") String mode, @QueryParam("hub.topic") String topic,
@QueryParam("hub.topic") String topic, @QueryParam("hub.challenge") String challenge, @QueryParam("hub.lease_seconds") String leaseSeconds,
@QueryParam("hub.challenge") String challenge,
@QueryParam("hub.lease_seconds") String leaseSeconds,
@QueryParam("hub.verify_token") String verifyToken) { @QueryParam("hub.verify_token") String verifyToken) {
if (!applicationSettingsService.get() if (!applicationSettingsService.get().isPubsubhubbub()) {
.isPubsubhubbub()) {
return Response.status(Status.FORBIDDEN).entity("pubsubhubbub is disabled").build(); return Response.status(Status.FORBIDDEN).entity("pubsubhubbub is disabled").build();
} }
@@ -76,8 +72,7 @@ public class PubSubHubbubCallbackREST {
if (feeds.isEmpty() == false) { if (feeds.isEmpty() == false) {
for (Feed feed : feeds) { for (Feed feed : feeds) {
log.debug("activated push notifications for {}", log.debug("activated push notifications for {}", feed.getPushTopic());
feed.getPushTopic());
feed.setPushLastPing(new Date()); feed.setPushLastPing(new Date());
} }
feedDAO.saveOrUpdate(feeds); feedDAO.saveOrUpdate(feeds);
@@ -92,8 +87,7 @@ public class PubSubHubbubCallbackREST {
@POST @POST
@Consumes({ MediaType.APPLICATION_ATOM_XML, "application/rss+xml" }) @Consumes({ MediaType.APPLICATION_ATOM_XML, "application/rss+xml" })
public Response callback() { public Response callback() {
if (!applicationSettingsService.get() if (!applicationSettingsService.get().isPubsubhubbub()) {
.isPubsubhubbub()) {
return Response.status(Status.FORBIDDEN).entity("pubsubhubbub is disabled").build(); return Response.status(Status.FORBIDDEN).entity("pubsubhubbub is disabled").build();
} }
try { try {

View File

@@ -34,10 +34,8 @@ public class ServerREST extends AbstractResourceREST {
ApplicationPropertiesService properties = ApplicationPropertiesService.get(); ApplicationPropertiesService properties = ApplicationPropertiesService.get();
ServerInfo infos = new ServerInfo(); ServerInfo infos = new ServerInfo();
infos.setAnnouncement(applicationSettingsService.get() infos.setAnnouncement(applicationSettingsService.get().getAnnouncement());
.getAnnouncement()); infos.getSupportedLanguages().putAll(startupBean.getSupportedLanguages());
infos.getSupportedLanguages().putAll(
startupBean.getSupportedLanguages());
infos.setVersion(properties.getVersion()); infos.setVersion(properties.getVersion());
infos.setGitCommit(properties.getGitCommit()); infos.setGitCommit(properties.getGitCommit());
return Response.ok(infos).build(); return Response.ok(infos).build();
@@ -57,8 +55,7 @@ public class ServerREST extends AbstractResourceREST {
HttpResult result = httpGetter.getBinary(url, 20000); HttpResult result = httpGetter.getBinary(url, 20000);
return Response.ok(result.getContent()).build(); return Response.ok(result.getContent()).build();
} catch (Exception e) { } catch (Exception e) {
return Response.status(Status.SERVICE_UNAVAILABLE) return Response.status(Status.SERVICE_UNAVAILABLE).entity(e.getMessage()).build();
.entity(e.getMessage()).build();
} }
} }
} }

View File

@@ -12,7 +12,6 @@ import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.StartupBean; import com.commafeed.backend.StartupBean;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.dao.UserRoleDAO; import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.dao.UserSettingsDAO;
@@ -58,15 +57,15 @@ public class UserREST extends AbstractResourceREST {
@Inject @Inject
PasswordEncryptionService encryptionService; PasswordEncryptionService encryptionService;
@Inject
CacheService cache;
@Inject @Inject
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@Path("/settings") @Path("/settings")
@GET @GET
@ApiOperation(value = "Retrieve user settings", notes = "Retrieve user settings", responseClass = "com.commafeed.frontend.model.Settings") @ApiOperation(
value = "Retrieve user settings",
notes = "Retrieve user settings",
responseClass = "com.commafeed.frontend.model.Settings")
public Response getSettings() { public Response getSettings() {
Settings s = new Settings(); Settings s = new Settings();
UserSettings settings = userSettingsDAO.findByUser(getUser()); UserSettings settings = userSettingsDAO.findByUser(getUser());
@@ -149,16 +148,13 @@ public class UserREST extends AbstractResourceREST {
@Path("/profile") @Path("/profile")
@POST @POST
@ApiOperation(value = "Save user's profile") @ApiOperation(value = "Save user's profile")
public Response save( public Response save(@ApiParam(required = true) ProfileModificationRequest request) {
@ApiParam(required = true) ProfileModificationRequest request) {
User user = getUser(); User user = getUser();
Preconditions.checkArgument(StringUtils.isBlank(request.getPassword()) Preconditions.checkArgument(StringUtils.isBlank(request.getPassword()) || request.getPassword().length() >= 6);
|| request.getPassword().length() >= 6);
if (StringUtils.isNotBlank(request.getEmail())) { if (StringUtils.isNotBlank(request.getEmail())) {
User u = userDAO.findByEmail(request.getEmail()); User u = userDAO.findByEmail(request.getEmail());
Preconditions.checkArgument(u == null Preconditions.checkArgument(u == null || user.getId().equals(u.getId()));
|| user.getId().equals(u.getId()));
} }
if (StartupBean.USERNAME_DEMO.equals(user.getName())) { if (StartupBean.USERNAME_DEMO.equals(user.getName())) {
@@ -167,8 +163,7 @@ public class UserREST extends AbstractResourceREST {
user.setEmail(StringUtils.trimToNull(request.getEmail())); user.setEmail(StringUtils.trimToNull(request.getEmail()));
if (StringUtils.isNotBlank(request.getPassword())) { if (StringUtils.isNotBlank(request.getPassword())) {
byte[] password = encryptionService.getEncryptedPassword( byte[] password = encryptionService.getEncryptedPassword(request.getPassword(), user.getSalt());
request.getPassword(), user.getSalt());
user.setPassword(password); user.setPassword(password);
user.setApiKey(userService.generateApiKey(user)); user.setApiKey(userService.generateApiKey(user));
} }
@@ -185,12 +180,10 @@ public class UserREST extends AbstractResourceREST {
@SecurityCheck(Role.NONE) @SecurityCheck(Role.NONE)
public Response register(@ApiParam(required = true) RegistrationRequest req) { public Response register(@ApiParam(required = true) RegistrationRequest req) {
try { try {
userService.register(req.getName(), req.getPassword(), userService.register(req.getName(), req.getPassword(), req.getEmail(), Arrays.asList(Role.USER));
req.getEmail(), Arrays.asList(Role.USER));
return Response.ok().build(); return Response.ok().build();
} catch (Exception e) { } catch (Exception e) {
return Response.status(Status.INTERNAL_SERVER_ERROR) return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
.entity(e.getMessage()).build();
} }
} }
@@ -199,12 +192,10 @@ public class UserREST extends AbstractResourceREST {
@POST @POST
@ApiOperation(value = "Delete the user account") @ApiOperation(value = "Delete the user account")
public Response delete() { public Response delete() {
if (StartupBean.USERNAME_ADMIN.equals(getUser().getName()) if (StartupBean.USERNAME_ADMIN.equals(getUser().getName()) || StartupBean.USERNAME_DEMO.equals(getUser().getName())) {
|| StartupBean.USERNAME_DEMO.equals(getUser().getName())) {
return Response.status(Status.FORBIDDEN).build(); return Response.status(Status.FORBIDDEN).build();
} }
userService.unregister(getUser()); userService.unregister(getUser());
cache.invalidateUserData(getUser());
return Response.ok().build(); return Response.ok().build();
} }
} }

View File

@@ -29,14 +29,12 @@ import com.commafeed.backend.services.ApplicationPropertiesService;
import com.commafeed.frontend.CommaFeedSession; import com.commafeed.frontend.CommaFeedSession;
/** /**
* Replace variables from templates on the fly in dev mode only. In production * Replace variables from templates on the fly in dev mode only. In production the substitution is done at build-time.
* the substitution is done at build-time.
* *
*/ */
public class InternationalizationDevelopmentFilter implements Filter { public class InternationalizationDevelopmentFilter implements Filter {
private static Logger log = LoggerFactory private static Logger log = LoggerFactory.getLogger(InternationalizationDevelopmentFilter.class);
.getLogger(InternationalizationDevelopmentFilter.class);
@Inject @Inject
UserSettingsDAO userSettingsDAO; UserSettingsDAO userSettingsDAO;
@@ -55,8 +53,7 @@ public class InternationalizationDevelopmentFilter implements Filter {
} }
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterChain chain) throws IOException, ServletException {
if (production) { if (production) {
chain.doFilter(request, response); chain.doFilter(request, response);
@@ -64,8 +61,7 @@ public class InternationalizationDevelopmentFilter implements Filter {
} }
final ServletOutputStream wrapper = new ServletOutputStreamWrapper(); final ServletOutputStream wrapper = new ServletOutputStreamWrapper();
ServletResponse interceptor = new HttpServletResponseWrapper( ServletResponse interceptor = new HttpServletResponseWrapper((HttpServletResponse) response) {
(HttpServletResponse) response) {
@Override @Override
public ServletOutputStream getOutputStream() throws IOException { public ServletOutputStream getOutputStream() throws IOException {
@@ -74,10 +70,8 @@ public class InternationalizationDevelopmentFilter implements Filter {
}; };
chain.doFilter(request, interceptor); chain.doFilter(request, interceptor);
UserSettings settings = userSettingsDAO.findByUser(CommaFeedSession UserSettings settings = userSettingsDAO.findByUser(CommaFeedSession.get().getUser());
.get().getUser()); String lang = (settings == null || settings.getLanguage() == null) ? "en" : settings.getLanguage();
String lang = (settings == null || settings.getLanguage() == null) ? "en"
: settings.getLanguage();
byte[] bytes = translate(wrapper.toString(), lang).getBytes("UTF-8"); byte[] bytes = translate(wrapper.toString(), lang).getBytes("UTF-8");
response.setContentLength(bytes.length); response.setContentLength(bytes.length);
@@ -91,8 +85,7 @@ public class InternationalizationDevelopmentFilter implements Filter {
Properties props = new Properties(); Properties props = new Properties();
InputStream is = null; InputStream is = null;
try { try {
is = getClass() is = getClass().getResourceAsStream("/i18n/" + lang + ".properties");
.getResourceAsStream("/i18n/" + lang + ".properties");
props.load(new InputStreamReader(is, "UTF-8")); props.load(new InputStreamReader(is, "UTF-8"));
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
@@ -111,8 +104,7 @@ public class InternationalizationDevelopmentFilter implements Filter {
while (m.find()) { while (m.find()) {
String var = m.group(1); String var = m.group(1);
Object replacement = props.get(var); Object replacement = props.get(var);
String replacementValue = replacement == null ? var : replacement String replacementValue = replacement == null ? var : replacement.toString().split("#")[0];
.toString().split("#")[0];
m.appendReplacement(sb, replacementValue); m.appendReplacement(sb, replacementValue);
} }
m.appendTail(sb); m.appendTail(sb);

View File

@@ -38,7 +38,7 @@ public class ModelFactory {
* *
*/ */
public static class MF { public static class MF {
public static <T> String i(T proxiedValue) { public static <T> String i(T proxiedValue) {
return ModelFactory.invokedProperty(proxiedValue); return ModelFactory.invokedProperty(proxiedValue);
} }

View File

@@ -17,16 +17,12 @@ import org.apache.wicket.util.template.PackageTextTemplate;
public class WicketUtils { public class WicketUtils {
public static void loadJS(IHeaderResponse response, Class<?> klass, public static void loadJS(IHeaderResponse response, Class<?> klass, String fileName) {
String fileName) { HeaderItem result = JavaScriptHeaderItem.forReference(new JavaScriptResourceReference(klass, fileName + ".js"));
HeaderItem result = JavaScriptHeaderItem
.forReference(new JavaScriptResourceReference(klass, fileName
+ ".js"));
response.render(result); response.render(result);
} }
public static void loadJS(IHeaderResponse response, Class<?> klass, public static void loadJS(IHeaderResponse response, Class<?> klass, String fileName, Map<String, ? extends Object> variables) {
String fileName, Map<String, ? extends Object> variables) {
HeaderItem result = null; HeaderItem result = null;
PackageTextTemplate template = null; PackageTextTemplate template = null;
try { try {
@@ -40,14 +36,12 @@ public class WicketUtils {
} }
public static HttpServletRequest getHttpServletRequest() { public static HttpServletRequest getHttpServletRequest() {
ServletWebRequest servletWebRequest = (ServletWebRequest) RequestCycle ServletWebRequest servletWebRequest = (ServletWebRequest) RequestCycle.get().getRequest();
.get().getRequest();
return servletWebRequest.getContainerRequest(); return servletWebRequest.getContainerRequest();
} }
public static HttpServletResponse getHttpServletResponse() { public static HttpServletResponse getHttpServletResponse() {
WebResponse webResponse = (WebResponse) RequestCycle.get() WebResponse webResponse = (WebResponse) RequestCycle.get().getResponse();
.getResponse();
return (HttpServletResponse) webResponse.getContainerResponse(); return (HttpServletResponse) webResponse.getContainerResponse();
} }
} }

View File

@@ -20,8 +20,7 @@ public class DisplayExceptionPage extends BasePage {
add(new Label("message", t.getMessage())); add(new Label("message", t.getMessage()));
add(new BookmarkablePageLink<Void>("homepage", getApplication() add(new BookmarkablePageLink<Void>("homepage", getApplication().getHomePage()));
.getHomePage()));
StringWriter stringWriter = new StringWriter(); StringWriter stringWriter = new StringWriter();
t.printStackTrace(new PrintWriter(stringWriter)); t.printStackTrace(new PrintWriter(stringWriter));

View File

@@ -21,8 +21,7 @@ public class CDIBootstrap implements Extension {
void afterBeanDiscovery(@Observes AfterBeanDiscovery abd, BeanManager bm) { void afterBeanDiscovery(@Observes AfterBeanDiscovery abd, BeanManager bm) {
} }
void afterDeploymentValidation(@Observes AfterDeploymentValidation event, void afterDeploymentValidation(@Observes AfterDeploymentValidation event, BeanManager manager) {
BeanManager manager) {
} }
@Produces @Produces

View File

@@ -5,20 +5,8 @@
http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm
http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"> http://java.sun.com/xml/ns/persistence/orm_2_0.xsd">
<named-query name="EntryStatus.unreadCounts"> <named-query name="Statuses.deleteOld">
<query>select s.subscription.id, count(s) from FeedEntryStatus s where s.user=:user and s.read=false group by s.subscription.id</query> <query>delete from FeedEntryStatus s where s.entryInserted &lt; :date and s.starred = false</query>
</named-query>
<named-query name="EntryStatus.existing">
<query>select new com.commafeed.backend.dao.FeedEntryDAO$EntryWithFeed(e, f) FROM FeedEntry e LEFT JOIN e.feedRelationships f WITH f.feed.id = :feedId where e.guidHash = :guidHash and e.url = :url</query>
</named-query>
<named-query name="EntryStatus.deleteByIds">
<query>delete from FeedEntryStatus s where s.id in :ids</query>
</named-query>
<named-query name="Feed.deleteEntryRelationships">
<query>delete from FeedFeedEntry ffe where ffe.feed.id = :feedId</query>
</named-query> </named-query>
</entity-mappings> </entity-mappings>

View File

@@ -346,4 +346,20 @@
<column name="subscription_id" /> <column name="subscription_id" />
</createIndex> </createIndex>
</changeSet> </changeSet>
<changeSet author="athou" id="add-trim-status-setting">
<addColumn tableName="APPLICATIONSETTINGS">
<column name="keepStatusDays" type="INT" />
</addColumn>
<update tableName="APPLICATIONSETTINGS">
<column name="keepStatusDays" valueNumeric="0"></column>
</update>
</changeSet>
<changeSet author="athou" id="status-cleanup">
<delete tableName="FEEDENTRYSTATUSES">
<where>read_status = false and starred = false</where>
</delete>
</changeSet>
</databaseChangeLog> </databaseChangeLog>

View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd">
<changeSet author="athou" id="change-entries-model">
<dropTable tableName="FEED_FEEDENTRIES" />
<delete tableName="FEEDENTRYSTATUSES"></delete>
<delete tableName="FEEDENTRIES"></delete>
<delete tableName="FEEDENTRYCONTENTS"></delete>
<addColumn tableName="FEEDENTRIES">
<column name="feed_id" type="BIGINT" defaultValue="1">
<constraints nullable="false" />
</column>
</addColumn>
<addForeignKeyConstraint constraintName="fk_feed_id"
baseTableName="FEEDENTRIES" baseColumnNames="feed_id"
referencedTableName="FEEDS" referencedColumnNames="id" />
<createIndex tableName="FEEDENTRIES" indexName="feed_updated_index">
<column name="feed_id" />
<column name="updated" />
</createIndex>
</changeSet>
<changeSet author="athou" id="add-content-hashes">
<addColumn tableName="FEEDENTRYCONTENTS">
<column name="author" type="VARCHAR(128)" />
<column name="contentHash" type="VARCHAR(40)" />
</addColumn>
<createIndex tableName="FEEDENTRYCONTENTS" indexName="content_hash_index">
<column name="contentHash" />
</createIndex>
<dropColumn tableName="FEEDENTRIES" columnName="author"/>
</changeSet>
<changeSet author="athou" id="add-new-index">
<createIndex tableName="FEEDENTRYSTATUSES" indexName="user_entry_index">
<column name="user_id" />
<column name="entry_id" />
</createIndex>
</changeSet>
<changeSet author="athou" id="drop-old-index">
<dropIndex tableName="FEEDENTRYSTATUSES" indexName="sub_entry_index" />
</changeSet>
<changeSet author="athou" id="force-feed-refresh">
<update tableName="FEEDS">
<column name="lastUpdated" valueComputed="null"></column>
<column name="lastPublishedDate" valueComputed="null"></column>
<column name="lastEntryDate" valueComputed="null"></column>
<column name="lastUpdateSuccess" valueComputed="null"></column>
<column name="message" valueComputed="null"></column>
<column name="errorCount" valueNumeric="0"></column>
<column name="disabledUntil" valueComputed="null"></column>
<column name="lastModifiedHeader" valueComputed="null"></column>
<column name="etagHeader" valueComputed="null"></column>
<column name="averageEntryInterval" valueNumeric="null"></column>
<column name="lastContentHash" valueComputed="null"></column>
</update>
</changeSet>
<changeSet author="athou" id="revamp-status-indexes">
<createIndex tableName="FEEDENTRYSTATUSES" indexName="user_starred_updated">
<column name="user_id"></column>
<column name="starred"></column>
<column name="entryUpdated"></column>
</createIndex>
<createIndex tableName="FEEDENTRYSTATUSES" indexName="sub_index">
<column name="subscription_id"></column>
</createIndex>
<dropIndex tableName="FEEDENTRYSTATUSES" indexName="sub_read_updated_index" />
<dropIndex tableName="FEEDENTRYSTATUSES" indexName="user_read_updated_index" />
<dropIndex tableName="FEEDENTRYSTATUSES" indexName="user_read_sub_index" />
<dropIndex tableName="FEEDENTRYSTATUSES" indexName="user_entry_index" />
</changeSet>
<changeSet author="athou" id="revamp-entries-indexes">
<dropIndex tableName="FEEDENTRIES" indexName="updated_index" />
<dropIndex tableName="FEEDENTRIES" indexName="inserted_index" />
</changeSet>
<changeSet author="athou" id="add-starred-index-for-cleanup">
<createIndex tableName="FEEDENTRYSTATUSES" indexName="insterted_starred_index">
<column name="entryInserted"></column>
<column name="starred"></column>
</createIndex>
</changeSet>
<changeSet author="athou" id="add-title-hashes">
<addColumn tableName="FEEDENTRYCONTENTS">
<column name="titleHash" type="VARCHAR(40)" />
</addColumn>
<createIndex tableName="FEEDENTRYCONTENTS" indexName="content_title_index">
<column name="contentHash" />
<column name="titleHash" />
</createIndex>
<dropIndex tableName="FEEDENTRYCONTENTS" indexName="content_hash_index" />
</changeSet>
</databaseChangeLog>

View File

@@ -5,5 +5,6 @@
<include file="changelogs/db.changelog-1.0.xml" /> <include file="changelogs/db.changelog-1.0.xml" />
<include file="changelogs/db.changelog-1.1.xml" /> <include file="changelogs/db.changelog-1.1.xml" />
<include file="changelogs/db.changelog-1.2.xml" />
</databaseChangeLog> </databaseChangeLog>

View File

@@ -46,7 +46,7 @@ function($scope, FeedService, CategoryService, MobileService) {
$scope.open = function() { $scope.open = function() {
$scope.sub = { $scope.sub = {
categoryId: $scope.sub.categoryId categoryId: $scope.sub.categoryId || 'all'
}; };
$scope.isOpen = true; $scope.isOpen = true;
}; };

View File

@@ -163,6 +163,14 @@
ng-model="settings.queryTimeout" /> ng-model="settings.queryTimeout" />
</div> </div>
</div> </div>
<div class="control-group">
<label class="control-label" for="keepStatusDays">Keep read status for (days)</label>
<div class="controls">
<input type="number" name="keepStatusDays" class="input-block-level"
ng-model="settings.keepStatusDays" />
<span class="help-inline">0 = keep forever</span>
</div>
</div>
<div class="control-group"> <div class="control-group">
<label class="control-label" for="crawlingPaused">Pause crawling</label> <label class="control-label" for="crawlingPaused">Pause crawling</label>
<div class="controls"> <div class="controls">

View File

@@ -79,7 +79,7 @@
<i ng-class="{'icon-star icon-star-yellow': entry.starred, 'icon-star-empty': !entry.starred}" <i ng-class="{'icon-star icon-star-yellow': entry.starred, 'icon-star-empty': !entry.starred}"
class="pointer"></i> class="pointer"></i>
</span> </span>
<label class="checkbox inline"> <label class="checkbox inline" ui-if="entry.markable">
<input type="checkbox" ng-checked="!entry.read" ng-click="mark(entry, !entry.read)" class="mousetrap"></input> <input type="checkbox" ng-checked="!entry.read" ng-click="mark(entry, !entry.read)" class="mousetrap"></input>
${view.keep_unread} ${view.keep_unread}
</label> </label>

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