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(&quot;foo&quot;))
+   *   .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(&quot;foo&quot;))
+   *   .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