Merge changes Ic8f371d9,Iae4cffcd,I4213004f
* changes:
Implement DynamicSet<T>, DynamicMap<T> to provide bindings in Guice
Automatically register plugin bindings
Define gerrit-extension-api module
diff --git a/gerrit-extension-api/.gitignore b/gerrit-extension-api/.gitignore
new file mode 100644
index 0000000..4e1ec9c
--- /dev/null
+++ b/gerrit-extension-api/.gitignore
@@ -0,0 +1,6 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-extension-api.iml
diff --git a/gerrit-extension-api/.settings/org.eclipse.core.resources.prefs b/gerrit-extension-api/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..fc11c3f
--- /dev/null
+++ b/gerrit-extension-api/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,5 @@
+#Thu Jul 28 11:02:36 PDT 2011
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
+encoding/<project>=UTF-8
diff --git a/gerrit-extension-api/.settings/org.eclipse.core.runtime.prefs b/gerrit-extension-api/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..8667cfd
--- /dev/null
+++ b/gerrit-extension-api/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,3 @@
+#Tue Sep 02 16:59:24 PDT 2008
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs b/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..470942d
--- /dev/null
+++ b/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,269 @@
+#Thu Jul 28 11:02:36 PDT 2011
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=16
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=0
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=0
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=2
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=true
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=80
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=3
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=2
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs b/gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..d4218a5
--- /dev/null
+++ b/gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,61 @@
+#Wed Jul 29 11:31:38 PDT 2009
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_Google Format
+formatter_settings_version=11
+org.eclipse.jdt.ui.ignorelowercasenames=true
+org.eclipse.jdt.ui.importorder=com.google;com;junit;net;org;java;javax;
+org.eclipse.jdt.ui.ondemandthreshold=99
+org.eclipse.jdt.ui.staticondemandthreshold=99
+org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=false
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=false
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=true
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=false
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_casts=false
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=false
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_blocks=false
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
new file mode 100644
index 0000000..0209f3f
--- /dev/null
+++ b/gerrit-extension-api/pom.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2012 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-parent</artifactId>
+ <version>2.5-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>gerrit-extension-api</artifactId>
+ <name>Gerrit Code Review - Extension API</name>
+
+ <description>
+ Interfaces describing the extension API
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.inject</groupId>
+ <artifactId>guice</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.inject.extensions</groupId>
+ <artifactId>guice-servlet</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.tomcat</groupId>
+ <artifactId>servlet-api</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <configuration>
+ <createSourcesJar>true</createSourcesJar>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java
new file mode 100644
index 0000000..4811e407
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation applied to auto-registered, exported types.
+ * <p>
+ * Plugins or extensions using auto-registration should apply this annotation to
+ * any non-abstract class they want exported for access.
+ * <p>
+ * For SSH commands the @Export annotation names the subcommand:
+ *
+ * <pre>
+ * @Export("print")
+ * class MyCommand extends SshCommand {
+ * </pre>
+ *
+ * For HTTP servlets, the @Export annotation names the URL the servlet is bound
+ * to, relative to the plugin or extension's namespace within the Gerrit
+ * container.
+ *
+ * <pre>
+ * @Export("/index.html")
+ * class ShowIndexHtml extends HttpServlet {
+ * </pre>
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface Export {
+ String value();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java
new file mode 100644
index 0000000..a3e72bc
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+
+final class ExportImpl implements Export, Serializable {
+ private static final long serialVersionUID = 0;
+ private final String value;
+
+ ExportImpl(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return Export.class;
+ }
+
+ @Override
+ public String value() {
+ return value;
+ }
+
+ @Override
+ public int hashCode() {
+ return (127 * "value".hashCode()) ^ value.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof Export && value.equals(((Export) o).value());
+ }
+
+ @Override
+ public String toString() {
+ return "@" + Export.class.getName() + "(value=" + value + ")";
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
index cd0b334..c48bcfb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
@@ -12,10 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.plugins;
+package com.google.gerrit.extensions.annotations;
-/** Handle for registered information. */
-public interface RegistrationHandle {
- /** Delete this registration. */
- public void remove();
+/** Static constructors for {@link Export} annotations. */
+public final class Exports {
+ /** Create an annotation to export under a specific name. */
+ public static Export named(String name) {
+ return new ExportImpl(name);
+ }
+
+ private Exports() {
+ }
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java
new file mode 100644
index 0000000..4799f5e
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for interfaces that accept auto-registered implementations.
+ * <p>
+ * Interfaces that accept automatically registered implementations into their
+ * {@link DynamicSet} must be tagged with this annotation.
+ * <p>
+ * Plugins or extensions that implement an {@code @ExtensionPoint} interface
+ * should use the {@link Listen} annotation to automatically register.
+ *
+ * @see Listen
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface ExtensionPoint {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginName.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java
similarity index 62%
copy from gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginName.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java
index 6a47b93..e4ba931 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginName.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.plugins;
+package com.google.gerrit.extensions.annotations;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -22,8 +22,18 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
-@Target({ElementType.PARAMETER, ElementType.FIELD})
+/**
+ * Annotation for auto-registered extension point implementations.
+ * <p>
+ * Plugins or extensions using auto-registration should apply this annotation to
+ * any non-abstract class that implements an unnamed extension point, such as a
+ * notification listener. Gerrit will automatically determine which extension
+ * points to apply based on the interfaces the type implements.
+ *
+ * @see Export
+ */
+@Target({ElementType.TYPE})
@Retention(RUNTIME)
@BindingAnnotation
-public @interface PluginName {
+public @interface Listen {
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginName.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.java
similarity index 71%
rename from gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginName.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.java
index 6a47b93..672bab2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginName.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.plugins;
+package com.google.gerrit.extensions.annotations;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -22,6 +22,19 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
+/**
+ * Annotation applied to a String containing the plugin or extension name.
+ * <p>
+ * A plugin or extension may receive this string by Guice injection to discover
+ * the name that an administrator has installed the plugin or extension under:
+ *
+ * <pre>
+ * @Inject
+ * MyType(@PluginName String myName) {
+ * ...
+ * }
+ * </pre>
+ */
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RUNTIME)
@BindingAnnotation
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
new file mode 100644
index 0000000..f114afd
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.util.Types;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * A map of members that can be modified as plugins reload.
+ * <p>
+ * Maps index their members by plugin name and export name.
+ * <p>
+ * DynamicMaps are always mapped as singletons in Guice, and only may contain
+ * singletons, as providers are resolved to an instance before the member is
+ * added to the map.
+ */
+public abstract class DynamicMap<T> {
+ /**
+ * Declare a singleton {@code DynamicMap<T>} with a binder.
+ * <p>
+ * Maps must be defined in a Guice module before they can be bound:
+ *
+ * <pre>
+ * DynamicMap.mapOf(binder(), Interface.class);
+ * bind(Interface.class)
+ * .annotatedWith(Exports.named("foo"))
+ * .to(Impl.class);
+ * </pre>
+ *
+ * @param binder a new binder created in the module.
+ * @param member type of value in the map.
+ */
+ public static <T> void mapOf(Binder binder, Class<T> member) {
+ mapOf(binder, TypeLiteral.get(member));
+ }
+
+ /**
+ * Declare a singleton {@code DynamicMap<T>} with a binder.
+ * <p>
+ * Maps must be defined in a Guice module before they can be bound:
+ *
+ * <pre>
+ * DynamicMap.mapOf(binder(), new TypeLiteral<Thing<Bar>>(){});
+ * bind(new TypeLiteral<Thing<Bar>>() {})
+ * .annotatedWith(Exports.named("foo"))
+ * .to(Impl.class);
+ * </pre>
+ *
+ * @param binder a new binder created in the module.
+ * @param member type of value in the map.
+ */
+ public static <T> void mapOf(Binder binder, TypeLiteral<T> member) {
+ @SuppressWarnings("unchecked")
+ Key<DynamicMap<T>> key = (Key<DynamicMap<T>>) Key.get(
+ Types.newParameterizedType(DynamicMap.class, member.getType()));
+ binder.bind(key)
+ .toProvider(new DynamicMapProvider<T>(member))
+ .in(Scopes.SINGLETON);
+ }
+
+ final ConcurrentMap<NamePair, T> items;
+
+ DynamicMap() {
+ items = new ConcurrentHashMap<NamePair, T>(16, 0.75f, 1);
+ }
+
+ /**
+ * Lookup an implementation by name.
+ *
+ * @param pluginName local name of the plugin providing the item.
+ * @param exportName name the plugin exports the item as.
+ * @return the implementation. Null if the plugin is not running, or if the
+ * plugin does not export this name.
+ */
+ public T get(String pluginName, String exportName) {
+ return items.get(new NamePair(pluginName, exportName));
+ }
+
+ /**
+ * Get the names of all running plugins supplying this type.
+ *
+ * @return sorted set of active plugins that supply at least one item.
+ */
+ public SortedSet<String> plugins() {
+ SortedSet<String> r = new TreeSet<String>();
+ for (NamePair p : items.keySet()) {
+ r.add(p.pluginName);
+ }
+ return Collections.unmodifiableSortedSet(r);
+ }
+
+ /**
+ * Get the items exported by a single plugin.
+ *
+ * @param pluginName name of the plugin.
+ * @return items exported by a plugin, keyed by the export name.
+ */
+ public SortedMap<String, T> byPlugin(String pluginName) {
+ SortedMap<String, T> r = new TreeMap<String, T>();
+ for (Map.Entry<NamePair, T> e : items.entrySet()) {
+ if (e.getKey().pluginName.equals(pluginName)) {
+ r.put(e.getKey().exportName, e.getValue());
+ }
+ }
+ return Collections.unmodifiableSortedMap(r);
+ }
+
+ static class NamePair {
+ private final String pluginName;
+ private final String exportName;
+
+ NamePair(String pn, String en) {
+ this.pluginName = pn;
+ this.exportName = en;
+ }
+
+ @Override
+ public int hashCode() {
+ return pluginName.hashCode() * 31 + exportName.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof NamePair) {
+ NamePair np = (NamePair) other;
+ return pluginName.equals(np) && exportName.equals(np);
+ }
+ return false;
+ }
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
new file mode 100644
index 0000000..d771d13
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+
+import java.util.List;
+
+class DynamicMapProvider<T> implements Provider<DynamicMap<T>> {
+ private final TypeLiteral<T> type;
+
+ @Inject
+ private Injector injector;
+
+ DynamicMapProvider(TypeLiteral<T> type) {
+ this.type = type;
+ }
+
+ public DynamicMap<T> get() {
+ PrivateInternals_DynamicMapImpl<T> m =
+ new PrivateInternals_DynamicMapImpl<T>();
+ List<Binding<T>> bindings = injector.findBindingsByType(type);
+ if (bindings != null) {
+ for (Binding<T> b : bindings) {
+ m.put("gerrit", b.getKey(), b.getProvider().get());
+ }
+ }
+ return m;
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
new file mode 100644
index 0000000..7f46ad4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -0,0 +1,231 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.internal.UniqueAnnotations;
+import com.google.inject.name.Named;
+import com.google.inject.util.Types;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A set of members that can be modified as plugins reload.
+ * <p>
+ * DynamicSets are always mapped as singletons in Guice, and only may contain
+ * singletons, as providers are resolved to an instance before the member is
+ * added to the set.
+ */
+public class DynamicSet<T> implements Iterable<T> {
+ /**
+ * Declare a singleton {@code DynamicSet<T>} with a binder.
+ * <p>
+ * Sets must be defined in a Guice module before they can be bound:
+ * <pre>
+ * DynamicSet.setOf(binder(), Interface.class);
+ * DynamicSet.bind(binder(), Interface.class).to(Impl.class);
+ * </pre>
+ *
+ * @param binder a new binder created in the module.
+ * @param member type of entry in the set.
+ */
+ public static <T> void setOf(Binder binder, Class<T> member) {
+ setOf(binder, TypeLiteral.get(member));
+ }
+
+ /**
+ * Declare a singleton {@code DynamicSet<T>} with a binder.
+ * <p>
+ * Sets must be defined in a Guice module before they can be bound:
+ * <pre>
+ * DynamicSet.setOf(binder(), new TypeLiteral<Thing<Foo>>() {});
+ * </pre>
+ *
+ * @param binder a new binder created in the module.
+ * @param member type of entry in the set.
+ */
+ public static <T> void setOf(Binder binder, TypeLiteral<T> member) {
+ @SuppressWarnings("unchecked")
+ Key<DynamicSet<T>> key = (Key<DynamicSet<T>>) Key.get(
+ Types.newParameterizedType(DynamicSet.class, member.getType()));
+ binder.bind(key)
+ .toProvider(new DynamicSetProvider<T>(member))
+ .in(Scopes.SINGLETON);
+ }
+
+ /**
+ * Bind one implementation into the set using a unique annotation.
+ *
+ * @param binder a new binder created in the module.
+ * @param type type of entries in the set.
+ * @return a binder to continue configuring the new set member.
+ */
+ public static <T> LinkedBindingBuilder<T> bind(Binder binder, Class<T> type) {
+ return bind(binder, TypeLiteral.get(type));
+ }
+
+ /**
+ * Bind one implementation into the set using a unique annotation.
+ *
+ * @param binder a new binder created in the module.
+ * @param type type of entries in the set.
+ * @return a binder to continue configuring the new set member.
+ */
+ public static <T> LinkedBindingBuilder<T> bind(Binder binder, TypeLiteral<T> type) {
+ return binder.bind(type).annotatedWith(UniqueAnnotations.create());
+ }
+
+ /**
+ * Bind a named implementation into the set.
+ *
+ * @param binder a new binder created in the module.
+ * @param type type of entries in the set.
+ * @param name {@code @Named} annotation to apply instead of a unique
+ * annotation.
+ * @return a binder to continue configuring the new set member.
+ */
+ public static <T> LinkedBindingBuilder<T> bind(Binder binder,
+ Class<T> type,
+ Named name) {
+ return bind(binder, TypeLiteral.get(type));
+ }
+
+ /**
+ * Bind a named implementation into the set.
+ *
+ * @param binder a new binder created in the module.
+ * @param type type of entries in the set.
+ * @param name {@code @Named} annotation to apply instead of a unique
+ * annotation.
+ * @return a binder to continue configuring the new set member.
+ */
+ public static <T> LinkedBindingBuilder<T> bind(Binder binder,
+ TypeLiteral<T> type,
+ Named name) {
+ return binder.bind(type).annotatedWith(name);
+ }
+
+ private final CopyOnWriteArrayList<AtomicReference<T>> items;
+
+ DynamicSet(Collection<AtomicReference<T>> base) {
+ items = new CopyOnWriteArrayList<AtomicReference<T>>(base);
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ final Iterator<AtomicReference<T>> itr = items.iterator();
+ return new Iterator<T>() {
+ private T next;
+
+ @Override
+ public boolean hasNext() {
+ while (next == null && itr.hasNext()) {
+ next = itr.next().get();
+ }
+ return next != null;
+ }
+
+ @Override
+ public T next() {
+ if (hasNext()) {
+ T result = next;
+ next = null;
+ return result;
+ }
+ throw new NoSuchElementException();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ /**
+ * Add one new element to the set.
+ *
+ * @param item the item to add to the collection. Must not be null.
+ * @return handle to remove the item at a later point in time.
+ */
+ public RegistrationHandle add(final T item) {
+ final AtomicReference<T> ref = new AtomicReference<T>(item);
+ items.add(ref);
+ return new RegistrationHandle() {
+ @Override
+ public void remove() {
+ if (ref.compareAndSet(item, null)) {
+ items.remove(ref);
+ }
+ }
+ };
+ }
+
+ /**
+ * Add one new element that may be hot-replaceable in the future.
+ *
+ * @param key unique description from the item's Guice binding. This can be
+ * later obtained from the registration handle to facilitate matching
+ * with the new equivalent instance during a hot reload.
+ * @param item the item to add to the collection right now. Must not be null.
+ * @return a handle that can remove this item later, or hot-swap the item
+ * without it ever leaving the collection.
+ */
+ public ReloadableRegistrationHandle<T> add(Key<T> key, T item) {
+ AtomicReference<T> ref = new AtomicReference<T>(item);
+ items.add(ref);
+ return new ReloadableHandle(ref, key, item);
+ }
+
+ private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
+ private final AtomicReference<T> ref;
+ private final Key<T> key;
+ private final T item;
+
+ ReloadableHandle(AtomicReference<T> ref, Key<T> key, T item) {
+ this.ref = ref;
+ this.key = key;
+ this.item = item;
+ }
+
+ @Override
+ public void remove() {
+ if (ref.compareAndSet(item, null)) {
+ items.remove(ref);
+ }
+ }
+
+ @Override
+ public Key<T> getKey() {
+ return key;
+ }
+
+ @Override
+ public ReloadableHandle replace(Key<T> newKey, T newItem) {
+ if (ref.compareAndSet(item, newItem)) {
+ return new ReloadableHandle(ref, newKey, newItem);
+ }
+ return null;
+ }
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
new file mode 100644
index 0000000..694fbd8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+class DynamicSetProvider<T> implements Provider<DynamicSet<T>> {
+ private final TypeLiteral<T> type;
+
+ @Inject
+ private Injector injector;
+
+ DynamicSetProvider(TypeLiteral<T> type) {
+ this.type = type;
+ }
+
+ public DynamicSet<T> get() {
+ return new DynamicSet<T>(find(injector, type));
+ }
+
+ private static <T> List<AtomicReference<T>> find(
+ Injector src,
+ TypeLiteral<T> type) {
+ List<Binding<T>> bindings = src.findBindingsByType(type);
+ int cnt = bindings != null ? bindings.size() : 0;
+ if (cnt == 0) {
+ return Collections.emptyList();
+ }
+ List<AtomicReference<T>> r = new ArrayList<AtomicReference<T>>(cnt);
+ for (Binding<T> b : bindings) {
+ r.add(new AtomicReference<T>(b.getProvider().get()));
+ }
+ return r;
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
new file mode 100644
index 0000000..0ce4014
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.Key;
+
+/** <b>DO NOT USE</b> */
+public class PrivateInternals_DynamicMapImpl<T> extends DynamicMap<T> {
+ PrivateInternals_DynamicMapImpl() {
+ }
+
+ /**
+ * Store one new element into the map.
+ *
+ * @param pluginName unique name of the plugin providing the export.
+ * @param exportName name the plugin has exported the item as.
+ * @param item the item to add to the collection. Must not be null.
+ * @return handle to remove the item at a later point in time.
+ */
+ public RegistrationHandle put(
+ String pluginName, String exportName,
+ final T item) {
+ final NamePair key = new NamePair(pluginName, exportName);
+ items.put(key, item);
+ return new RegistrationHandle() {
+ @Override
+ public void remove() {
+ items.remove(key, item);
+ }
+ };
+ }
+
+ /**
+ * Store one new element that may be hot-replaceable in the future.
+ *
+ * @param pluginName unique name of the plugin providing the export.
+ * @param key unique description from the item's Guice binding. This can be
+ * later obtained from the registration handle to facilitate matching
+ * with the new equivalent instance during a hot reload. The key must
+ * use an {@link @Export} annotation.
+ * @param item the item to add to the collection right now. Must not be null.
+ * @return a handle that can remove this item later, or hot-swap the item
+ * without it ever leaving the collection.
+ */
+ public ReloadableRegistrationHandle<T> put(
+ String pluginName, Key<T> key,
+ T item) {
+ String exportName = ((Export) key.getAnnotation()).value();
+ NamePair np = new NamePair(pluginName, exportName);
+ items.put(np, item);
+ return new ReloadableHandle(np, key, item);
+ }
+
+ private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
+ private final NamePair np;
+ private final Key<T> key;
+ private final T item;
+
+ ReloadableHandle(NamePair np, Key<T> key, T item) {
+ this.np = np;
+ this.key = key;
+ this.item = item;
+ }
+
+ @Override
+ public void remove() {
+ items.remove(np, item);
+ }
+
+ @Override
+ public Key<T> getKey() {
+ return key;
+ }
+
+ @Override
+ public ReloadableHandle replace(Key<T> newKey, T newItem) {
+ if (items.replace(np, item, newItem)) {
+ return new ReloadableHandle(np, newKey, newItem);
+ }
+ return null;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
similarity index 93%
rename from gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
index cd0b334..2243786 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.plugins;
+package com.google.gerrit.extensions.registration;
/** Handle for registered information. */
public interface RegistrationHandle {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
similarity index 71%
copy from gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
index cd0b334..b7d78c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
@@ -12,10 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.plugins;
+package com.google.gerrit.extensions.registration;
-/** Handle for registered information. */
-public interface RegistrationHandle {
- /** Delete this registration. */
- public void remove();
+import com.google.inject.Key;
+
+public interface ReloadableRegistrationHandle<T> extends RegistrationHandle {
+ public Key<T> getKey();
+
+ public RegistrationHandle replace(Key<T> key, T item);
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
new file mode 100644
index 0000000..2d957f2
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.plugins;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import com.google.inject.servlet.ServletModule;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServlet;
+
+class HttpAutoRegisterModuleGenerator extends ServletModule
+ implements ModuleGenerator {
+ private final Map<String, Class<HttpServlet>> serve = Maps.newHashMap();
+
+ @Override
+ protected void configureServlets() {
+ for (Map.Entry<String, Class<HttpServlet>> e : serve.entrySet()) {
+ bind(e.getValue()).in(Scopes.SINGLETON);
+ serve(e.getKey()).with(e.getValue());
+ }
+ }
+
+ @Override
+ public void setPluginName(String name) {
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void export(Export export, Class<?> type)
+ throws InvalidPluginException {
+ if (HttpServlet.class.isAssignableFrom(type)) {
+ Class<HttpServlet> old = serve.get(export.value());
+ if (old != null) {
+ throw new InvalidPluginException(String.format(
+ "@Export(\"%s\") has duplicate bindings:\n %s\n %s",
+ export.value(), old.getName(), type.getName()));
+ }
+ serve.put(export.value(), (Class<HttpServlet>) type);
+ } else {
+ throw new InvalidPluginException(String.format(
+ "Class %s with @Export(\"%s\") must extend %s",
+ type.getName(), export.value(),
+ HttpServlet.class.getName()));
+ }
+ }
+
+ @Override
+ public Module create() throws InvalidPluginException {
+ return this;
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
index 0ad90c2..2e5001b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
@@ -14,6 +14,7 @@
package com.google.gerrit.httpd.plugins;
+import com.google.gerrit.server.plugins.ModuleGenerator;
import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.plugins.StartPluginListener;
import com.google.inject.internal.UniqueAnnotations;
@@ -32,5 +33,8 @@
bind(ReloadPluginListener.class)
.annotatedWith(UniqueAnnotations.create())
.to(HttpPluginServlet.class);
+
+ bind(ModuleGenerator.class)
+ .to(HttpAutoRegisterModuleGenerator.class);
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 86f886c..23dbaac 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -17,9 +17,9 @@
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.server.MimeUtilFileTypeRegistry;
import com.google.gerrit.server.plugins.Plugin;
-import com.google.gerrit.server.plugins.RegistrationHandle;
import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.plugins.StartPluginListener;
import com.google.inject.Inject;
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index f35608c..70397d8 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -123,6 +123,12 @@
<dependency>
<groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-extension-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.gerrit</groupId>
<artifactId>gerrit-util-cli</artifactId>
<version>${project.version}</version>
</dependency>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
new file mode 100644
index 0000000..5aee9bf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -0,0 +1,392 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import static com.google.gerrit.server.plugins.PluginGuiceEnvironment.is;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.annotations.Listen;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
+
+import org.eclipse.jgit.util.IO;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+class AutoRegisterModules {
+ private static final int SKIP_ALL = ClassReader.SKIP_CODE
+ | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
+ private final String pluginName;
+ private final PluginGuiceEnvironment env;
+ private final JarFile jarFile;
+ private final ClassLoader classLoader;
+ private final ModuleGenerator sshGen;
+ private final ModuleGenerator httpGen;
+
+ private Set<Class<?>> sysSingletons;
+ private Map<TypeLiteral<?>, Class<?>> sysListen;
+
+ Module sysModule;
+ Module sshModule;
+ Module httpModule;
+
+ AutoRegisterModules(String pluginName,
+ PluginGuiceEnvironment env,
+ JarFile jarFile,
+ ClassLoader classLoader) {
+ this.pluginName = pluginName;
+ this.env = env;
+ this.jarFile = jarFile;
+ this.classLoader = classLoader;
+ this.sshGen = env.hasSshModule() ? env.newSshModuleGenerator() : null;
+ this.httpGen = env.hasHttpModule() ? env.newHttpModuleGenerator() : null;
+ }
+
+ AutoRegisterModules discover() throws InvalidPluginException {
+ sysSingletons = Sets.newHashSet();
+ sysListen = Maps.newHashMap();
+
+ if (sshGen != null) {
+ sshGen.setPluginName(pluginName);
+ }
+ if (httpGen != null) {
+ httpGen.setPluginName(pluginName);
+ }
+
+ scan();
+
+ if (!sysSingletons.isEmpty() || !sysListen.isEmpty()) {
+ sysModule = makeSystemModule();
+ }
+ if (sshGen != null) {
+ sshModule = sshGen.create();
+ }
+ if (httpGen != null) {
+ httpModule = httpGen.create();
+ }
+ return this;
+ }
+
+ private Module makeSystemModule() {
+ return new AbstractModule() {
+ @Override
+ protected void configure() {
+ for (Class<?> clazz : sysSingletons) {
+ bind(clazz).in(Scopes.SINGLETON);
+ }
+ for (Map.Entry<TypeLiteral<?>, Class<?>> e : sysListen.entrySet()) {
+ @SuppressWarnings("unchecked")
+ TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+ @SuppressWarnings("unchecked")
+ Class<Object> impl = (Class<Object>) e.getValue();
+
+ Annotation n = impl.getAnnotation(Export.class);
+ if (n == null) {
+ n = impl.getAnnotation(javax.inject.Named.class);
+ }
+ if (n == null) {
+ n = impl.getAnnotation(com.google.inject.name.Named.class);
+ }
+ if (n == null) {
+ n = UniqueAnnotations.create();
+ }
+ bind(type).annotatedWith(n).to(impl);
+ }
+ }
+ };
+ }
+
+ private void scan() throws InvalidPluginException {
+ Enumeration<JarEntry> e = jarFile.entries();
+ while (e.hasMoreElements()) {
+ JarEntry entry = e.nextElement();
+ if (skip(entry)) {
+ continue;
+ }
+
+ ClassData def = new ClassData();
+ try {
+ new ClassReader(read(entry)).accept(def, SKIP_ALL);
+ } catch (IOException err) {
+ throw new InvalidPluginException("Cannot auto-register", err);
+ } catch (RuntimeException err) {
+ PluginLoader.log.warn(String.format(
+ "Plugin %s has invaild class file %s inside of %s",
+ pluginName, entry.getName(), jarFile.getName()), err);
+ continue;
+ }
+
+ if (def.exportedAsName != null) {
+ if (def.isConcrete()) {
+ export(def);
+ } else {
+ PluginLoader.log.warn(String.format(
+ "Plugin %s tries to @Export(\"%s\") abstract class %s",
+ pluginName, def.exportedAsName, def.className));
+ }
+ } else if (def.listen) {
+ if (def.isConcrete()) {
+ listen(def);
+ } else {
+ PluginLoader.log.warn(String.format(
+ "Plugin %s tries to @Listen abstract class %s",
+ pluginName, def.className));
+ }
+ }
+ }
+ }
+
+ private void export(ClassData def) throws InvalidPluginException {
+ Class<?> clazz;
+ try {
+ clazz = Class.forName(def.className, false, classLoader);
+ } catch (ClassNotFoundException err) {
+ throw new InvalidPluginException(String.format(
+ "Cannot load %s with @Export(\"%s\")",
+ def.className, def.exportedAsName), err);
+ }
+
+ Export export = clazz.getAnnotation(Export.class);
+ if (export == null) {
+ PluginLoader.log.warn(String.format(
+ "In plugin %s asm incorrectly parsed %s with @Export(\"%s\")",
+ pluginName, clazz.getName(), def.exportedAsName));
+ return;
+ }
+
+ if (is("org.apache.sshd.server.Command", clazz)) {
+ if (sshGen != null) {
+ sshGen.export(export, clazz);
+ }
+ } else if (is("javax.servlet.http.HttpServlet", clazz)) {
+ if (httpGen != null) {
+ httpGen.export(export, clazz);
+ listen(clazz, clazz);
+ }
+ } else {
+ int cnt = sysListen.size();
+ listen(clazz, clazz);
+ if (cnt == sysListen.size()) {
+ // If no bindings were recorded, the extension isn't recognized.
+ throw new InvalidPluginException(String.format(
+ "Class %s with @Export(\"%s\") not supported",
+ clazz.getName(), export.value()));
+ }
+ }
+ }
+
+ private void listen(ClassData def) throws InvalidPluginException {
+ Class<?> clazz;
+ try {
+ clazz = Class.forName(def.className, false, classLoader);
+ } catch (ClassNotFoundException err) {
+ throw new InvalidPluginException(String.format(
+ "Cannot load %s with @Listen",
+ def.className), err);
+ }
+
+ Listen listen = clazz.getAnnotation(Listen.class);
+ if (listen != null) {
+ listen(clazz, clazz);
+ } else {
+ PluginLoader.log.warn(String.format(
+ "In plugin %s asm incorrectly parsed %s with @Listen",
+ pluginName, clazz.getName()));
+ }
+ }
+
+ private void listen(java.lang.reflect.Type type, Class<?> clazz)
+ throws InvalidPluginException {
+ while (type != null) {
+ Class<?> rawType;
+ if (type instanceof ParameterizedType) {
+ rawType = (Class<?>) ((ParameterizedType) type).getRawType();
+ } else if (type instanceof Class) {
+ rawType = (Class<?>) type;
+ } else {
+ return;
+ }
+
+ if (rawType.getAnnotation(ExtensionPoint.class) != null) {
+ TypeLiteral<?> tl = TypeLiteral.get(type);
+ if (env.hasDynamicSet(tl)) {
+ sysSingletons.add(clazz);
+ sysListen.put(tl, clazz);
+ } else if (env.hasDynamicMap(tl)) {
+ if (clazz.getAnnotation(Export.class) == null) {
+ throw new InvalidPluginException(String.format(
+ "Class %s requires @Export(\"name\") annotation for %s",
+ clazz.getName(), rawType.getName()));
+ }
+ sysSingletons.add(clazz);
+ sysListen.put(tl, clazz);
+ } else {
+ throw new InvalidPluginException(String.format(
+ "Cannot register %s, server does not accept %s",
+ clazz.getName(), rawType.getName()));
+ }
+ return;
+ }
+
+ java.lang.reflect.Type[] interfaces = rawType.getGenericInterfaces();
+ if (interfaces != null) {
+ for (java.lang.reflect.Type i : interfaces) {
+ listen(i, clazz);
+ }
+ }
+
+ type = rawType.getGenericSuperclass();
+ }
+ }
+
+ private static boolean skip(JarEntry entry) {
+ if (!entry.getName().endsWith(".class")) {
+ return true; // Avoid non-class resources.
+ }
+ if (entry.getSize() <= 0) {
+ return true; // Directories have 0 size.
+ }
+ if (entry.getSize() >= 1024 * 1024) {
+ return true; // Do not scan huge class files.
+ }
+ return false;
+ }
+
+ private byte[] read(JarEntry entry) throws IOException {
+ byte[] data = new byte[(int) entry.getSize()];
+ InputStream in = jarFile.getInputStream(entry);
+ try {
+ IO.readFully(in, data, 0, data.length);
+ } finally {
+ in.close();
+ }
+ return data;
+ }
+
+ private static class ClassData implements ClassVisitor {
+ private static final String EXPORT = Type.getType(Export.class).getDescriptor();
+ private static final String LISTEN = Type.getType(Listen.class).getDescriptor();
+
+ String className;
+ int access;
+ String exportedAsName;
+ boolean listen;
+
+ boolean isConcrete() {
+ return (access & Opcodes.ACC_ABSTRACT) == 0
+ && (access & Opcodes.ACC_INTERFACE) == 0;
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature,
+ String superName, String[] interfaces) {
+ this.className = Type.getObjectType(name).getClassName();
+ this.access = access;
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ if (visible && EXPORT.equals(desc)) {
+ return new AbstractAnnotationVisitor() {
+ @Override
+ public void visit(String name, Object value) {
+ exportedAsName = (String) value;
+ }
+ };
+ }
+ if (visible && LISTEN.equals(desc)) {
+ listen = true;
+ return null;
+ }
+ return null;
+ }
+
+ @Override
+ public void visitSource(String arg0, String arg1) {
+ }
+
+ @Override
+ public void visitOuterClass(String arg0, String arg1, String arg2) {
+ }
+
+ @Override
+ public MethodVisitor visitMethod(int arg0, String arg1, String arg2,
+ String arg3, String[] arg4) {
+ return null;
+ }
+
+ @Override
+ public void visitInnerClass(String arg0, String arg1, String arg2, int arg3) {
+ }
+
+ @Override
+ public FieldVisitor visitField(int arg0, String arg1, String arg2,
+ String arg3, Object arg4) {
+ return null;
+ }
+
+ @Override
+ public void visitEnd() {
+ }
+
+ @Override
+ public void visitAttribute(Attribute arg0) {
+ }
+ }
+
+ private static abstract class AbstractAnnotationVisitor implements
+ AnnotationVisitor {
+ @Override
+ public AnnotationVisitor visitAnnotation(String arg0, String arg1) {
+ return null;
+ }
+
+ @Override
+ public AnnotationVisitor visitArray(String arg0) {
+ return null;
+ }
+
+ @Override
+ public void visitEnum(String arg0, String arg1, String arg2) {
+ }
+
+ @Override
+ public void visitEnd() {
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
similarity index 70%
copy from gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
index cd0b334..31be10c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
@@ -14,8 +14,14 @@
package com.google.gerrit.server.plugins;
-/** Handle for registered information. */
-public interface RegistrationHandle {
- /** Delete this registration. */
- public void remove();
+public class InvalidPluginException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidPluginException(String message) {
+ super(message);
+ }
+
+ public InvalidPluginException(String message, Throwable why) {
+ super(message, why);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
similarity index 69%
copy from gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
index cd0b334..92e3b1d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
@@ -14,8 +14,13 @@
package com.google.gerrit.server.plugins;
-/** Handle for registered information. */
-public interface RegistrationHandle {
- /** Delete this registration. */
- public void remove();
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.Module;
+
+public interface ModuleGenerator {
+ void setPluginName(String name);
+
+ void export(Export export, Class<?> type) throws InvalidPluginException;
+
+ Module create() throws InvalidPluginException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index 9e8da32..c47f370 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -15,17 +15,22 @@
package com.google.gerrit.server.plugins;
import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
import com.google.gerrit.lifecycle.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
-import com.google.inject.servlet.GuiceFilter;
import org.eclipse.jgit.storage.file.FileSnapshot;
import java.io.File;
+import java.util.Collections;
+import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@@ -36,7 +41,7 @@
static {
// Guice logs warnings about multiple injectors being created.
// Silence this in case HTTP plugins are used.
- java.util.logging.Logger.getLogger(GuiceFilter.class.getName())
+ java.util.logging.Logger.getLogger("com.google.inject.servlet.GuiceFilter")
.setLevel(java.util.logging.Level.OFF);
}
@@ -45,6 +50,7 @@
private final FileSnapshot snapshot;
private final JarFile jarFile;
private final Manifest manifest;
+ private final ClassLoader classLoader;
private Class<? extends Module> sysModule;
private Class<? extends Module> sshModule;
private Class<? extends Module> httpModule;
@@ -53,12 +59,14 @@
private Injector sshInjector;
private Injector httpInjector;
private LifecycleManager manager;
+ private List<ReloadableRegistrationHandle<?>> reloadableHandles;
public Plugin(String name,
File srcJar,
FileSnapshot snapshot,
JarFile jarFile,
Manifest manifest,
+ ClassLoader classLoader,
@Nullable Class<? extends Module> sysModule,
@Nullable Class<? extends Module> sshModule,
@Nullable Class<? extends Module> httpModule) {
@@ -67,6 +75,7 @@
this.snapshot = snapshot;
this.jarFile = jarFile;
this.manifest = manifest;
+ this.classLoader = classLoader;
this.sysModule = sysModule;
this.sshModule = sshModule;
this.httpModule = httpModule;
@@ -108,25 +117,48 @@
Injector root = newRootInjector(env);
manager = new LifecycleManager();
+ AutoRegisterModules auto = null;
+ if (sysModule == null && sshModule == null && httpModule == null) {
+ auto = new AutoRegisterModules(name, env, jarFile, classLoader);
+ auto.discover();
+ }
+
if (sysModule != null) {
sysInjector = root.createChildInjector(root.getInstance(sysModule));
manager.add(sysInjector);
+ } else if (auto != null && auto.sysModule != null) {
+ sysInjector = root.createChildInjector(auto.sysModule);
+ manager.add(sysInjector);
} else {
sysInjector = root;
}
- if (sshModule != null && env.hasSshModule()) {
- sshInjector = sysInjector.createChildInjector(
- env.getSshModule(),
- sysInjector.getInstance(sshModule));
- manager.add(sshInjector);
+ if (env.hasSshModule()) {
+ if (sshModule != null) {
+ sshInjector = sysInjector.createChildInjector(
+ env.getSshModule(),
+ sysInjector.getInstance(sshModule));
+ manager.add(sshInjector);
+ } else if (auto != null && auto.sshModule != null) {
+ sshInjector = sysInjector.createChildInjector(
+ env.getSshModule(),
+ auto.sshModule);
+ manager.add(sshInjector);
+ }
}
- if (httpModule != null && env.hasHttpModule()) {
- httpInjector = sysInjector.createChildInjector(
- env.getHttpModule(),
- sysInjector.getInstance(httpModule));
- manager.add(httpInjector);
+ if (env.hasHttpModule()) {
+ if (httpModule != null) {
+ httpInjector = sysInjector.createChildInjector(
+ env.getHttpModule(),
+ sysInjector.getInstance(httpModule));
+ manager.add(httpInjector);
+ } else if (auto != null && auto.httpModule != null) {
+ httpInjector = sysInjector.createChildInjector(
+ env.getHttpModule(),
+ auto.httpModule);
+ manager.add(httpInjector);
+ }
}
manager.start();
@@ -159,6 +191,10 @@
return jarFile;
}
+ public Injector getSysInjector() {
+ return sysInjector;
+ }
+
@Nullable
public Injector getSshInjector() {
return sshInjector;
@@ -170,6 +206,13 @@
}
public void add(final RegistrationHandle handle) {
+ if (handle instanceof ReloadableRegistrationHandle) {
+ if (reloadableHandles == null) {
+ reloadableHandles = Lists.newArrayList();
+ }
+ reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
+ }
+
add(new LifecycleListener() {
@Override
public void start() {
@@ -186,6 +229,13 @@
manager.add(listener);
}
+ List<ReloadableRegistrationHandle<?>> getReloadableHandles() {
+ if (reloadableHandles != null) {
+ return reloadableHandles;
+ }
+ return Collections.emptyList();
+ }
+
@Override
public String toString() {
return "Plugin [" + name + "]";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 0e8a95d..1b94c0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -14,21 +14,36 @@
package com.google.gerrit.server.plugins;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
import com.google.gerrit.lifecycle.LifecycleListener;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
+import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
+import javax.annotation.Nullable;
import javax.inject.Inject;
/**
@@ -44,10 +59,22 @@
private final CopyConfigModule copyConfigModule;
private final List<StartPluginListener> onStart;
private final List<ReloadPluginListener> onReload;
+
private Module sysModule;
private Module sshModule;
private Module httpModule;
+ private Provider<ModuleGenerator> sshGen;
+ private Provider<ModuleGenerator> httpGen;
+
+ private Map<TypeLiteral<?>, DynamicSet<?>> sysSets;
+ private Map<TypeLiteral<?>, DynamicSet<?>> sshSets;
+ private Map<TypeLiteral<?>, DynamicSet<?>> httpSets;
+
+ private Map<TypeLiteral<?>, DynamicMap<?>> sysMaps;
+ private Map<TypeLiteral<?>, DynamicMap<?>> sshMaps;
+ private Map<TypeLiteral<?>, DynamicMap<?>> httpMaps;
+
@Inject
PluginGuiceEnvironment(Injector sysInjector, CopyConfigModule ccm) {
this.sysInjector = sysInjector;
@@ -58,6 +85,21 @@
onReload = new CopyOnWriteArrayList<ReloadPluginListener>();
onReload.addAll(listeners(sysInjector, ReloadPluginListener.class));
+
+ sysSets = dynamicSetsOf(sysInjector);
+ sysMaps = dynamicMapsOf(sysInjector);
+ }
+
+ boolean hasDynamicSet(TypeLiteral<?> type) {
+ return sysSets.containsKey(type)
+ || (sshSets != null && sshSets.containsKey(type))
+ || (httpSets != null && httpSets.containsKey(type));
+ }
+
+ boolean hasDynamicMap(TypeLiteral<?> type) {
+ return sysMaps.containsKey(type)
+ || (sshMaps != null && sshMaps.containsKey(type))
+ || (httpMaps != null && httpMaps.containsKey(type));
}
Module getSysModule() {
@@ -79,6 +121,9 @@
public void setSshInjector(Injector injector) {
sshModule = copy(injector);
+ sshGen = injector.getProvider(ModuleGenerator.class);
+ sshSets = dynamicSetsOf(injector);
+ sshMaps = dynamicMapsOf(injector);
onStart.addAll(listeners(injector, StartPluginListener.class));
onReload.addAll(listeners(injector, ReloadPluginListener.class));
}
@@ -91,8 +136,15 @@
return sshModule;
}
+ ModuleGenerator newSshModuleGenerator() {
+ return sshGen.get();
+ }
+
public void setHttpInjector(Injector injector) {
httpModule = copy(injector);
+ httpGen = injector.getProvider(ModuleGenerator.class);
+ httpSets = dynamicSetsOf(injector);
+ httpMaps = dynamicMapsOf(injector);
onStart.addAll(listeners(injector, StartPluginListener.class));
onReload.addAll(listeners(injector, ReloadPluginListener.class));
}
@@ -105,31 +157,265 @@
return httpModule;
}
+ ModuleGenerator newHttpModuleGenerator() {
+ return httpGen.get();
+ }
+
void onStartPlugin(Plugin plugin) {
for (StartPluginListener l : onStart) {
l.onStartPlugin(plugin);
}
+
+ attachSet(sysSets, plugin.getSysInjector(), plugin);
+ attachSet(sshSets, plugin.getSshInjector(), plugin);
+ attachSet(httpSets, plugin.getHttpInjector(), plugin);
+
+ attachMap(sysMaps, plugin.getSysInjector(), plugin);
+ attachMap(sshMaps, plugin.getSshInjector(), plugin);
+ attachMap(httpMaps, plugin.getHttpInjector(), plugin);
+ }
+
+ private void attachSet(Map<TypeLiteral<?>, DynamicSet<?>> sets,
+ @Nullable Injector src,
+ Plugin plugin) {
+ if (src != null && sets != null && !sets.isEmpty()) {
+ for (Map.Entry<TypeLiteral<?>, DynamicSet<?>> e : sets.entrySet()) {
+ @SuppressWarnings("unchecked")
+ TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+ @SuppressWarnings("unchecked")
+ DynamicSet<Object> set = (DynamicSet<Object>) e.getValue();
+
+ for (Binding<Object> b : bindings(src, type)) {
+ plugin.add(set.add(b.getKey(), b.getProvider().get()));
+ }
+ }
+ }
+ }
+
+ private void attachMap(Map<TypeLiteral<?>, DynamicMap<?>> maps,
+ @Nullable Injector src,
+ Plugin plugin) {
+ if (src != null && maps != null && !maps.isEmpty()) {
+ for (Map.Entry<TypeLiteral<?>, DynamicMap<?>> e : maps.entrySet()) {
+ @SuppressWarnings("unchecked")
+ TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+ @SuppressWarnings("unchecked")
+ PrivateInternals_DynamicMapImpl<Object> set =
+ (PrivateInternals_DynamicMapImpl<Object>) e.getValue();
+
+ for (Binding<Object> b : bindings(src, type)) {
+ plugin.add(set.put(
+ plugin.getName(),
+ b.getKey(),
+ b.getProvider().get()));
+ }
+ }
+ }
}
void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
for (ReloadPluginListener l : onReload) {
l.onReloadPlugin(oldPlugin, newPlugin);
}
+
+ // Index all old registrations by the raw type. These may be replaced
+ // during the reattach calls below. Any that are not replaced will be
+ // removed when the old plugin does its stop routine.
+ ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> old =
+ LinkedListMultimap.create();
+ for (ReloadableRegistrationHandle<?> h : oldPlugin.getReloadableHandles()) {
+ old.put(h.getKey().getTypeLiteral(), h);
+ }
+
+ reattachMap(old, sysMaps, newPlugin.getSysInjector(), newPlugin);
+ reattachMap(old, sshMaps, newPlugin.getSshInjector(), newPlugin);
+ reattachMap(old, httpMaps, newPlugin.getHttpInjector(), newPlugin);
+
+ reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin);
+ reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin);
+ reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin);
}
- private static <T> List<T> listeners(Injector src, Class<T> type) {
- List<Binding<T>> bindings = src.findBindingsByType(TypeLiteral.get(type));
- List<T> found = Lists.newArrayListWithCapacity(bindings.size());
- for (Binding<T> b : bindings) {
- found.add(b.getProvider().get());
+ private void reattachMap(
+ ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles,
+ Map<TypeLiteral<?>, DynamicMap<?>> maps,
+ @Nullable Injector src,
+ Plugin newPlugin) {
+ if (src == null || maps == null || maps.isEmpty()) {
+ return;
+ }
+
+ for (Map.Entry<TypeLiteral<?>, DynamicMap<?>> e : maps.entrySet()) {
+ @SuppressWarnings("unchecked")
+ TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+ @SuppressWarnings("unchecked")
+ PrivateInternals_DynamicMapImpl<Object> map =
+ (PrivateInternals_DynamicMapImpl<Object>) e.getValue();
+
+ Map<Annotation, ReloadableRegistrationHandle<?>> am = Maps.newHashMap();
+ for (ReloadableRegistrationHandle<?> h : oldHandles.get(type)) {
+ Annotation a = h.getKey().getAnnotation();
+ if (a != null && !UNIQUE_ANNOTATION.isInstance(a)) {
+ am.put(a, h);
+ }
+ }
+
+ for (Binding<?> binding : bindings(src, e.getKey())) {
+ @SuppressWarnings("unchecked")
+ Binding<Object> b = (Binding<Object>) binding;
+ Key<Object> key = b.getKey();
+
+ @SuppressWarnings("unchecked")
+ ReloadableRegistrationHandle<Object> h =
+ (ReloadableRegistrationHandle<Object>) am.remove(key.getAnnotation());
+ if (h != null) {
+ replace(newPlugin, h, b);
+ oldHandles.remove(type, h);
+ } else {
+ newPlugin.add(map.put(
+ newPlugin.getName(),
+ b.getKey(),
+ b.getProvider().get()));
+ }
+ }
+ }
+ }
+
+ /** Type used to declare unique annotations. Guice hides this, so extract it. */
+ private static final Class<?> UNIQUE_ANNOTATION =
+ UniqueAnnotations.create().getClass();
+
+ private void reattachSet(
+ ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles,
+ Map<TypeLiteral<?>, DynamicSet<?>> sets,
+ @Nullable Injector src,
+ Plugin newPlugin) {
+ if (src == null || sets == null || sets.isEmpty()) {
+ return;
+ }
+
+ for (Map.Entry<TypeLiteral<?>, DynamicSet<?>> e : sets.entrySet()) {
+ @SuppressWarnings("unchecked")
+ TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+ @SuppressWarnings("unchecked")
+ DynamicSet<Object> set = (DynamicSet<Object>) e.getValue();
+
+ // Index all old handles that match this DynamicSet<T> keyed by
+ // annotations. Ignore the unique annotations, thereby favoring
+ // the @Named annotations or some other non-unique naming.
+ Map<Annotation, ReloadableRegistrationHandle<?>> am = Maps.newHashMap();
+ List<ReloadableRegistrationHandle<?>> old = oldHandles.get(type);
+ Iterator<ReloadableRegistrationHandle<?>> oi = old.iterator();
+ while (oi.hasNext()) {
+ ReloadableRegistrationHandle<?> h = oi.next();
+ Annotation a = h.getKey().getAnnotation();
+ if (a != null && !UNIQUE_ANNOTATION.isInstance(a)) {
+ am.put(a, h);
+ oi.remove();
+ }
+ }
+
+ // Replace old handles with new bindings, favoring cases where there
+ // is an exact match on an @Named annotation. If there is no match
+ // pick any handle and replace it. We generally expect only one
+ // handle of each DynamicSet type when using unique annotations, but
+ // possibly multiple ones if @Named was used. Plugin authors that want
+ // atomic replacement across reloads should use @Named annotations with
+ // stable names that do not change across plugin versions to ensure the
+ // handles are swapped correctly.
+ oi = old.iterator();
+ for (Binding<?> binding : bindings(src, type)) {
+ @SuppressWarnings("unchecked")
+ Binding<Object> b = (Binding<Object>) binding;
+ Key<Object> key = b.getKey();
+
+ @SuppressWarnings("unchecked")
+ ReloadableRegistrationHandle<Object> h1 =
+ (ReloadableRegistrationHandle<Object>) am.remove(key.getAnnotation());
+ if (h1 != null) {
+ replace(newPlugin, h1, b);
+ } else if (oi.hasNext()) {
+ @SuppressWarnings("unchecked")
+ ReloadableRegistrationHandle<Object> h2 =
+ (ReloadableRegistrationHandle<Object>) oi.next();
+ oi.remove();
+ replace(newPlugin, h2, b);
+ } else {
+ newPlugin.add(set.add(b.getKey(), b.getProvider().get()));
+ }
+ }
+ }
+ }
+
+ private static <T> void replace(Plugin newPlugin,
+ ReloadableRegistrationHandle<T> h, Binding<T> b) {
+ RegistrationHandle n = h.replace(b.getKey(), b.getProvider().get());
+ if (n != null){
+ newPlugin.add(n);
+ }
+ }
+
+ static <T> List<T> listeners(Injector src, Class<T> type) {
+ List<Binding<T>> bindings = bindings(src, TypeLiteral.get(type));
+ int cnt = bindings != null ? bindings.size() : 0;
+ List<T> found = Lists.newArrayListWithCapacity(cnt);
+ if (bindings != null) {
+ for (Binding<T> b : bindings) {
+ found.add(b.getProvider().get());
+ }
}
return found;
}
+ private static <T> List<Binding<T>> bindings(Injector src, TypeLiteral<T> type) {
+ return src.findBindingsByType(type);
+ }
+
+ private static Map<TypeLiteral<?>, DynamicSet<?>> dynamicSetsOf(Injector src) {
+ Map<TypeLiteral<?>, DynamicSet<?>> m = Maps.newHashMap();
+ for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+ TypeLiteral<?> type = e.getKey().getTypeLiteral();
+ if (type.getRawType() == DynamicSet.class) {
+ ParameterizedType p = (ParameterizedType) type.getType();
+ m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
+ (DynamicSet<?>) e.getValue().getProvider().get());
+ }
+ }
+ return m;
+ }
+
+ private static Map<TypeLiteral<?>, DynamicMap<?>> dynamicMapsOf(Injector src) {
+ Map<TypeLiteral<?>, DynamicMap<?>> m = Maps.newHashMap();
+ for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+ TypeLiteral<?> type = e.getKey().getTypeLiteral();
+ if (type.getRawType() == DynamicMap.class) {
+ ParameterizedType p = (ParameterizedType) type.getType();
+ m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
+ (DynamicMap<?>) e.getValue().getProvider().get());
+ }
+ }
+ return m;
+ }
+
private static Module copy(Injector src) {
+ Set<TypeLiteral<?>> dynamicTypes = Sets.newHashSet();
+ for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+ TypeLiteral<?> type = e.getKey().getTypeLiteral();
+ if (type.getRawType() == DynamicSet.class
+ || type.getRawType() == DynamicMap.class) {
+ ParameterizedType t = (ParameterizedType) type.getType();
+ dynamicTypes.add(TypeLiteral.get(t.getActualTypeArguments()[0]));
+ }
+ }
+
final Map<Key<?>, Binding<?>> bindings = Maps.newLinkedHashMap();
for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
- if (shouldCopy(e.getKey())) {
+ if (!dynamicTypes.contains(e.getKey().getTypeLiteral())
+ && shouldCopy(e.getKey())) {
bindings.put(e.getKey(), e.getValue());
}
}
@@ -202,22 +488,22 @@
return true;
}
- private static boolean is(String name, Class<?> type) {
- Class<?> p = type;
- while (p != null) {
- if (name.equals(p.getName())) {
+ static boolean is(String name, Class<?> type) {
+ while (type != null) {
+ if (name.equals(type.getName())) {
return true;
}
- p = p.getSuperclass();
- }
- Class<?>[] interfaces = type.getInterfaces();
- if (interfaces != null) {
- for (Class<?> i : interfaces) {
- if (is(name, i)) {
- return true;
+ Class<?>[] interfaces = type.getInterfaces();
+ if (interfaces != null) {
+ for (Class<?> i : interfaces) {
+ if (is(name, i)) {
+ return true;
+ }
}
}
+
+ type = type.getSuperclass();
}
return false;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 330dc46..16cd78c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -337,6 +337,7 @@
return new Plugin(name,
srcJar, snapshot,
jarFile, manifest,
+ pluginLoader,
sysModule, sshModule, httpModule);
} finally {
if (!keep) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
index 3560d99..d70d32f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
@@ -15,7 +15,7 @@
package com.google.gerrit.sshd;
import com.google.common.collect.Maps;
-import com.google.gerrit.server.plugins.RegistrationHandle;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.inject.Binding;
import com.google.inject.Inject;
import com.google.inject.Injector;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
new file mode 100644
index 0000000..b843893
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+
+import org.apache.sshd.server.Command;
+
+import java.util.Map;
+
+class SshAutoRegisterModuleGenerator
+ extends AbstractModule
+ implements ModuleGenerator {
+ private final Map<String, Class<Command>> commands = Maps.newHashMap();
+ private CommandName command;
+
+ @Override
+ protected void configure() {
+ bind(Commands.key(command))
+ .toProvider(new DispatchCommandProvider(command));
+ for (Map.Entry<String, Class<Command>> e : commands.entrySet()) {
+ bind(Commands.key(command, e.getKey())).to(e.getValue());
+ }
+ }
+
+ public void setPluginName(String name) {
+ command = Commands.named(name);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void export(Export export, Class<?> type)
+ throws InvalidPluginException {
+ Preconditions.checkState(command != null, "pluginName must be provided");
+ if (Command.class.isAssignableFrom(type)) {
+ Class<Command> old = commands.get(export.value());
+ if (old != null) {
+ throw new InvalidPluginException(String.format(
+ "@Export(\"%s\") has duplicate bindings:\n %s\n %s",
+ export.value(), old.getName(), type.getName()));
+ }
+ commands.put(export.value(), (Class<Command>) type);
+ } else {
+ throw new InvalidPluginException(String.format(
+ "Class %s with @Export(\"%s\") must extend %s or implement %s",
+ type.getName(), export.value(),
+ SshCommand.class.getName(), Command.class.getName()));
+ }
+ }
+
+ @Override
+ public Module create() throws InvalidPluginException {
+ Preconditions.checkState(command != null, "pluginName must be provided");
+ return this;
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index cd78796..bc094f9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -31,6 +31,7 @@
import com.google.gerrit.server.config.GerritRequestModule;
import com.google.gerrit.server.git.QueueProvider;
import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.plugins.ModuleGenerator;
import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.plugins.StartPluginListener;
import com.google.gerrit.server.project.ProjectControl;
@@ -94,6 +95,7 @@
install(new LifecycleModule() {
@Override
protected void configure() {
+ bind(ModuleGenerator.class).to(SshAutoRegisterModuleGenerator.class);
bind(SshPluginStarterCallback.class);
bind(StartPluginListener.class)
.annotatedWith(UniqueAnnotations.create())
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
index d9015c6..28d267c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
@@ -15,7 +15,7 @@
package com.google.gerrit.sshd.commands;
import com.google.common.base.Preconditions;
-import com.google.gerrit.server.plugins.PluginName;
+import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.sshd.CommandName;
import com.google.gerrit.sshd.Commands;
import com.google.gerrit.sshd.DispatchCommandProvider;
diff --git a/pom.xml b/pom.xml
index 1282619..f366c4d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -87,6 +87,7 @@
<module>gerrit-gwtdebug</module>
<module>gerrit-war</module>
+ <module>gerrit-extension-api</module>
<module>gerrit-plugin-api</module>
<module>gerrit-gwtui</module>
diff --git a/tools/deploy_api.sh b/tools/deploy_api.sh
new file mode 100755
index 0000000..eda841f
--- /dev/null
+++ b/tools/deploy_api.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+
+SRC=$(ls gerrit-plugin-api/target/gerrit-plugin-api-*-sources.jar)
+VER=${SRC#gerrit-plugin-api/target/gerrit-plugin-api-}
+VER=${VER%-sources.jar}
+
+type=release
+case $VER in
+*-SNAPSHOT)
+ echo >&2 "fatal: Cannot deploy $VER"
+ echo >&2 " Use ./tools/version.sh --release && mvn clean package"
+ exit 1
+ ;;
+*-[0-9]*-g*) type=snapshot ;;
+esac
+URL=s3://gerrit-api@commondatastorage.googleapis.com/$type
+
+echo "Deploying API $VER to $URL"
+for module in gerrit-extension-api gerrit-plugin-api
+do
+ mvn deploy:deploy-file \
+ -DgroupId=com.google.gerrit \
+ -DartifactId=$module \
+ -Dversion=$VER \
+ -Dpackaging=jar \
+ -Dfile=$module/target/$module-$VER.jar \
+ -DrepositoryId=gerrit-api-repository \
+ -Durl=$URL
+
+ mvn deploy:deploy-file \
+ -DgroupId=com.google.gerrit \
+ -DartifactId=$module \
+ -Dversion=$VER \
+ -Dpackaging=java-source \
+ -Dfile=$module/target/$module-$VER-sources.jar \
+ -Djava-source=false \
+ -DrepositoryId=gerrit-api-repository \
+ -Durl=$URL
+done
diff --git a/tools/deploy_plugin_api.sh b/tools/deploy_plugin_api.sh
deleted file mode 100755
index fe19177..0000000
--- a/tools/deploy_plugin_api.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/sh
-
-SRC=$(ls gerrit-plugin-api/target/gerrit-plugin-api-*-sources.jar)
-VER=${SRC#gerrit-plugin-api/target/gerrit-plugin-api-}
-VER=${VER%-sources.jar}
-JAR=gerrit-plugin-api/target/gerrit-plugin-api-$VER.jar
-
-type=release
-case $VER in
-*-SNAPSHOT)
- echo >&2 "fatal: Cannot deploy $VER"
- echo >&2 " Use ./tools/version.sh --release && mvn clean package"
- exit 1
- ;;
-*-[0-9]*-g*) type=snapshot ;;
-esac
-URL=s3://gerrit-api@commondatastorage.googleapis.com/$type
-
-echo "Deploying gerrit-plugin-api $VER to $URL"
-mvn deploy:deploy-file \
- -DgroupId=com.google.gerrit \
- -DartifactId=gerrit-plugin-api \
- -Dversion=$VER \
- -Dpackaging=jar \
- -Dfile=$JAR \
- -DrepositoryId=gerrit-api-repository \
- -Durl=$URL
-
-mvn deploy:deploy-file \
- -DgroupId=com.google.gerrit \
- -DartifactId=gerrit-plugin-api \
- -Dversion=$VER \
- -Dpackaging=java-source \
- -Dfile=$SRC \
- -Djava-source=false \
- -DrepositoryId=gerrit-api-repository \
- -Durl=$URL