v2.5

git-svn-id: svn://svn.code.sf.net/p/mockftpserver/code@274 531de8e6-9941-0410-b38b-9a92acbe0330
diff --git a/tags/2.5/.classpath b/tags/2.5/.classpath
new file mode 100644
index 0000000..61fc388
--- /dev/null
+++ b/tags/2.5/.classpath
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<classpath>

+	<classpathentry kind="src" path="src/main/java"/>

+	<classpathentry excluding="**/*.java" kind="src" path="src/main/resources"/>

+	<classpathentry kind="src" path="src/test/java"/>

+	<classpathentry kind="src" path="src/test/groovy"/>

+	<classpathentry excluding="**/*.java" kind="src" path="src/test/resources"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>

+	<classpathentry kind="var" path="M2_REPO/junit-addons/junit-addons/1.4/junit-addons-1.4.jar"/>

+	<classpathentry kind="var" path="M2_REPO/log4j/log4j/1.2.13/log4j-1.2.13.jar"/>

+	<classpathentry kind="var" path="M2_REPO/commons-net/commons-net/1.4.1/commons-net-1.4.1.jar"/>

+	<classpathentry kind="var" path="M2_REPO/junit/junit/3.8.1/junit-3.8.1.jar"/>

+	<classpathentry kind="var" path="M2_REPO/commons-logging/commons-logging/1.1/commons-logging-1.1.jar"/>

+	<classpathentry kind="var" path="M2_REPO/oro/oro/2.0.8/oro-2.0.8.jar"/>

+	<classpathentry kind="var" path="M2_REPO/org/springframework/spring/2.0.7/spring-2.0.7.jar"/>

+	<classpathentry kind="var" path="M2_REPO/easymock/easymock/1.2_Java1.3/easymock-1.2_Java1.3.jar"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3.8.1"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Groovy 1.7.10"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: org.slf4j:slf4j-api:1.6.6"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: org.codehaus.groovy:groovy-all:1.7.10"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: commons-net:commons-net:1.4.1"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: oro:oro:2.0.8"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: easymock:easymock:1.2_Java1.3"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: junit-addons:junit-addons:1.4"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: junit:junit:3.8.1"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: xerces:xercesImpl:2.6.2"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: xerces:xmlParserAPIs:2.6.2"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: org.springframework:spring:2.0.7"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: commons-logging:commons-logging:1.1"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: log4j:log4j:1.2.17"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: logkit:logkit:1.0.1"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: avalon-framework:avalon-framework:4.1.3"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: javax.servlet:servlet-api:2.3"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Maven: org.slf4j:slf4j-log4j12:1.6.6"/>

+	<classpathentry kind="output" path="target/classes"/>

+</classpath>

diff --git a/tags/2.5/.project b/tags/2.5/.project
new file mode 100644
index 0000000..59ef6fc
--- /dev/null
+++ b/tags/2.5/.project
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<projectDescription>

+	<name>MockFtpServer</name>

+	<comment></comment>

+	<projects>

+	</projects>

+	<buildSpec>

+		<buildCommand>

+			<name>org.eclipse.jdt.core.javabuilder</name>

+			<arguments>

+			</arguments>

+		</buildCommand>

+		<buildCommand>

+			<name>org.codehaus.groovy.eclipse.groovyBuilder</name>

+			<arguments>

+			</arguments>

+		</buildCommand>

+	</buildSpec>

+	<natures>

+		<nature>org.eclipse.jdt.core.javanature</nature>

+		<nature>org.codehaus.groovy.eclipse.groovyNature</nature>

+	</natures>

+</projectDescription>

diff --git a/tags/2.5/.settings/org.codehaus.groovy.eclipse.preferences.prefs b/tags/2.5/.settings/org.codehaus.groovy.eclipse.preferences.prefs
new file mode 100644
index 0000000..c422985
--- /dev/null
+++ b/tags/2.5/.settings/org.codehaus.groovy.eclipse.preferences.prefs
@@ -0,0 +1,3 @@
+#Tue Mar 18 19:49:13 EDT 2008

+eclipse.preferences.version=1

+groovy.compiler.output.path=bin-groovy

diff --git a/tags/2.5/.settings/org.eclipse.jdt.core.prefs b/tags/2.5/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..900b691
--- /dev/null
+++ b/tags/2.5/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,268 @@
+#Thu Jan 31 20:14:07 EST 2008

+eclipse.preferences.version=1

+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled

+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.2

+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve

+org.eclipse.jdt.core.compiler.compliance=1.4

+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=warning

+org.eclipse.jdt.core.compiler.problem.enumIdentifier=warning

+org.eclipse.jdt.core.compiler.source=1.3

+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=0

+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=80

+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0

+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=1

+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1

+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=1

+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=false

+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_comments=true

+org.eclipse.jdt.core.formatter.comment.format_header=false

+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=100

+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=false

+org.eclipse.jdt.core.formatter.indentation.size=4

+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation=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=do not insert

+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=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=insert

+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=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=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=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=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=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=false

+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false

+org.eclipse.jdt.core.formatter.lineSplit=120

+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=1

+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true

+org.eclipse.jdt.core.formatter.tabulation.char=space

+org.eclipse.jdt.core.formatter.tabulation.size=4

+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/tags/2.5/.settings/org.eclipse.jdt.ui.prefs b/tags/2.5/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..538a82b
--- /dev/null
+++ b/tags/2.5/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,5 @@
+#Thu Jan 31 20:14:06 EST 2008

+eclipse.preferences.version=1

+formatter_profile=_Chris Profile

+formatter_settings_version=11

+internal.default.compliance=default

diff --git a/tags/2.5/CHANGELOG.txt b/tags/2.5/CHANGELOG.txt
new file mode 100644
index 0000000..b9bc465
--- /dev/null
+++ b/tags/2.5/CHANGELOG.txt
@@ -0,0 +1,171 @@
+MockFtpServer Change Log

+-------------------------------------------------------------------------------

+

+Changes in version 2.5 (May 2014)

+------------------------------------------

+- Fix #23 PWD response should have commentary: "{0}" is current directory.

+  Also adjusted reply text for MKD to adhere to RFC959: "{0}" created.

+- Removed deprecation of assertSessionReply(int,Object)

+- Fix broken internal links on the web site pages.

+- Update “Log4J Configuration Required to See Log Output” section on “FakeFtpServer – Getting Started” with info for SLF4J.

+- Added MockFtpServer logo image. Thanks to cooltext.com.

+

+

+Changes in version 2.4 (15 Jul 2012)

+------------------------------------------

+- FEATURE #2466395: Remove log4j dependency. Switch to using SLF4J (http://www.slf4j.org/).

+- FEATURE #3544349: Return MockFtpServer information as part of connect 220 response.

+- Upgrade to Groovy 1.7.10; fix Maven site plugin incompatibility.

+- Change “pom.xml” to use SFTP to deploy to Maven repo.

+

+

+Changes in version 2.3 (05 Jun 2011)

+------------------------------------------

+- FEATURE #2996739: Use a dynamically chosen free port number ("ephemeral")for the server control port

+  if you specify 0 for the serverControlPort property of FakeFtpServer or StubFtpServer. Then call

+  getServerControlPort() AFTER start() has been called to determine the actual port number being used.

+  This is useful if you are running on a system where the default port (21) is already in use or cannot

+  be bound from a user process (such as Unix).

+- FEATURE #3304849: Add a new readData(int numBytes) to Session

+- BUG #3103132: shutting down takes too long.

+ 

+

+Changes in version 2.2 (23 Mar 2010)

+------------------------------------------

+- FakeFtpServer: Support renaming of directories.

+    * BUG FIX: #2823519 "The RnfrCommandHandler is currently set to only support renaming of files": https://sourceforge.net/tracker/?func=detail&atid=1006533&aid=2823519&group_id=208647.

+    * Change fake RNTO and RNFR CommandHandlers to allow renaming directories.

+- BUG FIX: #2828362: "Unit tests using FakeFtpServer are slow" https://sourceforge.net/tracker/?func=detail&atid=1006533&aid=2828362&group_id=208647. DefaultSession.readCommand().

+    * Reduce default socket read interval time to 20ms.

+- BUG FIX: #2953392: "AbstractFtpServer waits endless if binding to port fails" https://sourceforge.net/tracker/?func=detail&atid=1006533&aid=2953392&group_id=208647.

+- FakeFtpServer (AbstractFakeFileSystem): Change rename() to fail if the TO file already exists.

+- Add sample directory listing(s) to online docs for StubFtpServer ListCommandHandler. Update online docs/javadoc describing that multiple directory entries in a file listing can be simulated.

+- PatternUtil: Support plus sign ('+') within wildcard strings. See convertStringWithWildcardsToRegex().

+- TESTS: Rename AbstractTest to AbstractTestCase and AbstractGroovyTest to AbstractGroovyTestCase.

+

+

+Changes in version 2.1 (16 Jun 2009)

+------------------------------------------

+- Added support for IPv6 (EPRT and EPSV commands) to FakeFtpServer and StubFtpServer. Thanks to Fernando Martinez for testing.

+- BUG FIX: #2696898: �WindowsFakeFilesystem DirectoryEntry case sensitivity� (https://sourceforge.net/tracker/?func=detail&atid=1006533&aid=2696898&group_id=208647).

+- BUG FIX: #2797980: �UnixFakeFileSystem IsValidName Regex incorrect� (https://sourceforge.net/tracker/?func=detail&atid=1006533&aid=2797980&group_id=208647).

+- Add getServerControlPort() to AbstractFtpServer.

+- Create HostAndPort class. Refactor both PortCommandHandler(s) and the PortParser classes to use HostAndPort.

+- TESTS: Convert PortParserTest to Groovy.

+

+

+Changes in version 2.0.2 (09 Mar 2009)

+------------------------------------------

+- BUG FIX: #2654577: 'month' in UnixDirectoryListingFormatter is Locale specific. http://sourceforge.net/tracker/index.php?func=detail&aid=2654577&group_id=208647&atid=1006533.

+- BUG FIX: #2653626: Cannot start() server after calling stop(). https://sourceforge.net/tracker2/index.php?func=detail&aid=2653626&group_id=208647&atid=1006533.

+

+

+Changes in version 2.0.1 (09 Feb 2009)

+------------------------------------------

+- BUG FIX: #2543193 �"cd .." and "pwd" don't work properly together� (https://sourceforge.net/tracker2/?func=detail&aid=2543193&group_id=208647&atid=1006533).

+- BUG FIX: #2540548 �Missing new line on directory listing� (https://sourceforge.net/tracker2/?func=detail&aid=2540548&group_id=208647&atid=1006533).

+- BUG FIX: #2540366 �FileEntry.setContents( byte [] contents ) change the content� (https://sourceforge.net/tracker2/?func=detail&aid=2540366&group_id=208647&atid=1006533).

+ - AbstractFtpServer: Use entrySet() to iterate through sessions (From Rijk van Haaften).

+

+

+Changes in version 2.0 (03 Jan 2009)

+------------------------------------------

+- BUG FIX: #2462794 filesystem.pathDoesNotExist key is missing from the ReplyText resource bundle. See https://sourceforge.net/tracker2/?func=detail&aid=2462794&group_id=208647&atid=1006533

+- BUG FIX: #2462973 FileEntry.cloneWithNewPath doesn't clone out field. See https://sourceforge.net/tracker/index.php?func=detail&aid=2462973&group_id=208647&atid=1006533

+- Add note to online doc about requiring Log4J configuration file if you want to see log output.

+

+

+Changes in version 2.0-rc3 (14 Dec 2008)

+------------------------------------------

+- BUG FIX: ClassCastException in AbstractFtpServer during server cleanup.

+- Reorganize sample code and include in online doc.

+

+

+Changes in version 2.0-rc2 (12 Dec 2008)

+------------------------------------------

+- BUG FIX: AbstractFtpServer: Fix bug when iterating through sessions.

+- [BREAKING CHANGE] Move ConnectCommandHandler into core package.

+- [BREAKING CHANGE] Unify Fake and Stub CommandHandlers. Change ServerConfiguration to remove getReplyTextBundle(); make AbstractFakeCommandHandler implement ReplyTextBundleAware instead. Change FakeFtpServer to check for ReplyTextBundleAware and set replyTextBundle. Pull common from stub/fake into AbstractCommandHandler.

+- [BREAKING CHANGE] Rename AbstractCommandHandler to AbstractTrackingCommandHandler.

+- Create AbstractStaticReplyCommandHandler, and make both AbstractStubCommandHandler and StaticReplyCommandHandler subclasses.

+- Create new UnrecognizedCommandHandler, and use to return 502 reply from FakeFtpServer and StubFtpServer when a requested command is not supported.

+- Add support for STAT command; Add systemStatus property to FakeFtpServer.

+- Add support for SMNT command to FakeFtpServer;

+- AbstractFtpServer: Add createSession() method. Make some attributes protected.

+- StubFtpServer: Introduce AbstractStorCommandHandler. Remove final from stub CommandHandler classes.

+- Cleanup code and javadoc

+- DOCS:	Add �Requirements� section to main (index) page. Also �Maven� section.

+- DOCS: Add "Configuring CommandHandler for New (Unsupported) Command" and �Creating Your Own Custom CommandHandler Class� sections to StubFtpServer Getting Started Guide.

+- DOCS: Add "Configuring Custom CommandHandlers" section to Getting Started Guide (FakeFtpServer).

+- TESTS: Move AbstractCommandHandlerTest into core package.

+- TESTS: Create sample test of FakeFtpServer with StaticReplyCommandHandler command handler(s).

+- Create source jar during package and include within assemblies.

+- Change "assembly.xml" to include "fakeftpserver*.xml" files.

+

+

+Changes in version 2.0-rc1 (23 Nov 2008)

+------------------------------------------

+NEW FakeFtpServer.

+  This is an alternative "mock" FTP server implementation. FakeFtpServer provides a higher-level abstraction

+  than StubFtpServer. You define a virtual file system, including directories and files, as well as a set of

+  valid user accounts and credentials. The FakeFtpServer then responds with appropriate replies and reply

+  codes based on that configuration. See online documentation for more information.

+StubFtpServer

+- StubFtpServer: Refactored to inherit from common AbstractFtpServer superclass.

+- Change default org.mockftpserver.stub.command.CdupCommandHandler CDUP reply code from 250 to 200.

+- Rename ReplyCodes.SEND_DATA_INITIAL_OK and SEND_DATA_FINAL_OK to TRANSFER_DATA_.. indicate bi-directionality.

+- Rename Command.getRequiredString(int) to getRequiredParameter(int).

+- Change StubFtpServer CommandHandlers to reply with 501 if required command parameters are missing. Changed AbstractCommandHandler and AbstractCommandHandlerTest.

+- Refactor (Stub)PortCommandHandler - pull out common logic into PortParser util class.

+

+

+Changes in version 1.2.4 (01 Sep 2008)

+------------------------------------------

+- BUG FIX: StubFtpServer: Only execute serverSocket.close() if serverSocket != null.

+- BUG FIX: Terminate replies with <CRLF> (\r\n).

+- DOCS: Fix Getting Started Guide code example: setOverrideFinalReplyCode() to setFinalReplyCode().

+- DOCS: Add note to Getting Started Guide about calling setServerControlPort() if on Unix system.

+

+

+Changes in version 1.2.3 (13 Aug 2008)

+------------------------------------------

+- BUG FIX: Tracker item #2047355. Parse host IP numbers as unsigned bytes.

+  See https://sourceforge.net/tracker/index.php?func=detail&aid=2047355&group_id=208647&atid=1006533

+

+

+Changes in version 1.2.2 (27 May 2008)

+------------------------------------------

+- BUG FIX: Move serverThread.start() into synchronized block to avoid server hang if

+  server thread runs faster than main thread.

+  See https://sourceforge.net/tracker/?func=detail&atid=1006533&aid=1925590&group_id=208647

+

+

+Changes in version 1.2.1 (10 Mar 2008)

+------------------------------------------

+- Change Maven POM (pom.xml) to enable sync-ing with central Maven repository (ibiblio).

+

+Changes in version 1.2 (29 Feb 2008)

+------------------------------------------

+- BUG FIX: StubFtpServer: Add wait/notify to ensure that the server starts up and opens the server 

+  control port before the start() method returns. This fixes a potential race condition, which

+  shows up on some Linux systems. (Thanks to Aasman Bajaj for identifying the problem and providing the fix)

+- Modify tests to make server port configurable (through "ftp.server.port" system property), allowing 

+  tests to run on non-Windows systems. 

+

+

+Changes in version 1.1 (20 Feb 2008)

+------------------------------------------

+- StubFtpServer: Allow configuring server control connection port other than the default (21).

+- AbstractTest: Add some test convenience methods.

+

+

+Changes in version 1.0 final (11 Dec 2007)

+------------------------------------------

+- Implement default CommandHandlers for NLST, REIN, SMNT, SITE, ABOR and ALLO commands.

+- Handle command names in any case.

+- CwdCommandHandler: Fix PATHNAME_KEY constant value; change to "pathname".

+

+

+Changes in version 1.0-RC1 (1 Nov 2007)

+---------------------------------------

+Initial release.
\ No newline at end of file
diff --git a/tags/2.5/LICENSE.txt b/tags/2.5/LICENSE.txt
new file mode 100644
index 0000000..29f81d8
--- /dev/null
+++ b/tags/2.5/LICENSE.txt
@@ -0,0 +1,201 @@
+                                 Apache License

+                           Version 2.0, January 2004

+                        http://www.apache.org/licenses/

+

+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

+

+   1. Definitions.

+

+      "License" shall mean the terms and conditions for use, reproduction,

+      and distribution as defined by Sections 1 through 9 of this document.

+

+      "Licensor" shall mean the copyright owner or entity authorized by

+      the copyright owner that is granting the License.

+

+      "Legal Entity" shall mean the union of the acting entity and all

+      other entities that control, are controlled by, or are under common

+      control with that entity. For the purposes of this definition,

+      "control" means (i) the power, direct or indirect, to cause the

+      direction or management of such entity, whether by contract or

+      otherwise, or (ii) ownership of fifty percent (50%) or more of the

+      outstanding shares, or (iii) beneficial ownership of such entity.

+

+      "You" (or "Your") shall mean an individual or Legal Entity

+      exercising permissions granted by this License.

+

+      "Source" form shall mean the preferred form for making modifications,

+      including but not limited to software source code, documentation

+      source, and configuration files.

+

+      "Object" form shall mean any form resulting from mechanical

+      transformation or translation of a Source form, including but

+      not limited to compiled object code, generated documentation,

+      and conversions to other media types.

+

+      "Work" shall mean the work of authorship, whether in Source or

+      Object form, made available under the License, as indicated by a

+      copyright notice that is included in or attached to the work

+      (an example is provided in the Appendix below).

+

+      "Derivative Works" shall mean any work, whether in Source or Object

+      form, that is based on (or derived from) the Work and for which the

+      editorial revisions, annotations, elaborations, or other modifications

+      represent, as a whole, an original work of authorship. For the purposes

+      of this License, Derivative Works shall not include works that remain

+      separable from, or merely link (or bind by name) to the interfaces of,

+      the Work and Derivative Works thereof.

+

+      "Contribution" shall mean any work of authorship, including

+      the original version of the Work and any modifications or additions

+      to that Work or Derivative Works thereof, that is intentionally

+      submitted to Licensor for inclusion in the Work by the copyright owner

+      or by an individual or Legal Entity authorized to submit on behalf of

+      the copyright owner. For the purposes of this definition, "submitted"

+      means any form of electronic, verbal, or written communication sent

+      to the Licensor or its representatives, including but not limited to

+      communication on electronic mailing lists, source code control systems,

+      and issue tracking systems that are managed by, or on behalf of, the

+      Licensor for the purpose of discussing and improving the Work, but

+      excluding communication that is conspicuously marked or otherwise

+      designated in writing by the copyright owner as "Not a Contribution."

+

+      "Contributor" shall mean Licensor and any individual or Legal Entity

+      on behalf of whom a Contribution has been received by Licensor and

+      subsequently incorporated within the Work.

+

+   2. Grant of Copyright License. Subject to the terms and conditions of

+      this License, each Contributor hereby grants to You a perpetual,

+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable

+      copyright license to reproduce, prepare Derivative Works of,

+      publicly display, publicly perform, sublicense, and distribute the

+      Work and such Derivative Works in Source or Object form.

+

+   3. Grant of Patent License. Subject to the terms and conditions of

+      this License, each Contributor hereby grants to You a perpetual,

+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable

+      (except as stated in this section) patent license to make, have made,

+      use, offer to sell, sell, import, and otherwise transfer the Work,

+      where such license applies only to those patent claims licensable

+      by such Contributor that are necessarily infringed by their

+      Contribution(s) alone or by combination of their Contribution(s)

+      with the Work to which such Contribution(s) was submitted. If You

+      institute patent litigation against any entity (including a

+      cross-claim or counterclaim in a lawsuit) alleging that the Work

+      or a Contribution incorporated within the Work constitutes direct

+      or contributory patent infringement, then any patent licenses

+      granted to You under this License for that Work shall terminate

+      as of the date such litigation is filed.

+

+   4. Redistribution. You may reproduce and distribute copies of the

+      Work or Derivative Works thereof in any medium, with or without

+      modifications, and in Source or Object form, provided that You

+      meet the following conditions:

+

+      (a) You must give any other recipients of the Work or

+          Derivative Works a copy of this License; and

+

+      (b) You must cause any modified files to carry prominent notices

+          stating that You changed the files; and

+

+      (c) You must retain, in the Source form of any Derivative Works

+          that You distribute, all copyright, patent, trademark, and

+          attribution notices from the Source form of the Work,

+          excluding those notices that do not pertain to any part of

+          the Derivative Works; and

+

+      (d) If the Work includes a "NOTICE" text file as part of its

+          distribution, then any Derivative Works that You distribute must

+          include a readable copy of the attribution notices contained

+          within such NOTICE file, excluding those notices that do not

+          pertain to any part of the Derivative Works, in at least one

+          of the following places: within a NOTICE text file distributed

+          as part of the Derivative Works; within the Source form or

+          documentation, if provided along with the Derivative Works; or,

+          within a display generated by the Derivative Works, if and

+          wherever such third-party notices normally appear. The contents

+          of the NOTICE file are for informational purposes only and

+          do not modify the License. You may add Your own attribution

+          notices within Derivative Works that You distribute, alongside

+          or as an addendum to the NOTICE text from the Work, provided

+          that such additional attribution notices cannot be construed

+          as modifying the License.

+

+      You may add Your own copyright statement to Your modifications and

+      may provide additional or different license terms and conditions

+      for use, reproduction, or distribution of Your modifications, or

+      for any such Derivative Works as a whole, provided Your use,

+      reproduction, and distribution of the Work otherwise complies with

+      the conditions stated in this License.

+

+   5. Submission of Contributions. Unless You explicitly state otherwise,

+      any Contribution intentionally submitted for inclusion in the Work

+      by You to the Licensor shall be under the terms and conditions of

+      this License, without any additional terms or conditions.

+      Notwithstanding the above, nothing herein shall supersede or modify

+      the terms of any separate license agreement you may have executed

+      with Licensor regarding such Contributions.

+

+   6. Trademarks. This License does not grant permission to use the trade

+      names, trademarks, service marks, or product names of the Licensor,

+      except as required for reasonable and customary use in describing the

+      origin of the Work and reproducing the content of the NOTICE file.

+

+   7. Disclaimer of Warranty. Unless required by applicable law or

+      agreed to in writing, Licensor provides the Work (and each

+      Contributor provides its Contributions) on an "AS IS" BASIS,

+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or

+      implied, including, without limitation, any warranties or conditions

+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A

+      PARTICULAR PURPOSE. You are solely responsible for determining the

+      appropriateness of using or redistributing the Work and assume any

+      risks associated with Your exercise of permissions under this License.

+

+   8. Limitation of Liability. In no event and under no legal theory,

+      whether in tort (including negligence), contract, or otherwise,

+      unless required by applicable law (such as deliberate and grossly

+      negligent acts) or agreed to in writing, shall any Contributor be

+      liable to You for damages, including any direct, indirect, special,

+      incidental, or consequential damages of any character arising as a

+      result of this License or out of the use or inability to use the

+      Work (including but not limited to damages for loss of goodwill,

+      work stoppage, computer failure or malfunction, or any and all

+      other commercial damages or losses), even if such Contributor

+      has been advised of the possibility of such damages.

+

+   9. Accepting Warranty or Additional Liability. While redistributing

+      the Work or Derivative Works thereof, You may choose to offer,

+      and charge a fee for, acceptance of support, warranty, indemnity,

+      or other liability obligations and/or rights consistent with this

+      License. However, in accepting such obligations, You may act only

+      on Your own behalf and on Your sole responsibility, not on behalf

+      of any other Contributor, and only if You agree to indemnify,

+      defend, and hold each Contributor harmless for any liability

+      incurred by, or claims asserted against, such Contributor by reason

+      of your accepting any such warranty or additional liability.

+

+   END OF TERMS AND CONDITIONS

+

+   APPENDIX: How to apply the Apache License to your work.

+

+      To apply the Apache License to your work, attach the following

+      boilerplate notice, with the fields enclosed by brackets "[]"

+      replaced with your own identifying information. (Don't include

+      the brackets!)  The text should be enclosed in the appropriate

+      comment syntax for the file format. We also recommend that a

+      file or class name and description of purpose be included on the

+      same "printed page" as the copyright notice for easier

+      identification within third-party archives.

+

+   Copyright [yyyy] [name of copyright owner]

+

+   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.

diff --git a/tags/2.5/MockFtpServer.eml b/tags/2.5/MockFtpServer.eml
new file mode 100644
index 0000000..762ff2c
--- /dev/null
+++ b/tags/2.5/MockFtpServer.eml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<component LANGUAGE_LEVEL="JDK_1_4" inheritJdk="true">

+	<output-test url="file://$MODULE_DIR$/target/test-classes"/>

+	<contentEntry url="file://$MODULE_DIR$">

+		<testFolder url="file://$MODULE_DIR$/src/test/java"/>

+		<testFolder url="file://$MODULE_DIR$/src/test/groovy"/>

+		<testFolder url="file://$MODULE_DIR$/src/test/resources"/>

+		<excludeFolder url="file://$MODULE_DIR$/target"/>

+	</contentEntry>

+	<lib name="Maven: org.codehaus.groovy:groovy-all:1.7.10" scope="TEST"/>

+	<lib name="Maven: commons-net:commons-net:1.4.1" scope="TEST"/>

+	<lib name="Maven: oro:oro:2.0.8" scope="TEST"/>

+	<lib name="Maven: easymock:easymock:1.2_Java1.3" scope="TEST"/>

+	<lib name="Maven: junit-addons:junit-addons:1.4" scope="TEST"/>

+	<lib name="Maven: junit:junit:3.8.1" scope="TEST"/>

+	<lib name="Maven: xerces:xercesImpl:2.6.2" scope="TEST"/>

+	<lib name="Maven: xerces:xmlParserAPIs:2.6.2" scope="TEST"/>

+	<lib name="Maven: org.springframework:spring:2.0.7" scope="TEST"/>

+	<lib name="Maven: commons-logging:commons-logging:1.1" scope="TEST"/>

+	<lib name="Maven: log4j:log4j:1.2.17" scope="TEST"/>

+	<lib name="Maven: logkit:logkit:1.0.1" scope="TEST"/>

+	<lib name="Maven: avalon-framework:avalon-framework:4.1.3" scope="TEST"/>

+	<lib name="Maven: javax.servlet:servlet-api:2.3" scope="TEST"/>

+	<lib name="Maven: org.slf4j:slf4j-log4j12:1.6.6" scope="TEST"/>

+	<levels>

+		<level name="Groovy 1.7.10" value="project"/>

+		<level name="Maven: org.slf4j:slf4j-api:1.6.6" value="project"/>

+		<level name="Maven: org.codehaus.groovy:groovy-all:1.7.10" value="project"/>

+		<level name="Maven: commons-net:commons-net:1.4.1" value="project"/>

+		<level name="Maven: oro:oro:2.0.8" value="project"/>

+		<level name="Maven: easymock:easymock:1.2_Java1.3" value="project"/>

+		<level name="Maven: junit-addons:junit-addons:1.4" value="project"/>

+		<level name="Maven: junit:junit:3.8.1" value="project"/>

+		<level name="Maven: xerces:xercesImpl:2.6.2" value="project"/>

+		<level name="Maven: xerces:xmlParserAPIs:2.6.2" value="project"/>

+		<level name="Maven: org.springframework:spring:2.0.7" value="project"/>

+		<level name="Maven: commons-logging:commons-logging:1.1" value="project"/>

+		<level name="Maven: log4j:log4j:1.2.17" value="project"/>

+		<level name="Maven: logkit:logkit:1.0.1" value="project"/>

+		<level name="Maven: avalon-framework:avalon-framework:4.1.3" value="project"/>

+		<level name="Maven: javax.servlet:servlet-api:2.3" value="project"/>

+		<level name="Maven: org.slf4j:slf4j-log4j12:1.6.6" value="project"/>

+	</levels>

+</component>

diff --git a/tags/2.5/MockFtpServer.iml b/tags/2.5/MockFtpServer.iml
new file mode 100644
index 0000000..565b9a1
--- /dev/null
+++ b/tags/2.5/MockFtpServer.iml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<module classpath="eclipse" org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" relativePaths="true" type="JAVA_MODULE" version="4">

+  <component name="FacetManager">

+    <facet type="Spring" name="Spring">

+      <configuration />

+    </facet>

+  </component>

+</module>

+

diff --git a/tags/2.5/MockFtpServer.ipr b/tags/2.5/MockFtpServer.ipr
new file mode 100644
index 0000000..f3377e5
--- /dev/null
+++ b/tags/2.5/MockFtpServer.ipr
@@ -0,0 +1,522 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<project version="4">

+  <component name="BuildJarProjectSettings">

+    <option name="BUILD_JARS_ON_MAKE" value="false" />

+  </component>

+  <component name="CodeStyleProjectProfileManger">

+    <option name="PROJECT_PROFILE" />

+    <option name="USE_PROJECT_LEVEL_SETTINGS" value="false" />

+  </component>

+  <component name="CodeStyleSettingsManager">

+    <option name="PER_PROJECT_SETTINGS">

+      <value>

+        <ADDITIONAL_INDENT_OPTIONS fileType="java">

+          <option name="INDENT_SIZE" value="4" />

+          <option name="CONTINUATION_INDENT_SIZE" value="8" />

+          <option name="TAB_SIZE" value="4" />

+          <option name="USE_TAB_CHARACTER" value="false" />

+          <option name="SMART_TABS" value="false" />

+          <option name="LABEL_INDENT_SIZE" value="0" />

+          <option name="LABEL_INDENT_ABSOLUTE" value="false" />

+          <option name="USE_RELATIVE_INDENTS" value="false" />

+        </ADDITIONAL_INDENT_OPTIONS>

+        <ADDITIONAL_INDENT_OPTIONS fileType="js">

+          <option name="INDENT_SIZE" value="4" />

+          <option name="CONTINUATION_INDENT_SIZE" value="8" />

+          <option name="TAB_SIZE" value="4" />

+          <option name="USE_TAB_CHARACTER" value="false" />

+          <option name="SMART_TABS" value="false" />

+          <option name="LABEL_INDENT_SIZE" value="0" />

+          <option name="LABEL_INDENT_ABSOLUTE" value="false" />

+          <option name="USE_RELATIVE_INDENTS" value="false" />

+        </ADDITIONAL_INDENT_OPTIONS>

+        <ADDITIONAL_INDENT_OPTIONS fileType="jsp">

+          <option name="INDENT_SIZE" value="4" />

+          <option name="CONTINUATION_INDENT_SIZE" value="8" />

+          <option name="TAB_SIZE" value="4" />

+          <option name="USE_TAB_CHARACTER" value="false" />

+          <option name="SMART_TABS" value="false" />

+          <option name="LABEL_INDENT_SIZE" value="0" />

+          <option name="LABEL_INDENT_ABSOLUTE" value="false" />

+          <option name="USE_RELATIVE_INDENTS" value="false" />

+        </ADDITIONAL_INDENT_OPTIONS>

+        <ADDITIONAL_INDENT_OPTIONS fileType="xml">

+          <option name="INDENT_SIZE" value="4" />

+          <option name="CONTINUATION_INDENT_SIZE" value="8" />

+          <option name="TAB_SIZE" value="4" />

+          <option name="USE_TAB_CHARACTER" value="false" />

+          <option name="SMART_TABS" value="false" />

+          <option name="LABEL_INDENT_SIZE" value="0" />

+          <option name="LABEL_INDENT_ABSOLUTE" value="false" />

+          <option name="USE_RELATIVE_INDENTS" value="false" />

+        </ADDITIONAL_INDENT_OPTIONS>

+      </value>

+    </option>

+  </component>

+  <component name="CompilerConfiguration">

+    <option name="DEFAULT_COMPILER" value="Javac" />

+    <resourceExtensions>

+      <entry name=".+\.(properties|xml|html|dtd|tld)" />

+      <entry name=".+\.(gif|png|jpeg|jpg)" />

+    </resourceExtensions>

+    <wildcardResourcePatterns>

+      <entry name="?*.properties" />

+      <entry name="?*.xml" />

+      <entry name="?*.gif" />

+      <entry name="?*.png" />

+      <entry name="?*.jpeg" />

+      <entry name="?*.jpg" />

+      <entry name="?*.html" />

+      <entry name="?*.dtd" />

+      <entry name="?*.tld" />

+    </wildcardResourcePatterns>

+    <annotationProcessing>

+      <profile default="true" name="Default" enabled="false">

+        <processorPath useClasspath="true" />

+      </profile>

+      <profile default="false" name="Maven default annotation processors profile" enabled="true">

+        <sourceOutputDir name="target/generated-sources/annotations" />

+        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />

+        <outputRelativeToContentRoot value="true" />

+        <processorPath useClasspath="true" />

+        <module name="MockFtpServer" />

+      </profile>

+    </annotationProcessing>

+    <bytecodeTargetLevel>

+      <module name="MockFtpServer" target="1.4" />

+    </bytecodeTargetLevel>

+  </component>

+  <component name="CopyrightManager" default="">

+    <module2copyright />

+  </component>

+  <component name="DependenciesAnalyzeManager">

+    <option name="myForwardDirection" value="false" />

+  </component>

+  <component name="DependencyValidationManager">

+    <option name="SKIP_IMPORT_STATEMENTS" value="false" />

+  </component>

+  <component name="EclipseCompilerSettings">

+    <option name="GENERATE_NO_WARNINGS" value="true" />

+    <option name="DEPRECATION" value="false" />

+  </component>

+  <component name="EclipseEmbeddedCompilerSettings">

+    <option name="DEBUGGING_INFO" value="true" />

+    <option name="GENERATE_NO_WARNINGS" value="true" />

+    <option name="DEPRECATION" value="false" />

+    <option name="ADDITIONAL_OPTIONS_STRING" value="" />

+    <option name="MAXIMUM_HEAP_SIZE" value="128" />

+  </component>

+  <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />

+  <component name="EntryPointsManager">

+    <entry_points version="2.0" />

+  </component>

+  <component name="IdProvider" IDEtalkID="8A6189ED5C30FC95107AD305B649986C" />

+  <component name="JavadocGenerationManager">

+    <option name="OUTPUT_DIRECTORY" />

+    <option name="OPTION_SCOPE" value="protected" />

+    <option name="OPTION_HIERARCHY" value="true" />

+    <option name="OPTION_NAVIGATOR" value="true" />

+    <option name="OPTION_INDEX" value="true" />

+    <option name="OPTION_SEPARATE_INDEX" value="true" />

+    <option name="OPTION_DOCUMENT_TAG_USE" value="false" />

+    <option name="OPTION_DOCUMENT_TAG_AUTHOR" value="false" />

+    <option name="OPTION_DOCUMENT_TAG_VERSION" value="false" />

+    <option name="OPTION_DOCUMENT_TAG_DEPRECATED" value="true" />

+    <option name="OPTION_DEPRECATED_LIST" value="true" />

+    <option name="OTHER_OPTIONS" value="" />

+    <option name="HEAP_SIZE" />

+    <option name="LOCALE" />

+    <option name="OPEN_IN_BROWSER" value="true" />

+  </component>

+  <component name="MavenProjectsManager">

+    <option name="originalFiles">

+      <list>

+        <option value="$PROJECT_DIR$/pom.xml" />

+      </list>

+    </option>

+  </component>

+  <component name="Palette2">

+    <group name="Swing">

+      <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">

+        <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />

+      </item>

+      <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">

+        <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />

+      </item>

+      <item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">

+        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />

+      </item>

+      <item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">

+        <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />

+      </item>

+      <item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">

+        <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />

+        <initial-values>

+          <property name="text" value="Button" />

+        </initial-values>

+      </item>

+      <item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">

+        <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />

+        <initial-values>

+          <property name="text" value="RadioButton" />

+        </initial-values>

+      </item>

+      <item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">

+        <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />

+        <initial-values>

+          <property name="text" value="CheckBox" />

+        </initial-values>

+      </item>

+      <item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">

+        <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />

+        <initial-values>

+          <property name="text" value="Label" />

+        </initial-values>

+      </item>

+      <item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">

+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">

+          <preferred-size width="150" height="-1" />

+        </default-constraints>

+      </item>

+      <item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">

+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">

+          <preferred-size width="150" height="-1" />

+        </default-constraints>

+      </item>

+      <item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">

+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">

+          <preferred-size width="150" height="-1" />

+        </default-constraints>

+      </item>

+      <item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">

+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">

+          <preferred-size width="150" height="50" />

+        </default-constraints>

+      </item>

+      <item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">

+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">

+          <preferred-size width="150" height="50" />

+        </default-constraints>

+      </item>

+      <item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">

+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">

+          <preferred-size width="150" height="50" />

+        </default-constraints>

+      </item>

+      <item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">

+        <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />

+      </item>

+      <item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">

+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">

+          <preferred-size width="150" height="50" />

+        </default-constraints>

+      </item>

+      <item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">

+        <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">

+          <preferred-size width="150" height="50" />

+        </default-constraints>

+      </item>

+      <item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">

+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">

+          <preferred-size width="150" height="50" />

+        </default-constraints>

+      </item>

+      <item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">

+        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">

+          <preferred-size width="200" height="200" />

+        </default-constraints>

+      </item>

+      <item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">

+        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">

+          <preferred-size width="200" height="200" />

+        </default-constraints>

+      </item>

+      <item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">

+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />

+      </item>

+      <item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">

+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />

+      </item>

+      <item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">

+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />

+      </item>

+      <item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">

+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />

+      </item>

+      <item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">

+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">

+          <preferred-size width="-1" height="20" />

+        </default-constraints>

+      </item>

+      <item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">

+        <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />

+      </item>

+      <item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">

+        <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />

+      </item>

+    </group>

+  </component>

+  <component name="ProjectCodeStyleSettingsManager">

+    <option name="PER_PROJECT_SETTINGS">

+      <value>

+        <XML>

+          <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />

+        </XML>

+        <codeStyleSettings language="JavaScript">

+          <indentOptions>

+            <option name="CONTINUATION_INDENT_SIZE" value="8" />

+          </indentOptions>

+        </codeStyleSettings>

+      </value>

+    </option>

+  </component>

+  <component name="ProjectDetails">

+    <option name="projectName" value="MockFtpServer" />

+  </component>

+  <component name="ProjectDictionaryState">

+    <dictionary name="Chris">

+      <words>

+        <w>inet</w>

+        <w>mockftpserver</w>

+        <w>pathname</w>

+        <w>programmatically</w>

+        <w>rnfr</w>

+        <w>rnto</w>

+        <w>subdirectory</w>

+      </words>

+    </dictionary>

+  </component>

+  <component name="ProjectKey">

+    <option name="state" value="https://mockftpserver.svn.sourceforge.net/svnroot/mockftpserver/MockFtpServer/MockFtpServer.ipr" />

+  </component>

+  <component name="ProjectModuleManager">

+    <modules>

+      <module fileurl="file://$PROJECT_DIR$/MockFtpServer.iml" filepath="$PROJECT_DIR$/MockFtpServer.iml" />

+    </modules>

+  </component>

+  <component name="ProjectResources">

+    <default-html-doctype>http://www.w3.org/1999/xhtml</default-html-doctype>

+  </component>

+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_4" assert-keyword="true" jdk-15="false" project-jdk-name="$project_jdk_name$" project-jdk-type="JavaSDK">

+    <output url="file://$PROJECT_DIR$/out" />

+  </component>

+  <component name="ResourceManagerContainer">

+    <option name="myResourceBundles">

+      <value>

+        <list size="0" />

+      </value>

+    </option>

+  </component>

+  <component name="SvnBranchConfigurationManager">

+    <option name="myConfigurationMap">

+      <map>

+        <entry key="$PROJECT_DIR$">

+          <value>

+            <SvnBranchConfiguration>

+              <option name="branchUrls">

+                <list>

+                  <option value="https://mockftpserver.svn.sourceforge.net/svnroot/mockftpserver/branches/1.x_Branch" />

+                  <option value="https://mockftpserver.svn.sourceforge.net/svnroot/mockftpserver/tags" />

+                  <option value="https://mockftpserver.svn.sourceforge.net/svnroot/mockftpserver/tags/1.2.4" />

+                </list>

+              </option>

+              <option name="trunkUrl" value="https://mockftpserver.svn.sourceforge.net/svnroot/mockftpserver/MockFtpServer" />

+            </SvnBranchConfiguration>

+          </value>

+        </entry>

+      </map>

+    </option>

+    <option name="myVersion" value="124" />

+    <option name="mySupportsUserInfoFilter" value="true" />

+  </component>

+  <component name="VcsDirectoryMappings">

+    <mapping directory="" vcs="svn" />

+  </component>

+  <component name="WebServicesPlugin" addRequiredLibraries="true" />

+  <component name="libraryTable">

+    <library name="Groovy 1.7.10">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/org/codehaus/groovy/groovy-all/1.7.10/groovy-all-1.7.10.jar!/" />

+      </CLASSES>

+      <JAVADOC />

+      <SOURCES />

+    </library>

+    <library name="Maven: avalon-framework:avalon-framework:4.1.3">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/avalon-framework/avalon-framework/4.1.3/avalon-framework-4.1.3.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/avalon-framework/avalon-framework/4.1.3/avalon-framework-4.1.3-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/avalon-framework/avalon-framework/4.1.3/avalon-framework-4.1.3-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: commons-logging:commons-logging:1.1">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/commons-logging/commons-logging/1.1/commons-logging-1.1.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/commons-logging/commons-logging/1.1/commons-logging-1.1-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/commons-logging/commons-logging/1.1/commons-logging-1.1-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: commons-net:commons-net:1.4.1">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/commons-net/commons-net/1.4.1/commons-net-1.4.1.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/commons-net/commons-net/1.4.1/commons-net-1.4.1-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/commons-net/commons-net/1.4.1/commons-net-1.4.1-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: easymock:easymock:1.2_Java1.3">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/easymock/easymock/1.2_Java1.3/easymock-1.2_Java1.3.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/easymock/easymock/1.2_Java1.3/easymock-1.2_Java1.3-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/easymock/easymock/1.2_Java1.3/easymock-1.2_Java1.3-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: javax.servlet:servlet-api:2.3">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/javax/servlet/servlet-api/2.3/servlet-api-2.3.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/javax/servlet/servlet-api/2.3/servlet-api-2.3-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/javax/servlet/servlet-api/2.3/servlet-api-2.3-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: junit-addons:junit-addons:1.4">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/junit-addons/junit-addons/1.4/junit-addons-1.4.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/junit-addons/junit-addons/1.4/junit-addons-1.4-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/junit-addons/junit-addons/1.4/junit-addons-1.4-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: junit:junit:3.8.1">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/junit/junit/3.8.1/junit-3.8.1.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/junit/junit/3.8.1/junit-3.8.1-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/junit/junit/3.8.1/junit-3.8.1-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: log4j:log4j:1.2.17">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/log4j/log4j/1.2.17/log4j-1.2.17.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/log4j/log4j/1.2.17/log4j-1.2.17-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/log4j/log4j/1.2.17/log4j-1.2.17-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: logkit:logkit:1.0.1">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/logkit/logkit/1.0.1/logkit-1.0.1.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/logkit/logkit/1.0.1/logkit-1.0.1-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/logkit/logkit/1.0.1/logkit-1.0.1-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: org.codehaus.groovy:groovy-all:1.7.10">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/org/codehaus/groovy/groovy-all/1.7.10/groovy-all-1.7.10.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/org/codehaus/groovy/groovy-all/1.7.10/groovy-all-1.7.10-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/org/codehaus/groovy/groovy-all/1.7.10/groovy-all-1.7.10-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: org.slf4j:slf4j-api:1.6.6">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/org/slf4j/slf4j-api/1.6.6/slf4j-api-1.6.6.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/org/slf4j/slf4j-api/1.6.6/slf4j-api-1.6.6-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/org/slf4j/slf4j-api/1.6.6/slf4j-api-1.6.6-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: org.slf4j:slf4j-log4j12:1.6.6">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/org/slf4j/slf4j-log4j12/1.6.6/slf4j-log4j12-1.6.6.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/org/slf4j/slf4j-log4j12/1.6.6/slf4j-log4j12-1.6.6-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/org/slf4j/slf4j-log4j12/1.6.6/slf4j-log4j12-1.6.6-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: org.springframework:spring:2.0.7">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/org/springframework/spring/2.0.7/spring-2.0.7.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/org/springframework/spring/2.0.7/spring-2.0.7-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/org/springframework/spring/2.0.7/spring-2.0.7-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: oro:oro:2.0.8">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/oro/oro/2.0.8/oro-2.0.8.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/oro/oro/2.0.8/oro-2.0.8-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/oro/oro/2.0.8/oro-2.0.8-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: xerces:xercesImpl:2.6.2">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/xerces/xercesImpl/2.6.2/xercesImpl-2.6.2.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/xerces/xercesImpl/2.6.2/xercesImpl-2.6.2-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/xerces/xercesImpl/2.6.2/xercesImpl-2.6.2-sources.jar!/" />

+      </SOURCES>

+    </library>

+    <library name="Maven: xerces:xmlParserAPIs:2.6.2">

+      <CLASSES>

+        <root url="jar://$M2_REPO$/xerces/xmlParserAPIs/2.6.2/xmlParserAPIs-2.6.2.jar!/" />

+      </CLASSES>

+      <JAVADOC>

+        <root url="jar://$M2_REPO$/xerces/xmlParserAPIs/2.6.2/xmlParserAPIs-2.6.2-javadoc.jar!/" />

+      </JAVADOC>

+      <SOURCES>

+        <root url="jar://$M2_REPO$/xerces/xmlParserAPIs/2.6.2/xmlParserAPIs-2.6.2-sources.jar!/" />

+      </SOURCES>

+    </library>

+  </component>

+</project>

+

diff --git a/tags/2.5/README.txt b/tags/2.5/README.txt
new file mode 100644
index 0000000..582fcc4
--- /dev/null
+++ b/tags/2.5/README.txt
@@ -0,0 +1,30 @@
+MockFtpServer version ${project.version}

+-------------------------------------------------------------------------------

+${project.url}

+

+The MockFtpServer project provides mock/dummy FTP server implementations for testing FTP client

+code. Two FTP Server implementations are provided, each at a different level of abstraction.

+

+FakeFtpServer provides a higher-level abstraction. You define a virtual file system, including

+directories and files, as well as a set of valid user accounts and credentials. The FakeFtpServer

+then responds with appropriate replies and reply codes based on that configuration.

+

+StubFtpServer, on the other hand, is a lower-level "stub" implementation. You configure the

+individual FTP server commands to return custom data or reply codes, allowing simulation of

+either success or failure scenarios. You can also verify expected command invocations.

+

+MockFtpServer is written in Java, and is ideally suited to testing Java code. But because

+communication with the FTP server is across sockets and TCP/IP, it can be used to test FTP client 

+code written in any language.

+

+See the online documentation for more information.

+

+See the FTP Protocol Spec (http://www.ietf.org/rfc/rfc0959.txt) for information about 

+FTP, commands, reply codes, etc..

+

+DEPENDENCIES

+

+MockFtpServer requires 

+ - Java (JDK) version 1.4 or later

+ - The Log4J jar, version 1.2.13 or later, accessible on the CLASSPATH

+   (http://logging.apache.org/log4j/index.html).

diff --git a/tags/2.5/pom.xml b/tags/2.5/pom.xml
new file mode 100644
index 0000000..d13fe08
--- /dev/null
+++ b/tags/2.5/pom.xml
@@ -0,0 +1,325 @@
+<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>

+    <groupId>org.mockftpserver</groupId>

+    <artifactId>MockFtpServer</artifactId>

+    <name>MockFtpServer</name>

+    <description>

+        The MockFtpServer project provides mock/dummy FTP server implementations for testing FTP client

+        code. Two FTP Server implementations are provided, each at a different level of abstraction.

+        FakeFtpServer provides a higher-level abstraction. You define a virtual file system, including

+        directories and files, as well as a set of valid user accounts and credentials. The FakeFtpServer

+        then responds with appropriate replies and reply codes based on that configuration.

+        StubFtpServer, on the other hand, is a lower-level "stub" implementation. You configure the

+        individual FTP server commands to return custom data or reply codes, allowing simulation of

+        either success or failure scenarios. You can also verify expected command invocations.

+    </description>

+    <packaging>jar</packaging>

+    <version>2.5</version>

+    <url>http://mockftpserver.sourceforge.net/</url>

+

+    <scm>

+        <connection>scm:svn:https://mockftpserver.svn.sourceforge.net/svnroot/mockftpserver/MockFtpServer</connection>

+        <developerConnection>scm:svn:https://mockftpserver.svn.sourceforge.net/svnroot/mockftpserver/MockFtpServer

+        </developerConnection>

+        <url>https://mockftpserver.svn.sourceforge.net/svnroot/mockftpserver</url>

+    </scm>

+

+    <dependencies>

+

+        <dependency>

+            <groupId>org.slf4j</groupId>

+            <artifactId>slf4j-api</artifactId>

+            <version>1.6.6</version>

+        </dependency>

+

+        <!-- TESTING ONLY -->

+

+        <dependency>

+          <groupId>org.codehaus.groovy</groupId>

+          <artifactId>groovy-all</artifactId>

+          <version>1.7.10</version>

+            <scope>test</scope>

+        </dependency>

+

+

+        <dependency>

+            <groupId>commons-net</groupId>

+            <artifactId>commons-net</artifactId>

+            <version>1.4.1</version>

+            <scope>test</scope>

+        </dependency>

+

+        <dependency>

+            <groupId>easymock</groupId>

+            <artifactId>easymock</artifactId>

+            <version>1.2_Java1.3</version>

+            <scope>test</scope>

+        </dependency>

+

+        <dependency>

+            <groupId>junit-addons</groupId>

+            <artifactId>junit-addons</artifactId>

+            <version>1.4</version>

+            <scope>test</scope>

+        </dependency>

+

+        <dependency>

+            <groupId>org.springframework</groupId>

+            <artifactId>spring</artifactId>

+            <version>2.0.7</version>

+            <scope>test</scope>

+        </dependency>

+

+        <dependency>

+            <groupId>org.slf4j</groupId>

+            <artifactId>slf4j-log4j12</artifactId>

+            <version>1.6.6</version>

+            <scope>test</scope>

+        </dependency>

+

+        <!-- Transitive dependency.

+        <dependency>

+          <groupId>junit</groupId>

+          <artifactId>junit</artifactId>

+          <version>3.8.1</version>

+          <scope>test</scope>

+        </dependency>

+        -->

+

+    </dependencies>

+

+    <distributionManagement>

+        <snapshotRepository>

+            <id>ossrh</id>

+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>

+        </snapshotRepository>

+        <repository>

+            <id>ossrh</id>

+            <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>

+        </repository>

+    </distributionManagement>

+

+    <build>

+        <plugins>

+

+            <plugin>

+               <groupId>org.codehaus.gmaven</groupId>

+               <artifactId>gmaven-plugin</artifactId>

+               <version>1.3</version>

+               <configuration>

+                 <providerSelection>1.7</providerSelection>

+               </configuration>

+               <executions>

+                 <execution>

+                   <goals>

+                       <goal>generateStubs</goal>

+                       <goal>compile</goal>

+                       <goal>generateTestStubs</goal>

+                       <goal>testCompile</goal>

+                      <!--if you want joint compilation, add stub generation goals here-->

+                   </goals>

+                 </execution>

+               </executions>

+               <dependencies>

+                 <dependency>

+                   <groupId>org.codehaus.gmaven.runtime</groupId>

+                   <artifactId>gmaven-runtime-1.7</artifactId>

+                   <version>1.3</version>

+                   <exclusions>

+                     <exclusion>

+                       <groupId>org.codehaus.groovy</groupId>

+                       <artifactId>groovy-all</artifactId>

+                     </exclusion>

+                   </exclusions>

+                 </dependency>

+                 <dependency>

+                   <groupId>org.codehaus.groovy</groupId>

+                   <artifactId>groovy-all</artifactId>

+                   <version>1.7.10</version>

+                 </dependency>

+               </dependencies>

+            </plugin>

+

+            <plugin>

+                <groupId>org.apache.maven.plugins</groupId>

+                <artifactId>maven-compiler-plugin</artifactId>

+                <configuration>

+                    <verbose>true</verbose>

+                    <source>1.4</source>

+                    <target>1.4</target>

+                    <fork>true</fork>

+                </configuration>

+            </plugin>

+

+            <!-- clean coverage data before collecting -->

+            <plugin>

+                <artifactId>cobertura-maven-plugin</artifactId>

+                <groupId>org.codehaus.mojo</groupId>

+                <version>2.0</version>

+                <executions>

+                    <execution>

+                        <goals>

+                            <goal>clean</goal>

+                        </goals>

+                    </execution>

+                </executions>

+            </plugin>

+

+            <plugin>

+                <groupId>org.apache.maven.plugins</groupId>

+                <artifactId>maven-jar-plugin</artifactId>

+                <configuration>

+                    <archive>

+                        <manifestEntries>

+                            <MockFtpServer-Version>${pom.version}</MockFtpServer-Version>

+                        </manifestEntries>

+                    </archive>

+                </configuration>

+            </plugin>

+

+            <!--<plugin>-->

+                <!--<groupId>org.apache.maven.plugins</groupId>-->

+                <!--<artifactId>maven-source-plugin</artifactId>-->

+                <!--<executions>-->

+                    <!--<execution>-->

+                        <!--<id>attach-sources</id>-->

+                        <!--<phase>package</phase>-->

+                        <!--<goals>-->

+                            <!--<goal>jar</goal>-->

+                        <!--</goals>-->

+                    <!--</execution>-->

+                <!--</executions>-->

+            <!--</plugin>-->

+

+            <plugin>

+                <groupId>org.apache.maven.plugins</groupId>

+                <artifactId>maven-source-plugin</artifactId>

+                <version>2.2.1</version>

+                <executions>

+                    <execution>

+                        <id>attach-sources</id>

+                        <goals>

+                            <goal>jar-no-fork</goal>

+                        </goals>

+                    </execution>

+                </executions>

+            </plugin>

+            <plugin>

+                <groupId>org.apache.maven.plugins</groupId>

+                <artifactId>maven-javadoc-plugin</artifactId>

+                <version>2.9.1</version>

+                <executions>

+                    <execution>

+                        <id>attach-javadocs</id>

+                        <goals>

+                            <goal>jar</goal>

+                        </goals>

+                    </execution>

+                </executions>

+            </plugin>

+

+

+            <plugin>

+                <artifactId>maven-assembly-plugin</artifactId>

+                <configuration>

+                    <descriptors>

+                        <descriptor>src/assembly/assembly.xml</descriptor>

+                    </descriptors>

+                </configuration>

+            </plugin>

+

+            <plugin>

+                <groupId>org.apache.maven.plugins</groupId>

+                <artifactId>maven-release-plugin</artifactId>

+                <configuration>

+                    <preparationGoals>clean site assembly:assembly</preparationGoals>

+                </configuration>

+            </plugin>

+

+            <plugin>

+              <groupId>org.apache.maven.plugins</groupId>

+              <artifactId>maven-site-plugin</artifactId>

+              <version>2.1</version>

+            </plugin>

+

+            <plugin>

+                <groupId>org.apache.maven.plugins</groupId>

+                <artifactId>maven-gpg-plugin</artifactId>

+                <version>1.5</version>

+                <executions>

+                    <execution>

+                        <id>sign-artifacts</id>

+                        <phase>verify</phase>

+                        <goals>

+                            <goal>sign</goal>

+                        </goals>

+                    </execution>

+                </executions>

+            </plugin>

+

+        </plugins>

+    </build>

+

+    <reporting>

+        <plugins>

+

+            <plugin>

+                <groupId>org.apache.maven.plugins</groupId>

+                <artifactId>maven-project-info-reports-plugin</artifactId>

+                <version>2.2</version>

+                <reportSets>

+                    <reportSet>

+                        <reports>

+                            <report>dependencies</report>

+                            <!-- <report>project-team</report> -->

+                            <!-- <report>mailing-list</report> -->

+                            <!-- <report>cim</report> -->

+                            <!-- <report>issue-tracking</report> -->

+                            <report>license</report>

+                            <!-- <report>scm</report> -->

+                        </reports>

+                    </reportSet>

+                </reportSets>

+            </plugin>

+

+            <plugin>

+                <groupId>org.codehaus.mojo</groupId>

+                <artifactId>cobertura-maven-plugin</artifactId>

+            </plugin>

+

+            <plugin>

+                <groupId>org.apache.maven.plugins</groupId>

+                <artifactId>maven-javadoc-plugin</artifactId>

+            </plugin>

+

+            <plugin>

+                <groupId>org.apache.maven.plugins</groupId>

+                <artifactId>maven-pmd-plugin</artifactId>

+            </plugin>

+

+        </plugins>

+    </reporting>

+

+    <developers>

+        <developer>

+            <id>chrismair</id>

+            <name>Chris Mair</name>

+            <email>chrismair@users.sourceforge.net</email>

+            <url>https://sourceforge.net/users/chrismair</url>

+            <roles>

+                <role>developer</role>

+            </roles>

+            <timezone>-4</timezone>

+        </developer>

+    </developers>

+

+    <licenses>

+        <license>

+            <name>Apache 2</name>

+            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>

+            <distribution>repo</distribution>

+        </license>

+    </licenses>

+

+</project>
\ No newline at end of file
diff --git a/tags/2.5/src/assembly/assembly.xml b/tags/2.5/src/assembly/assembly.xml
new file mode 100644
index 0000000..b0d1dd1
--- /dev/null
+++ b/tags/2.5/src/assembly/assembly.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>

+<assembly>

+    <id>bin</id>

+    <formats>

+        <format>tar.gz</format>

+        <format>zip</format>

+    </formats>

+    <includeSiteDirectory>false</includeSiteDirectory>

+

+    <files>

+        <file>

+            <source>README.txt</source>

+            <outputDirectory>/</outputDirectory>

+            <filtered>true</filtered>

+        </file>

+    </files>

+

+    <fileSets>

+        <fileSet>

+            <includes>

+                <include>CHANGELOG*</include>

+                <include>LICENSE*</include>

+                <include>NOTICE*</include>

+                <include>pom.xml</include>

+            </includes>

+        </fileSet>

+

+        <fileSet>

+            <directory>target</directory>

+            <outputDirectory></outputDirectory>

+            <includes>

+                <include>*.jar</include>

+            </includes>

+        </fileSet>

+

+        <fileSet>

+            <directory>src</directory>

+            <useDefaultExcludes>true</useDefaultExcludes>

+        </fileSet>

+

+        <fileSet>

+            <directory>target/site</directory>

+            <outputDirectory>docs</outputDirectory>

+            <includes>

+                <include>apidocs/**</include>

+                <include>css/**</include>

+                <include>images/**</include>

+                <include>*.html</include>

+            </includes>

+        </fileSet>

+

+        <fileSet>

+            <directory>samples</directory>

+            <outputDirectory>samples</outputDirectory>

+            <includes>

+                <include>rulesets/**</include>

+                <include>src/**</include>

+                <include>*.bat</include>

+                <include>*.groovy</include>

+                <include>*.xml</include>

+            </includes>

+        </fileSet>

+    </fileSets>

+

+    <dependencySets>

+        <dependencySet>

+          <unpack>false</unpack>

+          <scope>runtime</scope>

+          <outputDirectory>lib</outputDirectory>

+          <includes>

+              <include>*:log4j</include>

+          </includes>

+        </dependencySet>

+    </dependencySets>

+

+</assembly>

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/CommandSyntaxException.java b/tags/2.5/src/main/java/org/mockftpserver/core/CommandSyntaxException.java
new file mode 100644
index 0000000..688eb9e
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/CommandSyntaxException.java
@@ -0,0 +1,49 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.core;

+

+/**

+ * Represents an error indicating that a server command has been received that

+ * has invalid syntax. For instance, the command may be missing a required parameter.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class CommandSyntaxException extends MockFtpServerException {

+

+    /**

+     * @param message

+     */

+    public CommandSyntaxException(String message) {

+        super(message);

+    }

+

+    /**

+     * @param cause

+     */

+    public CommandSyntaxException(Throwable cause) {

+        super(cause);

+    }

+

+    /**

+     * @param message

+     * @param cause

+     */

+    public CommandSyntaxException(String message, Throwable cause) {

+        super(message, cause);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/IllegalStateException.java b/tags/2.5/src/main/java/org/mockftpserver/core/IllegalStateException.java
new file mode 100644
index 0000000..64d8d1e
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/IllegalStateException.java
@@ -0,0 +1,49 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.core;

+

+/**

+ * Represents an error indicating that the server is in an illegal state, or

+ * that a server command is invoked when its preconditions have not been met.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class IllegalStateException extends MockFtpServerException {

+

+    /**

+     * @param message

+     */

+    public IllegalStateException(String message) {

+        super(message);

+    }

+

+    /**

+     * @param cause

+     */

+    public IllegalStateException(Throwable cause) {

+        super(cause);

+    }

+

+    /**

+     * @param message

+     * @param cause

+     */

+    public IllegalStateException(String message, Throwable cause) {

+        super(message, cause);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/MockFtpServerException.java b/tags/2.5/src/main/java/org/mockftpserver/core/MockFtpServerException.java
new file mode 100644
index 0000000..d412ecd
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/MockFtpServerException.java
@@ -0,0 +1,52 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core;

+

+/**

+ * Represents an error specific to the MockFtpServer project.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public class MockFtpServerException extends RuntimeException {

+

+    /**

+     * Create a new instance with the specified detail message and no cause

+     * @param message - the exception detail message

+     */

+    public MockFtpServerException(String message) {

+        super(message);

+    }

+

+    /**

+     * Create a new instance with the specified detail message and no cause

+     * @param cause - the Throwable cause

+     */

+    public MockFtpServerException(Throwable cause) {

+        super(cause);

+    }

+

+    /**

+     * Create a new instance with the specified detail message and cause

+     * @param message - the exception detail message

+     * @param cause - the Throwable cause

+     */

+    public MockFtpServerException(String message, Throwable cause) {

+        super(message, cause);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/NotLoggedInException.java b/tags/2.5/src/main/java/org/mockftpserver/core/NotLoggedInException.java
new file mode 100644
index 0000000..e779904
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/NotLoggedInException.java
@@ -0,0 +1,49 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.core;

+

+/**

+ * Represents an error indicating that the current user has not yet logged in, but

+ * is required to in order to invoke the requested command.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class NotLoggedInException extends MockFtpServerException {

+

+    /**

+     * @param message

+     */

+    public NotLoggedInException(String message) {

+        super(message);

+    }

+

+    /**

+     * @param cause

+     */

+    public NotLoggedInException(Throwable cause) {

+        super(cause);

+    }

+

+    /**

+     * @param message

+     * @param cause

+     */

+    public NotLoggedInException(String message, Throwable cause) {

+        super(message, cause);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/AbstractCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/AbstractCommandHandler.java
new file mode 100644
index 0000000..a384bb0
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/AbstractCommandHandler.java
@@ -0,0 +1,89 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.util.Assert;

+

+import java.util.ResourceBundle;

+

+/**

+ * The abstract superclass for CommandHandler classes.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractCommandHandler implements CommandHandler, ReplyTextBundleAware {

+

+    protected final Logger LOG = LoggerFactory.getLogger(getClass());

+

+    private ResourceBundle replyTextBundle;

+

+    //-------------------------------------------------------------------------

+    // Support for reply text ResourceBundle

+    //-------------------------------------------------------------------------

+

+    /**

+     * Return the ResourceBundle containing the reply text messages

+     *

+     * @return the replyTextBundle

+     * @see ReplyTextBundleAware#getReplyTextBundle()

+     */

+    public ResourceBundle getReplyTextBundle() {

+        return replyTextBundle;

+    }

+

+    /**

+     * Set the ResourceBundle containing the reply text messages

+     *

+     * @param replyTextBundle - the replyTextBundle to set

+     * @see ReplyTextBundleAware#setReplyTextBundle(java.util.ResourceBundle)

+     */

+    public void setReplyTextBundle(ResourceBundle replyTextBundle) {

+        this.replyTextBundle = replyTextBundle;

+    }

+

+    // -------------------------------------------------------------------------

+    // Utility methods for subclasses

+    // -------------------------------------------------------------------------

+

+    /**

+     * Return the specified text surrounded with double quotes

+     *

+     * @param text - the text to surround with quotes

+     * @return the text with leading and trailing double quotes

+     * @throws org.mockftpserver.core.util.AssertFailedException

+     *          - if text is null

+     */

+    protected static String quotes(String text) {

+        Assert.notNull(text, "text");

+        final String QUOTES = "\"";

+        return QUOTES + text + QUOTES;

+    }

+

+    /**

+     * Assert that the specified number is a valid reply code

+     *

+     * @param replyCode - the reply code to check

+     * @throws org.mockftpserver.core.util.AssertFailedException

+     *          - if the replyCode is invalid

+     */

+    protected void assertValidReplyCode(int replyCode) {

+        Assert.isTrue(replyCode > 0, "The number [" + replyCode + "] is not a valid reply code");

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/AbstractStaticReplyCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/AbstractStaticReplyCommandHandler.java
new file mode 100644
index 0000000..efe8494
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/AbstractStaticReplyCommandHandler.java
@@ -0,0 +1,104 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.mockftpserver.core.session.Session;

+

+/**

+ * The abstract superclass for CommandHandler classes that default to sending

+ * back a configured reply code and text. You can customize the returned reply

+ * code by setting the required <code>replyCode</code> property. If only the

+ * <code>replyCode</code> property is set, then the default reply text corresponding to that

+ * reply code is used in the response. You can optionally configure the reply text by setting

+ * the <code>replyMessageKey</code> or <code>replyText</code> property.

+ * <p>

+ * Subclasses can optionally override the reply code and/or text for the reply by calling

+ * {@link #setReplyCode(int)}, {@link #setReplyMessageKey(String)} and {@link #setReplyText(String)}.

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+public abstract class AbstractStaticReplyCommandHandler extends AbstractTrackingCommandHandler {

+

+    // Defaults to zero; must be set to non-zero

+    protected int replyCode = 0;

+

+    // Defaults to null; if set to non-null, this value will override the default reply text associated with

+    // the replyCode.

+    protected String replyText = null;

+

+    // The message key for the reply text. Defaults to null. If null, use the default message associated

+    // with the reply code

+    protected String replyMessageKey = null;

+

+    /**

+     * Set the reply code.

+     *

+     * @param replyCode - the replyCode

+     *

+     * @throws org.mockftpserver.core.util.AssertFailedException - if the replyCode is not valid

+     */

+    public void setReplyCode(int replyCode) {

+        assertValidReplyCode(replyCode);

+        this.replyCode = replyCode;

+    }

+

+    /**

+     * Set the reply text. If null, then use the (default) message key for the replyCode.

+     *

+     * @param replyText - the replyText

+     */

+    public void setReplyText(String replyText) {

+        this.replyText = replyText;

+    }

+

+    /**

+     * Set the message key for the reply text. If null, then use the default message key.

+     *

+     * @param replyMessageKey - the replyMessageKey to set

+     */

+    public void setReplyMessageKey(String replyMessageKey) {

+        this.replyMessageKey = replyMessageKey;

+    }

+

+    // -------------------------------------------------------------------------

+    // Utility methods for subclasses

+    // -------------------------------------------------------------------------

+

+    /**

+     * Send the reply using the replyCode and message key/text configured for this command handler.

+     * @param session - the Session

+     *

+     * @throws org.mockftpserver.core.util.AssertFailedException if the replyCode is not valid

+     */

+    protected void sendReply(Session session) {

+        sendReply(session, null);

+    }

+

+    /**

+     * Send the reply using the replyCode and message key/text configured for this command handler.

+     * @param session - the Session

+     * @param messageParameter - message parameter; may be null

+     *

+     * @throws org.mockftpserver.core.util.AssertFailedException if the replyCode is not valid

+     */

+    protected void sendReply(Session session, Object messageParameter) {

+        Object[] parameters = (messageParameter == null) ? null : new Object[] { messageParameter };

+        sendReply(session, replyCode, replyMessageKey, replyText, parameters);

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/AbstractTrackingCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/AbstractTrackingCommandHandler.java
new file mode 100644
index 0000000..bb817ab
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/AbstractTrackingCommandHandler.java
@@ -0,0 +1,194 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.mockftpserver.core.CommandSyntaxException;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.Assert;

+import org.mockftpserver.core.util.AssertFailedException;

+

+import java.text.MessageFormat;

+import java.util.ArrayList;

+import java.util.List;

+import java.util.MissingResourceException;

+

+/**

+ * The abstract superclass for CommandHandler classes that manage the List of InvocationRecord

+ * objects corresponding to each invocation of the command handler, and provide helper methods for subclasses.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractTrackingCommandHandler extends AbstractCommandHandler implements InvocationHistory {

+

+    private List invocations = new ArrayList();

+

+    // -------------------------------------------------------------------------

+    // Template Method

+    // -------------------------------------------------------------------------

+

+    /**

+     * Handle the specified command for the session. This method is declared to throw Exception,

+     * allowing CommandHandler implementations to avoid unnecessary exception-handling. All checked

+     * exceptions are expected to be wrapped and handled by the caller.

+     *

+     * @param command - the Command to be handled

+     * @param session - the session on which the Command was submitted

+     * @throws Exception

+     * @throws AssertFailedException - if the command or session is null

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command,

+     *      org.mockftpserver.core.session.Session)

+     */

+    public final void handleCommand(Command command, Session session) throws Exception {

+        Assert.notNull(command, "command");

+        Assert.notNull(session, "session");

+        InvocationRecord invocationRecord = new InvocationRecord(command, session.getClientHost());

+        invocations.add(invocationRecord);

+        try {

+            handleCommand(command, session, invocationRecord);

+        }

+        catch (CommandSyntaxException e) {

+            sendReply(session, ReplyCodes.COMMAND_SYNTAX_ERROR, null, null, null);

+        }

+        invocationRecord.lock();

+    }

+

+    /**

+     * Handle the specified command for the session. This method is declared to throw Exception,

+     * allowing CommandHandler implementations to avoid unnecessary exception-handling. All checked

+     * exceptions are expected to be wrapped and handled by the caller.

+     *

+     * @param command          - the Command to be handled

+     * @param session          - the session on which the Command was submitted

+     * @param invocationRecord - the InvocationRecord; CommandHandlers are expected to add

+     *                         handler-specific data to the InvocationRecord, as appropriate

+     * @throws Exception

+     */

+    protected abstract void handleCommand(Command command, Session session, InvocationRecord invocationRecord)

+            throws Exception;

+

+    // -------------------------------------------------------------------------

+    // Utility methods for subclasses

+    // -------------------------------------------------------------------------

+

+    /**

+     * Send a reply for this command on the control connection.

+     * <p/>

+     * The reply code is designated by the <code>replyCode</code> property, and the reply text

+     * is determined by the following rules:

+     * <ol>

+     * <li>If the <code>replyText</code> property is non-null, then use that.</li>

+     * <li>Otherwise, if <code>replyMessageKey</code> is non-null, the use that to retrieve a

+     * localized message from the <code>replyText</code> ResourceBundle.</li>

+     * <li>Otherwise, retrieve the reply text from the <code>replyText</code> ResourceBundle,

+     * using the reply code as the key.</li>

+     * </ol>

+     * If the arguments Object[] is not null, then these arguments are substituted within the

+     * reply text using the {@link MessageFormat} class.

+     *

+     * @param session         - the Session

+     * @param replyCode       - the reply code

+     * @param replyMessageKey - if not null (and replyText is null), this is used as the ResourceBundle

+     *                        message key instead of the reply code.

+     * @param replyText       - if non-null, this is used as the reply text

+     * @param arguments       - the array of arguments to be formatted and substituted within the reply

+     *                        text; may be null

+     * @throws AssertFailedException - if session is null

+     * @see MessageFormat

+     */

+    protected void sendReply(Session session, int replyCode, String replyMessageKey, String replyText,

+                             Object[] arguments) {

+

+        Assert.notNull(session, "session");

+        assertValidReplyCode(replyCode);

+

+        String key = (replyMessageKey != null) ? replyMessageKey : Integer.toString(replyCode);

+        String text = getTextForReplyCode(replyCode, key, replyText, arguments);

+        String replyTextToLog = (text == null) ? "" : " " + text;

+        LOG.info("Sending reply [" + replyCode + replyTextToLog + "]");

+        session.sendReply(replyCode, text);

+    }

+

+    // -------------------------------------------------------------------------

+    // InvocationHistory - Support for command history

+    // -------------------------------------------------------------------------

+

+    /**

+     * @return the number of invocation records stored for this command handler instance

+     * @see org.mockftpserver.core.command.InvocationHistory#numberOfInvocations()

+     */

+    public int numberOfInvocations() {

+        return invocations.size();

+    }

+

+    /**

+     * Return the InvocationRecord representing the command invoction data for the nth invocation

+     * for this command handler instance. One InvocationRecord should be stored for each invocation

+     * of the CommandHandler.

+     *

+     * @param index - the index of the invocation record to return. The first record is at index zero.

+     * @return the InvocationRecord for the specified index

+     * @throws AssertFailedException - if there is no invocation record corresponding to the specified index

+     * @see org.mockftpserver.core.command.InvocationHistory#getInvocation(int)

+     */

+    public InvocationRecord getInvocation(int index) {

+        return (InvocationRecord) invocations.get(index);

+    }

+

+    /**

+     * Clear out the invocation history for this CommandHandler. After invoking this method, the

+     * <code>numberOfInvocations()</code> method will return zero.

+     *

+     * @see org.mockftpserver.core.command.InvocationHistory#clearInvocations()

+     */

+    public void clearInvocations() {

+        invocations.clear();

+    }

+

+    // -------------------------------------------------------------------------

+    // Internal Helper Methods

+    // -------------------------------------------------------------------------

+

+    /**

+     * Return the text for the specified reply code, formatted using the message arguments, if

+     * supplied. If overrideText is not null, then return that. Otherwise, return the text mapped to

+     * the code from the replyText ResourceBundle. If the ResourceBundle contains no mapping, then

+     * return null.

+     * <p/>

+     * If arguments is not null, then the returned reply text if formatted using the

+     * {@link MessageFormat} class.

+     *

+     * @param code         - the reply code

+     * @param messageKey   - the key used to retrieve the reply text from the replyTextBundle

+     * @param overrideText - if not null, this is used instead of the text from the replyTextBundle.

+     * @param arguments    - the array of arguments to be formatted and substituted within the reply

+     *                     text; may be null

+     * @return the text for the reply code; may be null

+     */

+    private String getTextForReplyCode(int code, String messageKey, String overrideText, Object[] arguments) {

+        try {

+            String t = (overrideText == null) ? getReplyTextBundle().getString(messageKey) : overrideText;

+            String formattedMessage = MessageFormat.format(t, arguments);

+            return (formattedMessage == null) ? null : formattedMessage.trim();

+        }

+        catch (MissingResourceException e) {

+            // No reply text is mapped for the specified key

+            LOG.warn("No reply text defined for reply code [" + code + "]");

+            return null;

+        }

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/Command.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/Command.java
new file mode 100644
index 0000000..935a407
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/Command.java
@@ -0,0 +1,171 @@
+/*

+ * Copyright 20078 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.mockftpserver.core.CommandSyntaxException;

+import org.mockftpserver.core.util.Assert;

+

+import java.util.Arrays;

+import java.util.List;

+

+/**

+ * Represents a command received from an FTP client, containing a command name and parameters.

+ * Objects of this class are immutable.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class Command {

+

+    private String name;

+    private String[] parameters;

+

+    /**

+     * Construct a new immutable instance with the specified command name and parameters

+     *

+     * @param name       - the command name; may not be null

+     * @param parameters - the command parameters; may be empty; may not be null

+     */

+    public Command(String name, String[] parameters) {

+        Assert.notNull(name, "name");

+        Assert.notNull(parameters, "parameters");

+        this.name = name;

+        this.parameters = copy(parameters);

+    }

+

+    /**

+     * Construct a new immutable instance with the specified command name and parameters

+     *

+     * @param name       - the command name; may not be null

+     * @param parameters - the command parameters; may be empty; may not be null

+     */

+    public Command(String name, List parameters) {

+        this(name, (String[]) parameters.toArray(new String[parameters.size()]));

+    }

+

+    /**

+     * @return the name

+     */

+    public String getName() {

+        return name;

+    }

+

+    /**

+     * @return the parameters

+     */

+    public String[] getParameters() {

+        return copy(parameters);

+    }

+

+    /**

+     * Get the String value of the parameter at the specified index

+     *

+     * @param index - the index

+     * @return the parameter value as a String

+     * @throws org.mockftpserver.core.util.AssertFailedException

+     *          if the parameter index is invalid or the value is not a valid String

+     */

+    public String getRequiredParameter(int index) {

+        assertValidIndex(index);

+        return parameters[index];

+    }

+

+    /**

+     * Get the String value of the parameter at the specified index; return null if no parameter exists for the index

+     *

+     * @param index - the index

+     * @return the parameter value as a String, or null if this Command does not have a parameter for that index

+     */

+    public String getParameter(int index) {

+        return (parameters.length > index) ? parameters[index] : null;

+    }

+

+    /**

+     * Get the String value of the parameter at the specified index; return null if no

+     * parameter exists for the index. This is an alias for {@link #getParameter(int)}.

+     *

+     * @param index - the index

+     * @return the parameter value as a String, or null if this Command does not have a parameter for that index

+     */

+    public String getOptionalString(int index) {

+        return getParameter(index);

+    }

+

+    /**

+     * @see java.lang.Object#equals(java.lang.Object)

+     */

+    public boolean equals(Object obj) {

+        if (this == obj) {

+            return true;

+        }

+        if (obj == null || !(obj instanceof Command)) {

+            return false;

+        }

+        return this.hashCode() == obj.hashCode();

+    }

+

+    /**

+     * @see java.lang.Object#hashCode()

+     */

+    public int hashCode() {

+        String str = name + Arrays.asList(parameters);

+        return str.hashCode();

+    }

+

+    /**

+     * Return the String representation of this object

+     *

+     * @see java.lang.Object#toString()

+     */

+    public String toString() {

+        return "Command[" + name + ":" + Arrays.asList(parameters) + "]";

+    }

+

+    /**

+     * Return the name, normalized to a common format - convert to upper case.

+     *

+     * @return the name converted to upper case

+     */

+    public static String normalizeName(String name) {

+        return name.toUpperCase();

+    }

+

+    /**

+     * Construct a shallow copy of the specified array

+     *

+     * @param array - the array to copy

+     * @return a new array with the same contents

+     */

+    private static String[] copy(String[] array) {

+        String[] newArray = new String[array.length];

+        System.arraycopy(array, 0, newArray, 0, array.length);

+        return newArray;

+    }

+

+    /**

+     * Assert that the index is valid

+     *

+     * @param index - the index

+     * @throws org.mockftpserver.core.CommandSyntaxException

+     *          - if the parameter index is invalid

+     */

+    private void assertValidIndex(int index) {

+        if (index < 0 || index >= parameters.length) {

+            throw new CommandSyntaxException("The parameter index " + index + " is not valid for " + this);

+        }

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/CommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/CommandHandler.java
new file mode 100644
index 0000000..e004b5b
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/CommandHandler.java
@@ -0,0 +1,42 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.mockftpserver.core.session.Session;

+

+/**

+ * Interface for classes that can handle an FTP command.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public interface CommandHandler {

+

+    /**

+     * Handle the specified command for the session. This method is declared to throw 

+     * Exception, allowing CommandHandler implementations to avoid unnecessary

+     * exception-handling. All checked exceptions are expected to be wrapped and handled 

+     * by the caller.

+     * 

+     * @param command - the Command to be handled

+     * @param session - the session on which the Command was submitted

+     * 

+     * @throws Exception

+     */

+    public void handleCommand(Command command, Session session) throws Exception;

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/CommandNames.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/CommandNames.java
new file mode 100644
index 0000000..83f29f5
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/CommandNames.java
@@ -0,0 +1,74 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+/**

+ * FTP command name constants.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class CommandNames {

+

+    public static final String ABOR = "ABOR";

+    public static final String ACCT = "ACCT";

+    public static final String ALLO = "ALLO";

+    public static final String APPE = "APPE";

+    public static final String CDUP = "CDUP";

+    public static final String CWD = "CWD";

+    public static final String DELE = "DELE";

+    public static final String EPRT = "EPRT";

+    public static final String EPSV = "EPSV";

+    public static final String HELP = "HELP";

+    public static final String LIST = "LIST";

+    public static final String MKD = "MKD";

+    public static final String MODE = "MODE";

+    public static final String NLST = "NLST";

+    public static final String NOOP = "NOOP";

+    public static final String PASS = "PASS";

+    public static final String PASV = "PASV";

+    public static final String PORT = "PORT";

+    public static final String PWD = "PWD";

+    public static final String QUIT = "QUIT";

+    public static final String REIN = "REIN";

+    public static final String REST = "REST";

+    public static final String RETR = "RETR";

+    public static final String RMD = "RMD";

+    public static final String RNFR = "RNFR";

+    public static final String RNTO = "RNTO";

+    public static final String SITE = "SITE";

+    public static final String SMNT = "SMNT";

+    public static final String STAT = "STAT";

+    public static final String STOR = "STOR";

+    public static final String STOU = "STOU";

+    public static final String STRU = "STRU";

+    public static final String SYST = "SYST";

+    public static final String TYPE = "TYPE";

+    public static final String USER = "USER";

+

+    public static final String XPWD = "XPWD";

+

+    // Special commands - not "real" FTP commands

+    public static final String CONNECT = "CONNECT";

+    public static final String UNSUPPORTED = "UNSUPPORTED";

+

+    /**

+     * Private constructor. This class should not be instantiated.

+     */

+    private CommandNames() {

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/ConnectCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/ConnectCommandHandler.java
new file mode 100644
index 0000000..a54557b
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/ConnectCommandHandler.java
@@ -0,0 +1,48 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler that encapsulates the sending of the reply for the initial connection from

+ * the FTP client to the server. Send back a reply code of 220, indicating a successful connection.

+ * <p>

+ * Note that this is a "special" CommandHandler, in that it handles the initial connection from the

+ * client, rather than an explicit FTP command.

+ * <p>

+ * Each invocation record stored by this CommandHandler contains no data elements.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class ConnectCommandHandler extends AbstractStaticReplyCommandHandler implements CommandHandler {

+

+    /**

+     * Constructor. Initiate the replyCode.

+     */

+    public ConnectCommandHandler() {

+        setReplyCode(ReplyCodes.CONNECT_OK);

+    }

+

+    /**

+     * @see AbstractTrackingCommandHandler#handleCommand(Command, org.mockftpserver.core.session.Session, InvocationRecord)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        sendReply(session);

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/InvocationHistory.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/InvocationHistory.java
new file mode 100644
index 0000000..71c66c2
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/InvocationHistory.java
@@ -0,0 +1,50 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+/**

+ * Interface for an object that can retrieve and clear the history of InvocationRecords 

+ * for a command handler.

+ * 

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+public interface InvocationHistory {

+

+    /**

+     * @return the number of invocation records stored for this command handler instance

+     */

+    public int numberOfInvocations();

+

+    /**

+     * Return the InvocationRecord representing the command invoction data for the nth invocation

+     * for this command handler instance. One InvocationRecord should be stored for each invocation

+     * of the CommandHandler.

+     * 

+     * @param index - the index of the invocation record to return. The first record is at index zero.

+     * @return the InvocationRecord for the specified index

+     * 

+     * @throws AssertFailedException - if there is no invocation record corresponding to the specified index     */

+    public InvocationRecord getInvocation(int index);

+

+    /**

+     * Clear out the invocation history for this CommandHandler. After invoking this method, the

+     * <code>numberOfInvocations()</code> method will return zero.

+     */

+    public void clearInvocations();

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/InvocationRecord.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/InvocationRecord.java
new file mode 100644
index 0000000..4f92d78
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/InvocationRecord.java
@@ -0,0 +1,182 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.mockftpserver.core.util.Assert;

+import org.mockftpserver.core.util.AssertFailedException;

+

+import java.net.InetAddress;

+import java.util.Collections;

+import java.util.Date;

+import java.util.HashMap;

+import java.util.Map;

+import java.util.Set;

+

+/**

+ * Represents information about a single FTP Command invocation. Manages and provides access to

+ * the Command, the host address (<code>InetAddress</code>) of the client that submitted the

+ * Command and the timestamp of the Command submission.

+ * <p>

+ * This class also supports storing zero or more arbitrary mappings of <i>key</i> to value, where <i>key</i> is

+ * a String and <i>value</i> is any Object. Convenience methods are provided that enable retrieving

+ * type-specific data by its <i>key</i>. The data stored in an {@link InvocationRecord} is CommandHandler-specific.

+ * <p>

+ * The {@link #lock()} method makes an instance of this class immutable. After an instance is locked,

+ * calling the {@link #set(String, Object)} method will throw an <code>AssertFailedException</code>.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class InvocationRecord {

+

+    private Command command;

+    private Date time;

+    private InetAddress clientHost;

+    private Map data = new HashMap();

+    private boolean locked = false;

+

+    /**

+     * Create a new instance

+     *

+     * @param command    - the Command

+     * @param clientHost - the client host

+     */

+    public InvocationRecord(Command command, InetAddress clientHost) {

+        this.command = command;

+        this.time = new Date();

+        this.clientHost = clientHost;

+    }

+

+    /**

+     * Lock this instance, making it immutable. After an instance is locked,

+     * calling the {@link #set(String, Object)} method will throw an

+     * <code>AssertFailedException</code>.

+     */

+    public void lock() {

+        locked = true;

+    }

+

+    /**

+     * Return true if this object has been locked, false otherwise. See {@link #lock()}.

+     *

+     * @return true if this object has been locked, false otherwise.

+     */

+    public boolean isLocked() {

+        return locked;

+    }

+

+    /**

+     * @return the client host that submitted the command, as an InetAddress

+     */

+    public InetAddress getClientHost() {

+        return clientHost;

+    }

+

+    /**

+     * @return the Command

+     */

+    public Command getCommand() {

+        return command;

+    }

+

+    /**

+     * @return the time that the command was processed; this may differ slightly from when the command was received.

+     */

+    public Date getTime() {

+        // Return a copy of the Date object to preserve immutability

+        return new Date(time.getTime());

+    }

+

+    /**

+     * Store the value for the specified key. If this object already contained a mapping

+     * for this key, the old value is replaced by the specified value. This method throws

+     * an <code>AssertFailedException</code> if this object has been locked. See {@link #lock()}.

+     *

+     * @param key   - the key; must not be null

+     * @param value - the value to store for the specified key

+     * @throws AssertFailedException - if the key is null or this object has been locked.

+     */

+    public void set(String key, Object value) {

+        Assert.notNull(key, "key");

+        Assert.isFalse(locked, "The InvocationRecord is locked!");

+        data.put(key, value);

+    }

+

+    /**

+     * Returns <code>true</code> if this object contains a mapping for the specified key.

+     *

+     * @param key - the key; must not be null

+     * @return <code>true</code> if there is a mapping for the key

+     * @throws AssertFailedException - if the key is null

+     */

+    public boolean containsKey(String key) {

+        Assert.notNull(key, "key");

+        return data.containsKey(key);

+    }

+

+    /**

+     * Returns a Set view of the keys for the data stored in this object.

+     * Changes to the returned Set have no effect on the data stored within this object

+     * .

+     *

+     * @return the Set of keys for the data stored within this object

+     */

+    public Set keySet() {

+        return Collections.unmodifiableSet(data.keySet());

+    }

+

+    /**

+     * Get the String value associated with the specified key. Returns null if there is

+     * no mapping for this key. A return value of null does not necessarily indicate that

+     * this object contains no mapping for the key; it's also possible that the value was

+     * explicitly set to null for the key. The containsKey operation may be used to

+     * distinguish these two cases.

+     *

+     * @param key - the key; must not be null

+     * @return the String data stored at the specified key; may be null

+     * @throws ClassCastException    - if the object for the specified key is not a String

+     * @throws AssertFailedException - if the key is null

+     */

+    public String getString(String key) {

+        Assert.notNull(key, "key");

+        return (String) data.get(key);

+    }

+

+    /**

+     * Get the Object value associated with the specified key. Returns null if there is

+     * no mapping for this key. A return value of null does not necessarily indicate that

+     * this object contains no mapping for the key; it's also possible that the value was

+     * explicitly set to null for the key. The containsKey operation may be used to

+     * distinguish these two cases.

+     *

+     * @param key - the key; must not be null

+     * @return the data stored at the specified key, as an Object; may be null

+     * @throws AssertFailedException - if the key is null

+     */

+    public Object getObject(String key) {

+        Assert.notNull(key, "key");

+        return data.get(key);

+    }

+

+    /**

+     * Return the String representation of this object

+     *

+     * @see java.lang.Object#toString()

+     */

+    public String toString() {

+        return "InvocationRecord[time=" + time + " client-host=" + clientHost + " command=" + command + " data=" + data + "]";

+    }

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/ReplyCodes.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/ReplyCodes.java
new file mode 100644
index 0000000..98c8b62
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/ReplyCodes.java
@@ -0,0 +1,83 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+/**

+ * Reply Code constants.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class ReplyCodes {

+

+    public static final int ABOR_OK = 226;

+    public static final int ACCT_OK = 230;

+    public static final int ALLO_OK = 200;

+    public static final int CDUP_OK = 200;

+    public static final int CWD_OK = 250;

+    public static final int DELE_OK = 250;

+    public static final int EPRT_OK = 200;

+    public static final int EPSV_OK = 229;

+    public static final int HELP_OK = 214;

+    public static final int MKD_OK = 257;

+    public static final int MODE_OK = 200;

+    public static final int NOOP_OK = 200;

+    public static final int PASS_OK = 230;

+    public static final int PASS_NEED_ACCOUNT = 332;

+    public static final int PASS_LOG_IN_FAILED = 530;

+    public static final int PASV_OK = 227;

+    public static final int PORT_OK = 200;

+    public static final int PWD_OK = 257;

+    public static final int QUIT_OK = 221;

+    public static final int REIN_OK = 220;

+    public static final int REST_OK = 350;

+    public static final int RMD_OK = 250;

+    public static final int RNFR_OK = 350;

+    public static final int RNTO_OK = 250;

+    public static final int SITE_OK = 200;

+    public static final int SMNT_OK = 250;

+    public static final int STAT_SYSTEM_OK = 211;

+    public static final int STAT_FILE_OK = 213;

+    public static final int STRU_OK = 200;

+    public static final int SYST_OK = 215;

+    public static final int TYPE_OK = 200;

+    public static final int USER_LOGGED_IN_OK = 230;

+    public static final int USER_NEED_PASSWORD_OK = 331;

+    public static final int USER_NO_SUCH_USER = 530;

+    public static final int USER_ACCOUNT_NOT_VALID = 530;

+

+    public static final int TRANSFER_DATA_INITIAL_OK = 150;

+    public static final int TRANSFER_DATA_FINAL_OK = 226;

+

+    public static final int CONNECT_OK = 220;

+

+    // GENERIC

+    public static final int SYSTEM_ERROR = 451;

+    public static final int COMMAND_SYNTAX_ERROR = 501;

+    public static final int COMMAND_NOT_SUPPORTED = 502;

+    public static final int ILLEGAL_STATE = 503;       // Bad sequence

+    public static final int NOT_LOGGED_IN = 530;

+    public static final int READ_FILE_ERROR = 550;

+    public static final int WRITE_FILE_ERROR = 553;

+    public static final int FILENAME_NOT_VALID = 553;

+

+    /**

+     * Private constructor. This class should not be instantiated.

+     */

+    private ReplyCodes() {

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/ReplyTextBundleAware.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/ReplyTextBundleAware.java
new file mode 100644
index 0000000..42b5955
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/ReplyTextBundleAware.java
@@ -0,0 +1,43 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import java.util.ResourceBundle;

+

+/**

+ * Interface for objects that allow getting and setting a reply text ResourceBundle. This

+ * interface is implemented by CommandHandlers so that the StubFtpServer can automatically

+ * set the default reply text ResourceBundle for the CommandHandler.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public interface ReplyTextBundleAware {

+

+    /**

+     * Return the ResourceBundle containing the reply text messages

+     * @return the replyTextBundle

+     */

+    public ResourceBundle getReplyTextBundle();

+

+    /**

+     * Set the ResourceBundle containing the reply text messages

+     * @param replyTextBundle - the replyTextBundle to set

+     */

+    public void setReplyTextBundle(ResourceBundle replyTextBundle);

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/ReplyTextBundleUtil.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/ReplyTextBundleUtil.java
new file mode 100644
index 0000000..b3fa0e4
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/ReplyTextBundleUtil.java
@@ -0,0 +1,53 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.mockftpserver.core.util.Assert;

+

+import java.util.ResourceBundle;

+

+/**

+ * Contains common utility method to conditionally set the reply text ResourceBundle on a

+ * CommandHandler instance.

+ * 

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+public final class ReplyTextBundleUtil {

+

+    /**

+     * Set the <code>replyTextBundle</code> property of the specified CommandHandler to the 

+     * <code>ResourceBundle</code> if and only if the <code>commandHandler</code> implements the 

+     * {@link ReplyTextBundleAware} interface AND its <code>replyTextBundle</code> property 

+     * has not been set (is null).

+     * 

+     * @param commandHandler - the CommandHandler instance

+     * @param replyTextBundle - the ResourceBundle to use for localizing reply text

+     * 

+     * @throws org.mockftpserver.core.util.AssertFailedException - if the commandHandler is null

+     */

+    public static void setReplyTextBundleIfAppropriate(CommandHandler commandHandler, ResourceBundle replyTextBundle) {

+        Assert.notNull(commandHandler, "commandHandler");

+        if (commandHandler instanceof ReplyTextBundleAware) {

+            ReplyTextBundleAware replyTextBundleAware = (ReplyTextBundleAware) commandHandler;

+            if (replyTextBundleAware.getReplyTextBundle() == null) {

+                replyTextBundleAware.setReplyTextBundle(replyTextBundle);

+            }

+        }

+    }

+    

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/SimpleCompositeCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/SimpleCompositeCommandHandler.java
new file mode 100644
index 0000000..520867f
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/SimpleCompositeCommandHandler.java
@@ -0,0 +1,129 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.Assert;

+import org.mockftpserver.core.util.AssertFailedException;

+

+import java.util.ArrayList;

+import java.util.Iterator;

+import java.util.List;

+import java.util.ResourceBundle;

+

+/**

+ * Composite CommandHandler that manages an internal list of CommandHandlers to which it delegates.

+ * The internal CommandHandlers are maintained in an ordered list. Starting with the first 

+ * CommandHandler in the list, each invocation of this composite handler will invoke (delegate to) 

+ * the current internal CommandHander. Then it moves on the next CommandHandler in the internal list.  

+ * <p>

+ * The following example replaces the CWD CommandHandler with a <code>SimpleCompositeCommandHandler</code>. 

+ * The first invocation of the CWD command will fail (reply code 500). The seconds will succeed.

+ * <pre><code>

+ * 

+ * StubFtpServer stubFtpServer = new StubFtpServer();

+ * 

+ * CommandHandler commandHandler1 = new StaticReplyCommandHandler(500);

+ * CommandHandler commandHandler2 = new CwdCommandHandler();

+ * 

+ * SimpleCompositeCommandHandler simpleCompositeCommandHandler = new SimpleCompositeCommandHandler();

+ * simpleCompositeCommandHandler.addCommandHandler(commandHandler1);

+ * simpleCompositeCommandHandler.addCommandHandler(commandHandler2);

+ * 

+ * stubFtpServer.setCommandHandler("CWD", simpleCompositeCommandHandler);

+ * </code></pre>

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class SimpleCompositeCommandHandler implements CommandHandler, ReplyTextBundleAware {

+

+    private List commandHandlers = new ArrayList();

+    private int invocationIndex = 0;

+    

+    /**

+     * Add a CommandHandler to the internal list of handlers.

+     * 

+     * @param commandHandler - the CommandHandler

+     *      

+     * @throws AssertFailedException - if the commandHandler is null      

+     */

+    public void addCommandHandler(CommandHandler commandHandler) {

+        Assert.notNull(commandHandler, "commandHandler");

+        commandHandlers.add(commandHandler);

+    }

+    

+    /**

+     * Set the List of CommandHandlers to which to delegate. This replaces any CommandHandlers that

+     * have been defined previously.

+     * @param commandHandlers - the complete List of CommandHandlers to which invocations are delegated

+     */

+    public void setCommandHandlers(List commandHandlers) {

+        Assert.notNull(commandHandlers, "commandHandlers");

+        this.commandHandlers = new ArrayList(commandHandlers);

+    }

+    

+    /**

+     * Return the CommandHandler corresponding to the specified invocation index. In other words, return

+     * the CommandHandler instance to which the Nth {@link #handleCommand(Command, Session)} has been or will

+     * be delegated (where N=index).

+     * @param index - the index of the desired invocation (zero-based).

+     * @return the CommandHandler

+     * 

+     * @throws AssertFailedException - if no CommandHandler is defined for the index or the index is not valid

+     */

+    public CommandHandler getCommandHandler(int index) {

+        Assert.isTrue(index < commandHandlers.size(), "No CommandHandler defined for index " + index);

+        Assert.isTrue(index >= 0, "The index cannot be less than zero: " + index);

+        return (CommandHandler) commandHandlers.get(index);

+    }

+    

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session) throws Exception {

+        Assert.notNull(command, "command");

+        Assert.notNull(session, "session");

+        Assert.isTrue(commandHandlers.size() > invocationIndex, "No CommandHandler defined for invocation #" + invocationIndex);

+        

+        CommandHandler commandHandler = (CommandHandler) commandHandlers.get(invocationIndex);

+        invocationIndex++;

+        commandHandler.handleCommand(command, session);

+    }

+

+    /**

+     * Returns null. This is a composite, and has no reply text bundle.

+     * 

+     * @see org.mockftpserver.core.command.ReplyTextBundleAware#getReplyTextBundle()

+     */

+    public ResourceBundle getReplyTextBundle() {

+        return null;

+    }

+

+    /**

+     * Call <code>setReplyTextBundle()</code> on each of the command handlers within the internal list.

+     * 

+     * @see org.mockftpserver.core.command.ReplyTextBundleAware#setReplyTextBundle(java.util.ResourceBundle)

+     */

+    public void setReplyTextBundle(ResourceBundle replyTextBundle) {

+        for (Iterator iter = commandHandlers.iterator(); iter.hasNext();) {

+            CommandHandler commandHandler = (CommandHandler) iter.next();

+            ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, replyTextBundle);

+        }

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/StaticReplyCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/StaticReplyCommandHandler.java
new file mode 100644
index 0000000..135039f
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/StaticReplyCommandHandler.java
@@ -0,0 +1,69 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.AssertFailedException;

+

+/**

+ * CommandHandler that sends back the configured reply code and text. You can customize the 

+ * returned reply code by setting the required <code>replyCode</code> property. If only the

+ * <code>replyCode</code> property is set, then the default reply text corresponding to that

+ * reply code is used in the response. You can optionally configure the reply text by setting

+ * the <code>replyMessageKey</code> or <code>replyText</code> property.

+ * <p>

+ * Each invocation record stored by this CommandHandler contains no data elements.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class StaticReplyCommandHandler extends AbstractStaticReplyCommandHandler {

+

+    /**

+     * Create a new uninitialized instance

+     */

+    public StaticReplyCommandHandler() {

+    }

+    

+    /**

+     * Create a new instance with the specified replyCode

+     * @param replyCode - the replyCode to use

+     * @throws AssertFailedException - if the replyCode is null

+     */

+    public StaticReplyCommandHandler(int replyCode) {

+        setReplyCode(replyCode);

+    }

+    

+    /**

+     * Create a new instance with the specified replyCode and replyText

+     * @param replyCode - the replyCode to use

+     * @param replyText - the replyText

+     * @throws AssertFailedException - if the replyCode is null

+     */

+    public StaticReplyCommandHandler(int replyCode, String replyText) {

+        setReplyCode(replyCode);

+        setReplyText(replyText);

+    }

+

+    /**

+     * @see AbstractTrackingCommandHandler#handleCommand(Command, org.mockftpserver.core.session.Session, InvocationRecord)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/command/UnsupportedCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/core/command/UnsupportedCommandHandler.java
new file mode 100644
index 0000000..d452327
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/command/UnsupportedCommandHandler.java
@@ -0,0 +1,49 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler that encapsulates the sending of the reply when a requested command is not

+ * recognized/supported. Send back a reply code of 502, indicating command not implemented.

+ * <p>

+ * Note that this is a "special" CommandHandler, in that it handles any unrecognized command,

+ * rather than an explicit FTP command.

+ * <p>

+ * Each invocation record stored by this CommandHandler contains no data elements.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class UnsupportedCommandHandler extends AbstractStaticReplyCommandHandler implements CommandHandler {

+

+    /**

+     * Constructor. Initiate the replyCode.

+     */

+    public UnsupportedCommandHandler() {

+        setReplyCode(ReplyCodes.COMMAND_NOT_SUPPORTED);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.AbstractTrackingCommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        LOG.warn("No CommandHandler is defined for command [" + command.getName() + "]");

+        sendReply(session, command.getName());

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/server/AbstractFtpServer.java b/tags/2.5/src/main/java/org/mockftpserver/core/server/AbstractFtpServer.java
new file mode 100644
index 0000000..149d652
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/server/AbstractFtpServer.java
@@ -0,0 +1,375 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.server;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.MockFtpServerException;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.session.DefaultSession;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.socket.DefaultServerSocketFactory;

+import org.mockftpserver.core.socket.ServerSocketFactory;

+import org.mockftpserver.core.util.Assert;

+

+import java.io.IOException;

+import java.net.*;

+import java.util.HashMap;

+import java.util.Iterator;

+import java.util.Map;

+import java.util.ResourceBundle;

+

+/**

+ * This is the abstract superclass for "mock" implementations of an FTP Server,

+ * suitable for testing FTP client code or standing in for a live FTP server. It supports

+ * the main FTP commands by defining handlers for each of the corresponding low-level FTP

+ * server commands (e.g. RETR, DELE, LIST). These handlers implement the {@link org.mockftpserver.core.command.CommandHandler}

+ * interface.

+ * <p/>

+ * By default, mock FTP Servers bind to the server control port of 21. You can use a different server control

+ * port by setting the <code>serverControlPort</code> property. If you specify a value of <code>0</code>, 

+ * then a free port number will be chosen automatically; call <code>getServerControlPort()</code> AFTER

+ * <code>start()</code> has been called to determine the actual port number being used. Using a non-default

+ * port number is usually necessary when running on Unix or some other system where that port number is

+ * already in use or cannot be bound from a user process.

+ * <p/>

+ * <h4>Command Handlers</h4>

+ * You can set the existing {@link CommandHandler} defined for an FTP server command

+ * by calling the {@link #setCommandHandler(String, CommandHandler)} method, passing

+ * in the FTP server command name and {@link CommandHandler} instance.

+ * You can also replace multiple command handlers at once by using the {@link #setCommandHandlers(Map)}

+ * method. That is especially useful when configuring the server through the <b>Spring Framework</b>.

+ * <p/>

+ * You can retrieve the existing {@link CommandHandler} defined for an FTP server command by

+ * calling the {@link #getCommandHandler(String)} method, passing in the FTP server command name.

+ * <p/>

+ * <h4>FTP Command Reply Text ResourceBundle</h4>

+ * The default text asociated with each FTP command reply code is contained within the

+ * "ReplyText.properties" ResourceBundle file. You can customize these messages by providing a

+ * locale-specific ResourceBundle file on the CLASSPATH, according to the normal lookup rules of

+ * the ResourceBundle class (e.g., "ReplyText_de.properties"). Alternatively, you can

+ * completely replace the ResourceBundle file by calling the calling the

+ * {@link #setReplyTextBaseName(String)} method.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ * @see org.mockftpserver.fake.FakeFtpServer

+ * @see org.mockftpserver.stub.StubFtpServer

+ */

+public abstract class AbstractFtpServer implements Runnable {

+

+    /**

+     * Default basename for reply text ResourceBundle

+     */

+    public static final String REPLY_TEXT_BASENAME = "ReplyText";

+    private static final int DEFAULT_SERVER_CONTROL_PORT = 21;

+

+    protected Logger LOG = LoggerFactory.getLogger(getClass());

+

+    // Simple value object that holds the socket and thread for a single session

+    private static class SessionInfo {

+        Socket socket;

+        Thread thread;

+    }

+

+    protected ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory();

+    private ServerSocket serverSocket = null;

+    private ResourceBundle replyTextBundle;

+    private volatile boolean terminate = false;

+    private Map commandHandlers;

+    private Thread serverThread;

+    private int serverControlPort = DEFAULT_SERVER_CONTROL_PORT;

+    private final Object startLock = new Object();

+

+    // Map of Session -> SessionInfo

+    private Map sessions = new HashMap();

+

+    /**

+     * Create a new instance. Initialize the default command handlers and

+     * reply text ResourceBundle.

+     */

+    public AbstractFtpServer() {

+        replyTextBundle = ResourceBundle.getBundle(REPLY_TEXT_BASENAME);

+        commandHandlers = new HashMap();

+    }

+

+    /**

+     * Start a new Thread for this server instance

+     */

+    public void start() {

+        serverThread = new Thread(this);

+

+        synchronized (startLock) {

+            try {

+                // Start here in case server thread runs faster than main thread.

+                // See https://sourceforge.net/tracker/?func=detail&atid=1006533&aid=1925590&group_id=208647

+                serverThread.start();

+

+                // Wait until the server thread is initialized

+                startLock.wait();

+            }

+            catch (InterruptedException e) {

+                e.printStackTrace();

+                throw new MockFtpServerException(e);

+            }

+        }

+    }

+

+    /**

+     * The logic for the server thread

+     *

+     * @see Runnable#run()

+     */

+    public void run() {

+        try {

+            LOG.info("Starting the server on port " + serverControlPort);

+            serverSocket = serverSocketFactory.createServerSocket(serverControlPort);

+            if (serverControlPort == 0) {

+                this.serverControlPort = serverSocket.getLocalPort();

+                LOG.info("Actual server port is " + this.serverControlPort);

+            }

+

+            // Notify to allow the start() method to finish and return

+            synchronized (startLock) {

+                startLock.notify();

+            }

+

+            while (!terminate) {

+                try {

+                    Socket clientSocket = serverSocket.accept();

+                    LOG.info("Connection accepted from host " + clientSocket.getInetAddress());

+

+                    Session session = createSession(clientSocket);

+                    Thread sessionThread = new Thread(session);

+                    sessionThread.start();

+

+                    SessionInfo sessionInfo = new SessionInfo();

+                    sessionInfo.socket = clientSocket;

+                    sessionInfo.thread = sessionThread;

+                    sessions.put(session, sessionInfo);

+                }

+                catch (SocketException e) {

+                    LOG.trace("Socket exception: " + e.toString());

+                }

+            }

+        }

+        catch (IOException e) {

+            LOG.error("Error", e);

+        }

+        finally {

+

+            LOG.debug("Cleaning up server...");

+

+            // Ensure that the start() method is not still blocked

+            synchronized (startLock) {

+                startLock.notifyAll();

+            }

+

+            try {

+                if (serverSocket != null) {

+                    serverSocket.close();

+                }

+                closeSessions();

+            }

+            catch (IOException e) {

+                LOG.error("Error cleaning up server", e);

+            }

+            catch (InterruptedException e) {

+                LOG.error("Error cleaning up server", e);

+            }

+            LOG.info("Server stopped.");

+            terminate = false;

+        }

+    }

+

+    /**

+     * Stop this server instance and wait for it to terminate.

+     */

+    public void stop() {

+

+        LOG.trace("Stopping the server...");

+        terminate = true;

+

+        if (serverSocket != null) {

+            try {

+                serverSocket.close();

+            } catch (IOException e) {

+                throw new MockFtpServerException(e);

+            }

+        }

+

+        try {

+            if (serverThread != null) {

+                serverThread.join();

+            }

+        }

+        catch (InterruptedException e) {

+            e.printStackTrace();

+            throw new MockFtpServerException(e);

+        }

+    }

+

+    /**

+     * Return the CommandHandler defined for the specified command name

+     *

+     * @param name - the command name

+     * @return the CommandHandler defined for name

+     */

+    public CommandHandler getCommandHandler(String name) {

+        return (CommandHandler) commandHandlers.get(Command.normalizeName(name));

+    }

+

+    /**

+     * Override the default CommandHandlers with those in the specified Map of

+     * commandName>>CommandHandler. This will only override the default CommandHandlers

+     * for the keys in <code>commandHandlerMapping</code>. All other default CommandHandler

+     * mappings remain unchanged.

+     *

+     * @param commandHandlerMapping - the Map of commandName->CommandHandler; these override the defaults

+     * @throws org.mockftpserver.core.util.AssertFailedException

+     *          - if the commandHandlerMapping is null

+     */

+    public void setCommandHandlers(Map commandHandlerMapping) {

+        Assert.notNull(commandHandlerMapping, "commandHandlers");

+        for (Iterator iter = commandHandlerMapping.keySet().iterator(); iter.hasNext();) {

+            String commandName = (String) iter.next();

+            setCommandHandler(commandName, (CommandHandler) commandHandlerMapping.get(commandName));

+        }

+    }

+

+    /**

+     * Set the CommandHandler for the specified command name. If the CommandHandler implements

+     * the {@link org.mockftpserver.core.command.ReplyTextBundleAware} interface and its <code>replyTextBundle</code> attribute

+     * is null, then set its <code>replyTextBundle</code> to the <code>replyTextBundle</code> of

+     * this StubFtpServer.

+     *

+     * @param commandName    - the command name to which the CommandHandler will be associated

+     * @param commandHandler - the CommandHandler

+     * @throws org.mockftpserver.core.util.AssertFailedException

+     *          - if the commandName or commandHandler is null

+     */

+    public void setCommandHandler(String commandName, CommandHandler commandHandler) {

+        Assert.notNull(commandName, "commandName");

+        Assert.notNull(commandHandler, "commandHandler");

+        commandHandlers.put(Command.normalizeName(commandName), commandHandler);

+        initializeCommandHandler(commandHandler);

+    }

+

+    /**

+     * Set the reply text ResourceBundle to a new ResourceBundle with the specified base name,

+     * accessible on the CLASSPATH. See {@link java.util.ResourceBundle#getBundle(String)}.

+     *

+     * @param baseName - the base name of the resource bundle, a fully qualified class name

+     */

+    public void setReplyTextBaseName(String baseName) {

+        replyTextBundle = ResourceBundle.getBundle(baseName);

+    }

+

+    /**

+     * Return the ReplyText ResourceBundle. Set the bundle through the  {@link #setReplyTextBaseName(String)}  method.

+     *

+     * @return the reply text ResourceBundle

+     */

+    public ResourceBundle getReplyTextBundle() {

+        return replyTextBundle;

+    }

+

+    /**

+     * Set the port number to which the server control connection socket will bind. The default value is 21.

+     *

+     * @param serverControlPort - the port number for the server control connection ServerSocket

+     */

+    public void setServerControlPort(int serverControlPort) {

+        this.serverControlPort = serverControlPort;

+    }

+

+    /**

+     * Return the port number to which the server control connection socket will bind. The default value is 21.

+     *

+     * @return the port number for the server control connection ServerSocket

+     */

+    public int getServerControlPort() {

+        return serverControlPort;

+    }

+

+    /**

+     * Return true if this server is fully shutdown -- i.e., there is no active (alive) threads and

+     * all sockets are closed. This method is intended for testing only.

+     *

+     * @return true if this server is fully shutdown

+     */

+    public boolean isShutdown() {

+        boolean shutdown = !serverThread.isAlive() && serverSocket.isClosed();

+

+        for (Iterator iter = sessions.values().iterator(); iter.hasNext();) {

+            SessionInfo sessionInfo = (SessionInfo) iter.next();

+            shutdown = shutdown && sessionInfo.socket.isClosed() && !sessionInfo.thread.isAlive();

+        }

+        return shutdown;

+    }

+

+    /**

+     * Return true if this server has started -- i.e., there is an active (alive) server threads

+     * and non-null server socket. This method is intended for testing only.

+     *

+     * @return true if this server has started

+     */

+    public boolean isStarted() {

+        return serverThread != null && serverThread.isAlive() && serverSocket != null;

+    }

+

+    //-------------------------------------------------------------------------

+    // Internal Helper Methods

+    //-------------------------------------------------------------------------

+

+    /**

+     * Create a new Session instance for the specified client Socket

+     *

+     * @param clientSocket - the Socket associated with the client

+     * @return a Session

+     */

+    protected Session createSession(Socket clientSocket) {

+        return new DefaultSession(clientSocket, commandHandlers);

+    }

+

+    private void closeSessions() throws InterruptedException, IOException {

+        for (Iterator iter = sessions.entrySet().iterator(); iter.hasNext();) {

+            Map.Entry entry = (Map.Entry) iter.next();

+            Session session = (Session) entry.getKey();

+            SessionInfo sessionInfo = (SessionInfo) entry.getValue();

+            session.close();

+            sessionInfo.thread.join(500L);

+            Socket sessionSocket = sessionInfo.socket;

+            if (sessionSocket != null) {

+                sessionSocket.close();

+            }

+        }

+    }

+

+    //------------------------------------------------------------------------------------

+    // Abstract method declarations

+    //------------------------------------------------------------------------------------

+

+    /**

+     * Initialize a CommandHandler that has been registered to this server. What "initialization"

+     * means is dependent on the subclass implementation.

+     *

+     * @param commandHandler - the CommandHandler to initialize

+     */

+    protected abstract void initializeCommandHandler(CommandHandler commandHandler);

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/session/DefaultSession.java b/tags/2.5/src/main/java/org/mockftpserver/core/session/DefaultSession.java
new file mode 100644
index 0000000..3a8ada6
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/session/DefaultSession.java
@@ -0,0 +1,483 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.session;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.MockFtpServerException;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.socket.DefaultServerSocketFactory;

+import org.mockftpserver.core.socket.DefaultSocketFactory;

+import org.mockftpserver.core.socket.ServerSocketFactory;

+import org.mockftpserver.core.socket.SocketFactory;

+import org.mockftpserver.core.util.Assert;

+import org.mockftpserver.core.util.AssertFailedException;

+

+import java.io.BufferedReader;

+import java.io.ByteArrayOutputStream;

+import java.io.IOException;

+import java.io.InputStream;

+import java.io.InputStreamReader;

+import java.io.OutputStream;

+import java.io.PrintWriter;

+import java.io.Writer;

+import java.net.InetAddress;

+import java.net.ServerSocket;

+import java.net.Socket;

+import java.net.SocketTimeoutException;

+import java.util.ArrayList;

+import java.util.HashMap;

+import java.util.List;

+import java.util.Map;

+import java.util.Set;

+import java.util.StringTokenizer;

+

+/**

+ * Default implementation of the {@link Session} interface.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class DefaultSession implements Session {

+

+    private static final Logger LOG = LoggerFactory.getLogger(DefaultSession.class);

+    private static final String END_OF_LINE = "\r\n";

+    protected static final int DEFAULT_CLIENT_DATA_PORT = 21;

+

+    protected SocketFactory socketFactory = new DefaultSocketFactory();

+    protected ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory();

+

+    private BufferedReader controlConnectionReader;

+    private Writer controlConnectionWriter;

+    private Socket controlSocket;

+    private Socket dataSocket;

+    ServerSocket passiveModeDataSocket; // non-private for testing

+    private InputStream dataInputStream;

+    private OutputStream dataOutputStream;

+    private Map commandHandlers;

+    private int clientDataPort = DEFAULT_CLIENT_DATA_PORT;

+    private InetAddress clientHost;

+    private InetAddress serverHost;

+    private Map attributes = new HashMap();

+    private volatile boolean terminate = false;

+

+    /**

+     * Create a new initialized instance

+     *

+     * @param controlSocket   - the control connection socket

+     * @param commandHandlers - the Map of command name -> CommandHandler. It is assumed that the

+     *                        command names are all normalized to upper case. See {@link Command#normalizeName(String)}.

+     */

+    public DefaultSession(Socket controlSocket, Map commandHandlers) {

+        Assert.notNull(controlSocket, "controlSocket");

+        Assert.notNull(commandHandlers, "commandHandlers");

+

+        this.controlSocket = controlSocket;

+        this.commandHandlers = commandHandlers;

+        this.serverHost = controlSocket.getLocalAddress();

+    }

+

+    /**

+     * Return the InetAddress representing the client host for this session

+     *

+     * @return the client host

+     * @see org.mockftpserver.core.session.Session#getClientHost()

+     */

+    public InetAddress getClientHost() {

+        return controlSocket.getInetAddress();

+    }

+

+    /**

+     * Return the InetAddress representing the server host for this session

+     *

+     * @return the server host

+     * @see org.mockftpserver.core.session.Session#getServerHost()

+     */

+    public InetAddress getServerHost() {

+        return serverHost;

+    }

+

+    /**

+     * Send the specified reply code and text across the control connection.

+     * The reply text is trimmed before being sent.

+     *

+     * @param code - the reply code

+     * @param text - the reply text to send; may be null

+     */

+    public void sendReply(int code, String text) {

+        assertValidReplyCode(code);

+

+        StringBuffer buffer = new StringBuffer(Integer.toString(code));

+

+        if (text != null && text.length() > 0) {

+            String replyText = text.trim();

+            if (replyText.indexOf("\n") != -1) {

+                int lastIndex = replyText.lastIndexOf("\n");

+                buffer.append("-");

+                for (int i = 0; i < replyText.length(); i++) {

+                    char c = replyText.charAt(i);

+                    buffer.append(c);

+                    if (i == lastIndex) {

+                        buffer.append(Integer.toString(code));

+                        buffer.append(" ");

+                    }

+                }

+            } else {

+                buffer.append(" ");

+                buffer.append(replyText);

+            }

+        }

+        LOG.debug("Sending Reply [" + buffer.toString() + "]");

+        writeLineToControlConnection(buffer.toString());

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#openDataConnection()

+     */

+    public void openDataConnection() {

+        try {

+            if (passiveModeDataSocket != null) {

+                LOG.debug("Waiting for (passive mode) client connection from client host [" + clientHost

+                        + "] on port " + passiveModeDataSocket.getLocalPort());

+                // TODO set socket timeout

+                try {

+                    dataSocket = passiveModeDataSocket.accept();

+                    LOG.debug("Successful (passive mode) client connection to port "

+                            + passiveModeDataSocket.getLocalPort());

+                }

+                catch (SocketTimeoutException e) {

+                    throw new MockFtpServerException(e);

+                }

+            } else {

+                Assert.notNull(clientHost, "clientHost");

+                LOG.debug("Connecting to client host [" + clientHost + "] on data port [" + clientDataPort

+                        + "]");

+                dataSocket = socketFactory.createSocket(clientHost, clientDataPort);

+            }

+            dataOutputStream = dataSocket.getOutputStream();

+            dataInputStream = dataSocket.getInputStream();

+        }

+        catch (IOException e) {

+            throw new MockFtpServerException(e);

+        }

+    }

+

+    /**

+     * Switch to passive mode

+     *

+     * @return the local port to be connected to by clients for data transfers

+     * @see org.mockftpserver.core.session.Session#switchToPassiveMode()

+     */

+    public int switchToPassiveMode() {

+        try {

+            passiveModeDataSocket = serverSocketFactory.createServerSocket(0);

+            return passiveModeDataSocket.getLocalPort();

+        }

+        catch (IOException e) {

+            throw new MockFtpServerException("Error opening passive mode server data socket", e);

+        }

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#closeDataConnection()

+     */

+    public void closeDataConnection() {

+        try {

+            LOG.debug("Flushing and closing client data socket");

+            dataOutputStream.flush();

+            dataOutputStream.close();

+            dataInputStream.close();

+            dataSocket.close();

+        }

+        catch (IOException e) {

+            LOG.error("Error closing client data socket", e);

+        }

+    }

+

+    /**

+     * Write a single line to the control connection, appending a newline

+     *

+     * @param line - the line to write

+     */

+    private void writeLineToControlConnection(String line) {

+        try {

+            controlConnectionWriter.write(line + END_OF_LINE);

+            controlConnectionWriter.flush();

+        }

+        catch (IOException e) {

+            LOG.error("Error writing to control connection", e);

+            throw new MockFtpServerException("Error writing to control connection", e);

+        }

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#close()

+     */

+    public void close() {

+        LOG.trace("close()");

+        terminate = true;

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#sendData(byte[], int)

+     */

+    public void sendData(byte[] data, int numBytes) {

+        Assert.notNull(data, "data");

+        try {

+            dataOutputStream.write(data, 0, numBytes);

+        }

+        catch (IOException e) {

+            throw new MockFtpServerException(e);

+        }

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#readData()

+     */

+    public byte[] readData() {

+        return readData(Integer.MAX_VALUE);

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#readData()

+     */

+    public byte[] readData(int numBytes) {

+        ByteArrayOutputStream bytes = new ByteArrayOutputStream();

+        int numBytesRead = 0;

+        try {

+            while (numBytesRead < numBytes) {

+                int b = dataInputStream.read();

+                if (b == -1) {

+                    break;

+                }

+                bytes.write(b);

+                numBytesRead++;

+            }

+            return bytes.toByteArray();

+        }

+        catch (IOException e) {

+            throw new MockFtpServerException(e);

+        }

+    }

+

+    /**

+     * Wait for and read the command sent from the client on the control connection.

+     *

+     * @return the Command sent from the client; may be null if the session has been closed

+     *         <p/>

+     *         Package-private to enable testing

+     */

+    Command readCommand() {

+

+        final long socketReadIntervalMilliseconds = 20L;

+

+        try {

+            while (true) {

+                if (terminate) {

+                    return null;

+                }

+                // Don't block; only read command when it is available

+                if (controlConnectionReader.ready()) {

+                    String command = controlConnectionReader.readLine();

+                    LOG.info("Received command: [" + command + "]");

+                    return parseCommand(command);

+                }

+                try {

+                    Thread.sleep(socketReadIntervalMilliseconds);

+                }

+                catch (InterruptedException e) {

+                    throw new MockFtpServerException(e);

+                }

+            }

+        }

+        catch (IOException e) {

+            LOG.error("Read failed", e);

+            throw new MockFtpServerException(e);

+        }

+    }

+

+    /**

+     * Parse the command String into a Command object

+     *

+     * @param commandString - the command String

+     * @return the Command object parsed from the command String

+     */

+    Command parseCommand(String commandString) {

+        Assert.notNullOrEmpty(commandString, "commandString");

+

+        List parameters = new ArrayList();

+        String name;

+

+        int indexOfFirstSpace = commandString.indexOf(" ");

+        if (indexOfFirstSpace != -1) {

+            name = commandString.substring(0, indexOfFirstSpace);

+            StringTokenizer tokenizer = new StringTokenizer(commandString.substring(indexOfFirstSpace + 1),

+                    ",");

+            while (tokenizer.hasMoreTokens()) {

+                parameters.add(tokenizer.nextToken());

+            }

+        } else {

+            name = commandString;

+        }

+

+        String[] parametersArray = new String[parameters.size()];

+        return new Command(name, (String[]) parameters.toArray(parametersArray));

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#setClientDataHost(java.net.InetAddress)

+     */

+    public void setClientDataHost(InetAddress clientHost) {

+        this.clientHost = clientHost;

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#setClientDataPort(int)

+     */

+    public void setClientDataPort(int dataPort) {

+        this.clientDataPort = dataPort;

+

+        // Clear out any passive data connection mode information

+        if (passiveModeDataSocket != null) {

+            try {

+                this.passiveModeDataSocket.close();

+            }

+            catch (IOException e) {

+                throw new MockFtpServerException(e);

+            }

+            passiveModeDataSocket = null;

+        }

+    }

+

+    /**

+     * @see java.lang.Runnable#run()

+     */

+    public void run() {

+        try {

+

+            InputStream inputStream = controlSocket.getInputStream();

+            OutputStream outputStream = controlSocket.getOutputStream();

+            controlConnectionReader = new BufferedReader(new InputStreamReader(inputStream));

+            controlConnectionWriter = new PrintWriter(outputStream, true);

+

+            LOG.debug("Starting the session...");

+

+            CommandHandler connectCommandHandler = (CommandHandler) commandHandlers.get(CommandNames.CONNECT);

+            connectCommandHandler.handleCommand(new Command(CommandNames.CONNECT, new String[0]), this);

+

+            while (!terminate) {

+                readAndProcessCommand();

+            }

+        }

+        catch (Exception e) {

+            LOG.error("Error:", e);

+            throw new MockFtpServerException(e);

+        }

+        finally {

+            LOG.debug("Cleaning up the session");

+            try {

+                controlConnectionReader.close();

+                controlConnectionWriter.close();

+            }

+            catch (IOException e) {

+                LOG.error("Error:", e);

+            }

+            LOG.debug("Session stopped.");

+        }

+    }

+

+    /**

+     * Read and process the next command from the control connection

+     *

+     * @throws Exception - if any error occurs

+     */

+    private void readAndProcessCommand() throws Exception {

+

+        Command command = readCommand();

+        if (command != null) {

+            String normalizedCommandName = Command.normalizeName(command.getName());

+            CommandHandler commandHandler = (CommandHandler) commandHandlers.get(normalizedCommandName);

+

+            if (commandHandler == null) {

+                commandHandler = (CommandHandler) commandHandlers.get(CommandNames.UNSUPPORTED);

+            }

+

+            Assert.notNull(commandHandler, "CommandHandler for command [" + normalizedCommandName + "]");

+            commandHandler.handleCommand(command, this);

+        }

+    }

+

+    /**

+     * Assert that the specified number is a valid reply code

+     *

+     * @param replyCode - the reply code to check

+     */

+    private void assertValidReplyCode(int replyCode) {

+        Assert.isTrue(replyCode > 0, "The number [" + replyCode + "] is not a valid reply code");

+    }

+

+    /**

+     * Return the attribute value for the specified name. Return null if no attribute value

+     * exists for that name or if the attribute value is null.

+     *

+     * @param name - the attribute name; may not be null

+     * @return the value of the attribute stored under name; may be null

+     * @see org.mockftpserver.core.session.Session#getAttribute(java.lang.String)

+     */

+    public Object getAttribute(String name) {

+        Assert.notNull(name, "name");

+        return attributes.get(name);

+    }

+

+    /**

+     * Store the value under the specified attribute name.

+     *

+     * @param name  - the attribute name; may not be null

+     * @param value - the attribute value; may be null

+     * @see org.mockftpserver.core.session.Session#setAttribute(java.lang.String, java.lang.Object)

+     */

+    public void setAttribute(String name, Object value) {

+        Assert.notNull(name, "name");

+        attributes.put(name, value);

+    }

+

+    /**

+     * Return the Set of names under which attributes have been stored on this session.

+     * Returns an empty Set if no attribute values are stored.

+     *

+     * @return the Set of attribute names

+     * @see org.mockftpserver.core.session.Session#getAttributeNames()

+     */

+    public Set getAttributeNames() {

+        return attributes.keySet();

+    }

+

+    /**

+     * Remove the attribute value for the specified name. Do nothing if no attribute

+     * value is stored for the specified name.

+     *

+     * @param name - the attribute name; may not be null

+     * @throws AssertFailedException - if name is null

+     * @see org.mockftpserver.core.session.Session#removeAttribute(java.lang.String)

+     */

+    public void removeAttribute(String name) {

+        Assert.notNull(name, "name");

+        attributes.remove(name);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/session/Session.java b/tags/2.5/src/main/java/org/mockftpserver/core/session/Session.java
new file mode 100644
index 0000000..9143f77
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/session/Session.java
@@ -0,0 +1,135 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.session;

+

+import java.net.InetAddress;

+import java.util.Set;

+

+/**

+ * Represents an FTP session state and behavior

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public interface Session extends Runnable {

+

+    /**

+     * Close the session, closing the underlying sockets

+     */

+    public void close();

+

+    /**

+     * Send the specified reply code and text across the control connection.

+     * 

+     * @param replyCode - the reply code

+     * @param replyText - the reply text to send; may be null

+     */

+    public void sendReply(int replyCode, String replyText);

+

+    /**

+     * Open the data connection, attaching to the predefined port number on the client

+     */

+    public void openDataConnection();

+

+    /**

+     * Close the data connection

+     */

+    public void closeDataConnection();

+

+    /**

+     * Switch to passive mode

+     * @return the local port to be connected to by clients for data transfers

+     */

+    public int switchToPassiveMode();

+    

+    /**

+     * Write the specified data using the data connection

+     * 

+     * @param data - the data to write

+     * @param numBytes - the number of bytes from data to send

+     */

+    public void sendData(byte[] data, int numBytes);

+

+    /**

+     * Read data from the client across the data connection

+     * 

+     * @return the data that was read

+     */

+    public byte[] readData();

+

+    /**

+     * Read and return (up to) numBytes of data from the client across the data connection

+     *

+     * @return the data that was read; the byte[] will be up to numBytes bytes long

+     */

+    public byte[] readData(int numBytes);

+

+    /**

+     * Return the InetAddress representing the client host for this session

+     * @return the client host

+     */

+    public InetAddress getClientHost();

+    

+    /**

+     * Return the InetAddress representing the server host for this session

+     * @return the server host

+     */

+    public InetAddress getServerHost();

+    

+    /**

+     * @param clientHost - the client host for the data connection

+     */

+    public void setClientDataHost(InetAddress clientHost);

+

+    /**

+     * @param clientDataPort - the port number on the client side for the data connection

+     */

+    public void setClientDataPort(int clientDataPort);

+

+    /**

+     * Return the attribute value for the specified name. Return null if no attribute value

+     * exists for that name or if the attribute value is null.

+     * @param name - the attribute name; may not be null

+     * @return the value of the attribute stored under name; may be null

+     * @throws AssertFailedException - if name is null

+     */

+    public Object getAttribute(String name);

+    

+    /**

+     * Store the value under the specified attribute name.

+     * @param name - the attribute name; may not be null

+     * @param value - the attribute value; may be null

+     * @throws AssertFailedException - if name is null

+     */

+    public void setAttribute(String name, Object value);

+    

+    /**

+     * Remove the attribute value for the specified name. Do nothing if no attribute

+     * value is stored for the specified name.

+     * @param name - the attribute name; may not be null

+     * @throws AssertFailedException - if name is null

+     */

+    public void removeAttribute(String name);

+

+    /**

+     * Return the Set of names under which attributes have been stored on this session.

+     * Returns an empty Set if no attribute values are stored.

+     * @return the Set of attribute names

+     */

+    public Set getAttributeNames();

+    

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/session/SessionKeys.java b/tags/2.5/src/main/java/org/mockftpserver/core/session/SessionKeys.java
new file mode 100644
index 0000000..7a8b1cf
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/session/SessionKeys.java
@@ -0,0 +1,30 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.core.session;

+

+/**

+ * Constants for names of properties (attributes) stored in the session.

+ */

+public class SessionKeys {

+

+    public static final String USERNAME = "username";

+    public static final String USER_ACCOUNT = "userAccount";

+    public static final String CURRENT_DIRECTORY = "currentDirectory";

+    public static final String RENAME_FROM = "renameFrom";

+    public static final String ACCOUNT_NAME = "accountName";

+    public static final String ASCII_TYPE = "asciiType";

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/socket/DefaultServerSocketFactory.java b/tags/2.5/src/main/java/org/mockftpserver/core/socket/DefaultServerSocketFactory.java
new file mode 100644
index 0000000..ac6cd0f
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/socket/DefaultServerSocketFactory.java
@@ -0,0 +1,41 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.socket;

+

+import java.io.IOException;

+import java.net.ServerSocket;

+

+/**

+ * Default implementation of the {@link ServerSocketFactory}; creates standard {@link ServerSocket} instances.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public class DefaultServerSocketFactory implements ServerSocketFactory {

+    

+    /**

+     * Create a new ServerSocket for the specified port.

+     * @param port - the port

+     * @return a new ServerSocket

+     * @throws IOException

+

+     * @see org.mockftpserver.core.socket.ServerSocketFactory#createServerSocket(int)

+     */

+    public ServerSocket createServerSocket(int port) throws IOException {

+        return new ServerSocket(port);

+    }

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/socket/DefaultSocketFactory.java b/tags/2.5/src/main/java/org/mockftpserver/core/socket/DefaultSocketFactory.java
new file mode 100644
index 0000000..0a365e7
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/socket/DefaultSocketFactory.java
@@ -0,0 +1,43 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.socket;

+

+import java.io.IOException;

+import java.net.InetAddress;

+import java.net.Socket;

+

+/**

+ * Default implementation of the {@link SocketFactory}; creates standard {@link Socket} instances.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public class DefaultSocketFactory implements SocketFactory {

+    

+    /**

+     * Create a new Socket instance for the specified host and port.

+     * @param host - the IP address of the host endpoint to which the socket is connect

+     * @param port - the port number of the enpoint to which the socket is connected

+     * @return a new Socket

+     * @throws IOException

+     * 

+     * @see org.mockftpserver.core.socket.SocketFactory#createSocket(java.net.InetAddress, int)

+     */

+    public Socket createSocket(InetAddress host, int port) throws IOException {

+        return new Socket(host, port);

+    }

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/socket/ServerSocketFactory.java b/tags/2.5/src/main/java/org/mockftpserver/core/socket/ServerSocketFactory.java
new file mode 100644
index 0000000..af810a1
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/socket/ServerSocketFactory.java
@@ -0,0 +1,38 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.socket;

+

+import java.io.IOException;

+import java.net.ServerSocket;

+

+/**

+ * Interface for factory that creates new {@link ServerSocket} instances.

+ * Using this abstraction enables unit testing.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public interface ServerSocketFactory {

+    

+    /**

+     * Create a new ServerSocket for the specified port

+     * @param port - the port

+     * @return a new ServerSocket

+     * @throws IOException

+     */

+    public ServerSocket createServerSocket(int port) throws IOException;

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/socket/SocketFactory.java b/tags/2.5/src/main/java/org/mockftpserver/core/socket/SocketFactory.java
new file mode 100644
index 0000000..57d9778
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/socket/SocketFactory.java
@@ -0,0 +1,40 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.socket;

+

+import java.io.IOException;

+import java.net.InetAddress;

+import java.net.Socket;

+

+/**

+ * Interface for factory that create new {@link Socket} instances.

+ * Using this abstraction enables unit testing.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public interface SocketFactory {

+

+    /**

+     * Create a new Socket instance for the specified host and port.

+     * @param host - the IP address of the host endpoint to which the socket is connect

+     * @param port - the port number of the enpoint to which the socket is connected

+     * @return a new Socket

+     * @throws IOException

+     */

+    public Socket createSocket(InetAddress host, int port) throws IOException;

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/util/Assert.java b/tags/2.5/src/main/java/org/mockftpserver/core/util/Assert.java
new file mode 100644
index 0000000..de4a8cd
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/util/Assert.java
@@ -0,0 +1,138 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.util;

+

+import java.util.Collection;

+import java.util.Map;

+

+/**

+ * Provides static helper methods to make runtime assertions. Throws an

+ * <code>AssertFailedException</code> when the assertion fails. All methods are static.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class Assert {

+    

+    /**

+     * Verify that arg is null. Throw an AssertFailedException if it is not null.

+     * @param arg - the method parameter value

+     * @param argName - the name of the parameter; used in the exception message

+     * @throws AssertFailedException - if arg is not null

+     */

+    public static void isNull(Object arg, String argName) {

+        if (arg != null) {

+            throw new AssertFailedException("The value for \"" + argName + "\" must be null");

+        }

+    }

+

+	/**

+	 * Verify that arg is not null. Throw an AssertFailedException if it is null.

+	 * @param arg - the method parameter value

+     * @param argName - the name of the parameter; used in the exception message

+	 * @throws AssertFailedException - if arg is null

+	 */

+    public static void notNull(Object arg, String argName) {

+		if (arg == null) {

+            throw new AssertFailedException("The value of \"" + argName + "\" is null");

+		}

+	}

+

+	/**

+	 * Verify that condition is true. Throw an AssertFailedException if it is false.

+	 * @param condition - the condition that should be true

+	 * @throws AssertFailedException - if condition is false

+	 */

+	public static void isTrue(boolean condition, String message) {

+		if (!condition) {

+			throw new AssertFailedException(message);

+		}

+	}

+

+	/**

+	 * Verify that condition is false. Throw an AssertFailedException if it is true.

+	 * @param condition - the condition that should be false

+	 * @throws AssertFailedException - if condition is true

+	 */

+	public static void isFalse(boolean condition, String message) {

+		if (condition) {

+			throw new AssertFailedException(message);

+		}

+	}

+

+	/**

+	 * Verify that the collection is not null or empty. Throw an 

+	 * AssertFailedException if it is null or empty.

+	 * @param collection - the Collection

+     * @param argName - the name of the parameter; used in the exception message

+	 * @throws AssertFailedException - if collection is null or empty

+	 */

+    public static void notNullOrEmpty(Collection collection, String argName) {

+        notNull(collection, argName);

+		if (collection.isEmpty()) {

+            throw new AssertFailedException("The \"" + argName + "\" Collection is empty");

+		}

+	}

+

+	/**

+	 * Verify that the Map is not null or empty. Throw an AssertFailedException 

+	 * if it is null or empty.

+	 * @param map - the Map

+     * @param argName - the name of the parameter; used in the exception message

+	 * @throws AssertFailedException - if map is null or empty

+	 */

+    public static void notNullOrEmpty(Map map, String argName) {

+        notNull(map, argName);

+		if (map.isEmpty()) {

+            throw new AssertFailedException("The \"" + argName + "\" Map is empty");

+		}

+	}

+

+	/**

+	 * Verify that the array is not null or empty. Throw an 

+	 * AssertFailedException if it is null or empty.

+	 * @param array - the array

+     * @param argName - the name of the parameter; used in the exception message

+	 * @throws AssertFailedException - if array is null or empty

+	 */

+    public static void notNullOrEmpty(Object[] array, String argName) {

+        notNull(array, argName);

+		if (array.length == 0) {

+            throw new AssertFailedException("The \"" + argName + "\" array is empty");

+		}

+	}

+

+	/**

+	 * Verify that the String is not null or empty. Throw an 

+	 * AssertFailedException if it is null or empty.

+	 * @param string - the String

+     * @param argName - the name of the parameter; used in the exception message

+	 * @throws AssertFailedException - if string is null or empty

+	 */

+    public static void notNullOrEmpty(String string, String argName) {

+        notNull(string, argName);

+		if (string.trim().length() == 0) {

+            throw new AssertFailedException("The \"" + argName + "\" String is empty");

+		}

+	}

+

+	/**

+	 * Private constructor. All methods are static

+	 */

+	private Assert() {

+	}

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/util/AssertFailedException.java b/tags/2.5/src/main/java/org/mockftpserver/core/util/AssertFailedException.java
new file mode 100644
index 0000000..a0b190e
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/util/AssertFailedException.java
@@ -0,0 +1,36 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.util;

+

+/**

+ * Exception that indicates that a runtime assertion from the 

+ * {@link org.mockftpserver.core.util.Assert} has failed. 

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class AssertFailedException extends RuntimeException {

+

+    /**

+     * Create a new instance for the specified message

+     * @param message - the exception message

+     */

+    public AssertFailedException(final String message) {

+        super(message);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/util/HostAndPort.java b/tags/2.5/src/main/java/org/mockftpserver/core/util/HostAndPort.java
new file mode 100644
index 0000000..8fa32d4
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/util/HostAndPort.java
@@ -0,0 +1,42 @@
+/*

+ * Copyright 2009 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.util;

+

+import java.net.InetAddress;

+

+/**

+ * A data-only (transfer) object representing a host (InetAddress) and port number

+ * that together uniquely identify an endpoint for a socket connection.

+ *

+ * This class contains two public properties: host (java.net.InetAddress) and port (int).

+ *

+ * @author Chris Mair

+ * @version : $ - :  $

+ */

+public class HostAndPort {

+    public InetAddress host;

+    public int port;

+

+    /**

+     * Construct a new instance with the specified host and port

+     * @param host - the InetAddress host

+     * @param port - the port number

+     */

+    public HostAndPort(InetAddress host, int port) {

+        this.host = host;

+        this.port = port;

+    }

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/util/IoUtil.java b/tags/2.5/src/main/java/org/mockftpserver/core/util/IoUtil.java
new file mode 100644
index 0000000..c6f2055
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/util/IoUtil.java
@@ -0,0 +1,63 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.core.util;

+

+import java.io.ByteArrayOutputStream;

+import java.io.IOException;

+import java.io.InputStream;

+

+/**

+ * Contains static I/O-related utility methods.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class IoUtil {

+

+    /**

+     * Read the contents of the InputStream and return as a byte[].

+     *

+     * @param input - the InputStream to read

+     * @return the contents of the InputStream as a byte[]

+     * @throws AssertFailedException - if the InputStream is null

+     * @throws java.io.IOException   - if an error occurs reading the bytes

+     */

+    public static byte[] readBytes(InputStream input) throws IOException {

+        Assert.notNull(input, "input");

+        ByteArrayOutputStream outBytes = new ByteArrayOutputStream();

+

+        try {

+            while (true) {

+                int b = input.read();

+                if (b == -1) {

+                    break;

+                }

+                outBytes.write(b);

+            }

+        }

+        finally {

+            input.close();

+        }

+        return outBytes.toByteArray();

+    }

+

+    /**

+     * Private constructor to prevent instantiation. All members are static.

+     */

+    private IoUtil() {

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/util/PatternUtil.java b/tags/2.5/src/main/java/org/mockftpserver/core/util/PatternUtil.java
new file mode 100644
index 0000000..02f4484
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/util/PatternUtil.java
@@ -0,0 +1,85 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.core.util;

+

+/**

+ * Contains static utility methods related to pattern-matching and regular expressions.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class PatternUtil {

+

+    /**

+     * Return true if the specified String contains one or more wildcard characters ('?' or '*')

+     *

+     * @param string - the String to check

+     * @return true if the String contains wildcards

+     */

+    public static boolean containsWildcards(String string) {

+        return string.indexOf("*") != -1 || string.indexOf("?") != -1;

+    }

+

+    /**

+     * Convert the specified String, optionally containing wildcards (? or *), to a regular expression String

+     *

+     * @param stringWithWildcards - the String to convert, optionally containing wildcards (? or *)

+     * @return an equivalent regex String

+     * @throws AssertionError - if the stringWithWildcards is null

+     */

+    public static String convertStringWithWildcardsToRegex(String stringWithWildcards) {

+        Assert.notNull(stringWithWildcards, "stringWithWildcards");

+

+        StringBuffer result = new StringBuffer();

+        for (int i = 0; i < stringWithWildcards.length(); i++) {

+            char ch = stringWithWildcards.charAt(i);

+            switch (ch) {

+                case '*':

+                    result.append(".*");

+                    break;

+                case '?':

+                    result.append('.');

+                    break;

+                case '$':

+                case '|':

+                case '[':

+                case ']':

+                case '(':

+                case ')':

+                case '.':

+                case ':':

+                case '{':

+                case '}':

+                case '\\':

+                case '^':

+                case '+':

+                    result.append('\\');

+                    result.append(ch);

+                    break;

+                default:

+                    result.append(ch);

+            }

+        }

+        return result.toString();

+    }

+

+    /**

+     * Private constructor to prevent instantiation. All members are static.

+     */

+    private PatternUtil() {

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/util/PortParser.java b/tags/2.5/src/main/java/org/mockftpserver/core/util/PortParser.java
new file mode 100644
index 0000000..487711b
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/util/PortParser.java
@@ -0,0 +1,164 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.util;

+

+import org.mockftpserver.core.CommandSyntaxException;

+import org.mockftpserver.core.MockFtpServerException;

+

+import java.net.InetAddress;

+import java.net.UnknownHostException;

+import java.util.Arrays;

+import java.util.List;

+

+/**

+ * Utility class for parsing host and port values from command arguments.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class PortParser {

+

+    /**

+     * Parse the host address and port number of an extended address. This encoded format is used by

+     * the EPRT FTP command, and supports IPv6.

+     * <p/>

+     * The client network address can be in IPv4 format (e.g., "132.235.1.2") or

+     * IPv6 format (e.g., "1080::8:800:200C:417A"). See RFC2428 for more information.

+     *

+     * @param parameter - the single parameter String containing the encoded host and port number

+     * @return the populated HostAndPort object

+     */

+    public static HostAndPort parseExtendedAddressHostAndPort(String parameter) {

+        if (parameter == null || parameter.length() == 0) {

+            throw new CommandSyntaxException("The parameter string must not be empty or null");

+        }

+

+        String delimiter = parameter.substring(0,1);

+        String[] tokens = parameter.split("\\" + delimiter);

+

+        if (tokens.length < 4) {

+            throw new CommandSyntaxException("Error parsing host and port number [" + parameter + "]");

+        }

+

+        int port = Integer.parseInt(tokens[3]);

+

+        InetAddress host;

+        try {

+            host = InetAddress.getByName(tokens[2]);

+        }

+        catch (UnknownHostException e) {

+            throw new CommandSyntaxException("Error parsing host [" + tokens[2] + "]", e);

+        }

+

+        return new HostAndPort(host, port);

+    }

+

+    /**

+     * Parse a 32-bit IP address and 16-bit port number from the String[] of FTP command parameters.

+     * This is used by the FTP "PORT" command.

+     *

+     * @param parameters - the String[] of command parameters. It is the concatenation

+     *                   of a 32-bit internet host address and a 16-bit TCP port address. This address

+     *                   information is broken into 8-bit fields and the value of each field is encoded

+     *                   as a separate parameter whose value is a decimal number (in character string

+     *                   representation).  Thus, the six parameters for the port command would be:

+     *                   h1,h2,h3,h4,p1,p2

+     *                   where h1 is the high order 8 bits of the internet host address, and p1 is the

+     *                   high order 8 bits of the port number.

+     * @return the HostAndPort object with the host InetAddres and int port parsed from the parameters

+     * @throws org.mockftpserver.core.util.AssertFailedException

+     *                               - if parameters is null or contains an insufficient number of elements

+     * @throws NumberFormatException - if one of the parameters does not contain a parsable integer

+     */

+    public static HostAndPort parseHostAndPort(String[] parameters) {

+        verifySufficientParameters(parameters);

+

+        byte host1 = parseByte(parameters[0]);

+        byte host2 = parseByte(parameters[1]);

+        byte host3 = parseByte(parameters[2]);

+        byte host4 = parseByte(parameters[3]);

+

+        byte[] address = {host1, host2, host3, host4};

+        InetAddress inetAddress = null;

+        try {

+            inetAddress = InetAddress.getByAddress(address);

+        }

+        catch (UnknownHostException e) {

+            throw new MockFtpServerException("Error parsing host", e);

+        }

+

+        int port1 = Integer.parseInt(parameters[4]);

+        int port2 = Integer.parseInt(parameters[5]);

+        int port = (port1 << 8) + port2;

+

+        return new HostAndPort(inetAddress, port);

+    }

+

+    /**

+     * Convert the InetAddess and port number to a comma-delimited list of byte values,

+     * suitable for the response String from the PASV command.

+     *

+     * @param host - the InetAddress

+     * @param port - the port number

+     * @return the comma-delimited list of byte values, e.g., "196,168,44,55,23,77"

+     */

+    public static String convertHostAndPortToCommaDelimitedBytes(InetAddress host, int port) {

+        StringBuffer buffer = new StringBuffer();

+        byte[] address = host.getAddress();

+        for (int i = 0; i < address.length; i++) {

+            int positiveValue = (address[i] >= 0) ? address[i] : 256 + address[i];

+            buffer.append(positiveValue);

+            buffer.append(",");

+        }

+        int p1 = port >> 8;

+        int p2 = port % 256;

+        buffer.append(String.valueOf(p1));

+        buffer.append(",");

+        buffer.append(String.valueOf(p2));

+        return buffer.toString();

+    }

+

+    /**

+     * Verify that the parameters is not null and contains the required number of elements

+     *

+     * @param parameters - the String[] of command parameters

+     * @throws CommandSyntaxException - if parameters is null or contains an insufficient number of elements

+     */

+    private static void verifySufficientParameters(String[] parameters) {

+        if (parameters == null || parameters.length < 6) {

+            List parms = parameters == null ? null : Arrays.asList(parameters);

+            throw new CommandSyntaxException("The PORT command must contain least be 6 parameters: " + parms);

+        }

+    }

+

+    /**

+     * Parse the specified String as an unsigned decimal byte value (i.e., 0..255). We can't just use

+     * Byte.parseByte(string) because that parses the string as a signed byte.

+     *

+     * @param string - the String containing the decimal byte representation to be parsed

+     * @return the byte value

+     */

+    private static byte parseByte(String string) {

+        return (byte) (0xFF & Short.parseShort(string));

+    }

+

+    /**

+     * Private constructor. All methods are static.

+     */

+    private PortParser() {

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/core/util/StringUtil.java b/tags/2.5/src/main/java/org/mockftpserver/core/util/StringUtil.java
new file mode 100644
index 0000000..090a53c
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/core/util/StringUtil.java
@@ -0,0 +1,96 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.core.util;

+

+import java.util.Collection;

+import java.util.Iterator;

+

+/**

+ * Contains static String-related utility methods.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class StringUtil {

+

+    /**

+     * Pad the specified String with spaces to the right to the specified width. If the length

+     * of string is already equal to or greater than width, then just return string.

+     *

+     * @param string - the String to pad

+     * @param width  - the target width

+     * @return a String of at least width characters, padded on the right with spaces as necessary

+     */

+    public static String padRight(String string, int width) {

+        int numSpaces = width - string.length();

+        return (numSpaces > 0) ? string + spaces(numSpaces) : string;

+    }

+

+    /**

+     * Pad the specified String with spaces to the left to the specified width. If the length

+     * of string is already equal to or greater than width, then just return string.

+     *

+     * @param string - the String to pad

+     * @param width  - the target width

+     * @return a String of at least width characters, padded on the left with spaces as necessary

+     */

+    public static String padLeft(String string, int width) {

+        int numSpaces = width - string.length();

+        return (numSpaces > 0) ? spaces(numSpaces) + string : string;

+    }

+

+    /**

+     * Join the Strings within the parts Collection, inserting the delimiter in between elements

+     *

+     * @param parts     - the Collection of Strings to join

+     * @param delimiter - the delimiter String to insert between the parts

+     * @return the Strings within the parts collection joined together using the specified delimiter

+     */

+    public static String join(Collection parts, String delimiter) {

+        Assert.notNull(parts, "parts");

+        Assert.notNull(delimiter, "delimiter");

+

+        StringBuffer buf = new StringBuffer();

+        Iterator iter = parts.iterator();

+        while (iter.hasNext()) {

+            String component = (String) iter.next();

+            buf.append(component);

+            if (iter.hasNext()) {

+                buf.append(delimiter);

+            }

+        }

+        return buf.toString();

+    }

+

+    //--------------------------------------------------------------------------

+    // Internal Helper Methods

+    //--------------------------------------------------------------------------

+

+    private static String spaces(int numSpaces) {

+        StringBuffer buf = new StringBuffer();

+        for (int i = 0; i < numSpaces; i++) {

+            buf.append(" ");

+        }

+        return buf.toString();

+    }

+

+    /**

+     * Private constructor to prevent instantiation. All members are static.

+     */

+    private StringUtil() {

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/FakeFtpServer.java b/tags/2.5/src/main/java/org/mockftpserver/fake/FakeFtpServer.java
new file mode 100644
index 0000000..70f447c
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/FakeFtpServer.java
@@ -0,0 +1,294 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake;

+

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ConnectCommandHandler;

+import org.mockftpserver.core.command.ReplyTextBundleUtil;

+import org.mockftpserver.core.command.UnsupportedCommandHandler;

+import org.mockftpserver.core.server.AbstractFtpServer;

+import org.mockftpserver.fake.command.*;

+import org.mockftpserver.fake.filesystem.FileSystem;

+

+import java.util.HashMap;

+import java.util.List;

+import java.util.Map;

+

+/**

+ * <b>FakeFtpServer</b> is the top-level class for a "fake" implementation of an FTP Server,

+ * suitable for testing FTP client code or standing in for a live FTP server.

+ * <p/>

+ * <b>FakeFtpServer</b> provides a high-level abstraction for an FTP Server and is suitable

+ * for most testing and simulation scenarios. You define a filesystem (internal, in-memory) containing

+ * an arbitrary set of files and directories. These files and directories can (optionally) have

+ * associated access permissions. You also configure a set of one or more user accounts that

+ * control which users can login to the FTP server, and their home (default) directories. The

+ * user account is also used when assigning file and directory ownership for new files.

+ * <p> <b>FakeFtpServer</b> processes FTP client requests and responds with reply codes and

+ * reply messages consistent with its configuration and the contents of its internal filesystem,

+ * including file and directory permissions, if they have been configured.

+ * <p/>

+ * <b>FakeFtpServer</b> can be fully configured programmatically or within the

+ * <a href="http://www.springframework.org/">Spring Framework</a> or other dependency-injection container.

+ * <p/>

+ * In general the steps for setting up and starting the <b>FakeFtpServer</b> are:

+ * <ol>

+ * <li>Create a new <b>FakeFtpServer</b> instance, and optionally set the server control port.</li>

+ * <li>Create and configure a <b>FileSystem</b>, and attach to the <b>FakeFtpServer</b> instance.</li>

+ * <li>Create and configure one or more <b>UserAccount</b> objects and attach to the <b>FakeFtpServer</b> instance.</li>

+ * <li>Start the <b>FakeFtpServer</b> instance.</li>

+ * </ol>

+ * <h4>Example Code</h4>

+ * <pre><code>

+ * FakeFtpServer fakeFtpServer = new FakeFtpServer();

+ *

+ * FileSystem fileSystem = new WindowsFakeFileSystem();

+ * fileSystem.add(new DirectoryEntry("c:\\"));

+ * fileSystem.add(new DirectoryEntry("c:\\data"));

+ * fileSystem.add(new FileEntry("c:\\data\\file1.txt", "abcdef 1234567890"));

+ * fileSystem.add(new FileEntry("c:\\data\\run.exe"));

+ * fakeFtpServer.setFileSystem(fileSystem);

+ *

+ * // Create UserAccount with username, password, home-directory

+ * UserAccount userAccount = new UserAccount("joe", "joe123", "c:\\");

+ * fakeFtpServer.addUserAccounts(userAccount);

+ *

+ * fakeFtpServer.start();

+ * </code></pre>

+ *

+ * <h4>Example Code with Permissions</h4>

+ * You can optionally set the permissions and owner/group for each file and directory, as in the following example.

+ * <pre><code>

+ * FileSystem fileSystem = new UnixFakeFileSystem();

+ * DirectoryEntry directoryEntry1 = new DirectoryEntry("/");

+ * directoryEntry1.setPermissions(new Permissions("rwxrwx---"));

+ * directoryEntry1.setOwner("joe");

+ * directoryEntry1.setGroup("dev");

+ *

+ * DirectoryEntry directoryEntry2 = new DirectoryEntry("/data");

+ * directoryEntry2.setPermissions(Permissions.ALL);

+ * directoryEntry2.setOwner("joe");

+ * directoryEntry2.setGroup("dev");

+ *

+ * FileEntry fileEntry1 = new FileEntry("/data/file1.txt", "abcdef 1234567890");

+ * fileEntry1.setPermissionsFromString("rw-rw-rw-");

+ * fileEntry1.setOwner("joe");

+ * fileEntry1.setGroup("dev");

+ *

+ * FileEntry fileEntry2 = new FileEntry("/data/run.exe");

+ * fileEntry2.setPermissionsFromString("rwxrwx---");

+ * fileEntry2.setOwner("mary");

+ * fileEntry2.setGroup("dev");

+ *

+ * fileSystem.add(directoryEntry1);

+ * fileSystem.add(directoryEntry2);

+ * fileSystem.add(fileEntry1);

+ * fileSystem.add(fileEntry2);

+ *

+ * FakeFtpServer fakeFtpServer = new FakeFtpServer();

+ * fakeFtpServer.setFileSystem(fileSystem);

+ *

+ * // Create UserAccount with username, password, home-directory

+ * UserAccount userAccount = new UserAccount("joe", "joe123", "/");

+ * fakeFtpServer.addUserAccounts(userAccount);

+ *

+ * fakeFtpServer.start();

+ * </code></pre>

+ *

+ * <h4>FTP Server Control Port</h4>

+ * By default, <b>FakeFtpServer</b> binds to the server control port of 21. You can use a different server control

+ * port by setting the <code>serverControlPort</code> property. If you specify a value of <code>0</code>,

+ * then a free port number will be chosen automatically; call <code>getServerControlPort()</code> AFTER

+ * <code>start()</code> has been called to determine the actual port number being used. Using a non-default

+ * port number is usually necessary when running on Unix or some other system where that port number is

+ * already in use or cannot be bound from a user process.

+ *

+ * <h4>Other Configuration</h4>

+ * The <code>systemName</code> property specifies the value returned by the <code>SYST</code>

+ * command. Note that this is typically used by an FTP client to determine how to parse

+ * system-dependent reply text, such as directory listings. This value defaults to <code>"WINDOWS"</code>.

+ * <p/>

+ * The <code>helpText</code> property specifies a <i>Map</i> of help text replies sent by the

+ * <code>HELP</code> command. The keys in that <i>Map</i> correspond to the command names passed as

+ * parameters to the <code>HELP</code> command. An entry with the key of an empty string ("") indicates the

+ * text used as the default help text when no command name parameter is specified for the <code>HELP</code> command.

+ *

+ * <h4>FTP Command Reply Text ResourceBundle</h4>

+ * The default text asociated with each FTP command reply code is contained within the

+ * "ReplyText.properties" ResourceBundle file. You can customize these messages by providing a

+ * locale-specific ResourceBundle file on the CLASSPATH, according to the normal lookup rules of

+ * the ResourceBundle class (e.g., "ReplyText_de.properties"). Alternatively, you can

+ * completely replace the ResourceBundle file by calling the calling the

+ * {@link #setReplyTextBaseName(String)} method.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class FakeFtpServer extends AbstractFtpServer implements ServerConfiguration {

+

+    private FileSystem fileSystem;

+    private String systemName = "WINDOWS";

+    private String systemStatus = "Connected";

+    private Map helpText = new HashMap();

+    private Map userAccounts = new HashMap();

+

+    public FileSystem getFileSystem() {

+        return fileSystem;

+    }

+

+    public void setFileSystem(FileSystem fileSystem) {

+        this.fileSystem = fileSystem;

+    }

+

+    public String getSystemName() {

+        return systemName;

+    }

+

+    public void setSystemName(String systemName) {

+        this.systemName = systemName;

+    }

+

+    public Map getHelpText() {

+        return helpText;

+    }

+

+    public void setHelpText(Map helpText) {

+        this.helpText = helpText;

+    }

+

+    public FakeFtpServer() {

+        setCommandHandler(CommandNames.ACCT, new AcctCommandHandler());

+        setCommandHandler(CommandNames.ABOR, new AborCommandHandler());

+        setCommandHandler(CommandNames.ALLO, new AlloCommandHandler());

+        setCommandHandler(CommandNames.APPE, new AppeCommandHandler());

+        setCommandHandler(CommandNames.CWD, new CwdCommandHandler());

+        setCommandHandler(CommandNames.CDUP, new CdupCommandHandler());

+        setCommandHandler(CommandNames.DELE, new DeleCommandHandler());

+        setCommandHandler(CommandNames.EPRT, new EprtCommandHandler());

+        setCommandHandler(CommandNames.EPSV, new EpsvCommandHandler());

+        setCommandHandler(CommandNames.HELP, new HelpCommandHandler());

+        setCommandHandler(CommandNames.LIST, new ListCommandHandler());

+        setCommandHandler(CommandNames.MKD, new MkdCommandHandler());

+        setCommandHandler(CommandNames.MODE, new ModeCommandHandler());

+        setCommandHandler(CommandNames.NLST, new NlstCommandHandler());

+        setCommandHandler(CommandNames.NOOP, new NoopCommandHandler());

+        setCommandHandler(CommandNames.PASS, new PassCommandHandler());

+        setCommandHandler(CommandNames.PASV, new PasvCommandHandler());

+        setCommandHandler(CommandNames.PWD, new PwdCommandHandler());

+        setCommandHandler(CommandNames.PORT, new PortCommandHandler());

+        setCommandHandler(CommandNames.QUIT, new QuitCommandHandler());

+        setCommandHandler(CommandNames.REIN, new ReinCommandHandler());

+        setCommandHandler(CommandNames.REST, new RestCommandHandler());

+        setCommandHandler(CommandNames.RETR, new RetrCommandHandler());

+        setCommandHandler(CommandNames.RMD, new RmdCommandHandler());

+        setCommandHandler(CommandNames.RNFR, new RnfrCommandHandler());

+        setCommandHandler(CommandNames.RNTO, new RntoCommandHandler());

+        setCommandHandler(CommandNames.SITE, new SiteCommandHandler());

+        setCommandHandler(CommandNames.SMNT, new SmntCommandHandler());

+        setCommandHandler(CommandNames.STAT, new StatCommandHandler());

+        setCommandHandler(CommandNames.STOR, new StorCommandHandler());

+        setCommandHandler(CommandNames.STOU, new StouCommandHandler());

+        setCommandHandler(CommandNames.STRU, new StruCommandHandler());

+        setCommandHandler(CommandNames.SYST, new SystCommandHandler());

+        setCommandHandler(CommandNames.TYPE, new TypeCommandHandler());

+        setCommandHandler(CommandNames.USER, new UserCommandHandler());

+        setCommandHandler(CommandNames.XPWD, new PwdCommandHandler());

+

+        // "Special" Command Handlers

+        setCommandHandler(CommandNames.CONNECT, new ConnectCommandHandler());

+        setCommandHandler(CommandNames.UNSUPPORTED, new UnsupportedCommandHandler());

+    }

+

+    /**

+     * Initialize a CommandHandler that has been registered to this server.

+     *

+     * If the CommandHandler implements the <code>ServerConfigurationAware</code> interface, then set its

+     * <code>ServerConfiguration</code> property to <code>this</code>.

+     *

+     * If the CommandHandler implements the <code>ReplyTextBundleAware</code> interface, then set its

+     * <code>replyTextBundle</code> property using the reply text bundle for this server.

+     *

+     * @param commandHandler - the CommandHandler to initialize

+     */

+    protected void initializeCommandHandler(CommandHandler commandHandler) {

+        if (commandHandler instanceof ServerConfigurationAware) {

+            ServerConfigurationAware sca = (ServerConfigurationAware) commandHandler;

+            sca.setServerConfiguration(this);

+        }

+

+        ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, getReplyTextBundle());

+    }

+

+    /**

+     * @return the {@link UserAccount}        configured for this server for the specified user name

+     */

+    public UserAccount getUserAccount(String username) {

+        return (UserAccount) userAccounts.get(username);

+    }

+

+    /**

+     * Return the help text for a command or the default help text if no command name is specified

+     *

+     * @param name - the command name; may be empty or null to indicate  a request for the default help text

+     * @return the help text for the named command or the default help text if no name is supplied

+     */

+    public String getHelpText(String name) {

+        String key = name == null ? "" : name;

+        return (String) helpText.get(key);

+    }

+

+    /**

+     * Add a single UserAccount. If an account with the same <code>username</code> already exists,

+     * it will be replaced.

+     *

+     * @param userAccount - the UserAccount to add

+     */

+    public void addUserAccount(UserAccount userAccount) {

+        userAccounts.put(userAccount.getUsername(), userAccount);

+    }

+

+    /**

+     * Add the UserAccount objects in the <code>userAccountList</code> to the set of UserAccounts.

+     *

+     * @param userAccountList - the List of UserAccount objects to add

+     */

+    public void setUserAccounts(List userAccountList) {

+        for (int i = 0; i < userAccountList.size(); i++) {

+            UserAccount userAccount = (UserAccount) userAccountList.get(i);

+            userAccounts.put(userAccount.getUsername(), userAccount);

+        }

+    }

+

+    /**

+     * Return the system status description

+     *

+     * @return the system status

+     */

+    public String getSystemStatus() {

+        return systemStatus;

+    }

+

+    /**

+     * Set the system status description text, used by the STAT command handler.

+     *

+     * @param systemStatus - the system status description text

+     */

+    public void setSystemStatus(String systemStatus) {

+        this.systemStatus = systemStatus;

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/ServerConfiguration.java b/tags/2.5/src/main/java/org/mockftpserver/fake/ServerConfiguration.java
new file mode 100644
index 0000000..dbfdb4b
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/ServerConfiguration.java
@@ -0,0 +1,57 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake;

+

+import org.mockftpserver.fake.filesystem.FileSystem;

+

+/**

+ * Interface for objects that provide access to server-specific information.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public interface ServerConfiguration {

+

+    /**

+     * @return the {@link FileSystem}    for this server

+     */

+    public FileSystem getFileSystem();

+

+    /**

+     * @param username - the user name

+     * @return the {@link UserAccount}    configured for this server for the specified user name

+     */

+    public UserAccount getUserAccount(String username);

+

+    /**

+     * @return the System Name for this server (used by the SYST command)

+     */

+    public String getSystemName();

+

+    /**

+     * @return the System Status text for this server (used by the STAT command)

+     */

+    public String getSystemStatus();

+

+    /**

+     * Return the help text for a command or the default help text if no command name is specified

+     *

+     * @param name - the command name; may be empty or null to indicate  a request for the default help text

+     * @return the help text for the named command or the default help text if no name is supplied

+     */

+    public String getHelpText(String name);

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/ServerConfigurationAware.java b/tags/2.5/src/main/java/org/mockftpserver/fake/ServerConfigurationAware.java
new file mode 100644
index 0000000..d89aa36
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/ServerConfigurationAware.java
@@ -0,0 +1,26 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake;

+

+/**

+ * Interface for classes that provide setter and getter to access a ServerConfiguration instance.

+ */

+public interface ServerConfigurationAware {

+

+    public ServerConfiguration getServerConfiguration();

+

+    public void setServerConfiguration(ServerConfiguration serverConfiguration);

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/UserAccount.java b/tags/2.5/src/main/java/org/mockftpserver/fake/UserAccount.java
new file mode 100644
index 0000000..746351a
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/UserAccount.java
@@ -0,0 +1,294 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake;

+

+import org.mockftpserver.core.util.Assert;

+import org.mockftpserver.fake.filesystem.FileSystemEntry;

+import org.mockftpserver.fake.filesystem.Permissions;

+

+import java.util.List;

+

+/**

+ * Represents a single user account on the server, including the username, password, home

+ * directory, list of groups to which this user belongs, and default permissions applied to

+ * newly-created files and directories.

+ * <p/>

+ * The <code>username</code> and <code>homeDirectory</code> property must be non-null

+ * and non-empty. The <code>homeDirectory</code> property must also match the name of an existing

+ * directory within the file system configured for the <code>FakeFtpServer</code>.

+ * <p/>

+ * The group name applied to newly created files/directories is determined by the <code>groups</code> property.

+ * If null or empty, then the default group name ("users") is used. Otherwise, the first value in the

+ * <code>groups</code> List is used. The <code>groups</code> property defaults to an empty List.

+ * <p/>

+ * The default value for <code>defaultPermissionsForNewFile</code> is read and write permissions for

+ * all (user/group/world). The default value for <code>defaultPermissionsForNewDirectory</code> is read,

+ * write and execute permissions for all (user/group/world).

+ * <p/>

+ * The <code>isValidPassword()</code> method returns true if the specified password matches

+ * the password value configured for this user account. This implementation uses the

+ * <code>isEquals()</code> method to compare passwords.

+ * <p/>

+ * If you want to provide a custom comparison, for instance using encrypted passwords, you can

+ * subclass this class and override the <code>comparePassword()</code> method to provide your own

+ * custom implementation.

+ * <p/>

+ * If the <code>passwordCheckedDuringValidation</code> property is set to false, then the password

+ * value is ignored, and the <code>isValidPassword()</code> method just returns <code<true</code>.

+ * <p/>

+ * The <code>accountRequiredForLogin</code> property defaults to false. If it is set to true, then

+ * it is expected that the login for this account will require an ACCOUNT (ACCT) command after the

+ * PASSWORD (PASS) command is completed.

+ */

+public class UserAccount {

+

+    public static final String DEFAULT_USER = "system";

+    public static final String DEFAULT_GROUP = "users";

+    public static final Permissions DEFAULT_PERMISSIONS_FOR_NEW_FILE = new Permissions("rw-rw-rw-");

+    public static final Permissions DEFAULT_PERMISSIONS_FOR_NEW_DIRECTORY = Permissions.ALL;

+

+    private String username;

+    private String password;

+    private String homeDirectory;

+    private List groups;

+    private boolean passwordRequiredForLogin = true;

+    private boolean passwordCheckedDuringValidation = true;

+    private boolean accountRequiredForLogin = false;

+    private Permissions defaultPermissionsForNewFile = DEFAULT_PERMISSIONS_FOR_NEW_FILE;

+    private Permissions defaultPermissionsForNewDirectory = DEFAULT_PERMISSIONS_FOR_NEW_DIRECTORY;

+

+

+    /**

+     * Construct a new uninitialized instance.

+     */

+    public UserAccount() {

+    }

+

+    /**

+     * Construct a new initialized instance.

+     *

+     * @param username      - the user name

+     * @param password      - the password

+     * @param homeDirectory - the home directory

+     */

+    public UserAccount(String username, String password, String homeDirectory) {

+        setUsername(username);

+        setPassword(password);

+        setHomeDirectory(homeDirectory);

+    }

+

+    public String getUsername() {

+        return username;

+    }

+

+    public void setUsername(String username) {

+        this.username = username;

+    }

+

+    public String getPassword() {

+        return password;

+    }

+

+    public void setPassword(String password) {

+        this.password = password;

+    }

+

+    public String getHomeDirectory() {

+        return homeDirectory;

+    }

+

+    public void setHomeDirectory(String homeDirectory) {

+        this.homeDirectory = homeDirectory;

+    }

+

+    public List getGroups() {

+        return groups;

+    }

+

+    public void setGroups(List groups) {

+        this.groups = groups;

+    }

+

+    public boolean isPasswordRequiredForLogin() {

+        return passwordRequiredForLogin;

+    }

+

+    public void setPasswordRequiredForLogin(boolean passwordRequiredForLogin) {

+        this.passwordRequiredForLogin = passwordRequiredForLogin;

+    }

+

+    public boolean isPasswordCheckedDuringValidation() {

+        return passwordCheckedDuringValidation;

+    }

+

+    public void setPasswordCheckedDuringValidation(boolean passwordCheckedDuringValidation) {

+        this.passwordCheckedDuringValidation = passwordCheckedDuringValidation;

+    }

+

+    public boolean isAccountRequiredForLogin() {

+        return accountRequiredForLogin;

+    }

+

+    public void setAccountRequiredForLogin(boolean accountRequiredForLogin) {

+        this.accountRequiredForLogin = accountRequiredForLogin;

+    }

+

+    public Permissions getDefaultPermissionsForNewFile() {

+        return defaultPermissionsForNewFile;

+    }

+

+    public void setDefaultPermissionsForNewFile(Permissions defaultPermissionsForNewFile) {

+        this.defaultPermissionsForNewFile = defaultPermissionsForNewFile;

+    }

+

+    public Permissions getDefaultPermissionsForNewDirectory() {

+        return defaultPermissionsForNewDirectory;

+    }

+

+    public void setDefaultPermissionsForNewDirectory(Permissions defaultPermissionsForNewDirectory) {

+        this.defaultPermissionsForNewDirectory = defaultPermissionsForNewDirectory;

+    }

+

+    /**

+     * Return the name of the primary group to which this user belongs. If this account has no associated

+     * groups set, then this method returns the <code>DEFAULT_GROUP</code>. Otherwise, this method

+     * returns the first group name in the <code>groups</code> list.

+     *

+     * @return the name of the primary group for this user

+     */

+    public String getPrimaryGroup() {

+        return (groups == null || groups.isEmpty()) ? DEFAULT_GROUP : (String) groups.get(0);

+    }

+

+    /**

+     * Return true if the specified password is the correct, valid password for this user account.

+     * This implementation uses standard (case-sensitive) String comparison. Subclasses can provide

+     * custom comparison behavior, for instance using encrypted password values, by overriding this

+     * method.

+     *

+     * @param password - the password to compare against the configured value

+     * @return true if the password is correct and valid

+     * @throws org.mockftpserver.core.util.AssertFailedException

+     *          - if the username property is null

+     */

+    public boolean isValidPassword(String password) {

+        Assert.notNullOrEmpty(username, "username");

+        return !passwordCheckedDuringValidation || comparePassword(password);

+    }

+

+    /**

+     * @return true if this UserAccount object is valid; i.e. if the homeDirectory is non-null and non-empty.

+     */

+    public boolean isValid() {

+        return homeDirectory != null && homeDirectory.length() > 0;

+    }

+

+    /**

+     * @return the String representation of this object

+     */

+    public String toString() {

+        return "UserAccount[username=" + username + "; password=" + password + "; homeDirectory="

+                + homeDirectory + "; passwordRequiredForLogin=" + passwordRequiredForLogin + "]";

+    }

+

+    /**

+     * Return true if this user has read access to the file/directory represented by the specified FileSystemEntry object.

+     *

+     * @param entry - the FileSystemEntry representing the file or directory

+     * @return true if this use has read access

+     */

+    public boolean canRead(FileSystemEntry entry) {

+        Permissions permissions = entry.getPermissions();

+        if (permissions == null) {

+            return true;

+        }

+

+        if (equalOrBothNull(username, entry.getOwner())) {

+            return permissions.canUserRead();

+        }

+        if (groups != null && groups.contains(entry.getGroup())) {

+            return permissions.canGroupRead();

+        }

+        return permissions.canWorldRead();

+    }

+

+    /**

+     * Return true if this user has write access to the file/directory represented by the specified FileSystemEntry object.

+     *

+     * @param entry - the FileSystemEntry representing the file or directory

+     * @return true if this use has write access

+     */

+    public boolean canWrite(FileSystemEntry entry) {

+        Permissions permissions = entry.getPermissions();

+        if (permissions == null) {

+            return true;

+        }

+

+        if (equalOrBothNull(username, entry.getOwner())) {

+            return permissions.canUserWrite();

+        }

+        if (groups != null && groups.contains(entry.getGroup())) {

+            return permissions.canGroupWrite();

+        }

+        return permissions.canWorldWrite();

+    }

+

+    /**

+     * Return true if this user has execute access to the file/directory represented by the specified FileSystemEntry object.

+     *

+     * @param entry - the FileSystemEntry representing the file or directory

+     * @return true if this use has execute access

+     */

+    public boolean canExecute(FileSystemEntry entry) {

+        Permissions permissions = entry.getPermissions();

+        if (permissions == null) {

+            return true;

+        }

+

+        if (equalOrBothNull(username, entry.getOwner())) {

+            return permissions.canUserExecute();

+        }

+        if (groups != null && groups.contains(entry.getGroup())) {

+            return permissions.canGroupExecute();

+        }

+        return permissions.canWorldExecute();

+    }

+

+    /**

+     * Return true if the specified password matches the password configured for this user account.

+     * This implementation uses standard (case-sensitive) String comparison. Subclasses can provide

+     * custom comparison behavior, for instance using encrypted password values, by overriding this

+     * method.

+     *

+     * @param password - the password to compare against the configured value

+     * @return true if the passwords match

+     */

+    protected boolean comparePassword(String password) {

+        return password != null && password.equals(this.password);

+    }

+

+    /**

+     * Return true only if both Strings are null or they are equal (have the same contents).

+     *

+     * @param string1 - the first String

+     * @param string2 - the second String

+     * @return true if both are null or both are equal

+     */

+    protected boolean equalOrBothNull(String string1, String string2) {

+        return (string1 == null && string2 == null) || (string1 != null && string1.equals(string2));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/AborCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/AborCommandHandler.java
new file mode 100644
index 0000000..cae876c
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/AborCommandHandler.java
@@ -0,0 +1,39 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the ABOR command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>Otherwise, reply with 226</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class AborCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        sendReply(session, ReplyCodes.ABOR_OK, "abor");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/AbstractFakeCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/AbstractFakeCommandHandler.java
new file mode 100644
index 0000000..25bb23c
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/AbstractFakeCommandHandler.java
@@ -0,0 +1,442 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.CommandSyntaxException;

+import org.mockftpserver.core.IllegalStateException;

+import org.mockftpserver.core.NotLoggedInException;

+import org.mockftpserver.core.command.AbstractCommandHandler;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.session.SessionKeys;

+import org.mockftpserver.core.util.Assert;

+import org.mockftpserver.fake.ServerConfiguration;

+import org.mockftpserver.fake.ServerConfigurationAware;

+import org.mockftpserver.fake.UserAccount;

+import org.mockftpserver.fake.filesystem.FileSystem;

+import org.mockftpserver.fake.filesystem.FileSystemEntry;

+import org.mockftpserver.fake.filesystem.FileSystemException;

+import org.mockftpserver.fake.filesystem.InvalidFilenameException;

+

+import java.text.MessageFormat;

+import java.util.ArrayList;

+import java.util.Collections;

+import java.util.List;

+import java.util.MissingResourceException;

+

+/**

+ * Abstract superclass for CommandHandler classes for the "Fake" server.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractFakeCommandHandler extends AbstractCommandHandler implements ServerConfigurationAware {

+

+    protected static final String INTERNAL_ERROR_KEY = "internalError";

+

+    private ServerConfiguration serverConfiguration;

+

+    /**

+     * Reply code sent back when a FileSystemException is caught by the                 {@link #handleCommand(Command, Session)}

+     * This defaults to ReplyCodes.EXISTING_FILE_ERROR (550).

+     */

+    protected int replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;

+

+    public ServerConfiguration getServerConfiguration() {

+        return serverConfiguration;

+    }

+

+    public void setServerConfiguration(ServerConfiguration serverConfiguration) {

+        this.serverConfiguration = serverConfiguration;

+    }

+

+    /**

+     * Use template method to centralize and ensure common validation

+     */

+    public void handleCommand(Command command, Session session) {

+        Assert.notNull(serverConfiguration, "serverConfiguration");

+        Assert.notNull(command, "command");

+        Assert.notNull(session, "session");

+

+        try {

+            handle(command, session);

+        }

+        catch (CommandSyntaxException e) {

+            handleException(command, session, e, ReplyCodes.COMMAND_SYNTAX_ERROR);

+        }

+        catch (IllegalStateException e) {

+            handleException(command, session, e, ReplyCodes.ILLEGAL_STATE);

+        }

+        catch (NotLoggedInException e) {

+            handleException(command, session, e, ReplyCodes.NOT_LOGGED_IN);

+        }

+        catch (InvalidFilenameException e) {

+            handleFileSystemException(command, session, e, ReplyCodes.FILENAME_NOT_VALID, e.getPath());

+        }

+        catch (FileSystemException e) {

+            handleFileSystemException(command, session, e, replyCodeForFileSystemException, e.getPath());

+        }

+    }

+

+    /**

+     * Convenience method to return the FileSystem stored in the ServerConfiguration

+     *

+     * @return the FileSystem

+     */

+    protected FileSystem getFileSystem() {

+        return serverConfiguration.getFileSystem();

+    }

+

+    /**

+     * Handle the specified command for the session. All checked exceptions are expected to be wrapped or handled

+     * by the caller.

+     *

+     * @param command - the Command to be handled

+     * @param session - the session on which the Command was submitted

+     */

+    protected abstract void handle(Command command, Session session);

+

+    // -------------------------------------------------------------------------

+    // Utility methods for subclasses

+    // -------------------------------------------------------------------------

+

+    /**

+     * Send a reply for this command on the control connection.

+     * <p/>

+     * The reply code is designated by the <code>replyCode</code> property, and the reply text

+     * is retrieved from the <code>replyText</code> ResourceBundle, using the specified messageKey.

+     *

+     * @param session    - the Session

+     * @param replyCode  - the reply code

+     * @param messageKey - the resource bundle key for the reply text

+     * @throws AssertionError - if session is null

+     * @see MessageFormat

+     */

+    protected void sendReply(Session session, int replyCode, String messageKey) {

+        sendReply(session, replyCode, messageKey, Collections.EMPTY_LIST);

+    }

+

+    /**

+     * Send a reply for this command on the control connection.

+     * <p/>

+     * The reply code is designated by the <code>replyCode</code> property, and the reply text

+     * is retrieved from the <code>replyText</code> ResourceBundle, using the specified messageKey.

+     *

+     * @param session    - the Session

+     * @param replyCode  - the reply code

+     * @param messageKey - the resource bundle key for the reply text

+     * @param args       - the optional message arguments; defaults to []

+     * @throws AssertionError - if session is null

+     * @see MessageFormat

+     */

+    protected void sendReply(Session session, int replyCode, String messageKey, List args) {

+        Assert.notNull(session, "session");

+        assertValidReplyCode(replyCode);

+

+        String text = getTextForKey(messageKey);

+        String replyText = (args != null && !args.isEmpty()) ? MessageFormat.format(text, args.toArray()) : text;

+

+        String replyTextToLog = (replyText == null) ? "" : " " + replyText;

+        String argsToLog = (args != null && !args.isEmpty()) ? (" args=" + args) : "";

+        LOG.info("Sending reply [" + replyCode + replyTextToLog + "]" + argsToLog);

+        session.sendReply(replyCode, replyText);

+    }

+

+    /**

+     * Send a reply for this command on the control connection.

+     * <p/>

+     * The reply code is designated by the <code>replyCode</code> property, and the reply text

+     * is retrieved from the <code>replyText</code> ResourceBundle, using the reply code as the key.

+     *

+     * @param session   - the Session

+     * @param replyCode - the reply code

+     * @throws AssertionError - if session is null

+     * @see MessageFormat

+     */

+    protected void sendReply(Session session, int replyCode) {

+        sendReply(session, replyCode, Collections.EMPTY_LIST);

+    }

+

+    /**

+     * Send a reply for this command on the control connection.

+     * <p/>

+     * The reply code is designated by the <code>replyCode</code> property, and the reply text

+     * is retrieved from the <code>replyText</code> ResourceBundle, using the reply code as the key.

+     *

+     * @param session   - the Session

+     * @param replyCode - the reply code

+     * @param args      - the optional message arguments; defaults to []

+     * @throws AssertionError - if session is null

+     * @see MessageFormat

+     */

+    protected void sendReply(Session session, int replyCode, List args) {

+        sendReply(session, replyCode, Integer.toString(replyCode), args);

+    }

+

+    /**

+     * Handle the exception caught during handleCommand()

+     *

+     * @param command   - the Command

+     * @param session   - the Session

+     * @param exception - the caught exception

+     * @param replyCode - the reply code that should be sent back

+     */

+    private void handleException(Command command, Session session, Throwable exception, int replyCode) {

+        LOG.warn("Error handling command: " + command + "; " + exception, exception);

+        sendReply(session, replyCode);

+    }

+

+    /**

+     * Handle the exception caught during handleCommand()

+     *

+     * @param command   - the Command

+     * @param session   - the Session

+     * @param exception - the caught exception

+     * @param replyCode - the reply code that should be sent back

+     * @param arg       - the arg for the reply (message)

+     */

+    private void handleFileSystemException(Command command, Session session, FileSystemException exception, int replyCode, Object arg) {

+        LOG.warn("Error handling command: " + command + "; " + exception, exception);

+        sendReply(session, replyCode, exception.getMessageKey(), Collections.singletonList(arg));

+    }

+

+    /**

+     * Return the value of the named attribute within the session.

+     *

+     * @param session - the Session

+     * @param name    - the name of the session attribute to retrieve

+     * @return the value of the named session attribute

+     * @throws IllegalStateException - if the Session does not contain the named attribute

+     */

+    protected Object getRequiredSessionAttribute(Session session, String name) {

+        Object value = session.getAttribute(name);

+        if (value == null) {

+            throw new IllegalStateException("Session missing required attribute [" + name + "]");

+        }

+        return value;

+    }

+

+    /**

+     * Verify that the current user (if any) has already logged in successfully.

+     *

+     * @param session - the Session

+     */

+    protected void verifyLoggedIn(Session session) {

+        if (getUserAccount(session) == null) {

+            throw new NotLoggedInException("User has not logged in");

+        }

+    }

+

+    /**

+     * @param session - the Session

+     * @return the UserAccount stored in the specified session; may be null

+     */

+    protected UserAccount getUserAccount(Session session) {

+        return (UserAccount) session.getAttribute(SessionKeys.USER_ACCOUNT);

+    }

+

+    /**

+     * Verify that the specified condition related to the file system is true,

+     * otherwise throw a FileSystemException.

+     *

+     * @param condition  - the condition that must be true

+     * @param path       - the path involved in the operation; this will be included in the

+     *                   error message if the condition is not true.

+     * @param messageKey - the message key for the exception message

+     * @throws FileSystemException - if the condition is not true

+     */

+    protected void verifyFileSystemCondition(boolean condition, String path, String messageKey) {

+        if (!condition) {

+            throw new FileSystemException(path, messageKey);

+        }

+    }

+

+    /**

+     * Verify that the current user has execute permission to the specified path

+     *

+     * @param session - the Session

+     * @param path    - the file system path

+     * @throws FileSystemException - if the condition is not true

+     */

+    protected void verifyExecutePermission(Session session, String path) {

+        UserAccount userAccount = getUserAccount(session);

+        FileSystemEntry entry = getFileSystem().getEntry(path);

+        verifyFileSystemCondition(userAccount.canExecute(entry), path, "filesystem.cannotExecute");

+    }

+

+    /**

+     * Verify that the current user has write permission to the specified path

+     *

+     * @param session - the Session

+     * @param path    - the file system path

+     * @throws FileSystemException - if the condition is not true

+     */

+    protected void verifyWritePermission(Session session, String path) {

+        UserAccount userAccount = getUserAccount(session);

+        FileSystemEntry entry = getFileSystem().getEntry(path);

+        verifyFileSystemCondition(userAccount.canWrite(entry), path, "filesystem.cannotWrite");

+    }

+

+    /**

+     * Verify that the current user has read permission to the specified path

+     *

+     * @param session - the Session

+     * @param path    - the file system path

+     * @throws FileSystemException - if the condition is not true

+     */

+    protected void verifyReadPermission(Session session, String path) {

+        UserAccount userAccount = getUserAccount(session);

+        FileSystemEntry entry = getFileSystem().getEntry(path);

+        verifyFileSystemCondition(userAccount.canRead(entry), path, "filesystem.cannotRead");

+    }

+

+    /**

+     * Return the full, absolute path for the specified abstract pathname.

+     * If path is null, return the current directory (stored in the session). If

+     * path represents an absolute path, then return path as is. Otherwise, path

+     * is relative, so assemble the full path from the current directory

+     * and the specified relative path.

+     *

+     * @param session - the Session

+     * @param path    - the abstract pathname; may be null

+     * @return the resulting full, absolute path

+     */

+    protected String getRealPath(Session session, String path) {

+        String currentDirectory = (String) session.getAttribute(SessionKeys.CURRENT_DIRECTORY);

+        if (path == null) {

+            return currentDirectory;

+        }

+        if (getFileSystem().isAbsolute(path)) {

+            return path;

+        }

+        return getFileSystem().path(currentDirectory, path);

+    }

+

+    /**

+     * Return the end-of-line character(s) used when building multi-line responses

+     *

+     * @return "\r\n"

+     */

+    protected String endOfLine() {

+        return "\r\n";

+    }

+

+    private String getTextForKey(String key) {

+        String msgKey = (key != null) ? key : INTERNAL_ERROR_KEY;

+        try {

+            return getReplyTextBundle().getString(msgKey);

+        }

+        catch (MissingResourceException e) {

+            // No reply text is mapped for the specified key

+            LOG.warn("No reply text defined for key [" + msgKey + "]");

+            return null;

+        }

+    }

+

+    // -------------------------------------------------------------------------

+    // Login Support (used by USER and PASS commands)

+    // -------------------------------------------------------------------------

+

+    /**

+     * Validate the UserAccount for the specified username. If valid, return true. If the UserAccount does

+     * not exist or is invalid, log an error message, send back a reply code of 530 with an appropriate

+     * error message, and return false. A UserAccount is considered invalid if the homeDirectory property

+     * is not set or is set to a non-existent directory.

+     *

+     * @param username - the username

+     * @param session  - the session; used to send back an error reply if necessary

+     * @return true only if the UserAccount for the named user is valid

+     */

+    protected boolean validateUserAccount(String username, Session session) {

+        UserAccount userAccount = serverConfiguration.getUserAccount(username);

+        if (userAccount == null || !userAccount.isValid()) {

+            LOG.error("UserAccount missing or not valid for username [" + username + "]: " + userAccount);

+            sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.userAccountNotValid", list(username));

+            return false;

+        }

+

+        String home = userAccount.getHomeDirectory();

+        if (!getFileSystem().isDirectory(home)) {

+            LOG.error("Home directory configured for username [" + username + "] is not valid: " + home);

+            sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.homeDirectoryNotValid", list(username, home));

+            return false;

+        }

+

+        return true;

+    }

+

+    /**

+     * Log in the specified user for the current session. Send back a reply of 230 with a message indicated

+     * by the replyMessageKey and set the UserAccount and current directory (homeDirectory) in the session.

+     *

+     * @param userAccount     - the userAccount for the user to be logged in

+     * @param session         - the session

+     * @param replyCode       - the reply code to send

+     * @param replyMessageKey - the message key for the reply text

+     */

+    protected void login(UserAccount userAccount, Session session, int replyCode, String replyMessageKey) {

+        sendReply(session, replyCode, replyMessageKey);

+        session.setAttribute(SessionKeys.USER_ACCOUNT, userAccount);

+        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, userAccount.getHomeDirectory());

+    }

+

+    /**

+     * Convenience method to return a List with the specified single item

+     *

+     * @param item - the single item in the returned List

+     * @return a new List with that single item

+     */

+    protected List list(Object item) {

+        return Collections.singletonList(item);

+    }

+

+    /**

+     * Convenience method to return a List with the specified two items

+     *

+     * @param item1 - the first item in the returned List

+     * @param item2 - the second item in the returned List

+     * @return a new List with the specified items

+     */

+    protected List list(Object item1, Object item2) {

+        List list = new ArrayList(2);

+        list.add(item1);

+        list.add(item2);

+        return list;

+    }

+

+    /**

+     * Return true if the specified string is null or empty

+     *

+     * @param string - the String to check; may be null

+     * @return true only if the specified String is null or empyt

+     */

+    protected boolean notNullOrEmpty(String string) {

+        return string != null && string.length() > 0;

+    }

+

+    /**

+     * Return the string unless it is null or empty, in which case return the defaultString.

+     *

+     * @param string        - the String to check; may be null

+     * @param defaultString - the value to return if string is null or empty

+     * @return string if not null and not empty; otherwise return defaultString

+     */

+    protected String defaultIfNullOrEmpty(String string, String defaultString) {

+        return (notNullOrEmpty(string) ? string : defaultString);

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/AbstractStoreFileCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/AbstractStoreFileCommandHandler.java
new file mode 100644
index 0000000..f13e780
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/AbstractStoreFileCommandHandler.java
@@ -0,0 +1,121 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.fake.filesystem.FileEntry;

+import org.mockftpserver.fake.filesystem.FileSystemException;

+

+import java.io.IOException;

+import java.io.OutputStream;

+

+/**

+ * Abstract superclass for CommandHandlers that that store a file (STOR, STOU, APPE). Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530 and terminate</li>

+ * <li>If the pathname parameter is required but missing, then reply with 501 and terminate</li>

+ * <li>If the required pathname parameter does not specify a valid filename, then reply with 553 and terminate</li>

+ * <li>If the current user does not have write access to the named file, if it already exists, or else to its

+ * parent directory, then reply with 553 and terminate</li>

+ * <li>If the current user does not have execute access to the parent directory, then reply with 553 and terminate</li>

+ * <li>Send an initial reply of 150</li>

+ * <li>Read all available bytes from the data connection and store/append to the named file in the server file system</li>

+ * <li>If file write/store fails, then reply with 553 and terminate</li>

+ * <li>Send a final reply with 226</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractStoreFileCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        this.replyCodeForFileSystemException = ReplyCodes.WRITE_FILE_ERROR;

+

+        String filename = getOutputFile(command);

+        String path = getRealPath(session, filename);

+        verifyFileSystemCondition(!getFileSystem().isDirectory(path), path, "filesystem.isDirectory");

+        String parentPath = getFileSystem().getParent(path);

+        verifyFileSystemCondition(getFileSystem().isDirectory(parentPath), parentPath, "filesystem.isNotADirectory");

+

+        // User must have write permission to the file, if an existing file, or else to the directory if a new file

+        String pathMustBeWritable = getFileSystem().exists(path) ? path : parentPath;

+        verifyWritePermission(session, pathMustBeWritable);

+

+        // User must have execute permission to the parent directory

+        verifyExecutePermission(session, parentPath);

+

+        sendReply(session, ReplyCodes.TRANSFER_DATA_INITIAL_OK);

+

+        session.openDataConnection();

+        byte[] contents = session.readData();

+        session.closeDataConnection();

+

+        FileEntry file = (FileEntry) getFileSystem().getEntry(path);

+        if (file == null) {

+            file = new FileEntry(path);

+            getFileSystem().add(file);

+        }

+        file.setPermissions(getUserAccount(session).getDefaultPermissionsForNewFile());

+

+        if (contents != null && contents.length > 0) {

+            OutputStream out = file.createOutputStream(appendToOutputFile());

+            try {

+                out.write(contents);

+            }

+            catch (IOException e) {

+                LOG.error("Error writing to file [" + file.getPath() + "]", e);

+                throw new FileSystemException(file.getPath(), null, e);

+            }

+            finally {

+                try {

+                    out.close();

+                } catch (IOException e) {

+                    LOG.error("Error closing OutputStream for file [" + file.getPath() + "]", e);

+                }

+            }

+        }

+        sendReply(session, ReplyCodes.TRANSFER_DATA_FINAL_OK, getMessageKey(), list(filename));

+    }

+

+    /**

+     * Return the path (absolute or relative) for the output file. The default behavior is to return

+     * the required first parameter for the specified Command. Subclasses may override the default behavior.

+     *

+     * @param command - the Command

+     * @return the output file name

+     */

+    protected String getOutputFile(Command command) {

+        return command.getRequiredParameter(0);

+    }

+

+    /**

+     * @return true if this command should append the transferred contents to the output file; false means

+     *         overwrite an existing file. This default implentation returns false.

+     */

+    protected boolean appendToOutputFile() {

+        return false;

+    }

+

+    /**

+     * @return the message key for the reply message sent with the final (226) reply

+     */

+    protected abstract String getMessageKey();

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/AcctCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/AcctCommandHandler.java
new file mode 100644
index 0000000..2048396
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/AcctCommandHandler.java
@@ -0,0 +1,44 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.session.SessionKeys;

+

+/**

+ * CommandHandler for the ACCT command. Handler logic:

+ * <ol>

+ * <li>If the required account parameter is missing, then reply with 501</li>

+ * <li>If this command was not preceded by a valid USER command, then reply with 503</li>

+ * <li>Store the account name in the session and reply with 230</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class AcctCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        String accountName = command.getRequiredParameter(0);

+        String username = (String) getRequiredSessionAttribute(session, SessionKeys.USERNAME);

+

+        session.setAttribute(SessionKeys.ACCOUNT_NAME, accountName);

+        sendReply(session, ReplyCodes.ACCT_OK, "acct", list(username));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/AlloCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/AlloCommandHandler.java
new file mode 100644
index 0000000..46d3e37
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/AlloCommandHandler.java
@@ -0,0 +1,39 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the ALLO command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>Otherwise, reply with 200</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class AlloCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        sendReply(session, ReplyCodes.ALLO_OK, "allo");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/AppeCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/AppeCommandHandler.java
new file mode 100644
index 0000000..b5a7a7e
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/AppeCommandHandler.java
@@ -0,0 +1,53 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+/**

+ * CommandHandler for the APPE command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530 and terminate</li>

+ * <li>If the required pathname parameter is missing, then reply with 501 and terminate</li>

+ * <li>If the pathname parameter does not specify a valid filename, then reply with 553 and terminate</li>

+ * <li>If the current user does not have write access to the named file, if it already exists, or else to its

+ * parent directory, then reply with 553 and terminate</li>

+ * <li>If the current user does not have execute access to the parent directory, then reply with 553 and terminate</li>

+ * <li>Send an initial reply of 150</li>

+ * <li>Read all available bytes from the data connection and append to the named file in the server file system</li>

+ * <li>If file write/store fails, then reply with 553 and terminate</li>

+ * <li>Send a final reply with 226</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class AppeCommandHandler extends AbstractStoreFileCommandHandler {

+

+    /**

+     * @return the message key for the reply message sent with the final (226) reply

+     */

+    protected String getMessageKey() {

+        return "appe";

+    }

+

+    /**

+     * @return true if this command should append the transferred contents to the output file; false means

+     *         overwrite an existing file.

+     */

+    protected boolean appendToOutputFile() {

+        return true;

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/CdupCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/CdupCommandHandler.java
new file mode 100644
index 0000000..44db478
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/CdupCommandHandler.java
@@ -0,0 +1,53 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.session.SessionKeys;

+

+/**

+ * CommandHandler for the CDUP command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>If the current directory has no parent or if the current directory cannot be changed, then reply with 550 and terminate</li>

+ * <li>If the current user does not have execute access to the parent directory, then reply with 550 and terminate</li>

+ * <li>Otherwise, reply with 200 and change the current directory stored in the session to the parent directory</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class CdupCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        String currentDirectory = (String) getRequiredSessionAttribute(session, SessionKeys.CURRENT_DIRECTORY);

+        String path = getFileSystem().getParent(currentDirectory);

+

+        this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;

+        verifyFileSystemCondition(notNullOrEmpty(path), currentDirectory, "filesystem.parentDirectoryDoesNotExist");

+        verifyFileSystemCondition(getFileSystem().isDirectory(path), path, "filesystem.isNotADirectory");

+

+        // User must have execute permission to the parent directory

+        verifyExecutePermission(session, path);

+

+        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, path);

+        sendReply(session, ReplyCodes.CDUP_OK, "cdup", list(path));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/CwdCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/CwdCommandHandler.java
new file mode 100644
index 0000000..a441565
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/CwdCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.session.SessionKeys;

+

+/**

+ * CommandHandler for the CWD command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>If the required pathname parameter is missing, then reply with 501 and terminate</li>

+ * <li>If the pathname parameter does not specify an existing directory, then reply with 550 and terminate</li>

+ * <li>If the current user does not have execute access to the directory, then reply with 550 and terminate</li>

+ * <li>Otherwise, reply with 250 and change the current directory stored in the session</li>

+ * </ol>

+ * The supplied pathname may be absolute or relative to the current directory.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class CwdCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        String path = getRealPath(session, command.getRequiredParameter(0));

+

+        this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;

+        verifyFileSystemCondition(getFileSystem().exists(path), path, "filesystem.doesNotExist");

+        verifyFileSystemCondition(getFileSystem().isDirectory(path), path, "filesystem.isNotADirectory");

+

+        // User must have execute permission to the directory

+        verifyExecutePermission(session, path);

+

+        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, path);

+        sendReply(session, ReplyCodes.CWD_OK, "cwd", list(path));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/DeleCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/DeleCommandHandler.java
new file mode 100644
index 0000000..a59a6af
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/DeleCommandHandler.java
@@ -0,0 +1,52 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the DELE command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>If the required pathname parameter is missing, then reply with 501</li>

+ * <li>If the pathname parameter does not specify an existing file then reply with 550</li>

+ * <li>If the current user does not have write access to the parent directory, then reply with 550</li>

+ * <li>Otherwise, delete the named file and reply with 250</li>

+ * </ol>

+ * The supplied pathname may be absolute or relative to the current directory.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class DeleCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        String path = getRealPath(session, command.getRequiredParameter(0));

+

+        this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;

+        verifyFileSystemCondition(getFileSystem().isFile(path), path, "filesystem.isNotAFile");

+

+        // User must have write permission to the parent directory

+        verifyWritePermission(session, getFileSystem().getParent(path));

+

+        getFileSystem().delete(path);

+        sendReply(session, ReplyCodes.DELE_OK, "dele", list(path));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/EprtCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/EprtCommandHandler.java
new file mode 100644
index 0000000..0f24f10
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/EprtCommandHandler.java
@@ -0,0 +1,49 @@
+/*

+ * Copyright 2009 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.HostAndPort;

+import org.mockftpserver.core.util.PortParser;

+

+/**

+ * CommandHandler for the EPRT command. Handler logic:

+ * <ol>

+ * <li>Parse the client network address (InetAddress) and port number from the (single)

+ *     parameter string of the form: "EPRT<space><d><net-prt><d><net-addr><d><tcp-port><d>".

+ *     The client network address can be in IPv4 format (e.g., "132.235.1.2") or

+ *     IPv6 format (e.g., "1080::8:800:200C:417A")     

+ * <li>Send back a reply of 200</li>

+ * </ol>

+ * See RFC2428 for more information.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class EprtCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        String parameter = command.getRequiredParameter(0);

+        HostAndPort client = PortParser.parseExtendedAddressHostAndPort(parameter);

+        LOG.debug("host=" + client.host + " port=" + client.port);

+        session.setClientDataHost(client.host);

+        session.setClientDataPort(client.port);

+        sendReply(session, ReplyCodes.EPRT_OK, "eprt");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/EpsvCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/EpsvCommandHandler.java
new file mode 100644
index 0000000..a228a9c
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/EpsvCommandHandler.java
@@ -0,0 +1,47 @@
+/*

+ * Copyright 2009 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+import java.net.InetAddress;

+

+/**

+ * CommandHandler for the EPSV command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>Otherwise, request the Session to switch to passive data connection mode. Return a reply code

+ * of 229, along with response text including: "<i>(|||PORT|)</i>", where <i>PORT</i> is the 16-bit

+ * TCP port address of the data connection on the server to which the client must connect.</li>

+ * </ol>

+ * See RFC2428 for more information.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class EpsvCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        int port = session.switchToPassiveMode();

+        InetAddress server = session.getServerHost();

+        LOG.debug("server=" + server + " port=" + port);

+        sendReply(session, ReplyCodes.EPSV_OK, "epsv", list(Integer.toString(port)));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/HelpCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/HelpCommandHandler.java
new file mode 100644
index 0000000..afaa552
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/HelpCommandHandler.java
@@ -0,0 +1,55 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.StringUtil;

+

+import java.util.Arrays;

+import java.util.List;

+

+/**

+ * CommandHandler for the HELP command. Handler logic:

+ * <ol>

+ * <li>If the optional command-name parameter is specified, then reply with 214 along with the

+ * help text configured for that command (or empty if none)</li>

+ * <li>Otherwise, reply with 214 along with the configured default help text that has been configured

+ * (or empty if none)</li>

+ * </ol>

+ * <p/>

+ * The help text is configured within the {@link org.mockftpserver.fake.FakeFtpServer}.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ * @see org.mockftpserver.fake.ServerConfiguration

+ * @see org.mockftpserver.fake.FakeFtpServer

+ */

+public class HelpCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        List parameters = Arrays.asList(command.getParameters());

+        String key = StringUtil.join(parameters, " ");

+        String help = getServerConfiguration().getHelpText(key);

+        if (help == null) {

+            sendReply(session, ReplyCodes.HELP_OK, "help.noHelpTextDefined", list(key));

+        } else {

+            sendReply(session, ReplyCodes.HELP_OK, "help", list(help));

+        }

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/ListCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/ListCommandHandler.java
new file mode 100644
index 0000000..b8e89e8
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/ListCommandHandler.java
@@ -0,0 +1,80 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.StringUtil;

+import org.mockftpserver.fake.filesystem.FileSystemEntry;

+

+import java.util.ArrayList;

+import java.util.Iterator;

+import java.util.List;

+

+/**

+ * CommandHandler for the LIST command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530 and terminate</li>

+ * <li>Send an initial reply of 150</li>

+ * <li>If the current user does not have read access to the file or directory to be listed, then reply with 550 and terminate</li>

+ * <li>If an error occurs during processing, then send a reply of 451 and terminate</li>

+ * <li>If the optional pathname parameter is missing, then send a directory listing for

+ * the current directory across the data connection</li>

+ * <li>Otherwise, if the optional pathname parameter specifies a directory or group of files,

+ * then send a directory listing for the specified directory across the data connection</li>

+ * <li>Otherwise, if the optional pathname parameter specifies a filename, then send information

+ * for the specified file across the data connection</li>

+ * <li>Send a final reply with 226</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class ListCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        sendReply(session, ReplyCodes.TRANSFER_DATA_INITIAL_OK);

+

+        String path = getRealPath(session, command.getParameter(0));

+

+        // User must have read permission to the path

+        if (getFileSystem().exists(path)) {

+            this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;

+            verifyReadPermission(session, path);

+        }

+

+        this.replyCodeForFileSystemException = ReplyCodes.SYSTEM_ERROR;

+        List fileEntries = getFileSystem().listFiles(path);

+        Iterator iter = fileEntries.iterator();

+        List lines = new ArrayList();

+        while (iter.hasNext()) {

+            FileSystemEntry entry = (FileSystemEntry) iter.next();

+            lines.add(getFileSystem().formatDirectoryListing(entry));

+        }

+        String result = StringUtil.join(lines, endOfLine());

+        result += result.length() > 0 ? endOfLine() : "";

+

+        session.openDataConnection();

+        LOG.info("Sending [" + result + "]");

+        session.sendData(result.getBytes(), result.length());

+        session.closeDataConnection();

+

+        sendReply(session, ReplyCodes.TRANSFER_DATA_FINAL_OK);

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/MkdCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/MkdCommandHandler.java
new file mode 100644
index 0000000..3087254
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/MkdCommandHandler.java
@@ -0,0 +1,62 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.fake.filesystem.DirectoryEntry;

+

+/**

+ * CommandHandler for the MKD command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>If the required pathname parameter is missing, then reply with 501</li>

+ * <li>If the parent directory of the specified pathname does not exist, then reply with 550</li>

+ * <li>If the pathname parameter specifies an existing file or directory, or if the create directory fails, then reply with 550</li>

+ * <li>If the current user does not have write and execute access to the parent directory, then reply with 550</li>

+ * <li>Otherwise, reply with 257</li>

+ * </ol>

+ * The supplied pathname may be absolute or relative to the current directory.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class MkdCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        String path = getRealPath(session, command.getRequiredParameter(0));

+        String parent = getFileSystem().getParent(path);

+

+        this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;

+        verifyFileSystemCondition(getFileSystem().exists(parent), parent, "filesystem.doesNotExist");

+        verifyFileSystemCondition(!getFileSystem().exists(path), path, "filesystem.alreadyExists");

+

+        // User must have write permission to the parent directory

+        verifyWritePermission(session, parent);

+

+        // User must have execute permission to the parent directory

+        verifyExecutePermission(session, parent);

+

+        DirectoryEntry dirEntry = new DirectoryEntry(path);

+        getFileSystem().add(dirEntry);

+        dirEntry.setPermissions(getUserAccount(session).getDefaultPermissionsForNewDirectory());

+

+        sendReply(session, ReplyCodes.MKD_OK, "mkd", list(path));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/ModeCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/ModeCommandHandler.java
new file mode 100644
index 0000000..5ff8bb8
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/ModeCommandHandler.java
@@ -0,0 +1,39 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the MODE command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>Otherwise, reply with 200</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class ModeCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        sendReply(session, ReplyCodes.MODE_OK, "mode");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/NlstCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/NlstCommandHandler.java
new file mode 100644
index 0000000..dd11065
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/NlstCommandHandler.java
@@ -0,0 +1,70 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.StringUtil;

+

+import java.util.List;

+

+/**

+ * CommandHandler for the NLST command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530 and terminate</li>

+ * <li>Send an initial reply of 150</li>

+ * <li>If the current user does not have read access to the file or directory to be listed, then reply with 550 and terminate</li>

+ * <li>If an error occurs during processing, then send a reply of 451 and terminate</li>

+ * <li>If the optional pathname parameter is missing, then send a directory listing for

+ * the current directory across the data connection</li>

+ * <li>Otherwise, if the optional pathname parameter specifies a directory or group of files,

+ * then send a directory listing for the specified directory across the data connection</li>

+ * <li>Otherwise, if the pathname parameter does not specify an existing directory or group of files,

+ * then send an empty response across the data connection</li>

+ * <li>Send a final reply with 226</li>

+ * </ol>

+ * The directory listing sent includes filenames only, separated by end-of-line characters.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class NlstCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        sendReply(session, ReplyCodes.TRANSFER_DATA_INITIAL_OK);

+        String path = getRealPath(session, command.getParameter(0));

+

+        // User must have read permission to the path

+        if (getFileSystem().exists(path)) {

+            this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;

+            verifyReadPermission(session, path);

+        }

+

+        this.replyCodeForFileSystemException = ReplyCodes.SYSTEM_ERROR;

+        List names = getFileSystem().listNames(path);

+        String directoryListing = StringUtil.join(names, endOfLine());

+        directoryListing += directoryListing.length() > 0 ? endOfLine() : "";

+

+        session.openDataConnection();

+        session.sendData(directoryListing.getBytes(), directoryListing.length());

+        session.closeDataConnection();

+

+        sendReply(session, ReplyCodes.TRANSFER_DATA_FINAL_OK);

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/NoopCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/NoopCommandHandler.java
new file mode 100644
index 0000000..9cbb29d
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/NoopCommandHandler.java
@@ -0,0 +1,37 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the NOOP command. Handler logic:

+ * <ol>

+ * <li>Reply with 200</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class NoopCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        sendReply(session, ReplyCodes.NOOP_OK, "noop");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/PassCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/PassCommandHandler.java
new file mode 100644
index 0000000..d446696
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/PassCommandHandler.java
@@ -0,0 +1,55 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.session.SessionKeys;

+import org.mockftpserver.fake.UserAccount;

+

+/**

+ * CommandHandler for the PASS command. Handler logic:

+ * <ol>

+ * <li>If the required password parameter is missing, then reply with 501</li>

+ * <li>If this command was not preceded by a valid USER command, then reply with 503</li>

+ * <li>If the user account configured for the named user does not exist or is not valid, then reply with 530</li>

+ * <li>If the specified password is not correct, then reply with 530</li>

+ * <li>Otherwise, reply with 230</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class PassCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        String password = command.getRequiredParameter(0);

+        String username = (String) getRequiredSessionAttribute(session, SessionKeys.USERNAME);

+

+        if (validateUserAccount(username, session)) {

+            UserAccount userAccount = getServerConfiguration().getUserAccount(username);

+            if (userAccount.isValidPassword(password)) {

+                int replyCode = (userAccount.isAccountRequiredForLogin()) ? ReplyCodes.PASS_NEED_ACCOUNT : ReplyCodes.PASS_OK;

+                String replyMessageKey = (userAccount.isAccountRequiredForLogin()) ? "pass.needAccount" : "pass";

+                login(userAccount, session, replyCode, replyMessageKey);

+            } else {

+                sendReply(session, ReplyCodes.PASS_LOG_IN_FAILED, "pass.loginFailed");

+            }

+        }

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/PasvCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/PasvCommandHandler.java
new file mode 100644
index 0000000..d4fbf95
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/PasvCommandHandler.java
@@ -0,0 +1,52 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.PortParser;

+

+import java.net.InetAddress;

+

+/**

+ * CommandHandler for the PASV command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>Otherwise, request the Session to switch to passive data connection mode. Return a reply code of 227,

+ * along with response text of the form: "<i>(h1,h2,h3,h4,p1,p2)</i>", where <i>h1..h4</i> are the

+ * 4 bytes of the 32-bit internet host address of the server, and <i>p1..p2</i> are the 2

+ * bytes of the 16-bit TCP port address of the data connection on the server to which

+ * the client must connect. See RFC959 for more information.</i>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class PasvCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+

+        int port = session.switchToPassiveMode();

+        InetAddress server = session.getServerHost();

+        LOG.debug("server=" + server + " port=" + port);

+        String hostAndPort = PortParser.convertHostAndPortToCommaDelimitedBytes(server, port);

+

+        sendReply(session, ReplyCodes.PASV_OK, "pasv", list(hostAndPort));

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/PortCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/PortCommandHandler.java
new file mode 100644
index 0000000..7e7deb6
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/PortCommandHandler.java
@@ -0,0 +1,46 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.HostAndPort;

+import org.mockftpserver.core.util.PortParser;

+

+/**

+ * CommandHandler for the PORT command. Handler logic:

+ * <ol>

+ * <li>Parse the client data host (InetAddress) submitted from parameters 1-4

+ * <li>Parse the port number submitted on the invocation from parameter 5-6

+ * <li>Send backa a reply of 200</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ * @see org.mockftpserver.core.util.PortParser

+ */

+public class PortCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        HostAndPort client = PortParser.parseHostAndPort(command.getParameters());

+        LOG.debug("host=" + client.host + " port=" + client.port);

+        session.setClientDataHost(client.host);

+        session.setClientDataPort(client.port);

+        sendReply(session, ReplyCodes.PORT_OK, "port");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/PwdCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/PwdCommandHandler.java
new file mode 100644
index 0000000..31b657b
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/PwdCommandHandler.java
@@ -0,0 +1,42 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.session.SessionKeys;

+

+/**

+ * CommandHandler for the PWD command. Handler logic:

+ * <ol>

+ * <li>If the required "current directory" property is missing from the session, then reply with 550</li>

+ * <li>Otherwise, reply with 257 and the current directory</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class PwdCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        String currentDirectory = (String) session.getAttribute(SessionKeys.CURRENT_DIRECTORY);

+        this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;

+        verifyFileSystemCondition(notNullOrEmpty(currentDirectory), currentDirectory, "filesystem.currentDirectoryNotSet");

+        sendReply(session, ReplyCodes.PWD_OK, "pwd", list(currentDirectory));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/QuitCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/QuitCommandHandler.java
new file mode 100644
index 0000000..d30f6bd
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/QuitCommandHandler.java
@@ -0,0 +1,35 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the QUIT command. Return a reply code of 221 and close the current session.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class QuitCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        sendReply(session, ReplyCodes.QUIT_OK, "quit");

+        session.close();

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/ReinCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/ReinCommandHandler.java
new file mode 100644
index 0000000..cca6c9b
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/ReinCommandHandler.java
@@ -0,0 +1,40 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.session.SessionKeys;

+

+/**

+ * CommandHandler for the REIN command. Handler logic:

+ * <ol>

+ * <li>Terminates (logs out) the current user, if there is one</li>

+ * <li>Reply with 220</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class ReinCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        session.removeAttribute(SessionKeys.USER_ACCOUNT);

+        sendReply(session, ReplyCodes.REIN_OK, "rein");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/RestCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/RestCommandHandler.java
new file mode 100644
index 0000000..3e4265f
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/RestCommandHandler.java
@@ -0,0 +1,39 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the REST command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>Otherwise, reply with 350</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class RestCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        sendReply(session, ReplyCodes.REST_OK, "rest");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/RetrCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/RetrCommandHandler.java
new file mode 100644
index 0000000..739e5a4
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/RetrCommandHandler.java
@@ -0,0 +1,120 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.session.SessionKeys;

+import org.mockftpserver.core.util.IoUtil;

+import org.mockftpserver.fake.filesystem.FileEntry;

+import org.mockftpserver.fake.filesystem.FileSystemEntry;

+import org.mockftpserver.fake.filesystem.FileSystemException;

+

+import java.io.ByteArrayOutputStream;

+import java.io.IOException;

+import java.io.InputStream;

+

+/**

+ * CommandHandler for the RETR command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530 and terminate</li>

+ * <li>If the required pathname parameter is missing, then reply with 501 and terminate</li>

+ * <li>If the pathname parameter does not specify a valid, existing filename, then reply with 550 and terminate</li>

+ * <li>If the current user does not have read access to the file at the specified path or execute permission to its directory, then reply with 550 and terminate</li>

+ * <li>Send an initial reply of 150</li>

+ * <li>Send the contents of the named file across the data connection</li>

+ * <li>If there is an error reading the file, then reply with 550 and terminate</li>

+ * <li>Send a final reply with 226</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class RetrCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;

+

+        String path = getRealPath(session, command.getRequiredParameter(0));

+        FileSystemEntry entry = getFileSystem().getEntry(path);

+        verifyFileSystemCondition(entry != null, path, "filesystem.doesNotExist");

+        verifyFileSystemCondition(!entry.isDirectory(), path, "filesystem.isNotAFile");

+        FileEntry fileEntry = (FileEntry) entry;

+

+        // User must have read permission to the file

+        verifyReadPermission(session, path);

+

+        // User must have execute permission to the parent directory

+        verifyExecutePermission(session, getFileSystem().getParent(path));

+

+        sendReply(session, ReplyCodes.TRANSFER_DATA_INITIAL_OK);

+        InputStream input = fileEntry.createInputStream();

+        session.openDataConnection();

+        byte[] bytes = null;

+        try {

+            bytes = IoUtil.readBytes(input);

+        }

+        catch (IOException e) {

+            LOG.error("Error reading from file [" + fileEntry.getPath() + "]", e);

+            throw new FileSystemException(fileEntry.getPath(), null, e);

+        }

+        finally {

+            try {

+                input.close();

+            }

+            catch (IOException e) {

+                LOG.error("Error closing InputStream for file [" + fileEntry.getPath() + "]", e);

+            }

+        }

+

+        if (isAsciiMode(session)) {

+            bytes = convertLfToCrLf(bytes);

+        }

+        session.sendData(bytes, bytes.length);

+        session.closeDataConnection();

+        sendReply(session, ReplyCodes.TRANSFER_DATA_FINAL_OK);

+    }

+

+    /**

+     * Within the specified byte array, replace all LF (\n) that are NOT preceded by a CR (\r) into CRLF (\r\n).

+     *

+     * @param bytes - the bytes to be converted

+     * @return the result of converting LF to CRLF

+     */

+    protected byte[] convertLfToCrLf(byte[] bytes) {

+        ByteArrayOutputStream out = new ByteArrayOutputStream();

+        char lastChar = ' ';

+        for (int i = 0; i < bytes.length; i++) {

+            char ch = (char) bytes[i];

+            if (ch == '\n' && lastChar != '\r') {

+                out.write('\r');

+                out.write('\n');

+            } else {

+                out.write(bytes[i]);

+            }

+            lastChar = ch;

+        }

+        return out.toByteArray();

+    }

+

+    private boolean isAsciiMode(Session session) {

+        // Defaults to true

+        return session.getAttribute(SessionKeys.ASCII_TYPE) != Boolean.FALSE;

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/RmdCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/RmdCommandHandler.java
new file mode 100644
index 0000000..db5b537
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/RmdCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the RMD command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>If the required pathname parameter is missing, then reply with 501</li>

+ * <li>If the pathname parameter does not specify an existing, empty directory, then reply with 550</li>

+ * <li>If the current user does not have write access to the parent directory, then reply with 550</li>

+ * <li>Otherwise, delete the named directory and reply with 250</li>

+ * </ol>

+ * The supplied pathname may be absolute or relative to the current directory.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class RmdCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        String path = getRealPath(session, command.getRequiredParameter(0));

+

+        this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;

+        verifyFileSystemCondition(getFileSystem().exists(path), path, "filesystem.doesNotExist");

+        verifyFileSystemCondition(getFileSystem().isDirectory(path), path, "filesystem.isNotADirectory");

+        verifyFileSystemCondition(getFileSystem().listNames(path).size() == 0, path, "filesystem.directoryIsNotEmpty");

+

+        // User must have write permission to the parent directory

+        verifyWritePermission(session, getFileSystem().getParent(path));

+

+        getFileSystem().delete(path);

+        sendReply(session, ReplyCodes.RMD_OK, "rmd", list(path));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/RnfrCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/RnfrCommandHandler.java
new file mode 100644
index 0000000..3f38e1a
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/RnfrCommandHandler.java
@@ -0,0 +1,53 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.session.SessionKeys;

+

+/**

+ * CommandHandler for the RNFR command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>If the required FROM pathname parameter is missing, then reply with 501</li>

+ * <li>If the FROM pathname parameter does not specify a valid file or directory, then reply with 550</li>

+ * <li>If the current user does not have read access to the path, then reply with 550</li>

+ * <li>Otherwise, reply with 350 and store the FROM path in the session</li>

+ * </ol>

+ * The supplied pathname may be absolute or relative to the current directory.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class RnfrCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        String fromPath = getRealPath(session, command.getRequiredParameter(0));

+

+        this.replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR;

+        verifyFileSystemCondition(getFileSystem().exists(fromPath), fromPath, "filesystem.doesNotExist");

+

+        // User must have read permission to the file

+        verifyReadPermission(session, fromPath);

+

+        session.setAttribute(SessionKeys.RENAME_FROM, fromPath);

+        sendReply(session, ReplyCodes.RNFR_OK, "rnfr");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/RntoCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/RntoCommandHandler.java
new file mode 100644
index 0000000..2550f5e
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/RntoCommandHandler.java
@@ -0,0 +1,61 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.session.SessionKeys;

+

+/**

+ * CommandHandler for the RNTO command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>If this command was not preceded by a valid RNFR command, then reply with 503</li>

+ * <li>If the required TO pathname parameter is missing, then reply with 501</li>

+ * <li>If the TO pathname parameter does not specify a valid filename, then reply with 553</li>

+ * <li>If the TO pathname parameter specifies an existing directory, then reply with 553</li>

+ * <li>If the current user does not have write access to the parent directory, then reply with 553</li>

+ * <li>Otherwise, rename the file, remove the FROM path stored in the session by the RNFR command, and reply with 250</li>

+ * </ol>

+ * The supplied pathname may be absolute or relative to the current directory.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class RntoCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        String toPath = getRealPath(session, command.getRequiredParameter(0));

+        String fromPath = (String) getRequiredSessionAttribute(session, SessionKeys.RENAME_FROM);

+

+        this.replyCodeForFileSystemException = ReplyCodes.WRITE_FILE_ERROR;

+        verifyFileSystemCondition(!getFileSystem().isDirectory(toPath), toPath, "filesystem.isDirectory");

+

+        // User must have write permission to the directory

+        String parentPath = getFileSystem().getParent(toPath);

+        verifyFileSystemCondition(notNullOrEmpty(parentPath), parentPath, "filesystem.doesNotExist");

+        verifyFileSystemCondition(getFileSystem().exists(parentPath), parentPath, "filesystem.doesNotExist");

+        verifyWritePermission(session, parentPath);

+

+        getFileSystem().rename(fromPath, toPath);

+

+        session.removeAttribute(SessionKeys.RENAME_FROM);

+        sendReply(session, ReplyCodes.RNTO_OK, "rnto", list(fromPath, toPath));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/SiteCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/SiteCommandHandler.java
new file mode 100644
index 0000000..320bf28
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/SiteCommandHandler.java
@@ -0,0 +1,38 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the SITE command. Handler logic:

+ * <ol>

+ * <li>Reply with 200</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class SiteCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        sendReply(session, ReplyCodes.SITE_OK, "site");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/SmntCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/SmntCommandHandler.java
new file mode 100644
index 0000000..30bcb04
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/SmntCommandHandler.java
@@ -0,0 +1,39 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the SMNT command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>Otherwise, reply with 250</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class SmntCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        sendReply(session, ReplyCodes.SMNT_OK, "smnt");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/StatCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/StatCommandHandler.java
new file mode 100644
index 0000000..79c23ad
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/StatCommandHandler.java
@@ -0,0 +1,41 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the STAT command. Handler logic:

+ * <ol>

+ * <li>Reply with 211 along with the system status text that has been configured on the

+ * {@link org.mockftpserver.fake.FakeFtpServer}.</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ * @see org.mockftpserver.fake.ServerConfiguration

+ * @see org.mockftpserver.fake.FakeFtpServer

+ */

+public class StatCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        String systemStatus = getServerConfiguration().getSystemStatus();

+        sendReply(session, ReplyCodes.STAT_SYSTEM_OK, "stat", list(systemStatus));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/StorCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/StorCommandHandler.java
new file mode 100644
index 0000000..492b83e
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/StorCommandHandler.java
@@ -0,0 +1,45 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+/**

+ * CommandHandler for the STOR command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530 and terminate</li>

+ * <li>If the required pathname parameter is missing, then reply with 501 and terminate</li>

+ * <li>If the pathname parameter does not specify a valid filename, then reply with 553 and terminate</li>

+ * <li>If the current user does not have write access to the named file, if it already exists, or else to its

+ * parent directory, then reply with 553 and terminate</li>

+ * <li>If the current user does not have execute access to the parent directory, then reply with 553 and terminate</li>

+ * <li>Send an initial reply of 150</li>

+ * <li>Read all available bytes from the data connection and write out to the named file in the server file system</li>

+ * <li>If file write/store fails, then reply with 553 and terminate</li>

+ * <li>Send a final reply with 226</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class StorCommandHandler extends AbstractStoreFileCommandHandler {

+

+    /**

+     * @return the message key for the reply message sent with the final (226) reply

+     */

+    protected String getMessageKey() {

+        return "stor";

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/StouCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/StouCommandHandler.java
new file mode 100644
index 0000000..2379c53
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/StouCommandHandler.java
@@ -0,0 +1,55 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+

+/**

+ * CommandHandler for the STOU command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530 and terminate</li>

+ * <li>Create new unique filename within the current directory</li>

+ * <li>If the current user does not have write access to the named file, if it already exists, or else to its

+ * parent directory, then reply with 553 and terminate</li>

+ * <li>If the current user does not have execute access to the parent directory, then reply with 553 and terminate</li>

+ * <li>Send an initial reply of 150</li>

+ * <li>Read all available bytes from the data connection and write out to the unique file in the server file system</li>

+ * <li>If file write/store fails, then reply with 553 and terminate</li>

+ * <li>Send a final reply with 226, along with the new unique filename</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class StouCommandHandler extends AbstractStoreFileCommandHandler {

+

+    /**

+     * @return the message key for the reply message sent with the final (226) reply

+     */

+    protected String getMessageKey() {

+        return "stou";

+    }

+

+    /**

+     * Return the path (absolute or relative) for the output file.

+     */

+    protected String getOutputFile(Command command) {

+        String baseName = defaultIfNullOrEmpty(command.getOptionalString(0), "Temp");

+        String suffix = Long.toString(System.currentTimeMillis());

+        return baseName + suffix;

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/StruCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/StruCommandHandler.java
new file mode 100644
index 0000000..7d224e0
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/StruCommandHandler.java
@@ -0,0 +1,39 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the STRU command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>Otherwise, reply with 200</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class StruCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        sendReply(session, ReplyCodes.STRU_OK, "stru");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/SystCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/SystCommandHandler.java
new file mode 100644
index 0000000..1850d7a
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/SystCommandHandler.java
@@ -0,0 +1,42 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the SYST command. Handler logic:

+ * <ol>

+ * <li>Reply with 215 along with the system name</li>

+ * </ol>

+ * The default system name is "WINDOWS", but it can be customized on the

+ * {@link org.mockftpserver.fake.FakeFtpServer}  .

+ * <p/>

+ * See the available system names listed in the Assigned Numbers document (RFC 943).

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ * @see <a href="http://www.ietf.org/rfc/rfc943">RFC943</a>

+ */

+public class SystCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        sendReply(session, ReplyCodes.SYST_OK, "syst", list(getServerConfiguration().getSystemName()));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/TypeCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/TypeCommandHandler.java
new file mode 100644
index 0000000..9c7deb3
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/TypeCommandHandler.java
@@ -0,0 +1,43 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.session.SessionKeys;

+

+/**

+ * CommandHandler for the TYPE command. Handler logic:

+ * <ol>

+ * <li>If the user has not logged in, then reply with 530</li>

+ * <li>Otherwise, reply with 200</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class TypeCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        verifyLoggedIn(session);

+        String type = command.getRequiredParameter(0);

+        boolean asciiType = type == "A";

+        session.setAttribute(SessionKeys.ASCII_TYPE, Boolean.valueOf(asciiType));

+        sendReply(session, ReplyCodes.TYPE_OK, "type");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/command/UserCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/fake/command/UserCommandHandler.java
new file mode 100644
index 0000000..a1c1cb2
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/command/UserCommandHandler.java
@@ -0,0 +1,58 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.session.SessionKeys;

+import org.mockftpserver.fake.UserAccount;

+

+/**

+ * CommandHandler for the USER command. Handler logic:

+ * <ol>

+ * <li>If the required pathname parameter is missing, then reply with 501</li>

+ * <li>If the user account configured for the named user is not valid, then reply with 530</li>

+ * <li>If the named user does not need a password for login, then set the UserAccount and

+ * current directory in the session, and reply with 230</li>

+ * <li>Otherwise, set the username in the session and reply with 331</li>

+ * </ol>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class UserCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        String username = command.getRequiredParameter(0);

+        UserAccount userAccount = getServerConfiguration().getUserAccount(username);

+

+        if (userAccount != null) {

+            if (!validateUserAccount(username, session)) {

+                return;

+            }

+

+            // If the UserAccount is configured to not require password for login

+            if (!userAccount.isPasswordRequiredForLogin()) {

+                login(userAccount, session, ReplyCodes.USER_LOGGED_IN_OK, "user.loggedIn");

+                return;

+            }

+        }

+        session.setAttribute(SessionKeys.USERNAME, username);

+        sendReply(session, ReplyCodes.USER_NEED_PASSWORD_OK, "user.needPassword");

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/AbstractFakeFileSystem.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/AbstractFakeFileSystem.java
new file mode 100644
index 0000000..9db6f93
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/AbstractFakeFileSystem.java
@@ -0,0 +1,653 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.filesystem;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.util.Assert;

+import org.mockftpserver.core.util.PatternUtil;

+import org.mockftpserver.core.util.StringUtil;

+

+import java.util.ArrayList;

+import java.util.Collections;

+import java.util.Date;

+import java.util.HashMap;

+import java.util.Iterator;

+import java.util.List;

+import java.util.Map;

+

+/**

+ * Abstract superclass for implementation of the FileSystem interface that manage the files

+ * and directories in memory, simulating a real file system.

+ * <p/>

+ * If the <code>createParentDirectoriesAutomatically</code> property is set to <code>true</code>,

+ * then creating a directory or file will automatically create any parent directories (recursively)

+ * that do not already exist. If <code>false</code>, then creating a directory or file throws an

+ * exception if its parent directory does not exist. This value defaults to <code>true</code>.

+ * <p/>

+ * The <code>directoryListingFormatter</code> property holds an instance of            {@link DirectoryListingFormatter}                          ,

+ * used by the <code>formatDirectoryListing</code> method to format directory listings in a

+ * filesystem-specific manner. This property must be initialized by concrete subclasses.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractFakeFileSystem implements FileSystem {

+

+    private static final Logger LOG = LoggerFactory.getLogger(AbstractFakeFileSystem.class);

+

+    /**

+     * If <code>true</code>, creating a directory or file will automatically create

+     * any parent directories (recursively) that do not already exist. If <code>false</code>,

+     * then creating a directory or file throws an exception if its parent directory

+     * does not exist. This value defaults to <code>true</code>.

+     */

+    private boolean createParentDirectoriesAutomatically = true;

+

+    /**

+     * The {@link DirectoryListingFormatter} used by the {@link #formatDirectoryListing(FileSystemEntry)}

+     * method. This must be initialized by concrete subclasses.

+     */

+    private DirectoryListingFormatter directoryListingFormatter;

+

+    private Map entries = new HashMap();

+

+    //-------------------------------------------------------------------------

+    // Public API

+    //-------------------------------------------------------------------------

+

+    public boolean isCreateParentDirectoriesAutomatically() {

+        return createParentDirectoriesAutomatically;

+    }

+

+    public void setCreateParentDirectoriesAutomatically(boolean createParentDirectoriesAutomatically) {

+        this.createParentDirectoriesAutomatically = createParentDirectoriesAutomatically;

+    }

+

+    public DirectoryListingFormatter getDirectoryListingFormatter() {

+        return directoryListingFormatter;

+    }

+

+    public void setDirectoryListingFormatter(DirectoryListingFormatter directoryListingFormatter) {

+        this.directoryListingFormatter = directoryListingFormatter;

+    }

+

+    /**

+     * Add each of the entries in the specified List to this filesystem. Note that this does not affect

+     * entries already existing within this filesystem.

+     *

+     * @param entriesToAdd - the List of FileSystemEntry entries to add

+     */

+    public void setEntries(List entriesToAdd) {

+        for (Iterator iter = entriesToAdd.iterator(); iter.hasNext();) {

+            FileSystemEntry entry = (FileSystemEntry) iter.next();

+            add(entry);

+        }

+    }

+

+    /**

+     * Add the specified file system entry (file or directory) to this file system

+     *

+     * @param entry - the FileSystemEntry to add

+     */

+    public void add(FileSystemEntry entry) {

+        String path = entry.getPath();

+        checkForInvalidFilename(path);

+        if (getEntry(path) != null) {

+            throw new FileSystemException(path, "filesystem.pathAlreadyExists");

+        }

+

+        if (!parentDirectoryExists(path)) {

+            String parent = getParent(path);

+            if (createParentDirectoriesAutomatically) {

+                add(new DirectoryEntry(parent));

+            } else {

+                throw new FileSystemException(parent, "filesystem.parentDirectoryDoesNotExist");

+            }

+        }

+

+        // Set lastModified, if not already set

+        if (entry.getLastModified() == null) {

+            entry.setLastModified(new Date());

+        }

+

+        entries.put(getFileSystemEntryKey(path), entry);

+        entry.lockPath();

+    }

+

+    /**

+     * Delete the file or directory specified by the path. Return true if the file is successfully

+     * deleted, false otherwise. If the path refers to a directory, it must be empty. Return false

+     * if the path does not refer to a valid file or directory or if it is a non-empty directory.

+     *

+     * @param path - the path of the file or directory to delete

+     * @return true if the file or directory is successfully deleted

+     * @throws org.mockftpserver.core.util.AssertFailedException

+     *          - if path is null

+     * @see org.mockftpserver.fake.filesystem.FileSystem#delete(java.lang.String)

+     */

+    public boolean delete(String path) {

+        Assert.notNull(path, "path");

+

+        if (getEntry(path) != null && !hasChildren(path)) {

+            removeEntry(path);

+            return true;

+        }

+        return false;

+    }

+

+    /**

+     * Return true if there exists a file or directory at the specified path

+     *

+     * @param path - the path

+     * @return true if the file/directory exists

+     * @throws AssertionError - if path is null

+     * @see org.mockftpserver.fake.filesystem.FileSystem#exists(java.lang.String)

+     */

+    public boolean exists(String path) {

+        Assert.notNull(path, "path");

+        return getEntry(path) != null;

+    }

+

+    /**

+     * Return true if the specified path designates an existing directory, false otherwise

+     *

+     * @param path - the path

+     * @return true if path is a directory, false otherwise

+     * @throws AssertionError - if path is null

+     * @see org.mockftpserver.fake.filesystem.FileSystem#isDirectory(java.lang.String)

+     */

+    public boolean isDirectory(String path) {

+        Assert.notNull(path, "path");

+        FileSystemEntry entry = getEntry(path);

+        return entry != null && entry.isDirectory();

+    }

+

+    /**

+     * Return true if the specified path designates an existing file, false otherwise

+     *

+     * @param path - the path

+     * @return true if path is a file, false otherwise

+     * @throws AssertionError - if path is null

+     * @see org.mockftpserver.fake.filesystem.FileSystem#isFile(java.lang.String)

+     */

+    public boolean isFile(String path) {

+        Assert.notNull(path, "path");

+        FileSystemEntry entry = getEntry(path);

+        return entry != null && !entry.isDirectory();

+    }

+

+    /**

+     * Return the List of FileSystemEntry objects for the files in the specified directory or group of

+     * files. If the path specifies a single file, then return a list with a single FileSystemEntry

+     * object representing that file. If the path does not refer to an existing directory or

+     * group of files, then an empty List is returned.

+     *

+     * @param path - the path specifying a directory or group of files; may contain wildcards (? or *)

+     * @return the List of FileSystemEntry objects for the specified directory or file; may be empty

+     * @see org.mockftpserver.fake.filesystem.FileSystem#listFiles(java.lang.String)

+     */

+    public List listFiles(String path) {

+        if (isFile(path)) {

+            return Collections.singletonList(getEntry(path));

+        }

+

+        List entryList = new ArrayList();

+        List children = children(path);

+        Iterator iter = children.iterator();

+        while (iter.hasNext()) {

+            String childPath = (String) iter.next();

+            FileSystemEntry fileSystemEntry = getEntry(childPath);

+            entryList.add(fileSystemEntry);

+        }

+        return entryList;

+    }

+

+    /**

+     * Return the List of filenames in the specified directory path or file path. If the path specifies

+     * a single file, then return that single filename. The returned filenames do not

+     * include a path. If the path does not refer to a valid directory or file path, then an empty List

+     * is returned.

+     *

+     * @param path - the path specifying a directory or group of files; may contain wildcards (? or *)

+     * @return the List of filenames (not including paths) for all files in the specified directory

+     *         or file path; may be empty

+     * @throws AssertionError - if path is null

+     * @see org.mockftpserver.fake.filesystem.FileSystem#listNames(java.lang.String)

+     */

+    public List listNames(String path) {

+        if (isFile(path)) {

+            return Collections.singletonList(getName(path));

+        }

+

+        List filenames = new ArrayList();

+        List children = children(path);

+        Iterator iter = children.iterator();

+        while (iter.hasNext()) {

+            String childPath = (String) iter.next();

+            FileSystemEntry fileSystemEntry = getEntry(childPath);

+            filenames.add(fileSystemEntry.getName());

+        }

+        return filenames;

+    }

+

+    /**

+     * Rename the file or directory. Specify the FROM path and the TO path. Throw an exception if the FROM path or

+     * the parent directory of the TO path do not exist; or if the rename fails for another reason.

+     *

+     * @param fromPath - the source (old) path + filename

+     * @param toPath   - the target (new) path + filename

+     * @throws AssertionError      - if fromPath or toPath is null

+     * @throws FileSystemException - if the rename fails.

+     */

+    public void rename(String fromPath, String toPath) {

+        Assert.notNull(toPath, "toPath");

+        Assert.notNull(fromPath, "fromPath");

+

+        FileSystemEntry entry = getRequiredEntry(fromPath);

+

+        if (exists(toPath)) {

+            throw new FileSystemException(toPath, "filesystem.alreadyExists");

+        }

+

+        String normalizedFromPath = normalize(fromPath);

+        String normalizedToPath = normalize(toPath);

+

+        if (!entry.isDirectory()) {

+            renamePath(entry, normalizedToPath);

+            return;

+        }

+

+        if (normalizedToPath.startsWith(normalizedFromPath + this.getSeparator())) {

+            throw new FileSystemException(toPath, "filesystem.renameFailed");

+        }

+

+        // Create the TO directory entry first so that the destination path exists when you

+        // move the children. Remove the FROM path after all children have been moved

+        add(new DirectoryEntry(normalizedToPath));

+

+        List children = descendents(fromPath);

+        Iterator iter = children.iterator();

+        while (iter.hasNext()) {

+            String childPath = (String) iter.next();

+            FileSystemEntry child = getRequiredEntry(childPath);

+            String normalizedChildPath = normalize(child.getPath());

+            Assert.isTrue(normalizedChildPath.startsWith(normalizedFromPath), "Starts with FROM path");

+            String childToPath = normalizedToPath + normalizedChildPath.substring(normalizedFromPath.length());

+            renamePath(child, childToPath);

+        }

+        Assert.isTrue(children(normalizedFromPath).isEmpty(), "Must have no children: " + normalizedFromPath);

+        removeEntry(normalizedFromPath);

+    }

+

+    /**

+     * @see java.lang.Object#toString()

+     */

+    public String toString() {

+        return this.getClass().getName() + entries;

+    }

+

+    /**

+     * Return the formatted directory listing entry for the file represented by the specified FileSystemEntry

+     *

+     * @param fileSystemEntry - the FileSystemEntry representing the file or directory entry to be formatted

+     * @return the the formatted directory listing entry

+     */

+    public String formatDirectoryListing(FileSystemEntry fileSystemEntry) {

+        Assert.notNull(directoryListingFormatter, "directoryListingFormatter");

+        Assert.notNull(fileSystemEntry, "fileSystemEntry");

+        return directoryListingFormatter.format(fileSystemEntry);

+    }

+

+    /**

+     * Build a path from the two path components. Concatenate path1 and path2. Insert the path

+     * separator character in between if necessary (i.e., if both are non-empty and path1 does not already

+     * end with a separator character AND path2 does not begin with one).

+     *

+     * @param path1 - the first path component may be null or empty

+     * @param path2 - the second path component may be null or empty

+     * @return the normalized path resulting from concatenating path1 to path2

+     */

+    public String path(String path1, String path2) {

+        StringBuffer buf = new StringBuffer();

+        if (path1 != null && path1.length() > 0) {

+            buf.append(path1);

+        }

+        if (path2 != null && path2.length() > 0) {

+            if ((path1 != null && path1.length() > 0)

+                    && (!isSeparator(path1.charAt(path1.length() - 1)))

+                    && (!isSeparator(path2.charAt(0)))) {

+                buf.append(this.getSeparator());

+            }

+            buf.append(path2);

+        }

+        return normalize(buf.toString());

+    }

+

+    /**

+     * Return the parent path of the specified path. If <code>path</code> specifies a filename,

+     * then this method returns the path of the directory containing that file. If <code>path</code>

+     * specifies a directory, the this method returns its parent directory. If <code>path</code> is

+     * empty or does not have a parent component, then return an empty string.

+     * <p/>

+     * All path separators in the returned path are converted to the system-dependent separator character.

+     *

+     * @param path - the path

+     * @return the parent of the specified path, or null if <code>path</code> has no parent

+     * @throws AssertionError - if path is null

+     */

+    public String getParent(String path) {

+        List parts = normalizedComponents(path);

+        if (parts.size() < 2) {

+            return null;

+        }

+        parts.remove(parts.size() - 1);

+        return componentsToPath(parts);

+    }

+

+    /**

+     * Returns the name of the file or directory denoted by this abstract

+     * pathname.  This is just the last name in the pathname's name

+     * sequence.  If the pathname's name sequence is empty, then the empty string is returned.

+     *

+     * @param path - the path

+     * @return The name of the file or directory denoted by this abstract pathname, or the

+     *         empty string if this pathname's name sequence is empty

+     */

+    public String getName(String path) {

+        Assert.notNull(path, "path");

+        String normalized = normalize(path);

+        int separatorIndex = normalized.lastIndexOf(this.getSeparator());

+        return (separatorIndex == -1) ? normalized : normalized.substring(separatorIndex + 1);

+    }

+

+    /**

+     * Returns the FileSystemEntry object representing the file system entry at the specified path, or null

+     * if the path does not specify an existing file or directory within this file system.

+     *

+     * @param path - the path of the file or directory within this file system

+     * @return the FileSystemEntry containing the information for the file or directory, or else null

+     * @see FileSystem#getEntry(String)

+     */

+    public FileSystemEntry getEntry(String path) {

+        return (FileSystemEntry) entries.get(getFileSystemEntryKey(path));

+    }

+

+    //-------------------------------------------------------------------------

+    // Abstract Methods

+    //-------------------------------------------------------------------------

+

+    /**

+     * @param path - the path

+     * @return true if the specified dir/file path name is valid according to the current filesystem.

+     */

+    protected abstract boolean isValidName(String path);

+

+    /**

+     * @return the file system-specific file separator as a char

+     */

+    protected abstract char getSeparatorChar();

+

+    /**

+     * @param pathComponent - the component (piece) of the path to check

+     * @return true if the specified path component is a root for this filesystem

+     */

+    protected abstract boolean isRoot(String pathComponent);

+

+    /**

+     * Return true if the specified char is a separator character for this filesystem

+     *

+     * @param c - the character to test

+     * @return true if the specified char is a separator character

+     */

+    protected abstract boolean isSeparator(char c);

+

+    //-------------------------------------------------------------------------

+    // Internal Helper Methods

+    //-------------------------------------------------------------------------

+

+    /**

+     * @return the file system-specific file separator as a String

+     */

+    protected String getSeparator() {

+        return Character.toString(getSeparatorChar());

+    }

+

+    /**

+     * Return the normalized and unique key used to access the file system entry

+     *

+     * @param path - the path

+     * @return the corresponding normalized key

+     */

+    protected String getFileSystemEntryKey(String path) {

+        return normalize(path);

+    }

+

+    /**

+     * Return the standard, normalized form of the path.

+     *

+     * @param path - the path

+     * @return the path in a standard, unique, canonical form

+     * @throws AssertionError - if path is null

+     */

+    protected String normalize(String path) {

+        return componentsToPath(normalizedComponents(path));

+    }

+

+    /**

+     * Throw an InvalidFilenameException if the specified path is not valid.

+     *

+     * @param path - the path

+     */

+    protected void checkForInvalidFilename(String path) {

+        if (!isValidName(path)) {

+            throw new InvalidFilenameException(path);

+        }

+    }

+

+    /**

+     * Rename the file system entry to the specified path name

+     *

+     * @param entry  - the file system entry

+     * @param toPath - the TO path (normalized)

+     */

+    protected void renamePath(FileSystemEntry entry, String toPath) {

+        String normalizedFrom = normalize(entry.getPath());

+        String normalizedTo = normalize(toPath);

+        LOG.info("renaming from [" + normalizedFrom + "] to [" + normalizedTo + "]");

+        FileSystemEntry newEntry = entry.cloneWithNewPath(normalizedTo);

+        add(newEntry);

+        // Do this at the end, in case the addEntry() failed

+        removeEntry(normalizedFrom);

+    }

+

+    /**

+     * Return the FileSystemEntry for the specified path. Throw FileSystemException if the

+     * specified path does not exist.

+     *

+     * @param path - the path

+     * @return the FileSystemEntry

+     * @throws FileSystemException - if the specified path does not exist

+     */

+    protected FileSystemEntry getRequiredEntry(String path) {

+        FileSystemEntry entry = getEntry(path);

+        if (entry == null) {

+            LOG.error("Path does not exist: " + path);

+            throw new FileSystemException(normalize(path), "filesystem.doesNotExist");

+        }

+        return entry;

+    }

+

+    /**

+     * Return the components of the specified path as a List. The components are normalized, and

+     * the returned List does not include path separator characters.

+     *

+     * @param path - the path

+     * @return the List of normalized components

+     */

+    protected List normalizedComponents(String path) {

+        Assert.notNull(path, "path");

+        char otherSeparator = this.getSeparatorChar() == '/' ? '\\' : '/';

+        String p = path.replace(otherSeparator, this.getSeparatorChar());

+

+        // TODO better way to do this

+        if (p.equals(this.getSeparator())) {

+            return Collections.singletonList("");

+        }

+        List result = new ArrayList();

+        if (p.length() > 0) {

+            String[] parts = p.split("\\" + this.getSeparator());

+            for (int i = 0; i < parts.length; i++) {

+                String part = parts[i];

+                if (part.equals("..")) {

+                    result.remove(result.size() - 1);

+                } else if (!part.equals(".")) {

+                    result.add(part);

+                }

+            }

+        }

+        return result;

+    }

+

+    /**

+     * Build a path from the specified list of path components

+     *

+     * @param components - the list of path components

+     * @return the resulting path

+     */

+    protected String componentsToPath(List components) {

+        if (components.size() == 1) {

+            String first = (String) components.get(0);

+            if (first.length() == 0 || isRoot(first)) {

+                return first + this.getSeparator();

+            }

+        }

+        return StringUtil.join(components, this.getSeparator());

+    }

+

+    /**

+     * Return true if the specified path designates an absolute file path.

+     *

+     * @param path - the path

+     * @return true if path is absolute, false otherwise

+     * @throws AssertionError - if path is null

+     */

+    public boolean isAbsolute(String path) {

+        return isValidName(path);

+    }

+

+    /**

+     * Return true if the specified path exists

+     *

+     * @param path - the path

+     * @return true if the path exists

+     */

+    private boolean pathExists(String path) {

+        return getEntry(path) != null;

+    }

+

+    /**

+     * If the specified path has a parent, then verify that the parent exists

+     *

+     * @param path - the path

+     * @return true if the parent of the specified path exists

+     */

+    private boolean parentDirectoryExists(String path) {

+        String parent = getParent(path);

+        return parent == null || pathExists(parent);

+    }

+

+    /**

+     * Return true if the specified path represents a directory that contains one or more files or subdirectories

+     *

+     * @param path - the path

+     * @return true if the path has child entries

+     */

+    private boolean hasChildren(String path) {

+        if (!isDirectory(path)) {

+            return false;

+        }

+        String key = getFileSystemEntryKey(path);

+        Iterator iter = entries.keySet().iterator();

+        while (iter.hasNext()) {

+            String p = (String) iter.next();

+            if (p.startsWith(key) && !key.equals(p)) {

+                return true;

+            }

+        }

+        return false;

+    }

+

+    /**

+     * Return the List of files or subdirectory paths that are descendents of the specified path

+     *

+     * @param path - the path

+     * @return the List of the paths for the files and subdirectories that are children, grandchildren, etc.

+     */

+    private List descendents(String path) {

+        if (isDirectory(path)) {

+            String normalizedPath = getFileSystemEntryKey(path);

+            String separator = (normalizedPath.endsWith(getSeparator())) ? "" : getSeparator();

+            String normalizedDirPrefix = normalizedPath + separator;

+            List descendents = new ArrayList();

+            Iterator iter = entries.entrySet().iterator();

+            while (iter.hasNext()) {

+                Map.Entry mapEntry = (Map.Entry) iter.next();

+                String p = (String) mapEntry.getKey();

+                if (p.startsWith(normalizedDirPrefix) && !normalizedPath.equals(p)) {

+                    FileSystemEntry fileSystemEntry = (FileSystemEntry) mapEntry.getValue();

+                    descendents.add(fileSystemEntry.getPath());

+                }

+            }

+            return descendents;

+        }

+        return Collections.EMPTY_LIST;

+    }

+

+    /**

+     * Return the List of files or subdirectory paths that are children of the specified path

+     *

+     * @param path - the path

+     * @return the List of the paths for the files and subdirectories that are children

+     */

+    private List children(String path) {

+        String lastComponent = getName(path);

+        boolean containsWildcards = PatternUtil.containsWildcards(lastComponent);

+        String dir = containsWildcards ? getParent(path) : path;

+        String pattern = containsWildcards ? PatternUtil.convertStringWithWildcardsToRegex(getName(path)) : null;

+        LOG.debug("path=" + path + " lastComponent=" + lastComponent + " containsWildcards=" + containsWildcards + " dir=" + dir + " pattern=" + pattern);

+

+        List descendents = descendents(dir);

+        List children = new ArrayList();

+        String normalizedDir = normalize(dir);

+        Iterator iter = descendents.iterator();

+        while (iter.hasNext()) {

+            String descendentPath = (String) iter.next();

+

+            boolean patternEmpty = pattern == null || pattern.length() == 0;

+            if (normalizedDir.equals(getParent(descendentPath)) &&

+                    (patternEmpty || (getName(descendentPath).matches(pattern)))) {

+                children.add(descendentPath);

+            }

+        }

+        return children;

+    }

+

+    private void removeEntry(String path) {

+        entries.remove(getFileSystemEntryKey(path));

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/AbstractFileSystemEntry.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/AbstractFileSystemEntry.java
new file mode 100644
index 0000000..e737898
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/AbstractFileSystemEntry.java
@@ -0,0 +1,129 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.filesystem;

+

+import org.mockftpserver.core.util.Assert;

+

+import java.util.Date;

+

+/**

+ * The abstract superclass for concrete file system entry classes representing files and directories.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractFileSystemEntry implements FileSystemEntry {

+

+    private String path;

+    private boolean pathLocked = false;

+

+    private Date lastModified;

+    private String owner;

+    private String group;

+

+    public Date getLastModified() {

+        return lastModified;

+    }

+

+    public void setLastModified(Date lastModified) {

+        this.lastModified = lastModified;

+    }

+

+    public String getOwner() {

+        return owner;

+    }

+

+    public void setOwner(String owner) {

+        this.owner = owner;

+    }

+

+    public String getGroup() {

+        return group;

+    }

+

+    public void setGroup(String group) {

+        this.group = group;

+    }

+

+    public Permissions getPermissions() {

+        return permissions;

+    }

+

+    public void setPermissions(Permissions permissions) {

+        this.permissions = permissions;

+    }

+

+    private Permissions permissions;

+

+    /**

+     * Construct a new instance without setting its path

+     */

+    public AbstractFileSystemEntry() {

+    }

+

+    /**

+     * Construct a new instance with the specified value for its path

+     *

+     * @param path - the value for path

+     */

+    public AbstractFileSystemEntry(String path) {

+        this.path = path;

+    }

+

+    /**

+     * @return the path for this entry

+     */

+    public String getPath() {

+        return path;

+    }

+

+    /**

+     * @return the file name or directory name (no path) for this entry

+     */

+    public String getName() {

+        int separatorIndex1 = path.lastIndexOf('/');

+        int separatorIndex2 = path.lastIndexOf('\\');

+//        int separatorIndex = [separatorIndex1, separatorIndex2].max();

+        int separatorIndex = separatorIndex1 > separatorIndex2 ? separatorIndex1 : separatorIndex2;

+        return (separatorIndex == -1) ? path : path.substring(separatorIndex + 1);

+    }

+

+    /**

+     * Set the path for this entry. Throw an exception if pathLocked is true.

+     *

+     * @param path - the new path value

+     */

+    public void setPath(String path) {

+        Assert.isFalse(pathLocked, "path is locked");

+        this.path = path;

+    }

+

+    public void lockPath() {

+        this.pathLocked = true;

+    }

+

+    public void setPermissionsFromString(String permissionsString) {

+        this.permissions = new Permissions(permissionsString);

+    }

+

+    /**

+     * Abstract method -- must be implemented within concrete subclasses

+     *

+     * @return true if this file system entry represents a directory

+     */

+    public abstract boolean isDirectory();

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/DirectoryEntry.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/DirectoryEntry.java
new file mode 100644
index 0000000..b564924
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/DirectoryEntry.java
@@ -0,0 +1,82 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.filesystem;

+

+/**

+ * File system entry representing a directory

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class DirectoryEntry extends AbstractFileSystemEntry {

+

+    /**

+     * Construct a new instance without setting its path

+     */

+    public DirectoryEntry() {

+    }

+

+    /**

+     * Construct a new instance with the specified value for its path

+     *

+     * @param path - the value for path

+     */

+    public DirectoryEntry(String path) {

+        super(path);

+    }

+

+    /**

+     * Return true to indicate that this entry represents a directory

+     *

+     * @return true

+     */

+    public boolean isDirectory() {

+        return true;

+    }

+

+    /**

+     * Return the size of this directory. This method returns zero.

+     *

+     * @return the file size in bytes

+     */

+    public long getSize() {

+        return 0;

+    }

+

+    /**

+     * @see java.lang.Object#toString()

+     */

+    public String toString() {

+        return "Directory['" + getPath() + "' lastModified=" + getLastModified() + "  owner=" + getOwner() +

+                "  group=" + getGroup() + "  permissions=" + getPermissions() + "]";

+    }

+

+    /**

+     * Return a new FileSystemEntry that is a clone of this object, except having the specified path

+     *

+     * @param path - the new path value for the cloned file system entry

+     * @return a new FileSystemEntry that has all the same values as this object except for its path

+     */

+    public FileSystemEntry cloneWithNewPath(String path) {

+        DirectoryEntry clone = new DirectoryEntry(path);

+        clone.setLastModified(getLastModified());

+        clone.setOwner(getOwner());

+        clone.setGroup(getGroup());

+        clone.setPermissions(getPermissions());

+        return clone;

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/DirectoryListingFormatter.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/DirectoryListingFormatter.java
new file mode 100644
index 0000000..ef159e4
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/DirectoryListingFormatter.java
@@ -0,0 +1,35 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.filesystem;

+

+/**

+ * Interface for an object that can format a file system directory listing.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public interface DirectoryListingFormatter {

+

+    /**

+     * Format the directory listing for a single file/directory entry.

+     *

+     * @param fileSystemEntry - the FileSystemEntry for a single file system entry

+     * @return the formatted directory listing

+     */

+    String format(FileSystemEntry fileSystemEntry);

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/FileEntry.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/FileEntry.java
new file mode 100644
index 0000000..c6bfa04
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/FileEntry.java
@@ -0,0 +1,187 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.filesystem;

+

+import java.io.ByteArrayInputStream;

+import java.io.ByteArrayOutputStream;

+import java.io.IOException;

+import java.io.InputStream;

+import java.io.OutputStream;

+

+/**

+ * File system entry representing a file

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class FileEntry extends AbstractFileSystemEntry {

+

+    private static final byte[] EMPTY = new byte[0];

+

+    private byte[] bytes = EMPTY;

+    private ByteArrayOutputStream out;

+

+    /**

+     * Construct a new instance without setting its path

+     */

+    public FileEntry() {

+    }

+

+    /**

+     * Construct a new instance with the specified value for its path

+     *

+     * @param path - the value for path

+     */

+    public FileEntry(String path) {

+        super(path);

+    }

+

+    /**

+     * Construct a new instance with the specified path and file contents

+     *

+     * @param path     - the value for path

+     * @param contents - the contents of the file, as a String

+     */

+    public FileEntry(String path, String contents) {

+        super(path);

+        setContents(contents);

+    }

+

+    /**

+     * Return false to indicate that this entry represents a file

+     *

+     * @return false

+     */

+    public boolean isDirectory() {

+        return false;

+    }

+

+    /**

+     * Return the size of this file

+     *

+     * @return the file size in bytes

+     */

+    public long getSize() {

+        return getCurrentBytes().length;

+    }

+

+    /**

+     * Set the contents of the file represented by this entry

+     *

+     * @param contents - the String whose bytes are used as the contents

+     */

+    public void setContents(String contents) {

+        byte[] newBytes = (contents != null) ? contents.getBytes() : EMPTY;

+        setContentsInternal(newBytes);

+    }

+

+    /**

+     * Set the contents of the file represented by this entry

+     *

+     * @param contents - the byte[] used as the contents

+     */

+    public void setContents(byte[] contents) {

+        // Copy the bytes[] to guard against subsequent modification of the source array

+        byte[] newBytes = EMPTY;

+        if (contents != null) {

+            newBytes = new byte[contents.length];

+            System.arraycopy(contents, 0, newBytes, 0, contents.length);

+        }

+        setContentsInternal(newBytes);

+    }

+

+    /**

+     * Create and return an InputStream for reading the contents of the file represented by this entry

+     *

+     * @return an InputStream

+     */

+    public InputStream createInputStream() {

+        return new ByteArrayInputStream(getCurrentBytes());

+    }

+

+    /**

+     * Create and return an OutputStream for writing the contents of the file represented by this entry

+     *

+     * @param append - true if the OutputStream should append to any existing contents false if

+     *               any existing contents should be overwritten

+     * @return an OutputStream

+     * @throws FileSystemException - if an error occurs creating or initializing the OutputStream

+     */

+    public OutputStream createOutputStream(boolean append) {

+        // If appending and we already have an OutputStream, then continue to use it

+        if (append && out != null) {

+            return out;

+        }

+

+        out = new ByteArrayOutputStream();

+        byte[] initialContents = (append) ? bytes : EMPTY;

+        try {

+            out.write(initialContents);

+        }

+        catch (IOException e) {

+            throw new FileSystemException(getPath(), null, e);

+        }

+        return out;

+    }

+

+    /**

+     * Return a new FileSystemEntry that is a clone of this object, except having the specified path

+     *

+     * @param path - the new path value for the cloned file system entry

+     * @return a new FileSystemEntry that has all the same values as this object except for its path

+     */

+    public FileSystemEntry cloneWithNewPath(String path) {

+        FileEntry clone = new FileEntry(path);

+        clone.setLastModified(getLastModified());

+        clone.setOwner(getOwner());

+        clone.setGroup(getGroup());

+        clone.setPermissions(getPermissions());

+        clone.setContents(getCurrentBytes());

+        return clone;

+    }

+

+    //-------------------------------------------------------------------------

+    // Internal Helper Methods

+    //-------------------------------------------------------------------------

+

+    /**

+     * @return the current contents of this file entry as a byte[]

+     */

+    private byte[] getCurrentBytes() {

+        return (out != null) ? out.toByteArray() : bytes;

+    }

+

+    /**

+     * Set the contents of the file represented by this entry

+     *

+     * @param contents - the byte[] used as the contents

+     */

+    private void setContentsInternal(byte[] contents) {

+        this.bytes = contents;

+

+        // Get rid of any OutputStream

+        this.out = null;

+    }

+

+    /**

+     * @see java.lang.Object#toString()

+     */

+    public String toString() {

+        return "File['" + getPath() + "' size=" + getSize() + " lastModified=" + getLastModified() + " owner="

+                + getOwner() + " group=" + getGroup() + " permissions=" + getPermissions() + "]";

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/FileSystem.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/FileSystem.java
new file mode 100644
index 0000000..10c47e9
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/FileSystem.java
@@ -0,0 +1,161 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.filesystem;

+

+import java.util.List;

+

+/**

+ * Interface for a file system for managing files and directories.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public interface FileSystem {

+

+    /**

+     * Add the specified file system entry (file or directory) to this file system

+     *

+     * @param entry - the FileSystemEntry to add

+     */

+    public void add(FileSystemEntry entry);

+

+    /**

+     * Return the List of FileSystemEntry objects for the files in the specified directory path. If the

+     * path does not refer to a valid directory, then an empty List is returned.

+     *

+     * @param path - the path of the directory whose contents should be returned

+     * @return the List of FileSystemEntry objects for all files in the specified directory may be empty

+     */

+    public List listFiles(String path);

+

+    /**

+     * Return the List of filenames in the specified directory path. The returned filenames do not

+     * include a path. If the path does not refer to a valid directory, then an empty List is

+     * returned.

+     *

+     * @param path - the path of the directory whose contents should be returned

+     * @return the List of filenames (not including paths) for all files in the specified directory

+     *         may be empty

+     * @throws AssertionError - if path is null

+     */

+    public List listNames(String path);

+

+    /**

+     * Delete the file or directory specified by the path. Return true if the file is successfully

+     * deleted, false otherwise. If the path refers to a directory, it must be empty. Return false

+     * if the path does not refer to a valid file or directory or if it is a non-empty directory.

+     *

+     * @param path - the path of the file or directory to delete

+     * @return true if the file or directory is successfully deleted

+     * @throws AssertionError - if path is null

+     */

+    public boolean delete(String path);

+

+    /**

+     * Rename the file or directory. Specify the FROM path and the TO path. Throw an exception if the FROM path or

+     * the parent directory of the TO path do not exist; or if the rename fails for another reason.

+     *

+     * @param fromPath - the source (old) path + filename

+     * @param toPath   - the target (new) path + filename

+     * @throws AssertionError      - if fromPath or toPath is null

+     * @throws FileSystemException - if the rename fails.

+     */

+    public void rename(String fromPath, String toPath);

+

+    /**

+     * Return the formatted directory listing entry for the file represented by the specified FileSystemEntry

+     *

+     * @param fileSystemEntry - the FileSystemEntry representing the file or directory entry to be formatted

+     * @return the the formatted directory listing entry

+     */

+    public String formatDirectoryListing(FileSystemEntry fileSystemEntry);

+

+    //-------------------------------------------------------------------------

+    // Path-related Methods

+    //-------------------------------------------------------------------------

+

+    /**

+     * Return true if there exists a file or directory at the specified path

+     *

+     * @param path - the path

+     * @return true if the file/directory exists

+     * @throws AssertionError - if path is null

+     */

+    public boolean exists(String path);

+

+    /**

+     * Return true if the specified path designates an existing directory, false otherwise

+     *

+     * @param path - the path

+     * @return true if path is a directory, false otherwise

+     * @throws AssertionError - if path is null

+     */

+    public boolean isDirectory(String path);

+

+    /**

+     * Return true if the specified path designates an existing file, false otherwise

+     *

+     * @param path - the path

+     * @return true if path is a file, false otherwise

+     * @throws AssertionError - if path is null

+     */

+    public boolean isFile(String path);

+

+    /**

+     * Return true if the specified path designates an absolute file path. What

+     * constitutes an absolute path is dependent on the file system implementation.

+     *

+     * @param path - the path

+     * @return true if path is absolute, false otherwise

+     * @throws AssertionError - if path is null

+     */

+    public boolean isAbsolute(String path);

+

+    /**

+     * Build a path from the two path components. Concatenate path1 and path2. Insert the file system-dependent

+     * separator character in between if necessary (i.e., if both are non-empty and path1 does not already

+     * end with a separator character AND path2 does not begin with one).

+     *

+     * @param path1 - the first path component may be null or empty

+     * @param path2 - the second path component may be null or empty

+     * @return the path resulting from concatenating path1 to path2

+     */

+    public String path(String path1, String path2);

+

+    /**

+     * Returns the FileSystemEntry object representing the file system entry at the specified path, or null

+     * if the path does not specify an existing file or directory within this file system.

+     *

+     * @param path - the path of the file or directory within this file system

+     * @return the FileSystemEntry containing the information for the file or directory, or else null

+     */

+    public FileSystemEntry getEntry(String path);

+

+    /**

+     * Return the parent path of the specified path. If <code>path</code> specifies a filename,

+     * then this method returns the path of the directory containing that file. If <code>path</code>

+     * specifies a directory, the this method returns its parent directory. If <code>path</code> is

+     * empty or does not have a parent component, then return an empty string.

+     * <p/>

+     * All path separators in the returned path are converted to the system-dependent separator character.

+     *

+     * @param path - the path

+     * @return the parent of the specified path, or null if <code>path</code> has no parent

+     * @throws AssertionError - if path is null

+     */

+    public String getParent(String path);

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/FileSystemEntry.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/FileSystemEntry.java
new file mode 100644
index 0000000..be99513
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/FileSystemEntry.java
@@ -0,0 +1,99 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.filesystem;

+

+import java.util.Date;

+

+/**

+ * Interface for an entry within a fake file system, representing a single file or directory.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public interface FileSystemEntry {

+

+    /**

+     * Return true if this entry represents a directory, false otherwise

+     *

+     * @return true if this file system entry is a directory, false otherwise

+     */

+    public boolean isDirectory();

+

+    /**

+     * Return the path for this file system entry

+     *

+     * @return the path for this file system entry

+     */

+    public String getPath();

+

+    /**

+     * Return the file name or directory name (no path) for this entry

+     *

+     * @return the file name or directory name (no path) for this entry

+     */

+    public String getName();

+

+    /**

+     * Return the size of this file system entry

+     *

+     * @return the file size in bytes

+     */

+    public long getSize();

+

+    /**

+     * Return the timestamp Date for the last modification of this file system entry

+     *

+     * @return the last modified timestamp Date for this file system entry

+     */

+    public Date getLastModified();

+

+    /**

+     * Set the timestamp Date for the last modification of this file system entry

+     *

+     * @param lastModified - the lastModified value, as a Date

+     */

+    public void setLastModified(Date lastModified);

+

+    /**

+     * @return the username of the owner of this file system entry

+     */

+    public String getOwner();

+

+    /**

+     * @return the name of the owning group for this file system entry

+     */

+    public String getGroup();

+

+    /**

+     * @return the Permissions for this file system entry

+     */

+    public Permissions getPermissions();

+

+    /**

+     * Return a new FileSystemEntry that is a clone of this object, except having the specified path

+     *

+     * @param path - the new path value for the cloned file system entry

+     * @return a new FileSystemEntry that has all the same values as this object except for its path

+     */

+    public FileSystemEntry cloneWithNewPath(String path);

+

+    /**

+     * Lock down the path so it cannot be changed

+     */

+    public void lockPath();

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/FileSystemException.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/FileSystemException.java
new file mode 100644
index 0000000..5fa4e97
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/FileSystemException.java
@@ -0,0 +1,77 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.filesystem;

+

+import org.mockftpserver.core.MockFtpServerException;

+

+/**

+ * Represents an error that occurs while performing a FileSystem operation.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class FileSystemException extends MockFtpServerException {

+

+    /**

+     * The path involved in the file system operation that caused the exception

+     */

+    private String path;

+

+    /**

+     * The message key for the exception message

+     */

+    private String messageKey;

+

+    /**

+     * Construct a new instance for the specified path and message key

+     *

+     * @param path       - the path involved in the file system operation that caused the exception

+     * @param messageKey - the exception message key

+     */

+    public FileSystemException(String path, String messageKey) {

+        super(path);

+        this.path = path;

+        this.messageKey = messageKey;

+    }

+

+    /**

+     * @param path       - the path involved in the file system operation that caused the exception

+     * @param messageKey - the exception message key

+     * @param cause      - the exception cause, wrapped by this exception

+     */

+    public FileSystemException(String path, String messageKey, Throwable cause) {

+        super(path, cause);

+        this.path = path;

+        this.messageKey = messageKey;

+    }

+

+    public String getPath() {

+        return path;

+    }

+

+    public void setPath(String path) {

+        this.path = path;

+    }

+

+    public String getMessageKey() {

+        return messageKey;

+    }

+

+    public void setMessageKey(String messageKey) {

+        this.messageKey = messageKey;

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/InvalidFilenameException.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/InvalidFilenameException.java
new file mode 100644
index 0000000..b207be9
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/InvalidFilenameException.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.filesystem;
+
+/**
+ * Exception thrown when a path/filename is not valid. Causes include:
+ * <ul>
+ * <li>The filename contains invalid characters</li>
+ * <li>The path specifies a new filename, but its parent directory does not exist</li>
+ * <li>The path is expected to be a file, but actually specifies an existing directory</li>
+ * </ul>
+ */
+public class InvalidFilenameException extends FileSystemException {
+
+    private static final String MESSAGE_KEY = "filesystem.pathIsNotValid";
+
+    /**
+     * @param path - the path involved in the file system operation that caused the exception
+     */
+    public InvalidFilenameException(String path) {
+        super(path, MESSAGE_KEY);
+    }
+
+    /**
+     * @param path  - the path involved in the file system operation that caused the exception
+     * @param cause - the exception cause, wrapped by this exception
+     */
+    public InvalidFilenameException(String path, Throwable cause) {
+        super(path, MESSAGE_KEY, cause);
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/Permissions.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/Permissions.java
new file mode 100644
index 0000000..8c39637
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/Permissions.java
@@ -0,0 +1,157 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.filesystem;

+

+import org.mockftpserver.core.util.Assert;

+

+/**

+ * Represents and encapsulates the read/write/execute permissions for a file or directory.

+ * This is conceptually (and somewhat loosely) based on the permissions flags within the Unix

+ * file system. An instance of this class is immutable.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class Permissions {

+    public static final Permissions ALL = new Permissions("rwxrwxrwx");

+    public static final Permissions NONE = new Permissions("---------");

+    public static final Permissions DEFAULT = ALL;

+

+    private static final char READ_CHAR = 'r';

+    private static final char WRITE_CHAR = 'w';

+    private static final char EXECUTE_CHAR = 'x';

+

+    private String rwxString;

+

+    /**

+     * Costruct a new instance for the specified read/write/execute specification String

+     *

+     * @param rwxString - the read/write/execute specification String; must be 9 characters long, with chars

+     *                  at index 0,3,6 == '-' or 'r', chars at index 1,4,7 == '-' or 'w' and chars at index 2,5,8 == '-' or 'x'.

+     */

+    public Permissions(String rwxString) {

+        Assert.isTrue(rwxString.length() == 9, "The permissions string must be exactly 9 characters");

+        final String RWX = "(-|r)(-|w)(-|x)";

+        final String PATTERN = RWX + RWX + RWX;

+        Assert.isTrue(rwxString.matches(PATTERN), "The permissions string must match [" + PATTERN + "]");

+        this.rwxString = rwxString;

+    }

+

+    /**

+     * Return the read/write/execute specification String representing the set of permissions. For example:

+     * "rwxrwxrwx" or "rw-r-----".

+     *

+     * @return the String containing 9 characters that represent the read/write/execute permissions.

+     */

+    public String asRwxString() {

+        return rwxString;

+    }

+

+    /**

+     * @return the RWX string for this instance

+     */

+    public String getRwxString() {

+        return rwxString;

+    }

+

+    /**

+     * @see java.lang.Object#equals(java.lang.Object)

+     */

+    public boolean equals(Object object) {

+        return (object != null)

+                && (object.getClass() == this.getClass())

+                && (object.hashCode() == hashCode());

+    }

+

+    /**

+     * Return the hash code for this object.

+     *

+     * @see java.lang.Object#hashCode()

+     */

+    public int hashCode() {

+        return rwxString.hashCode();

+    }

+

+    /**

+     * @return true if and only if the user has read permission

+     */

+    public boolean canUserRead() {

+        return rwxString.charAt(0) == READ_CHAR;

+    }

+

+    /**

+     * @return true if and only if the user has write permission

+     */

+    public boolean canUserWrite() {

+        return rwxString.charAt(1) == WRITE_CHAR;

+    }

+

+    /**

+     * @return true if and only if the user has execute permission

+     */

+    public boolean canUserExecute() {

+        return rwxString.charAt(2) == EXECUTE_CHAR;

+    }

+

+    /**

+     * @return true if and only if the group has read permission

+     */

+    public boolean canGroupRead() {

+        return rwxString.charAt(3) == READ_CHAR;

+    }

+

+    /**

+     * @return true if and only if the group has write permission

+     */

+    public boolean canGroupWrite() {

+        return rwxString.charAt(4) == WRITE_CHAR;

+    }

+

+    /**

+     * @return true if and only if the group has execute permission

+     */

+    public boolean canGroupExecute() {

+        return rwxString.charAt(5) == EXECUTE_CHAR;

+    }

+

+    /**

+     * @return true if and only if the world has read permission

+     */

+    public boolean canWorldRead() {

+        return rwxString.charAt(6) == READ_CHAR;

+    }

+

+    /**

+     * @return true if and only if the world has write permission

+     */

+    public boolean canWorldWrite() {

+        return rwxString.charAt(7) == WRITE_CHAR;

+    }

+

+    /**

+     * @return true if and only if the world has execute permission

+     */

+    public boolean canWorldExecute() {

+        return rwxString.charAt(8) == EXECUTE_CHAR;

+    }

+

+    /**

+     * @return the String representation of this object.

+     */

+    public String toString() {

+        return "Permissions[" + rwxString + "]";

+    }

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/UnixDirectoryListingFormatter.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/UnixDirectoryListingFormatter.java
new file mode 100644
index 0000000..d507345
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/UnixDirectoryListingFormatter.java
@@ -0,0 +1,81 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.filesystem;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.util.StringUtil;

+

+import java.text.DateFormat;

+import java.text.SimpleDateFormat;

+import java.util.Locale;

+

+/**

+ * Unix-specific implementation of the DirectoryListingFormatter interface.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class UnixDirectoryListingFormatter implements DirectoryListingFormatter {

+

+    private static final Logger LOG = LoggerFactory.getLogger(UnixDirectoryListingFormatter.class);

+

+    private static final String DATE_FORMAT = "MMM dd  yyyy";

+    private static final int SIZE_WIDTH = 15;

+    private static final int OWNER_WIDTH = 8;

+    private static final int GROUP_WIDTH = 8;

+    private static final String NONE = "none";

+

+    private Locale locale = Locale.ENGLISH;

+

+    // "-rw-rw-r--    1 ftp      ftp           254 Feb 23  2007 robots.txt"

+    // "-rw-r--r--    1 ftp      ftp      30014925 Apr 15 00:19 md5.sums.gz"

+    // "-rwxr-xr-x   1 henry    users       5778 Dec  1  2005 planaccess.sql"

+

+    /**

+     * Format the directory listing for a single file/directory entry.

+     *

+     * @param fileSystemEntry - the FileSystemEntry for a single file system entry

+     * @return the formatted directory listing

+     */

+    public String format(FileSystemEntry fileSystemEntry) {

+        DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, locale);

+        String dateStr = dateFormat.format(fileSystemEntry.getLastModified());

+        String dirOrFile = fileSystemEntry.isDirectory() ? "d" : "-";

+        Permissions permissions = fileSystemEntry.getPermissions() != null ? fileSystemEntry.getPermissions() : Permissions.DEFAULT;

+        String permissionsStr = StringUtil.padRight(permissions.asRwxString(), 9);

+        String linkCountStr = "1";

+        String ownerStr = StringUtil.padRight(stringOrNone(fileSystemEntry.getOwner()), OWNER_WIDTH);

+        String groupStr = StringUtil.padRight(stringOrNone(fileSystemEntry.getGroup()), GROUP_WIDTH);

+        String sizeStr = StringUtil.padLeft(Long.toString(fileSystemEntry.getSize()), SIZE_WIDTH);

+        String listing = "" + dirOrFile + permissionsStr + "  " + linkCountStr + " " + ownerStr + " " + groupStr + " " + sizeStr + " " + dateStr + " " + fileSystemEntry.getName();

+        LOG.info("listing=[" + listing + "]");

+        return listing;

+    }

+

+    /**

+     * Set the Locale to be used in formatting the date within file/directory listings

+     * @param locale - the Locale instance

+     */

+    public void setLocale(Locale locale) {

+        this.locale = locale;

+    }

+

+    private String stringOrNone(String string) {

+        return (string == null) ? NONE : string;

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/UnixFakeFileSystem.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/UnixFakeFileSystem.java
new file mode 100644
index 0000000..3d4b579
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/UnixFakeFileSystem.java
@@ -0,0 +1,87 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.filesystem;

+

+import org.mockftpserver.core.util.Assert;

+

+/**

+ * Implementation of the {@link FileSystem} interface that simulates a Unix

+ * file system. The rules for file and directory names include:

+ * <ul>

+ * <li>Filenames are case-sensitive</li>

+ * <li>Forward slashes (/) are the only valid path separators</li>

+ * </ul>

+ * <p/>

+ * The <code>directoryListingFormatter</code> property is automatically initialized to an instance

+ * of {@link UnixDirectoryListingFormatter}.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class UnixFakeFileSystem extends AbstractFakeFileSystem {

+

+    public static final char SEPARATOR = '/';

+

+    /**

+     * Construct a new instance and initialize the directoryListingFormatter to a UnixDirectoryListingFormatter.

+     */

+    public UnixFakeFileSystem() {

+        this.setDirectoryListingFormatter(new UnixDirectoryListingFormatter());

+    }

+

+    //-------------------------------------------------------------------------

+    // Abstract Method Implementations

+    //-------------------------------------------------------------------------

+

+    protected char getSeparatorChar() {

+        return SEPARATOR;

+    }

+

+    /**

+     * Return true if the specified path designates a valid (absolute) file path. For Unix,

+     * a path is valid if it starts with the '/' character, followed by zero or more names

+     * (a sequence of any characters except '/'), delimited by '/'. The path may optionally

+     * contain a terminating '/'.

+     *

+     * @param path - the path

+     * @return true if path is valid, false otherwise

+     * @throws AssertionError - if path is null

+     */

+    protected boolean isValidName(String path) {

+        Assert.notNull(path, "path");

+        // Any character but '/'

+        return path.matches("\\/|(\\/[^\\/]+\\/?)+");

+

+    }

+

+    /**

+     * Return true if the specified char is a separator character ('\' or '/')

+     *

+     * @param c - the character to test

+     * @return true if the specified char is a separator character ('\' or '/')

+     */

+    protected boolean isSeparator(char c) {

+        return c == SEPARATOR;

+    }

+

+    /**

+     * @return true if the specified path component is a root for this filesystem

+     */

+    protected boolean isRoot(String pathComponent) {

+        return pathComponent.indexOf(":") != -1;

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/WindowsDirectoryListingFormatter.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/WindowsDirectoryListingFormatter.java
new file mode 100644
index 0000000..9479e64
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/WindowsDirectoryListingFormatter.java
@@ -0,0 +1,50 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.filesystem;

+

+import org.mockftpserver.core.util.StringUtil;

+

+import java.text.DateFormat;

+import java.text.SimpleDateFormat;

+import java.util.Locale;

+

+/**

+ * Windows-specific implementation of the DirectoryListingFormatter interface.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class WindowsDirectoryListingFormatter implements DirectoryListingFormatter {

+

+    private static final String DATE_FORMAT = "MM-dd-yy hh:mmaa";

+    private static final int SIZE_WIDTH = 15;

+

+    /**

+     * Format the directory listing for a single file/directory entry.

+     *

+     * @param fileSystemEntry - the FileSystemEntry for a single file system entry

+     * @return the formatted directory listing

+     */

+    public String format(FileSystemEntry fileSystemEntry) {

+        DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.ENGLISH);

+        String dateStr = dateFormat.format(fileSystemEntry.getLastModified());

+        String dirOrSize = fileSystemEntry.isDirectory()

+                ? StringUtil.padRight("<DIR>", SIZE_WIDTH)

+                : StringUtil.padLeft(Long.toString(fileSystemEntry.getSize()), SIZE_WIDTH);

+        return dateStr + "  " + dirOrSize + "  " + fileSystemEntry.getName();

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/WindowsFakeFileSystem.java b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/WindowsFakeFileSystem.java
new file mode 100644
index 0000000..71a3b36
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/fake/filesystem/WindowsFakeFileSystem.java
@@ -0,0 +1,102 @@
+/*

+ * Copyright 2008 the original author or authors.

+ *

+ * 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 org.mockftpserver.fake.filesystem;

+

+import org.mockftpserver.core.util.Assert;

+

+/**

+ * Implementation of the {@link FileSystem} interface that simulates a Microsoft

+ * Windows file system. The rules for file and directory names include:

+ * <ul>

+ * <li>Filenames are case-insensitive (and normalized to lower-case)</li>

+ * <li>Either forward slashes (/) or backward slashes (\) are valid path separators (but are normalized to '\')</li>

+ * <li>An absolute path starts with a drive specifier (e.g. 'a:' or 'c:') followed

+ * by '\' or '/', or else if it starts with "\\"</li>

+ * </ul>

+ * <p/>

+ * The <code>directoryListingFormatter</code> property is automatically initialized to an instance

+ * of {@link WindowsDirectoryListingFormatter}.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class WindowsFakeFileSystem extends AbstractFakeFileSystem {

+

+    public static final char SEPARATOR = '\\';

+    private static final String VALID_PATTERN = "\\p{Alpha}\\:" + "(\\\\|(\\\\[^\\\\\\:\\*\\?\\<\\>\\|\\\"]+)+)";

+    //static final VALID_PATTERN = /\p{Alpha}\:(\\|(\\[^\\\:\*\?\<\>\|\"]+)+)/

+    private static final String LAN_PREFIX = "\\\\";

+

+    /**

+     * Construct a new instance and initialize the directoryListingFormatter to a WindowsDirectoryListingFormatter.

+     */

+    public WindowsFakeFileSystem() {

+        this.setDirectoryListingFormatter(new WindowsDirectoryListingFormatter());

+    }

+

+    //-------------------------------------------------------------------------

+    // Abstract Or Overridden Method Implementations

+    //-------------------------------------------------------------------------

+

+    /**

+     * Return the normalized and unique key used to access the file system entry. Windows is case-insensitive,

+     * so normalize all paths to lower-case.

+     *

+     * @param path - the path

+     * @return the corresponding normalized key

+     */

+    protected String getFileSystemEntryKey(String path) {

+        return normalize(path).toLowerCase();

+    }

+

+    protected char getSeparatorChar() {

+        return SEPARATOR;

+    }

+

+    /**

+     * Return true if the specified path designates a valid (absolute) file path. For Windows

+     * paths, a path is valid if it starts with a drive specifier followed by

+     * '\' or '/', or if it starts with "\\".

+     *

+     * @param path - the path

+     * @return true if path is valid, false otherwise

+     * @throws AssertionError - if path is null

+     */

+    protected boolean isValidName(String path) {

+        // \/:*?"<>|

+        Assert.notNull(path, "path");

+        String standardized = path.replace('/', '\\');

+        return standardized.matches(VALID_PATTERN) || standardized.startsWith(LAN_PREFIX);

+    }

+

+    /**

+     * Return true if the specified char is a separator character ('\' or '/')

+     *

+     * @param c - the character to test

+     * @return true if the specified char is a separator character ('\' or '/')

+     */

+    protected boolean isSeparator(char c) {

+        return c == '\\' || c == '/';

+    }

+

+    /**

+     * @return true if the specified path component is a root for this filesystem

+     */

+    protected boolean isRoot(String pathComponent) {

+        return pathComponent.indexOf(":") != -1;

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/StubFtpServer.java b/tags/2.5/src/main/java/org/mockftpserver/stub/StubFtpServer.java
new file mode 100644
index 0000000..90c9a70
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/StubFtpServer.java
@@ -0,0 +1,148 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub;

+

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ConnectCommandHandler;

+import org.mockftpserver.core.command.ReplyTextBundleUtil;

+import org.mockftpserver.core.command.UnsupportedCommandHandler;

+import org.mockftpserver.core.server.AbstractFtpServer;

+import org.mockftpserver.stub.command.*;

+

+/**

+ * <b>StubFtpServer</b> is the top-level class for a "stub" implementation of an FTP Server,

+ * suitable for testing FTP client code or standing in for a live FTP server. It supports

+ * the main FTP commands by defining handlers for each of the corresponding low-level FTP

+ * server commands (e.g. RETR, DELE, LIST). These handlers implement the {@link CommandHandler}

+ * interface.

+ * <p/>

+ * <b>StubFtpServer</b> works out of the box with default command handlers that return

+ * success reply codes and empty data (for retrieved files, directory listings, etc.).

+ * The command handler for any command can be easily configured to return custom data

+ * or reply codes. Or it can be replaced with a custom {@link CommandHandler}

+ * implementation. This allows simulation of a complete range of both success and

+ * failure scenarios. The command handlers can also be interrogated to verify command

+ * invocation data such as command parameters and timestamps.

+ * <p/>

+ * <b>StubFtpServer</b> can be fully configured programmatically or within the

+ * <a href="http://www.springframework.org/">Spring Framework</a> or similar container.

+ * <p/>

+ * <h4>Starting the StubFtpServer</h4>

+ * Here is how to start the <b>StubFtpServer</b> with the default configuration.

+ * <pre><code>

+ * StubFtpServer stubFtpServer = new StubFtpServer();

+ * stubFtpServer.start();

+ * </code></pre>

+ * <p/>

+ * <h4>FTP Server Control Port</h4>

+ * By default, <b>StubFtpServer</b> binds to the server control port of 21. You can use a different server control

+ * port by setting the <code>serverControlPort</code> property. If you specify a value of <code>0</code>,

+ * then a free port number will be chosen automatically; call <code>getServerControlPort()</code> AFTER

+ * <code>start()</code> has been called to determine the actual port number being used. Using a non-default

+ * port number is usually necessary when running on Unix or some other system where that port number is

+ * already in use or cannot be bound from a user process.

+ * <p/>

+ * <h4>Retrieving Command Handlers</h4>

+ * You can retrieve the existing {@link CommandHandler} defined for an FTP server command

+ * by calling the {@link #getCommandHandler(String)} method, passing in the FTP server

+ * command name. For example:

+ * <pre><code>

+ * PwdCommandHandler pwdCommandHandler = (PwdCommandHandler) stubFtpServer.getCommandHandler("PWD");

+ * </code></pre>

+ * <p/>

+ * <h4>Replacing Command Handlers</h4>

+ * You can replace the existing {@link CommandHandler} defined for an FTP server command

+ * by calling the {@link #setCommandHandler(String, CommandHandler)} method, passing

+ * in the FTP server command name and {@link CommandHandler} instance. For example:

+ * <pre><code>

+ * PwdCommandHandler pwdCommandHandler = new PwdCommandHandler();

+ * pwdCommandHandler.setDirectory("some/dir");

+ * stubFtpServer.setCommandHandler("PWD", pwdCommandHandler);

+ * </code></pre>

+ * You can also replace multiple command handlers at once by using the {@link #setCommandHandlers(java.util.Map)}

+ * method. That is especially useful when configuring the server through the <b>Spring Framework</b>.

+ * <h4>FTP Command Reply Text ResourceBundle</h4>

+ * <p/>

+ * The default text asociated with each FTP command reply code is contained within the

+ * "ReplyText.properties" ResourceBundle file. You can customize these messages by providing a

+ * locale-specific ResourceBundle file on the CLASSPATH, according to the normal lookup rules of

+ * the ResourceBundle class (e.g., "ReplyText_de.properties"). Alternatively, you can

+ * completely replace the ResourceBundle file by calling the calling the

+ * {@link #setReplyTextBaseName(String)} method.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class StubFtpServer extends AbstractFtpServer {

+

+    /**

+     * Create a new instance. Initialize the default command handlers and

+     * reply text ResourceBundle.

+     */

+    public StubFtpServer() {

+        PwdCommandHandler pwdCommandHandler = new PwdCommandHandler();

+

+        // Initialize the default CommandHandler mappings

+        setCommandHandler(CommandNames.ABOR, new AborCommandHandler());

+        setCommandHandler(CommandNames.ACCT, new AcctCommandHandler());

+        setCommandHandler(CommandNames.ALLO, new AlloCommandHandler());

+        setCommandHandler(CommandNames.APPE, new AppeCommandHandler());

+        setCommandHandler(CommandNames.PWD, pwdCommandHandler);            // same as XPWD

+        setCommandHandler(CommandNames.CONNECT, new ConnectCommandHandler());

+        setCommandHandler(CommandNames.CWD, new CwdCommandHandler());

+        setCommandHandler(CommandNames.CDUP, new CdupCommandHandler());

+        setCommandHandler(CommandNames.DELE, new DeleCommandHandler());

+        setCommandHandler(CommandNames.EPRT, new EprtCommandHandler());

+        setCommandHandler(CommandNames.EPSV, new EpsvCommandHandler());

+        setCommandHandler(CommandNames.HELP, new HelpCommandHandler());

+        setCommandHandler(CommandNames.LIST, new ListCommandHandler());

+        setCommandHandler(CommandNames.MKD, new MkdCommandHandler());

+        setCommandHandler(CommandNames.MODE, new ModeCommandHandler());

+        setCommandHandler(CommandNames.NOOP, new NoopCommandHandler());

+        setCommandHandler(CommandNames.NLST, new NlstCommandHandler());

+        setCommandHandler(CommandNames.PASS, new PassCommandHandler());

+        setCommandHandler(CommandNames.PASV, new PasvCommandHandler());

+        setCommandHandler(CommandNames.PORT, new PortCommandHandler());

+        setCommandHandler(CommandNames.RETR, new RetrCommandHandler());

+        setCommandHandler(CommandNames.QUIT, new QuitCommandHandler());

+        setCommandHandler(CommandNames.REIN, new ReinCommandHandler());

+        setCommandHandler(CommandNames.REST, new RestCommandHandler());

+        setCommandHandler(CommandNames.RMD, new RmdCommandHandler());

+        setCommandHandler(CommandNames.RNFR, new RnfrCommandHandler());

+        setCommandHandler(CommandNames.RNTO, new RntoCommandHandler());

+        setCommandHandler(CommandNames.SITE, new SiteCommandHandler());

+        setCommandHandler(CommandNames.SMNT, new SmntCommandHandler());

+        setCommandHandler(CommandNames.STAT, new StatCommandHandler());

+        setCommandHandler(CommandNames.STOR, new StorCommandHandler());

+        setCommandHandler(CommandNames.STOU, new StouCommandHandler());

+        setCommandHandler(CommandNames.STRU, new StruCommandHandler());

+        setCommandHandler(CommandNames.SYST, new SystCommandHandler());

+        setCommandHandler(CommandNames.TYPE, new TypeCommandHandler());

+        setCommandHandler(CommandNames.USER, new UserCommandHandler());

+        setCommandHandler(CommandNames.UNSUPPORTED, new UnsupportedCommandHandler());

+        setCommandHandler(CommandNames.XPWD, pwdCommandHandler);           // same as PWD

+    }

+

+    //-------------------------------------------------------------------------

+    // Abstract method implementation

+    //-------------------------------------------------------------------------

+

+    protected void initializeCommandHandler(CommandHandler commandHandler) {

+        ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, getReplyTextBundle());

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/AborCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AborCommandHandler.java
new file mode 100644
index 0000000..6260344
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AborCommandHandler.java
@@ -0,0 +1,45 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the ABOR command. Return a reply code of 226.

+ * <p>

+ * Each invocation record stored by this CommandHandler contains no data elements.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class AborCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public AborCommandHandler() {

+        setReplyCode(ReplyCodes.ABOR_OK);

+    }

+

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/AbstractStorCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AbstractStorCommandHandler.java
new file mode 100644
index 0000000..4201516
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AbstractStorCommandHandler.java
@@ -0,0 +1,49 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * Abstract superclass for CommandHandler for commands that store a file. Send back two replies on the

+ * control connection: a reply code of 150 and another of 226.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #FILE_CONTENTS_KEY} ("fileContents") - the file contents (<code>byte[]</code>) sent on the data connection

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractStorCommandHandler extends AbstractStubDataCommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+    public static final String FILE_CONTENTS_KEY = "filecontents";

+

+    /**

+     * @see AbstractStubDataCommandHandler#processData(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    protected void processData(Command command, Session session, InvocationRecord invocationRecord) {

+        byte[] data = session.readData();

+        LOG.info("Received " + data.length + " bytes");

+        LOG.trace("Received data [" + new String(data) + "]");

+        invocationRecord.set(FILE_CONTENTS_KEY, data);

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/AbstractStubCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AbstractStubCommandHandler.java
new file mode 100644
index 0000000..5fdf852
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AbstractStubCommandHandler.java
@@ -0,0 +1,31 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractStaticReplyCommandHandler;

+

+/**

+ * The abstract superclass for CommandHandler classes for the {@link org.mockftpserver.stub.StubFtpServer}.

+ * <p>

+ * Subclasses can optionally override the reply code and/or text for the reply by calling

+ * {@link #setReplyCode(int)}, {@link #setReplyMessageKey(String)} and {@link #setReplyText(String)}.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractStubCommandHandler extends AbstractStaticReplyCommandHandler {

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/AbstractStubDataCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AbstractStubDataCommandHandler.java
new file mode 100644
index 0000000..2a7c899
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AbstractStubDataCommandHandler.java
@@ -0,0 +1,231 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractTrackingCommandHandler;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.AssertFailedException;

+

+/**

+ * Abstract superclass for CommandHandlers that read from or write to the data connection.

+ * <p/>

+ * Return two replies on the control connection: by default a reply code of 150 before the

+ * data transfer across the data connection and another reply of 226 after the data transfer

+ * is complete.

+ * <p/>

+ * This class implements the <i>Template Method</i> pattern. Subclasses must implement the abstract

+ * <code>processData</code> method to perform read or writes across the data connection.

+ * <p/>

+ * Subclasses can optionally override the {@link #beforeProcessData(Command, Session, InvocationRecord)}

+ * method for logic before the data transfer or the {@link #afterProcessData(Command, Session, InvocationRecord)}

+ * method for logic after the data transfer.

+ * <p/>

+ * Subclasses can optionally override the reply code and/or text for the initial reply (before

+ * the data transfer across the data connection) by calling {@link #setPreliminaryReplyCode(int)},

+ * {@link #setPreliminaryReplyMessageKey(String)} and/or {@link #setPreliminaryReplyText(String)}

+ * methods.

+ * <p/>

+ * Subclasses can optionally override the reply code and/or text for the final reply (after the

+ * the data transfer is complete) by calling {@link #setFinalReplyCode(int)},

+ * {@link #setFinalReplyMessageKey(String)} and/or {@link #setFinalReplyText(String)} methods.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractStubDataCommandHandler extends AbstractTrackingCommandHandler implements CommandHandler {

+

+    // The completion reply code sent before the data transfer

+    protected int preliminaryReplyCode = 0;

+

+    // The text for the preliminary reply. If null, use the default message associated with the reply code.

+    // If not null, this value overrides the preliminaryReplyMessageKey - i.e., this text is used instead of

+    // a localized message. 

+    protected String preliminaryReplyText = null;

+

+    // The message key for the preliminary reply text. If null, use the default message associated with 

+    // the reply code.

+    protected String preliminaryReplyMessageKey = null;

+

+    // The completion reply code sent after data transfer

+    protected int finalReplyCode = 0;

+

+    // The text for the completion reply. If null, use the default message associated with the reply code.

+    // If not null, this value overrides the finalReplyMessageKey - i.e., this text is used instead of

+    // a localized message. 

+    protected String finalReplyText = null;

+

+    // The message key for the completion reply text. If null, use the default message associated with the reply code 

+    protected String finalReplyMessageKey = null;

+

+    /**

+     * Constructor. Initialize the preliminary and final reply code.

+     */

+    protected AbstractStubDataCommandHandler() {

+        setPreliminaryReplyCode(ReplyCodes.TRANSFER_DATA_INITIAL_OK);

+        setFinalReplyCode(ReplyCodes.TRANSFER_DATA_FINAL_OK);

+    }

+

+    /**

+     * Handle the command. Perform the following steps:

+     * <ol>

+     * <li>Invoke the <code>beforeProcessData()</code> method</li>

+     * <li>Open the data connection</li>

+     * <li>Send an preliminary reply, default reply code 150</li>

+     * <li>Invoke the <code>processData()</code> method</li>

+     * <li>Close the data connection</li>

+     * <li>Send the final reply, default reply code 226</li>

+     * <li>Invoke the <code>afterProcessData()</code> method</li>

+     * </ol>

+     *

+     * @see org.mockftpserver.core.command.AbstractTrackingCommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    public final void handleCommand(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+

+        beforeProcessData(command, session, invocationRecord);

+

+        sendPreliminaryReply(session);

+        session.openDataConnection();

+        processData(command, session, invocationRecord);

+        session.closeDataConnection();

+        sendFinalReply(session);

+

+        afterProcessData(command, session, invocationRecord);

+    }

+

+    /**

+     * Send the final reply. The default implementation sends a reply code of 226 with the

+     * corresponding associated reply text.

+     *

+     * @param session - the Session

+     */

+    protected void sendFinalReply(Session session) {

+        sendReply(session, finalReplyCode, finalReplyMessageKey, finalReplyText, null);

+    }

+

+    /**

+     * Perform any necessary logic before transferring data across the data connection.

+     * Do nothing by default. Subclasses should override to validate command parameters and

+     * store information in the InvocationRecord.

+     *

+     * @param command          - the Command to be handled

+     * @param session          - the session on which the Command was submitted

+     * @param invocationRecord - the InvocationRecord; CommandHandlers are expected to add

+     *                         handler-specific data to the InvocationRecord, as appropriate

+     * @throws Exception

+     */

+    protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+        // Do nothing by default

+    }

+

+    /**

+     * Abstract method placeholder for subclass transfer of data across the data connection.

+     * Subclasses must override. The data connection is opened before this method and is

+     * closed after this method completes.

+     *

+     * @param command          - the Command to be handled

+     * @param session          - the session on which the Command was submitted

+     * @param invocationRecord - the InvocationRecord; CommandHandlers are expected to add

+     *                         handler-specific data to the InvocationRecord, as appropriate

+     * @throws Exception

+     */

+    protected abstract void processData(Command command, Session session, InvocationRecord invocationRecord) throws Exception;

+

+    /**

+     * Perform any necessary logic after transferring data across the data connection.

+     * Do nothing by default.

+     *

+     * @param command          - the Command to be handled

+     * @param session          - the session on which the Command was submitted

+     * @param invocationRecord - the InvocationRecord; CommandHandlers are expected to add

+     *                         handler-specific data to the InvocationRecord, as appropriate

+     * @throws Exception

+     */

+    protected void afterProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+        // Do nothing by default

+    }

+

+    /**

+     * Send the preliminary reply for this command on the control connection.

+     *

+     * @param session - the Session

+     */

+    private void sendPreliminaryReply(Session session) {

+        sendReply(session, preliminaryReplyCode, preliminaryReplyMessageKey, preliminaryReplyText, null);

+    }

+

+    /**

+     * Set the completion reply code sent after data transfer

+     *

+     * @param finalReplyCode - the final reply code

+     * @throws AssertFailedException - if the finalReplyCode is invalid

+     */

+    public void setFinalReplyCode(int finalReplyCode) {

+        assertValidReplyCode(finalReplyCode);

+        this.finalReplyCode = finalReplyCode;

+    }

+

+    /**

+     * Set the message key for the completion reply text sent after data transfer

+     *

+     * @param finalReplyMessageKey - the final reply message key

+     */

+    public void setFinalReplyMessageKey(String finalReplyMessageKey) {

+        this.finalReplyMessageKey = finalReplyMessageKey;

+    }

+

+    /**

+     * Set the text of the completion reply sent after data transfer

+     *

+     * @param finalReplyText - the final reply text

+     */

+    public void setFinalReplyText(String finalReplyText) {

+        this.finalReplyText = finalReplyText;

+    }

+

+    /**

+     * Set the completion reply code sent before data transfer

+     *

+     * @param preliminaryReplyCode - the preliminary reply code to set

+     * @throws AssertFailedException - if the preliminaryReplyCode is invalid

+     */

+    public void setPreliminaryReplyCode(int preliminaryReplyCode) {

+        assertValidReplyCode(preliminaryReplyCode);

+        this.preliminaryReplyCode = preliminaryReplyCode;

+    }

+

+    /**

+     * Set the message key for the completion reply text sent before data transfer

+     *

+     * @param preliminaryReplyMessageKey - the preliminary reply message key

+     */

+    public void setPreliminaryReplyMessageKey(String preliminaryReplyMessageKey) {

+        this.preliminaryReplyMessageKey = preliminaryReplyMessageKey;

+    }

+

+    /**

+     * Set the text of the completion reply sent before data transfer

+     *

+     * @param preliminaryReplyText - the preliminary reply text

+     */

+    public void setPreliminaryReplyText(String preliminaryReplyText) {

+        this.preliminaryReplyText = preliminaryReplyText;

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/AcctCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AcctCommandHandler.java
new file mode 100644
index 0000000..ea70df5
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AcctCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the ACCT command. Send back a reply code of 230.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>"acount" - the account submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class AcctCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String ACCOUNT_KEY = "account";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public AcctCommandHandler() {

+        setReplyCode(ReplyCodes.ACCT_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(ACCOUNT_KEY, command.getRequiredParameter(0));

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/AlloCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AlloCommandHandler.java
new file mode 100644
index 0000000..3e2eb8c
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AlloCommandHandler.java
@@ -0,0 +1,73 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.Assert;

+

+import java.util.StringTokenizer;

+

+/**

+ * CommandHandler for the ALLO (Allocate) command. Send back a reply code of 200.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #NUMBER_OF_BYTES_KEY} ("numberOfBytes") - the number of bytes submitted

+ * on the invocation (the first command parameter)

+ * <li>{@link #RECORD_SIZE_KEY} ("recordSize") - the record size optionally submitted

+ * on the invocation (the second command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class AlloCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String NUMBER_OF_BYTES_KEY = "numberOfBytes";

+    public static final String RECORD_SIZE_KEY = "recordSize";

+    private static final String RECORD_SIZE_DELIMITER = " R ";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public AlloCommandHandler() {

+        setReplyCode(ReplyCodes.ALLO_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        String parametersString = command.getRequiredParameter(0);

+

+        if (parametersString.indexOf(RECORD_SIZE_DELIMITER) == -1) {

+            invocationRecord.set(NUMBER_OF_BYTES_KEY, Integer.valueOf(parametersString));

+        } else {

+            // If the recordSize delimiter (" R ") is specified, then it must be followed by the recordSize.

+            StringTokenizer tokenizer = new StringTokenizer(parametersString, RECORD_SIZE_DELIMITER);

+            invocationRecord.set(NUMBER_OF_BYTES_KEY, Integer.valueOf(tokenizer.nextToken()));

+            Assert.isTrue(tokenizer.hasMoreTokens(), "Missing record size: [" + parametersString + "]");

+            invocationRecord.set(RECORD_SIZE_KEY, Integer.valueOf(tokenizer.nextToken()));

+        }

+

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/AppeCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AppeCommandHandler.java
new file mode 100644
index 0000000..8fc522b
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/AppeCommandHandler.java
@@ -0,0 +1,45 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the APPE (Append) command. Send back two replies on the control connection: a

+ * reply code of 150 and another of 226.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the directory submitted on the invocation (the first command parameter)

+ * <li>{@link #FILE_CONTENTS_KEY} ("fileContents") - the file contents (<code>byte[]</code>) sent on the data connection

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class AppeCommandHandler extends AbstractStorCommandHandler {

+

+    /**

+     * @see org.mockftpserver.stub.command.AbstractStubDataCommandHandler#beforeProcessData(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+        String filename = command.getRequiredParameter(0);

+        invocationRecord.set(PATHNAME_KEY, filename);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/CdupCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/CdupCommandHandler.java
new file mode 100644
index 0000000..5ed78e5
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/CdupCommandHandler.java
@@ -0,0 +1,45 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the CDUP (Change To Parent Directory) command. Send back a reply code of 250.

+ * <p>

+ * Each invocation record stored by this CommandHandler contains no data elements.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class CdupCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public CdupCommandHandler() {

+        setReplyCode(ReplyCodes.CDUP_OK);

+    }

+

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/CwdCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/CwdCommandHandler.java
new file mode 100644
index 0000000..412e85d
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/CwdCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the CWD (Change Working Directory) command. Send back a reply code of 250.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the directory submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class CwdCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+

+    /**

+     * Constructor. Initiate the replyCode.

+     */

+    public CwdCommandHandler() {

+        setReplyCode(ReplyCodes.CWD_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0));

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/DeleCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/DeleCommandHandler.java
new file mode 100644
index 0000000..ef2533b
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/DeleCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the DELE (Delete) command. Send back a reply code of 250.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the file name submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class DeleCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public DeleCommandHandler() {

+        setReplyCode(ReplyCodes.DELE_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0));

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/EprtCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/EprtCommandHandler.java
new file mode 100644
index 0000000..fc1ab1e
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/EprtCommandHandler.java
@@ -0,0 +1,71 @@
+/*

+ * Copyright 2009 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.PortParser;

+import org.mockftpserver.core.util.HostAndPort;

+

+import java.net.UnknownHostException;

+

+/**

+ * CommandHandler for the EPRT command. Send back a reply code of 200.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #HOST_KEY} ("host") - the client data host (InetAddress) submitted on the invocation (from parameters 1-4)

+ * <li>{@link #PORT_KEY} ("port") - the port number (Integer) submitted on the invocation (from parameter 5-6)

+ * </ul>

+ * See RFC2428 for more information.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class EprtCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String HOST_KEY = "host";

+    public static final String PORT_KEY = "port";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public EprtCommandHandler() {

+        setReplyCode(ReplyCodes.EPRT_OK);

+    }

+

+    /**

+     * Handle the command

+     *

+     * @throws java.net.UnknownHostException

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) throws UnknownHostException {

+        String parameter = command.getRequiredParameter(0);

+

+        HostAndPort client = PortParser.parseExtendedAddressHostAndPort(parameter);

+        LOG.debug("host=" + client.host + " port=" + client.port);

+        session.setClientDataHost(client.host);

+        session.setClientDataPort(client.port);

+        invocationRecord.set(HOST_KEY, client.host);

+        invocationRecord.set(PORT_KEY, new Integer(client.port));

+        sendReply(session);

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/EpsvCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/EpsvCommandHandler.java
new file mode 100644
index 0000000..602bd0d
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/EpsvCommandHandler.java
@@ -0,0 +1,61 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+import java.io.IOException;

+import java.net.InetAddress;

+

+/**

+ * CommandHandler for the EPSV (Extended Address Passive Mode) command. Request the Session to switch

+ * to passive data connection mode. Return a reply code of 229, along with response text of the form:

+ * "<i>Entering Extended Passive Mode (|||PORT|)</i>", where <i>PORT</i> is the 16-bit TCP port

+ * address of the data connection on the server to which the client must connect.

+ * See RFC2428 for more information.

+ * <p/>

+ * Each invocation record stored by this CommandHandler contains no data elements.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class EpsvCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public EpsvCommandHandler() {

+        setReplyCode(ReplyCodes.EPSV_OK);

+    }

+

+    /**

+     * @throws java.io.IOException

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord)

+            throws IOException {

+

+        int port = session.switchToPassiveMode();

+        InetAddress server = session.getServerHost();

+        LOG.debug("server=" + server + " port=" + port);

+        sendReply(session, Integer.toString(port));

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/FileRetrCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/FileRetrCommandHandler.java
new file mode 100644
index 0000000..84e35ac
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/FileRetrCommandHandler.java
@@ -0,0 +1,111 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.MockFtpServerException;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.Assert;

+import org.mockftpserver.core.util.AssertFailedException;

+

+import java.io.IOException;

+import java.io.InputStream;

+

+/**

+ * CommandHandler for the RETR command. Returns the contents of the specified file on the

+ * data connection, along with two replies on the control connection: a reply code of 150 and

+ * another of 226.

+ * <p/>

+ * The <code>file</code> property specifies the pathname for the file whose contents should

+ * be returned from this command. The file path is relative to the CLASSPATH (using the

+ * ClassLoader for this class).

+ * <p/>

+ * An exception is thrown if the <code>file</code> property has not been set or if the specified

+ * file does not exist or cannot be read.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the file submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class FileRetrCommandHandler extends AbstractStubDataCommandHandler implements CommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+    static final int BUFFER_SIZE = 512;     // package-private for testing

+

+    private String file;

+

+    /**

+     * Create new uninitialized instance

+     */

+    public FileRetrCommandHandler() {

+    }

+

+    /**

+     * Create new instance using the specified file pathname

+     *

+     * @param file - the path to the file

+     * @throws AssertFailedException - if the file is null

+     */

+    public FileRetrCommandHandler(String file) {

+        setFile(file);

+    }

+

+    /**

+     * @see org.mockftpserver.stub.command.AbstractStubDataCommandHandler#beforeProcessData(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+        Assert.notNull(file, "file");

+        invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0));

+    }

+

+    /**

+     * @see org.mockftpserver.stub.command.AbstractStubDataCommandHandler#processData(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    protected void processData(Command command, Session session, InvocationRecord invocationRecord) {

+        InputStream inputStream = getClass().getClassLoader().getResourceAsStream(file);

+        Assert.notNull(inputStream, "InputStream for [" + file + "]");

+        byte[] buffer = new byte[BUFFER_SIZE];

+        try {

+            int numBytes;

+            while ((numBytes = inputStream.read(buffer)) != -1) {

+                LOG.trace("Sending " + numBytes + " bytes...");

+                session.sendData(buffer, numBytes);

+            }

+        }

+        catch (IOException e) {

+            throw new MockFtpServerException(e);

+        }

+    }

+

+    /**

+     * Set the path of the file whose contents should be returned when this command is

+     * invoked. The path is relative to the CLASSPATH.

+     *

+     * @param file - the path to the file

+     * @throws AssertFailedException - if the file is null

+     */

+    public void setFile(String file) {

+        Assert.notNull(file, "file");

+        this.file = file;

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/HelpCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/HelpCommandHandler.java
new file mode 100644
index 0000000..8d9e59e
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/HelpCommandHandler.java
@@ -0,0 +1,68 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the HELP command. By default, return an empty help message,

+ * along with a reply code of 214. You can customize the returned help message by

+ * setting the <code>helpMessage</code> property.

+ * <p>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #COMMAND_NAME_KEY} ("commandName") - the command name optionally submitted on

+ * the invocation (the first command parameter). May be null.

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class HelpCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String COMMAND_NAME_KEY = "commandName";

+

+    private String helpMessage = "";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public HelpCommandHandler() {

+        setReplyCode(ReplyCodes.HELP_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.AbstractTrackingCommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(COMMAND_NAME_KEY, command.getOptionalString(0));

+        sendReply(session, helpMessage);

+    }

+

+    /**

+     * Set the help message String to be returned by this command

+     *

+     * @param helpMessage - the help message

+     */

+    public void setHelpMessage(String helpMessage) {

+        this.helpMessage = helpMessage;

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/ListCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/ListCommandHandler.java
new file mode 100644
index 0000000..d7ec131
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/ListCommandHandler.java
@@ -0,0 +1,91 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the LIST command. Return the configured directory listing on the data

+ * connection, along with two replies on the control connection: a reply code of 150 and

+ * another of 226. By default, return an empty directory listing. You can customize the

+ * returned directory listing by setting the <code>directoryListing</code> property.

+ * <p/>

+ * The interpretation of the value returned from this command is dependent upon the value returned

+ * by the SYST command. The format of the directory listing should match the format associated with

+ * the system named by the SYST command. For example, if the SYST command returns "WINDOWS", then

+ * the directory listing value from this command should match the Windows-specific format. See the

+ * <code>SystCommandHandler</code> to control the value returned for the SYST command.

+ * <p/>

+ * Here is an example value for <code>directoryListing</code> when the <code>SystCommandHandler</code>

+ * returns "WINDOWS". Note that multiple entries are separated by "\n":

+ * <code><pre>

+ *      CommandHandler listCommandHandler = new ListCommandHandler();

+ *      listCommandHandler.setDirectoryListing("11-09-01 12:30PM 406348 File2350.log\n" +

+ *          "11-01-01 1:30PM &lt;DIR&gt;  archive");

+ * </pre></code>

+ * <p/>

+ * And here is an example value for <code>directoryListing</code> when the <code>SystCommandHandler</code>

+ * returns "UNIX". Note that multiple entries are separated by "\n":

+ * <code><pre>

+ *      CommandHandler listCommandHandler = new ListCommandHandler();

+ *      listCommandHandler.setDirectoryListing("drwxrwxrwx  1 none     none                   0 Mar 20  2010 archive\n" +

+ *          "-rwxrwxrwx  1 none     none                  19 Mar 20  2010 abc.txt");

+ * </pre></code>

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the directory (or file) submitted on the

+ * invocation (the first command parameter); this parameter is optional, so the value may be null.

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ * @see SystCommandHandler

+ */

+public class ListCommandHandler extends AbstractStubDataCommandHandler implements CommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+

+    private String directoryListing = "";

+

+    /**

+     * @see org.mockftpserver.stub.command.AbstractStubDataCommandHandler#beforeProcessData(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+        invocationRecord.set(PATHNAME_KEY, command.getOptionalString(0));

+    }

+

+    /**

+     * @see org.mockftpserver.stub.command.AbstractStubDataCommandHandler#processData(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    protected void processData(Command command, Session session, InvocationRecord invocationRecord) {

+        session.sendData(directoryListing.getBytes(), directoryListing.length());

+    }

+

+    /**

+     * Set the contents of the directoryListing to send back on the data connection for this command.

+     * The passed-in value is trimmed automatically.

+     *

+     * @param directoryListing - the directoryListing to set

+     */

+    public void setDirectoryListing(String directoryListing) {

+        this.directoryListing = directoryListing.trim();

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/MkdCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/MkdCommandHandler.java
new file mode 100644
index 0000000..40113a5
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/MkdCommandHandler.java
@@ -0,0 +1,55 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the MKD (Make Directory) command. Send back a reply code of 257.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the directory submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class MkdCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public MkdCommandHandler() {

+        setReplyCode(ReplyCodes.MKD_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        String pathname = command.getRequiredParameter(0);

+        invocationRecord.set(PATHNAME_KEY, pathname);

+        sendReply(session, pathname);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/ModeCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/ModeCommandHandler.java
new file mode 100644
index 0000000..87b4f0f
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/ModeCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the MODE command. Send back a reply code of 200.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #MODE_KEY} ("mode") - the code for the transmission mode submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class ModeCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String MODE_KEY = "mode";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public ModeCommandHandler() {

+        setReplyCode(ReplyCodes.MODE_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(MODE_KEY, command.getRequiredParameter(0));

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/NlstCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/NlstCommandHandler.java
new file mode 100644
index 0000000..f3dc5f6
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/NlstCommandHandler.java
@@ -0,0 +1,68 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the NLST command. Return the configured directory listing on the data

+ * connection, along with two replies on the control connection: a reply code of 150 and

+ * another of 226. By default, return an empty directory listing. You can customize the

+ * returned directory listing by setting the <code>directoryListing</code> property.

+ * <p>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the directory (or file) submitted on the

+ * invocation (the first command parameter); this parameter is optional, so the value may be null.

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class NlstCommandHandler extends AbstractStubDataCommandHandler implements CommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+

+    private String directoryListing = "";

+

+    /**

+     * @see org.mockftpserver.stub.command.AbstractStubDataCommandHandler#beforeProcessData(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+        invocationRecord.set(PATHNAME_KEY, command.getOptionalString(0));

+    }

+

+    /**

+     * @see org.mockftpserver.stub.command.AbstractStubDataCommandHandler#processData(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    protected void processData(Command command, Session session, InvocationRecord invocationRecord) {

+        session.sendData(directoryListing.getBytes(), directoryListing.length());

+    }

+

+    /**

+     * Set the contents of the directoryListing to send back on the data connection for this command.

+     * The passed-in value is trimmed automatically.

+     *

+     * @param directoryListing - the directoryListing to set

+     */

+    public void setDirectoryListing(String directoryListing) {

+        this.directoryListing = directoryListing.trim();

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/NoopCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/NoopCommandHandler.java
new file mode 100644
index 0000000..1856db8
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/NoopCommandHandler.java
@@ -0,0 +1,45 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the NOOP command. Return a reply code of 200.

+ * <p>

+ * Each invocation record stored by this CommandHandler contains no data elements.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class NoopCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public NoopCommandHandler() {

+        setReplyCode(ReplyCodes.NOOP_OK);

+    }

+

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/PassCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/PassCommandHandler.java
new file mode 100644
index 0000000..ead9279
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/PassCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the PASS (Password) command. Send back a reply code of 230.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>"password" - the password submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class PassCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String PASSWORD_KEY = "password";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public PassCommandHandler() {

+        setReplyCode(ReplyCodes.PASS_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(PASSWORD_KEY, command.getRequiredParameter(0));

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/PasvCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/PasvCommandHandler.java
new file mode 100644
index 0000000..011462f
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/PasvCommandHandler.java
@@ -0,0 +1,68 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.Assert;

+import org.mockftpserver.core.util.PortParser;

+

+import java.io.IOException;

+import java.net.InetAddress;

+

+/**

+ * CommandHandler for the PASV (Passove Mode) command. Request the Session to switch to passive

+ * data connection mode. Return a reply code of 227, along with response text of the form:

+ * "<i>Entering Passive Mode. (h1,h2,h3,h4,p1,p2)</i>", where <i>h1..h4</i> are the 4

+ * bytes of the 32-bit internet host address of the server, and <i>p1..p2</i> are the 2

+ * bytes of the 16-bit TCP port address of the data connection on the server to which

+ * the client must connect. See RFC959 for more information.

+ * <p/>

+ * Each invocation record stored by this CommandHandler contains no data elements.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class PasvCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public PasvCommandHandler() {

+        setReplyCode(ReplyCodes.PASV_OK);

+    }

+

+    /**

+     * @throws IOException

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord)

+            throws IOException {

+

+        int port = session.switchToPassiveMode();

+        InetAddress server = session.getServerHost();

+

+        Assert.isTrue(port > -1, "The server-side port is invalid: " + port);

+        LOG.debug("server=" + server + " port=" + port);

+        String hostAndPort = "(" + PortParser.convertHostAndPortToCommaDelimitedBytes(server, port) + ")";

+

+        sendReply(session, hostAndPort);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/PortCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/PortCommandHandler.java
new file mode 100644
index 0000000..ce1bddb
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/PortCommandHandler.java
@@ -0,0 +1,68 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.HostAndPort;

+import org.mockftpserver.core.util.PortParser;

+

+import java.net.UnknownHostException;

+

+/**

+ * CommandHandler for the PORT command. Send back a reply code of 200.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #HOST_KEY} ("host") - the client data host (InetAddress) submitted on the invocation (from parameters 1-4)

+ * <li>{@link #PORT_KEY} ("port") - the port number (Integer) submitted on the invocation (from parameter 5-6)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class PortCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String HOST_KEY = "host";

+    public static final String PORT_KEY = "port";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public PortCommandHandler() {

+        setReplyCode(ReplyCodes.PORT_OK);

+    }

+

+    /**

+     * Handle the command

+     *

+     * @throws UnknownHostException

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) throws UnknownHostException {

+        HostAndPort client = PortParser.parseHostAndPort(command.getParameters());

+        LOG.debug("host=" + client.host + " port=" + client.port);

+        session.setClientDataHost(client.host);

+        session.setClientDataPort(client.port);

+        invocationRecord.set(HOST_KEY, client.host);

+        invocationRecord.set(PORT_KEY, new Integer(client.port));

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/PwdCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/PwdCommandHandler.java
new file mode 100644
index 0000000..80b3c63
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/PwdCommandHandler.java
@@ -0,0 +1,58 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the PWD (Print Working Directory) and XPWD commands. By default, return

+ * an empty directory name, along with a reply code of 257. You can customize the returned

+ * directory name by setting the <code>directory</code> property.

+ * <p>

+ * Each invocation record stored by this CommandHandler contains no data elements.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class PwdCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    private String directory = "";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public PwdCommandHandler() {

+        setReplyCode(ReplyCodes.PWD_OK);

+    }

+

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        sendReply(session, quotes(directory));

+    }

+

+    /**

+     * Set the directory String to be returned by this command

+     *

+     * @param directory - the directory

+     */

+    public void setDirectory(String directory) {

+        this.directory = directory;

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/QuitCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/QuitCommandHandler.java
new file mode 100644
index 0000000..e3265cb
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/QuitCommandHandler.java
@@ -0,0 +1,46 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the QUIT command. Return a reply code of 221.

+ * <p>

+ * Each invocation record stored by this CommandHandler contains no data elements.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class QuitCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public QuitCommandHandler() {

+        setReplyCode(ReplyCodes.QUIT_OK);

+    }

+

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        sendReply(session);

+        session.close();

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/ReinCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/ReinCommandHandler.java
new file mode 100644
index 0000000..c323619
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/ReinCommandHandler.java
@@ -0,0 +1,46 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * /**

+ * CommandHandler for the REIN (Reinitialize) command. Send back a reply code of 220.

+ * <p>

+ * Each invocation record stored by this CommandHandler contains no data elements.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class ReinCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public ReinCommandHandler() {

+        setReplyCode(ReplyCodes.REIN_OK);

+    }

+

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/RestCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/RestCommandHandler.java
new file mode 100644
index 0000000..bc55410
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/RestCommandHandler.java
@@ -0,0 +1,55 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the REST (Restart of interrupted transfer) command. Send back a reply code of 350.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #MARKER_KEY} ("marker") - the server marker submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class RestCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String MARKER_KEY = "marker";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public RestCommandHandler() {

+        setReplyCode(ReplyCodes.REST_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        String marker = command.getRequiredParameter(0);

+        invocationRecord.set(MARKER_KEY, marker);

+        sendReply(session, marker);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/RetrCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/RetrCommandHandler.java
new file mode 100644
index 0000000..b634cbe
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/RetrCommandHandler.java
@@ -0,0 +1,113 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.Assert;

+

+/**

+ * CommandHandler for the RETR (Retrieve) command. Return the configured file contents on the data

+ * connection, along with two replies on the control connection: a reply code of 150 and

+ * another of 226. By default, return an empty file (i.e., a zero-length byte[]). You can

+ * customize the returned file contents by setting the <code>fileContents</code> property,

+ * specified either as a String or as a byte array.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the file submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class RetrCommandHandler extends AbstractStubDataCommandHandler implements CommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+

+    private byte[] fileContents = new byte[0];

+

+    /**

+     * Create new uninitialized instance

+     */

+    public RetrCommandHandler() {

+    }

+

+    /**

+     * Create new instance using the specified fileContents

+     *

+     * @param fileContents - the file contents

+     * @throws org.mockftpserver.core.util.AssertFailedException

+     *          - if the fileContents is null

+     */

+    public RetrCommandHandler(String fileContents) {

+        setFileContents(fileContents);

+    }

+

+    /**

+     * Create new instance using the specified fileContents

+     *

+     * @param fileContents - the file contents

+     * @throws org.mockftpserver.core.util.AssertFailedException

+     *          - if the fileContents is null

+     */

+    public RetrCommandHandler(byte[] fileContents) {

+        setFileContents(fileContents);

+    }

+

+    /**

+     * @see org.mockftpserver.stub.command.AbstractStubDataCommandHandler#beforeProcessData(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+        String filename = command.getRequiredParameter(0);

+        invocationRecord.set(PATHNAME_KEY, filename);

+    }

+

+    /**

+     * @see org.mockftpserver.stub.command.AbstractStubDataCommandHandler#processData(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    protected void processData(Command command, Session session, InvocationRecord invocationRecord) {

+        LOG.info("Sending " + fileContents.length + " bytes");

+        session.sendData(fileContents, fileContents.length);

+    }

+

+    /**

+     * Set the file contents to return from subsequent command invocations

+     *

+     * @param fileContents - the fileContents to set

+     * @throws org.mockftpserver.core.util.AssertFailedException

+     *          - if the fileContents is null

+     */

+    public void setFileContents(String fileContents) {

+        Assert.notNull(fileContents, "fileContents");

+        setFileContents(fileContents.getBytes());

+    }

+

+    /**

+     * Set the file contents to return from subsequent command invocations

+     *

+     * @param fileContents - the file contents

+     * @throws org.mockftpserver.core.util.AssertFailedException

+     *          - if the fileContents is null

+     */

+    public void setFileContents(byte[] fileContents) {

+        Assert.notNull(fileContents, "fileContents");

+        this.fileContents = fileContents;

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/RmdCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/RmdCommandHandler.java
new file mode 100644
index 0000000..fbec38e
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/RmdCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the RMD (Remove Working Directory) command. Send back a reply code of 250.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the directory submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class RmdCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public RmdCommandHandler() {

+        setReplyCode(ReplyCodes.RMD_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0));

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/RnfrCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/RnfrCommandHandler.java
new file mode 100644
index 0000000..7dac272
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/RnfrCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the RNFR (Rename From) command. Send back a reply code of 350.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the file submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class RnfrCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public RnfrCommandHandler() {

+        setReplyCode(ReplyCodes.RNFR_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0));

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/RntoCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/RntoCommandHandler.java
new file mode 100644
index 0000000..db8c84c
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/RntoCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the RNTO (Rename To) command. Send back a reply code of 250.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the file submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class RntoCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public RntoCommandHandler() {

+        setReplyCode(ReplyCodes.RNTO_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0));

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/SiteCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/SiteCommandHandler.java
new file mode 100644
index 0000000..7632cd2
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/SiteCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the SITE (Site Parameters) command. Send back a reply code of 200.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PARAMETERS_KEY} ("parameters") - the site parameters submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class SiteCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String PARAMETERS_KEY = "parameters";

+

+    /**

+     * Constructor. Initiate the replyCode.

+     */

+    public SiteCommandHandler() {

+        setReplyCode(ReplyCodes.SITE_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(PARAMETERS_KEY, command.getRequiredParameter(0));

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/SmntCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/SmntCommandHandler.java
new file mode 100644
index 0000000..1d02bd8
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/SmntCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the SMNT (Structure Mount) command. Send back a reply code of 250.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the directory submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class SmntCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+

+    /**

+     * Constructor. Initiate the replyCode.

+     */

+    public SmntCommandHandler() {

+        setReplyCode(ReplyCodes.SMNT_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(PATHNAME_KEY, command.getRequiredParameter(0));

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/StatCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/StatCommandHandler.java
new file mode 100644
index 0000000..9b664a5
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/StatCommandHandler.java
@@ -0,0 +1,75 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the STAT (Status) command. By default, return empty status information,

+ * along with a reply code of 211 if no pathname parameter is specified or 213 if a

+ * pathname is specified. You can customize the returned status information by setting

+ * the <code>status</code> property.

+ * <p>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the directory (or file) submitted on the

+ * invocation (the first command parameter); this parameter is optional, so the value may be null.

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ * @see SystCommandHandler

+ */

+public class StatCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String PATHNAME_KEY = "pathname";

+

+    private String status = "";

+

+    /**

+     * Constructor.

+     */

+    public StatCommandHandler() {

+        // Do not initialize replyCode -- will be set dynamically

+    }

+

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        String pathname = command.getOptionalString(0);

+        invocationRecord.set(PATHNAME_KEY, pathname);

+

+        // Only use dynamic reply code if the replyCode property was NOT explicitly set

+        if (replyCode == 0) {

+            int code = (pathname == null) ? ReplyCodes.STAT_SYSTEM_OK : ReplyCodes.STAT_FILE_OK;

+            sendReply(session, code, replyMessageKey, replyText, new String[]{status});

+        } else {

+            sendReply(session, status);

+        }

+    }

+

+    /**

+     * Set the contents of the status to send back as the reply text for this command

+     *

+     * @param status - the status

+     */

+    public void setStatus(String status) {

+        this.status = status;

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/StorCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/StorCommandHandler.java
new file mode 100644
index 0000000..7de7c84
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/StorCommandHandler.java
@@ -0,0 +1,45 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the STOR (Store) command. Send back two replies on the control connection: a

+ * reply code of 150 and another of 226.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #PATHNAME_KEY} ("pathname") - the pathname of the directory submitted on the invocation (the first command parameter)

+ * <li>{@link #FILE_CONTENTS_KEY} ("fileContents") - the file contents (<code>byte[]</code>) sent on the data connection

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class StorCommandHandler extends AbstractStorCommandHandler {

+

+    /**

+     * @see org.mockftpserver.stub.command.AbstractStubDataCommandHandler#beforeProcessData(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session, org.mockftpserver.core.command.InvocationRecord)

+     */

+    protected void beforeProcessData(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+        String filename = command.getRequiredParameter(0);

+        invocationRecord.set(PATHNAME_KEY, filename);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/StouCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/StouCommandHandler.java
new file mode 100644
index 0000000..50f52d1
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/StouCommandHandler.java
@@ -0,0 +1,60 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the STOU (Store Unique) command. Send back two replies on the control connection: a

+ * reply code of 150 and another of 226. The text accompanying the final reply (226) is the

+ * unique filename, which is "" by default. You can customize the returned filename by setting

+ * the <code>filename</code> property.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #FILE_CONTENTS_KEY} ("fileContents") - the file contents (<code>byte[]</code>) sent on the data connection

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class StouCommandHandler extends AbstractStorCommandHandler {

+

+    private static final String FINAL_REPLY_TEXT_KEY = "226.WithFilename";

+

+    private String filename = "";

+

+    /**

+     * Override the default implementation to send a custom reply text that includes the STOU response filename

+     *

+     * @see org.mockftpserver.stub.command.AbstractStubDataCommandHandler#sendFinalReply(org.mockftpserver.core.session.Session)

+     */

+    protected void sendFinalReply(Session session) {

+        final String[] ARGS = {filename};

+        sendReply(session, ReplyCodes.TRANSFER_DATA_FINAL_OK, FINAL_REPLY_TEXT_KEY, null, ARGS);

+    }

+

+    /**

+     * Set the filename returned with the final reply of the STOU command

+     *

+     * @param filename - the filename

+     */

+    public void setFilename(String filename) {

+        this.filename = filename;

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/StruCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/StruCommandHandler.java
new file mode 100644
index 0000000..d1cd559
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/StruCommandHandler.java
@@ -0,0 +1,54 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the STRU (File Structure) command. Send back a reply code of 200.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #FILE_STRUCTURE_KEY} ("fileStructure") - the file structure code submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class StruCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String FILE_STRUCTURE_KEY = "fileStructure";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public StruCommandHandler() {

+        setReplyCode(ReplyCodes.STRU_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(FILE_STRUCTURE_KEY, command.getRequiredParameter(0));

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/SystCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/SystCommandHandler.java
new file mode 100644
index 0000000..2744633
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/SystCommandHandler.java
@@ -0,0 +1,63 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.Assert;

+

+/**

+ * CommandHandler for the SYST (System) command. Send back a reply code of 215. By default,

+ * return "WINDOWS" as the system name. You can customize the returned name by

+ * setting the <code>systemName</code> property.

+ * <p/>

+ * See the available system names listed in the Assigned Numbers document

+ * (<a href="http://www.ietf.org/rfc/rfc943">RFC 943</a>).

+ * <p/>

+ * Each invocation record stored by this CommandHandler contains no data elements.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class SystCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    private String systemName = "WINDOWS";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public SystCommandHandler() {

+        setReplyCode(ReplyCodes.SYST_OK);

+    }

+

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        sendReply(session, quotes(systemName));

+    }

+

+    /**

+     * Set the systemName String to be returned by this command

+     *

+     * @param systemName - the systemName

+     */

+    public void setSystemName(String systemName) {

+        Assert.notNull(systemName, "systemName");

+        this.systemName = systemName;

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/TypeCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/TypeCommandHandler.java
new file mode 100644
index 0000000..28f3af5
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/TypeCommandHandler.java
@@ -0,0 +1,58 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the TYPE command. Send back a reply code of 200.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #TYPE_INFO_KEY} ("typeInfo") - the type information submitted on the

+ * invocation, which is a String[2] containing the first two command parameter values.

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class TypeCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String TYPE_INFO_KEY = "typeInfo";

+

+    /**

+     * Constructor. Initialize the replyCode.

+     */

+    public TypeCommandHandler() {

+        setReplyCode(ReplyCodes.TYPE_OK);

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        LOG.debug("Processing TYPE: " + command);

+        String type = command.getRequiredParameter(0);

+        String format = command.getOptionalString(1);

+        invocationRecord.set(TYPE_INFO_KEY, new String[]{type, format});

+        sendReply(session);

+    }

+

+}

diff --git a/tags/2.5/src/main/java/org/mockftpserver/stub/command/UserCommandHandler.java b/tags/2.5/src/main/java/org/mockftpserver/stub/command/UserCommandHandler.java
new file mode 100644
index 0000000..a38ce5c
--- /dev/null
+++ b/tags/2.5/src/main/java/org/mockftpserver/stub/command/UserCommandHandler.java
@@ -0,0 +1,84 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.session.Session;

+

+/**

+ * CommandHandler for the USER command. The <code>passwordRequired</code> property defaults to true,

+ * indicating that a password is required following the user name. If true, this command handler

+ * returns a reply of 331. If false, return a reply of 230.

+ * <p/>

+ * Each invocation record stored by this CommandHandler includes the following data element key/values:

+ * <ul>

+ * <li>{@link #USERNAME_KEY} ("username") - the user name submitted on the invocation (the first command parameter)

+ * </ul>

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class UserCommandHandler extends AbstractStubCommandHandler implements CommandHandler {

+

+    public static final String USERNAME_KEY = "username";

+

+    private boolean passwordRequired = true;

+

+    /**

+     * Constructor.

+     */

+    public UserCommandHandler() {

+        // Do not initialize replyCode -- will be set dynamically

+    }

+

+    /**

+     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command, org.mockftpserver.core.session.Session)

+     */

+    public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+        invocationRecord.set(USERNAME_KEY, command.getRequiredParameter(0));

+

+        // Only use dynamic reply code if the replyCode property was NOT explicitly set

+        if (replyCode == 0) {

+            int code = (passwordRequired) ? ReplyCodes.USER_NEED_PASSWORD_OK : ReplyCodes.USER_LOGGED_IN_OK;

+            sendReply(session, code, replyMessageKey, replyText, null);

+        } else {

+            sendReply(session);

+        }

+    }

+

+    /**

+     * Return true if a password is required at login. See {@link #setPasswordRequired(boolean)}.

+     *

+     * @return the passwordRequired flag

+     */

+    public boolean isPasswordRequired() {

+        return passwordRequired;

+    }

+

+    /**

+     * Set true to indicate that a password is required. If true, this command handler returns a reply

+     * of 331. If false, return a reply of 230.

+     *

+     * @param passwordRequired - is a password required for login

+     */

+    public void setPasswordRequired(boolean passwordRequired) {

+        this.passwordRequired = passwordRequired;

+    }

+

+}

diff --git a/tags/2.5/src/main/resources/ReplyText.properties b/tags/2.5/src/main/resources/ReplyText.properties
new file mode 100644
index 0000000..505543d
--- /dev/null
+++ b/tags/2.5/src/main/resources/ReplyText.properties
@@ -0,0 +1,131 @@
+# Copyright 2008 the original author or authors.

+# 

+# 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.

+

+#-------------------------------------------------------------------------------

+# Mapping of reply code -> reply text

+#-------------------------------------------------------------------------------

+110=Restart marker reply.

+120=Service ready in nnn minutes.

+125=Data connection already open; transfer starting.

+150=File status okay; about to open data connection.

+200=Command okay.

+202=Command not implemented, superfluous at this site.

+211={0}.

+212={0}.

+213={0}.

+214={0}.

+215={0} system type.

+220=Service ready for new user. (MockFtpServer 2.5; see http://mockftpserver.sourceforge.net)

+221=Service closing control connection.

+225=Data connection open; no transfer in progress.

+226=Closing data connection. Requested file action successful.

+226.WithFilename=Closing data connection. Requested file action successful. Filename={0}.

+227=Entering Passive Mode {0}.

+229=Entering Extended Passive Mode (|||{0}|)

+230=User logged in, proceed.

+250=Requested file action okay, completed.

+257={0} created.

+331=User name okay, need password.

+332=Need account for login.

+350=Requested file action pending further information.

+421=Service not available, closing control connection.

+#    This may be a reply to any command if the service knows it must shut down.

+425=Can't open data connection.

+426=Connection closed; transfer aborted.

+450=Requested file action not taken.

+#   File unavailable (e.g., file busy).

+451=Requested action aborted: local error in processing.

+452=Requested action not taken.

+#    Insufficient storage space in system.

+500=Syntax error, command unrecognized.

+#    This may include errors such as command line too long.

+501=Syntax error in parameters or arguments.

+502=Command not implemented: {0}.

+503=Bad sequence of commands.

+504=Command not implemented for that parameter.

+530=Not logged in.

+532=Need account for storing files.

+550=File not found or not accessible: {0}.

+#    File unavailable (e.g., file not found, no access).

+551=Requested action aborted: page type unknown.

+552=Requested file action aborted.

+#    Exceeded storage allocation (for current directory or dataset).

+553=Requested action not taken for {0}

+#    File name not allowed.

+

+#-------------------------------------------------------------------------------

+# FTP Command-Specific Reply Messages

+#-------------------------------------------------------------------------------

+abor=ABOR completed.

+acct=ACCT completed for {0}.

+allo=ALLO completed.

+appe=Created or appended to file {0}.

+cdup=CDUP completed. New directory is {0}.

+cwd=CWD completed. New directory is {0}.

+dele="{0}" deleted.

+eprt=EPRT completed.

+epsv=Entering Extended Passive Mode (|||{0}|)

+help={0}.

+help.noHelpTextDefined=No help text has been defined for [{0}]

+mkd="{0}" created.

+mode=MODE completed.

+noop=NOOP completed.

+pass=User logged in, proceed.

+pass.needAccount=Need account for login.

+pass.loginFailed=Not logged in.

+pasv=({0})

+port=PORT completed.

+pwd="{0}" is current directory.

+quit=Service closing control connection.

+rein=REIN completed.

+rest=REST completed.

+rmd="{0}" removed.

+rnfr=Requested file action pending further information.

+rnto=Rename from {0} to {1} completed.

+site=SITE completed.

+smnt=SMNT completed.

+stat={0}.

+stou=Created file {0}.

+stor=Created file {0}.

+stru=STRU completed.

+syst="{0}"

+type=TYPE completed.

+user.loggedIn=User logged in, proceed.

+user.needPassword=User name okay, need password.

+

+#-------------------------------------------------------------------------------

+# FileSystem Messages

+#-------------------------------------------------------------------------------

+filesystem.alreadyExists=The path [{0}] already exists.

+filesystem.parentDirectoryDoesNotExist=The parent directory [{0}] does not exist.

+filesystem.doesNotExist=[{0}] does not exist.

+filesystem.isDirectory=[{0}] is a directory.

+filesystem.isFile=[{0}] is a file.

+filesystem.isNotADirectory=[{0}] is not a directory or does not exist.

+filesystem.isNotAFile=[{0}] is not a file or does not exist.

+filesystem.cannotRead=The current user does not have read permission for [{0}].

+filesystem.cannotWrite=The current user does not have write permission for [{0}].

+filesystem.cannotExecute=The current user does not have execute permission for [{0}].

+filesystem.directoryIsNotEmpty=The [{0}] directory is not empty.

+filesystem.renameFailed=The rename to [{0}] has failed.

+filesystem.pathIsNotValid=The path [{0}] is not valid.

+filesystem.currentDirectoryNotSet=The current directory has not been set.

+

+#-------------------------------------------------------------------------------

+# Other Common Messages

+#-------------------------------------------------------------------------------

+login.userAccountNotValid=UserAccount missing or invalid for user [{0}]

+login.homeDirectoryNotValid=The homeDirectory configured for user [{0}] is not a valid directory: [{1}]

+

+internalError=Internal error: {0} {1}
\ No newline at end of file
diff --git a/tags/2.5/src/site/apt/fakeftpserver-features.apt b/tags/2.5/src/site/apt/fakeftpserver-features.apt
new file mode 100644
index 0000000..f02904b
--- /dev/null
+++ b/tags/2.5/src/site/apt/fakeftpserver-features.apt
@@ -0,0 +1,113 @@
+		--------------------------------------------------

+				FakeFtpServer Features and Limitations

+		--------------------------------------------------

+

+FakeFtpServer Features

+~~~~~~~~~~~~~~~~~~~~~~

+

+  * Standalone dummy FTP server. Run either within the same JVM as test code or in a different JVM.

+

+  * Implements common FTP server commands.

+

+  * Works out of the box with reasonable and expected behavior. Can simulate most mainline success and error scenarios.

+

+  * In most cases, requires little or no knowledge or understanding of FTP server commands and reply codes. 

+

+  * Provides a simulated server file system, including support for file and directory permissions and owner and

+   group authorization based on Unix. This file system can be populated at startup (or thereafter) with

+   directories and files (including arbitrary content) to be retrieved by an FTP client. Any files sent to the server

+   by an FTP client exist within that file system as well, and can be accessed through the file system API, or

+   can even be subsequently retrieved by an FTP client.

+

+  * Allows defining the set of user accounts that control which users can login to the FTP server, and their home

+    (default) directories.

+

+  * Supports active and passive mode data transfers.

+

+  * Use a dynamically chosen free port number for the server control port instead of using the default (21)

+    or hard-coding some other value (set the serverControlPort property of the server to 0).

+

+  * Supports extended address (IPv6) data transfers (RFC2428)

+

+  * Fully supports configuration within the <<Spring Framework>> or other dependency-injection container.

+  

+  * Can be used to test FTP client code written in any language

+  

+FTP Scenarios Supported by FakeFtpServer

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  Some of the mainline success scenarios that you can simulate with <<FakeFtpServer>> include:

+

+    * Login (USER/PASS): with password, or when no password is required

+

+    * Retrieve existing file (RETR) from the server

+

+    * Send file to the server (STOR,STOU,APPE)

+

+    * List of file entries (LIST) and list of filenames (NLST)

+

+    * Print current working directory (PWD)

+

+    * Change current working directory (CWD)

+

+    * Rename an existing file (RNFR/RNTO)

+

+    * Delete an existing file (DELE)

+

+    * Create directory (MKD)

+

+    * Remove directory (RMD)

+

+    * Both active and passive mode (PASV) data transfers

+

+    * Extended Address (IPv6) data transfers (EPRT and EPSV commands)

+

+  Some of the error scenarios that you can simulate with <<FakeFtpServer>> include:

+

+    * Failed login (USER/PASS): no such user, wrong password

+

+    * Invalid client requests: missing required parameter, not logged in

+

+    * Failed retrieve (RETR): no such file, missing required access permissions for the current user

+

+    * Failed send (STOR,STOU,APPE): missing required access permissions for the current user

+

+    * Failed change current working directory (CWD): no such directory, missing required access permissions for the current user

+

+    * Failed delete an existing file (DELE): file does not exist, missing required access permissions for the current user

+

+    * Failed rename (RNFR/RNTO): no such file, missing required access permissions for the current user

+

+    * Failed create directory (MKD): parent directory does not exist, directory already exists, missing required access permissions for the current user

+

+    * Failed remove directory (RMD): no such directory, directory not empty, missing required access permissions for the current user

+

+

+FakeFtpServer Limitations

+~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  Not all FTP features, error scenarios and reply codes can be simulated using <<FakeFtpServer>>. Features and

+  scenarios not supported include:

+

+  * Leaving the data connection open across multiple client requests.

+

+  * Transmission mode other than 'Stream'. The STRU command is implemented but has no effect (NOOP).

+

+  * Data Types other than ASCII and IMAGE (binary).

+

+  * Vertical Format Control other than the default (NON PRINT).

+

+  * Record Structure and Page Structure. The STRU command is implemented but has no effect (NOOP).

+

+  * Error Recovery and Restart. The REST command is implemented but has no effect (NOOP).

+

+  * Structure Mount. The SMNT command is implemented but has no effect (NOOP).

+

+  * Abort. The ABOR command is implemented but has no effect (NOOP).

+

+  * Allocate. The ALLO command is implemented but has no effect (NOOP).

+

+  []

+

+  For unsupported features, error scenarios and reply codes, consider using <<StubFtpServer>> instead, which

+  provides a lower-level abstraction and finer control over exact server reply codes and responses.

diff --git a/tags/2.5/src/site/apt/fakeftpserver-filesystems.apt b/tags/2.5/src/site/apt/fakeftpserver-filesystems.apt
new file mode 100644
index 0000000..aaa0069
--- /dev/null
+++ b/tags/2.5/src/site/apt/fakeftpserver-filesystems.apt
@@ -0,0 +1,235 @@
+		--------------------------------------------------

+				FakeFtpServer Filesystems

+		--------------------------------------------------

+

+FakeFtpServer Filesystems

+~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  <<FakeFtpServer>> provides a simulated server file system, including support for file and directory permissions

+  and owner and group authorization based on Unix. This file system can be populated at startup (or thereafter) with

+  directories and files (including arbitrary content) to be retrieved by an FTP client. Any files sent to the server

+  by an FTP client exist within that file system as well, and can be accessed through the file system API, or

+  can even be subsequently retrieved by an FTP client.

+

+  The filesystem abstraction is accessed through the <<<FileSystem>>> interface in the

+  <<<org.mockftpserver.fake.filesystem>>> package. Two implementations of this interface are provided:

+  <<<WindowsFakeFileSystem>>> and <<<UnixFakeFileSystem>>>. They both manage the files and directories in memory,

+  simulating a real file system. You are also free to implement your own <<<FileSystem>>> implementation.

+

+  Note that both <<<WindowsFakeFileSystem>>> and <<<UnixFakeFileSystem>>> are <virtual> file systems, and do

+  not depend on the <real> operating systems or file systems on which <<FakeFtpServer>> is running. In other

+  words, you can configure and run a <<FakeFtpServer>> with a <<<WindowsFakeFileSystem>>> on top of a <real>

+  Unix system, or run a <<FakeFtpServer>> with a <<<UnixFakeFileSystem>>> on top of a <real> Windows system.

+

+  See the javadoc for these classes for more information.

+

+

+* WindowsFakeFileSystem

+~~~~~~~~~~~~~~~~~~~~~~~

+

+  <<WindowsFakeFileSystem>> is an implementation of the <<<FileSystem>>> interface that simulates a Microsoft

+  Windows file system. The rules for file and directory names include:

+

+    * Filenames are case-insensitive

+

+    * Either forward slashes (/) or backward slashes (\) are valid path separators (but are normalized to '\')

+

+    * An absolute path starts with a drive specifier (e.g. 'a:' or 'c:') followed by '\' or '/',

+      or else it starts with "\\"</li>

+

+

+* UnixFakeFileSystem

+~~~~~~~~~~~~~~~~~~~~

+

+  <<UnixFakeFileSystem>> is an implementation of the <<<FileSystem>>> interface that simulates a Unix

+  file system. The rules for file and directory names include:

+

+    * Filenames are case-sensitive

+

+    * Forward slashes (/) are the only valid path separators

+

+

+* WindowsFakeFileSystem and UnixFakeFileSystem: Common Behavior and Configuration

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  Both <<<WindowsFakeFileSystem>>> and <<<UnixFakeFileSystem>>> are subclasses of <<<AbstractFakeFileSystem>>>. They

+  manage the files and directories in memory, simulating a real file system.

+

+  If the <createParentDirectoriesAutomatically> property is set to <true>,

+  then creating a directory or file will automatically create any parent directories (recursively)

+  that do not already exist. If <false>, then creating a directory or file throws an

+  exception if its parent directory does not exist. This value defaults to <true>.

+

+  The <directoryListingFormatter> property holds an instance of <<DirectoryListingFormatter>>,

+  used by the <formatDirectoryListing> method to format directory listings in a

+  filesystem-specific manner. This property is initialized by concrete subclasses.

+

+

+* File Permissions, Owners and Groups

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  Each <file> or <directory> entry within a <<<FileSystem>>> has associated <owner>, <group> and <permissions>

+  attributes. All of these attributes are optional. If none are specified for a file or directory, then full

+  access by all users is the default.

+

+  If, however, these values are specified for a filesystem entry, then they affect whether a file can be created,

+  read, written or deleted, and whether a directory can be created, listed or deleted.

+

+  This approach for access control is conceptually (and somewhat loosely) based on the Unix file system, but

+  don't expect a comprehensive implementation fully matching Unix's capabilities.

+

+

+** Permissions

+~~~~~~~~~~~~~~

+

+  The permissions for a file or directory entry in the filesystem are represented by a 9-character string of

+  the form "rwxrwxrwx", consisting of three "rwx" triples. Each triple indicates the READ ("r"), WRITE ("w") and

+  EXECUTE ("x") permissions for a specific set of users. Each position can alternatively contain a "-" to

+  indicate no READ/WRITE/EXECUTE access, depending on its position.

+

+  The first "rwx" triple indicates the READ, WRITE and EXECUTE permissions for the owner of the file. The

+  second triple indicates the permissions for the group associated with the file. The third triple

+  indicates the permissions for the rest of the world.

+

+  For example, the permissions string "rwx--xrw-" is interpreted to mean that users have READ/WRITE/EXECUTE access,

+  the group has only EXECUTE, and the world has only READ and WRITE.

+

+  There are plenty of good tutorials and references for understanding Unix file permissions, including

+  {{{http://www.dartmouth.edu/~rc/help/faq/permissions.html}this one}}.

+

+  The <<<Permissions>>> class represents and encapsulates the read/write/execute permissions for a file or

+  directory. Its constructor takes a 9-character "rwx" String as described above.

+

+  The <<<AbstractFileSystemEntry>>> contains a <permissions> attribute, so that every file and directory in the

+  file system can be assigned a unique set of permissions from a <<<Permissions>>> object. There is also a

+  <<<setPermissionsFromString()>>> convenience setter that allows setting the permissions directly from a String.

+

+

+**  FileSystem Access Rules

+~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+***  When Are READ, WRITE or EXECUTE Access Required?

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  If the <permissions> are configured for a file or directory within the <<<FileSystem>>>, then

+  those permissions affect whether and how that file/directory can be accessed.

+  Here are the rules for applying permissions for file access:

+

+*------------------------*-------------------------------------------------------------------*

+| <<Operation>>          | <<Required Permissions>>                                          |

+*------------------------*-------------------------------------------------------------------*

+| Create a new file      | EXECUTE access to the directory and WRITE access to the directory |

+*------------------------*-------------------------------------------------------------------*

+| Read a file            | EXECUTE access to the directory and READ access to the file       |

+*------------------------*-------------------------------------------------------------------*

+| Write a file           | EXECUTE access to the directory and WRITE access to the file      |

+*------------------------*-------------------------------------------------------------------*

+| Delete a file          | WRITE access to the directory                                     |

+*------------------------*-------------------------------------------------------------------*

+| Rename a file          | READ access to the FROM file and WRITE access to the directory    |

+*------------------------*-------------------------------------------------------------------*

+| Create a directory     | WRITE and EXECUTE acccess to the parent directory                 |

+*------------------------*-------------------------------------------------------------------*

+| List a directory       | READ acccess to the directory/file                                |

+*------------------------*-------------------------------------------------------------------*

+| CD to a directory      | EXECUTE acccess to the directory                                  |

+*------------------------*-------------------------------------------------------------------*

+| Delete a directory     | WRITE acccess to the parent directory                             |

+*------------------------*-------------------------------------------------------------------*

+

+*** How Do Owner and Group Affect Access?

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  Each file and directory in the filesystem (subclass of <<<AbstractFileSystemEntry>>>) contains <owner>

+  and <group> attributes. These attributes are optional.

+

+  If the <owner> is configured for a file/directory, AND the <permissions> are configured as well,

+  then the <<owner>> triple from the <permissions> are applied if and only if the <<<UserAccount>>> for the

+  currently logged in FTP user (client) matches the <owner> configured for the file/directory.

+

+  Similarly, if the <group> is configured for a file/directory, AND the <permissions> are configured as well,

+  then the <<group>> triple from the <permissions> are applied if and only if <groups> configured for the

+  <<<UserAccount>>> for the currently logged in FTP user (client) contain the <group> configured for the file/directory.

+

+  Otherwise, the <<world>> triple from the <permissions> are applied.

+

+* Example Code

+~~~~~~~~~~~~~~

+

+  This example illustrates setting the permissions, owner and group for directories and files within the

+  <<<FakeFtpServer>>> filesystem. In this case, the filesystem is an instance of <<<WindowsFakeFileSystem>>>,

+  but the code would be almost exactly the same for <<<UnixFakeFileSystem>>> as well.

+

++------------------------------------------------------------------------------

+  final String USER1 = "joe";

+  final String USER2 = "mary";

+  final String GROUP = "dev";

+  final String CONTENTS = "abcdef 1234567890";

+

+  FileSystem fileSystem = new WindowsFakeFileSystem();

+  DirectoryEntry directoryEntry1 = new DirectoryEntry("c:\\");

+  directoryEntry1.setPermissions(new Permissions("rwxrwx---"));

+  directoryEntry1.setOwner(USER1);

+  directoryEntry1.setGroup(GROUP);

+

+  DirectoryEntry directoryEntry2 = new DirectoryEntry("c:\\data");

+  directoryEntry2.setPermissions(Permissions.ALL);

+  directoryEntry2.setOwner(USER1);

+  directoryEntry2.setGroup(GROUP);

+

+  FileEntry fileEntry1 = new FileEntry("c:\\data\\file1.txt", CONTENTS);

+  fileEntry1.setPermissionsFromString("rw-rw-rw-");

+  fileEntry1.setOwner(USER1);

+  fileEntry1.setGroup(GROUP);

+

+  FileEntry fileEntry2 = new FileEntry("c:\\data\\run.exe");

+  fileEntry2.setPermissionsFromString("rwxrwx---");

+  fileEntry2.setOwner(USER2);

+  fileEntry2.setGroup(GROUP);

+

+  fileSystem.add(directoryEntry1);

+  fileSystem.add(directoryEntry2);

+  fileSystem.add(fileEntry1);

+  fileSystem.add(fileEntry2);

+

+  FakeFtpServer fakeFtpServer = new FakeFtpServer();

+  fakeFtpServer.setFileSystem(fileSystem);

++------------------------------------------------------------------------------

+

+  Things to note about the above example:

+

+  * The <<<FakeFtpServer>>> instance is configured with a <<<WindowsFakeFileSystem>>> and a "c:\" root

+    directory with a "data" sub-directory containing two files. Permissions and owner/group are specified for

+    both directories and both files.

+

+  * The permissions for the directories are specified using the "permissions" setter, which takes an

+    instance of the <<<Permissions>>> class. The permissions for both files are specified using the

+    "permissionsFromString" shortcut method. Either way is fine -- use whichever method you prefer on

+    both files and directories.

+

+  []

+

+  When you want to retrieve and/or verify the contents of the <<<FakeFtpServer>>> filesystem, you can use

+  the <<<FileSystem#getEntry(String path)>>> method, as shown in the following code.

+

++------------------------------------------------------------------------------

+  DirectoryEntry dirEntry = (DirectoryEntry)fileSystem.getEntry("c:/data");

+

+  FileEntry fileEntry = (FileEntry)fileSystem.getEntry("c:/data/file1.txt");

+

+  FileEntry newFileEntry = (FileEntry)fileSystem.getEntry("c:/data/new.txt");

+  InputStream inputStream = newFileEntry.createInputStream();

+  // read the file contents using inputStream

++------------------------------------------------------------------------------

+

+  See the javadoc for <<<FileSystem>>>, <<<FileEntry>>> and <<<DirectoryEntry>>> for more information

+  on the methods available.

+

+

+** Example Using Spring Configuration

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  See the {{{./fakeftpserver-getting-started.html#Spring}FakeFtpServer Getting Started - Spring Configuration}}

+  for an example of how to configure a <<<FakeFtpServer>>> instance and associated filesystem in the

+  {{{http://www.springframework.org/}Spring Framework}}.

+  
\ No newline at end of file
diff --git a/tags/2.5/src/site/apt/fakeftpserver-getting-started.apt b/tags/2.5/src/site/apt/fakeftpserver-getting-started.apt
new file mode 100644
index 0000000..c342978
--- /dev/null
+++ b/tags/2.5/src/site/apt/fakeftpserver-getting-started.apt
@@ -0,0 +1,449 @@
+		--------------------------------------------------

+					FakeFtpServer Getting Started

+		--------------------------------------------------

+

+FakeFtpServer - Getting Started

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  <<FakeFtpServer>> is a "fake" implementation of an FTP server. It provides a high-level abstraction for

+  an FTP Server and is suitable for most testing and simulation scenarios. You define a virtual filesystem

+  (internal, in-memory) containing an arbitrary set of files and directories. These files and directories can

+  (optionally) have associated access permissions. You also configure a set of one or more user accounts that

+  control which users can login to the FTP server, and their home (default) directories. The user account is

+  also used when assigning file and directory ownership for new files.

+

+  <<FakeFtpServer>> processes FTP client requests and responds with reply codes and reply messages

+  consistent with its configured file system and user accounts, including file and directory permissions,

+  if they have been configured.

+

+  See the {{{./fakeftpserver-features.html}FakeFtpServer Features and Limitations}} page for more information on

+  which features and scenarios are supported.

+

+  In general the steps for setting up and starting the <<<FakeFtpServer>>> are:

+

+  * Create a new <<<FakeFtpServer>>> instance, and optionally set the server control port (use a value of 0

+    to automatically choose a free port number).

+

+  * Create and configure a <<<FileSystem>>>, and attach to the <<<FakeFtpServer>>> instance.

+

+  * Create and configure one or more <<<UserAccount>>> objects and attach to the <<<FakeFtpServer>>> instance.

+

+  []

+

+  Here is an example showing configuration and starting of an <<FakeFtpServer>> with a single user

+  account and a (simulated) Windows file system, defining a directory containing two files.

+

++------------------------------------------------------------------------------

+FakeFtpServer fakeFtpServer = new FakeFtpServer();

+fakeFtpServer.addUserAccount(new UserAccount("user", "password", "c:\\data"));

+

+FileSystem fileSystem = new WindowsFakeFileSystem();

+fileSystem.add(new DirectoryEntry("c:\\data"));

+fileSystem.add(new FileEntry("c:\\data\\file1.txt", "abcdef 1234567890"));

+fileSystem.add(new FileEntry("c:\\data\\run.exe"));

+fakeFtpServer.setFileSystem(fileSystem);

+

+fakeFtpServer.start();

++------------------------------------------------------------------------------

+

+  If you are running on a system where the default port (21) is already in use or cannot be bound

+  from a user process (such as Unix), you probably need to use a different server control port. Use the

+  <<<FakeFtpServer.setServerControlPort(int serverControlPort)>>> method to use a different port

+  number. If you specify a value of <<<0>>>, then the server will use a free port number. Then call

+  <<<getServerControlPort()>>> AFTER calling <<<start()>>> has been called to determine the actual port

+  number being used. Or, you can pass in a specific port number, such as 9187.

+

+  <<FakeFtpServer>>  can be fully configured programmatically or within the

+  {{{http://www.springframework.org/}Spring Framework}} or other dependency-injection container.

+  The {{{#Example}Example Test Using FakeFtpServer}} below illustrates programmatic configuration of

+  <<<FakeFtpServer>>>. Alternatively, the {{{#Spring}Configuration}} section later on illustrates how to use

+  the <Spring Framework> to configure a <<<FakeFtpServer>>> instance.

+

+* {Example} Test Using FakeFtpServer

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  This section includes a simplified example of FTP client code to be tested, and a JUnit 

+  test for it that programmatically configures and uses <<FakeFtpServer>>.

+

+** FTP Client Code

+~~~~~~~~~~~~~~~~~~

+

+  The following <<<RemoteFile>>> class includes a <<<readFile()>>> method that retrieves a remote 

+  ASCII file and returns its contents as a String. This class uses the <<<FTPClient>>> from the

+  {{{http://commons.apache.org/net/}Apache Commons Net}} framework.

+

++------------------------------------------------------------------------------  

+public class RemoteFile {

+

+    public static final String USERNAME = "user";

+    public static final String PASSWORD = "password";

+

+    private String server;

+    private int port;

+

+    public String readFile(String filename) throws IOException {

+

+        FTPClient ftpClient = new FTPClient();

+        ftpClient.connect(server, port);

+        ftpClient.login(USERNAME, PASSWORD);

+

+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

+        boolean success = ftpClient.retrieveFile(filename, outputStream);

+        ftpClient.disconnect();

+

+        if (!success) {

+            throw new IOException("Retrieve file failed: " + filename);

+        }

+        return outputStream.toString();

+    }

+

+    public void setServer(String server) {

+        this.server = server;

+    }

+

+    public void setPort(int port) {

+        this.port = port;

+    }

+

+    // Other methods ...

+}

++------------------------------------------------------------------------------

+

+** JUnit Test For FTP Client Code Using FakeFtpServer

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  The following <<<RemoteFileTest>>> class includes a couple of JUnit tests that use 

+  <<FakeFtpServer>>.

+

++------------------------------------------------------------------------------  

+import org.mockftpserver.fake.filesystem.FileEntry;

+import org.mockftpserver.fake.filesystem.FileSystem;

+import org.mockftpserver.fake.filesystem.UnixFakeFileSystem;

+import org.mockftpserver.fake.FakeFtpServer;

+import org.mockftpserver.fake.UserAccount;

+import org.mockftpserver.stub.example.RemoteFile;

+import org.mockftpserver.test.AbstractTest;

+import java.io.IOException;

+import java.util.List;

+

+public class RemoteFileTest extends AbstractTest {

+

+    private static final String HOME_DIR = "/";

+    private static final String FILE = "/dir/sample.txt";

+    private static final String CONTENTS = "abcdef 1234567890";

+

+    private RemoteFile remoteFile;

+    private FakeFtpServer fakeFtpServer;

+

+    public void testReadFile() throws Exception {

+        String contents = remoteFile.readFile(FILE);

+        assertEquals("contents", CONTENTS, contents);

+    }

+

+    public void testReadFileThrowsException() {

+        try {

+            remoteFile.readFile("NoSuchFile.txt");

+            fail("Expected IOException");

+        }

+        catch (IOException expected) {

+            // Expected this

+        }

+    }

+

+    protected void setUp() throws Exception {

+        super.setUp();

+        fakeFtpServer = new FakeFtpServer();

+        fakeFtpServer.setServerControlPort(0);  // use any free port

+

+        FileSystem fileSystem = new UnixFakeFileSystem();

+        fileSystem.add(new FileEntry(FILE, CONTENTS));

+        fakeFtpServer.setFileSystem(fileSystem);

+

+        UserAccount userAccount = new UserAccount(RemoteFile.USERNAME, RemoteFile.PASSWORD, HOME_DIR);

+        fakeFtpServer.addUserAccount(userAccount);

+

+        fakeFtpServer.start();

+        int port = fakeFtpServer.getServerControlPort();

+

+        remoteFile = new RemoteFile();

+        remoteFile.setServer("localhost");

+        remoteFile.setPort(port);

+    }

+

+    protected void tearDown() throws Exception {

+        super.tearDown();

+        fakeFtpServer.stop();

+    }

+}

++------------------------------------------------------------------------------

+

+  Things to note about the above test:

+  

+  * The <<<FakeFtpServer>>> instance is created and started in the <<<setUp()>>> method and

+    stopped in the <<<tearDown()>>> method, to ensure that it is stopped, even if the test fails.

+

+  * The server control port is set to 0 using <<<fakeFtpServer.setServerControlPort(PORT)>>>.

+    This means it will dynamically choose a free port. This is necessary if you are running on a

+    system where the default port (21) is already in use or cannot be bound from a user process (such as Unix).

+

+  * The <<<UnixFakeFileSystem>>> filesystem is configured and attached to the <<<FakeFtpServer>>> instance

+    in the <<<setUp()>>> method. That includes creating a predefined <<<"/dir/sample.txt">>> file with the

+    specified file contents. The <<<UnixFakeFileSystem>>> has a <<<createParentDirectoriesAutomatically>>>

+    attribute, which defaults to <<<true>>>, meaning that parent directories will be created automatically,

+    as necessary. In this case, that means that the <<<"/">>> and <<<"/dir">>> parent directories will be created,

+    even though not explicitly specified.

+

+  * A single <<<UserAccount>>> with the specified username, password and home directory is configured and

+    attached to the <<<FakeFtpServer>>> instance in the <<<setUp()>>> method. That configured user ("user")

+    is the only one that will be able to sucessfully log in to the <<<FakeFtpServer>>>. 

+

+

+* {Spring} Configuration

+~~~~~~~~~~~~~~~~~~~~~~~~

+

+  You can easily configure a <<<FakeFtpServer>>> instance in the

+  {{{http://www.springframework.org/}Spring Framework}} or another, similar dependency-injection container.

+

+** Simple Spring Configuration Example

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  The following example shows a <Spring> configuration file for a simple <<<FakeFtpServer>>> instance.

+

++------------------------------------------------------------------------------

+<?xml version="1.0" encoding="UTF-8"?>

+

+<beans xmlns="http://www.springframework.org/schema/beans"

+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

+       xsi:schemaLocation="http://www.springframework.org/schema/beans

+       		http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

+

+    <bean id="fakeFtpServer" class="org.mockftpserver.fake.FakeFtpServer">

+        <property name="serverControlPort" value="9981"/>

+        <property name="systemName" value="UNIX"/>

+        <property name="userAccounts">

+            <list>

+                <bean class="org.mockftpserver.fake.UserAccount">

+                    <property name="username" value="joe"/>

+                    <property name="password" value="password"/>

+                    <property name="homeDirectory" value="/"/>

+                </bean>

+            </list>

+        </property>

+

+        <property name="fileSystem">

+            <bean class="org.mockftpserver.fake.filesystem.UnixFakeFileSystem">

+                <property name="createParentDirectoriesAutomatically" value="false"/>

+                <property name="entries">

+                    <list>

+                        <bean class="org.mockftpserver.fake.filesystem.DirectoryEntry">

+                            <property name="path" value="/"/>

+                        </bean>

+                        <bean class="org.mockftpserver.fake.filesystem.FileEntry">

+                            <property name="path" value="/File.txt"/>

+                            <property name="contents" value="abcdefghijklmnopqrstuvwxyz"/>

+                        </bean>

+                    </list>

+                </property>

+            </bean>

+        </property>

+

+    </bean>

+

+</beans>

++------------------------------------------------------------------------------

+

+  Things to note about the above example:

+

+  * The <<<FakeFtpServer>>> instance has a single user account for username "joe", password "password"

+    and home (default) directory of "/".

+

+  * A <<<UnixFakeFileSystem>>> instance is configured with a predefined directory of "/" and a

+    "/File.txt" file with the specified contents.

+

+  []

+

+  And here is the Java code to load the above <Spring> configuration file and start the

+  configured <<FakeFtpServer>>.

+

++------------------------------------------------------------------------------

+ApplicationContext context = new ClassPathXmlApplicationContext("fakeftpserver-beans.xml");

+FakeFtpServer = (FakeFtpServer) context.getBean("FakeFtpServer");

+FakeFtpServer.start();

++------------------------------------------------------------------------------

+

+

+** Spring Configuration Example With File and Directory Permissions

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  The following example shows a <Spring> configuration file for a <<<FakeFtpServer>>> instance that

+  also configures file and directory permissions. This will enable the <<<FakeFtpServer>>> to reply

+  with proper error codes when the logged in user does not have the required permissions to access

+  directories or files.

+

++------------------------------------------------------------------------------

+<?xml version="1.0" encoding="UTF-8"?>

+

+beans xmlns="http://www.springframework.org/schema/beans"

+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

+       xsi:schemaLocation="http://www.springframework.org/schema/beans

+       		http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

+

+    <bean id="fakeFtpServer" class="org.mockftpserver.fake.FakeFtpServer">

+        <property name="serverControlPort" value="9981"/>

+        <property name="userAccounts">

+            <list>

+                <bean class="org.mockftpserver.fake.UserAccount">

+                    <property name="username" value="joe"/>

+                    <property name="password" value="password"/>

+                    <property name="homeDirectory" value="c:\"/>

+                </bean>

+            </list>

+        </property>

+

+        <property name="fileSystem">

+            <bean class="org.mockftpserver.fake.filesystem.WindowsFakeFileSystem">

+                <property name="createParentDirectoriesAutomatically" value="false"/>

+                <property name="entries">

+                    <list>

+                        <bean class="org.mockftpserver.fake.filesystem.DirectoryEntry">

+                            <property name="path" value="c:\"/>

+                            <property name="permissionsFromString" value="rwxrwxrwx"/>

+                            <property name="owner" value="joe"/>

+                            <property name="group" value="users"/>

+                        </bean>

+                        <bean class="org.mockftpserver.fake.filesystem.FileEntry">

+                            <property name="path" value="c:\File1.txt"/>

+                            <property name="contents" value="1234567890"/>

+                            <property name="permissionsFromString" value="rwxrwxrwx"/>

+                            <property name="owner" value="peter"/>

+                            <property name="group" value="users"/>

+                        </bean>

+                        <bean class="org.mockftpserver.fake.filesystem.FileEntry">

+                            <property name="path" value="c:\File2.txt"/>

+                            <property name="contents" value="abcdefghijklmnopqrstuvwxyz"/>

+                            <property name="permissions">

+                                <bean class="org.mockftpserver.fake.filesystem.Permissions">

+                                    <constructor-arg value="rwx------"/>

+                                </bean>

+                            </property>

+                            <property name="owner" value="peter"/>

+                            <property name="group" value="users"/>

+                        </bean>

+                    </list>

+                </property>

+            </bean>

+        </property>

+

+    </bean>

+</beans>

++------------------------------------------------------------------------------

+

+

+  Things to note about the above example:

+

+  * The <<<FakeFtpServer>>> instance is configured with a <<<WindowsFakeFileSystem>>> and a "c:\" root

+    directory containing two files. Permissions and owner/group are specified for that directory, as well

+    as the two predefined files contained within it.

+

+  * The permissions for "File1.txt" ("rwxrwxrwx") are specified using the "permissionsFromString" shortcut

+    method, while the permissions for "File2.txt" ("rwx------") are specified using the "permissions" setter,

+    which takes an instance of the <<<Permissions>>> class. Either method is fine.

+

+  []

+

+

+* Configuring Custom CommandHandlers

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  <<FakeFtpServer>> is intentionally designed to keep the lower-level details of FTP server implementation

+  hidden from the user. In most cases, you can simply define the files and directories in the file

+  system, configure one or more login users, and then fire up the server, expecting it to behave like

+  a <real> FTP server.

+

+  There are some cases, however, where you might want to further customize the internal behavior of the

+  server. Such cases might include:

+

+  * You want to have a particular FTP server command return a predetermined error reply

+

+  * You want to add support for a command that is not provided out of the box by <<FakeFtpServer>>

+

+  Note that if you need the FTP server to reply with entirely predetermined (canned) responses, then

+  you may want to consider using <<StubFtpServer>> instead.  

+

+

+** Using a StaticReplyCommandHandler

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  You can use one of the <CommandHandler> classes defined within the <<<org.mockftpserver.core.command>>>

+  package to configure a custom <CommandHandler>. The following example uses the <<<StaticReplyCommandHandler>>>

+  from that package to add support for the FEAT command. Note that in this case, we are setting the

+  <CommandHandler> for a new command (i.e., one that is not supported out of the box by <<FakeFtpServer>>).

+  We could just as easily set the <CommandHandler> for an existing command, overriding the default <CommandHandler>.

+

++------------------------------------------------------------------------------

+import org.mockftpserver.core.command.StaticReplyCommandHandler

+

+FakeFtpServer ftpServer = new FakeFtpServer()

+// ... set up files, directories and user accounts as usual

+

+StaticReplyCommandHandler featCommandHandler = new StaticReplyCommandHandler(211, "No Features");

+ftpServer.setCommandHandler("FEAT", featCommandHandler);

+

+// ...

+ftpServer.start()

++------------------------------------------------------------------------------

+

+

+** Using a Stub CommandHandler

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  You can also use a <<StubFtpServer>> <CommandHandler> -- i.e., one defined within the

+  <<<org.mockftpserver.stub.command>>> package. The following example uses the <stub> version of the

+  <<<CwdCommandHandler>>> from that package.

+

++------------------------------------------------------------------------------

+import org.mockftpserver.stub.command.CwdCommandHandler

+

+FakeFtpServer ftpServer = new FakeFtpServer()

+// ... set up files, directories and user accounts as usual

+

+final int REPLY_CODE = 502;

+CwdCommandHandler cwdCommandHandler = new CwdCommandHandler();

+cwdCommandHandler.setReplyCode(REPLY_CODE);

+ftpServer.setCommandHandler(CommandNames.CWD, cwdCommandHandler);

+

+// ...

+ftpServer.start()

++------------------------------------------------------------------------------

+

+

+** Creating Your Own Custom CommandHandler Class

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  If one of the existing <CommandHandler> classes does not fulfill your needs, you can alternately create

+  your own custom <CommandHandler> class. The only requirement is that it implement the

+  <<<org.mockftpserver.core.command.CommandHandler>>> interface. You would, however, likely benefit from

+  inheriting from one of the existing abstract <CommandHandler> superclasses, such as

+  <<<org.mockftpserver.core.command.AbstractStaticReplyCommandHandler>>> or

+  <<<org.mockftpserver.core.command.AbstractCommandHandler>>>. See the javadoc of these classes for

+  more information.

+

+

+* FTP Command Reply Text ResourceBundle

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  The default text asociated with each FTP command reply code is contained within the

+  "ReplyText.properties" ResourceBundle file. You can customize these messages by providing a

+  locale-specific ResourceBundle file on the CLASSPATH, according to the normal lookup rules of 

+  the ResourceBundle class (e.g., "ReplyText_de.properties"). Alternatively, you can 

+  completely replace the ResourceBundle file by calling the calling the 

+  <<<FakeFtpServer.setReplyTextBaseName(String)>>> method.

+

+* SLF4J Configuration Required to See Log Output

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  Note that <<FakeFtpServer>> uses {{{http://www.slf4j.org/}SLF4J}} for logging. If you want to

+  see the logging output, then you must configure <<SLF4J>>. (If no binding is found on the class

+  path, then <<SLF4J>> will default to a no-operation implementation.)

+

+  See the {{{http://www.slf4j.org/manual.html}SLF4J User Manual}} for more information.

diff --git a/tags/2.5/src/site/apt/fakeftpserver-versus-stubftpserver.apt b/tags/2.5/src/site/apt/fakeftpserver-versus-stubftpserver.apt
new file mode 100644
index 0000000..66d6fa6
--- /dev/null
+++ b/tags/2.5/src/site/apt/fakeftpserver-versus-stubftpserver.apt
@@ -0,0 +1,61 @@
+		--------------------------------------------------

+				FakeFtpServer versus StubFtpServer

+		--------------------------------------------------

+

+FakeFtpServer or StubFtpServer?

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  The <<MockFtpServer>> project includes two separate <mock> implementations of an FTP Server. Which one you

+  use is dependent on what kind of FTP scenario(s) you wish to simulate, and what level of control you need

+  over exact server replies.

+

+* FakeFtpServer

+~~~~~~~~~~~~~~~

+

+  <<FakeFtpServer>> provides a high-level abstraction for an FTP Server and is suitable for most testing

+  and simulation scenarios. You define a filesystem (internal, in-memory) containing an arbitrary set of

+  files and directories. These files and directories can (optionally) have associated access permissions.

+  You also configure a set of one or more user accounts that control which users can login to the FTP server,

+  and their home (default) directories. The user account is also used when assigning file and directory

+  ownership for new files.

+

+  <<FakeFtpServer>> processes FTP client requests and responds with reply codes and reply messages

+  consistent with its configuration and the contents of its internal filesystem, including file and

+  directory permissions, if they have been configured.

+

+  <<FakeFtpServer>>  can be fully configured programmatically or within a

+  {{{http://www.springframework.org/}Spring Framework}} or other dependency-injection container.

+

+  See the {{{./fakeftpserver-features.html}FakeFtpServer Features and Limitations}} page for more information on

+  which features and scenarios are supported.

+

+* StubFtpServer

+~~~~~~~~~~~~~~~

+

+  <<StubFtpServer>> is a "stub" implementation of an FTP server. It supports the main FTP commands by

+  implementing command handlers for each of the corresponding low-level FTP server commands (e.g. RETR,

+  DELE, LIST). These <CommandHandler>s can be individually configured to return custom data or reply codes,

+  allowing simulation of a complete range of both success and failure scenarios. The <CommandHandler>s can

+  also be interrogated to verify command invocation data such as command parameters and timestamps.

+

+  <<StubFtpServer>> works out of the box with reasonable defaults, but can be fully configured programmatically

+  or within a {{{http://www.springframework.org/}Spring Framework}} or other dependency-injection container.

+

+  See the {{{./stubftpserver-features.html}StubFtpServer Features and Limitations}} page for more information on

+  which features and scenarios are supported.

+

+* So, Which One Should I Use?

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  In general, if your testing and simulation needs are pretty straightforward, then using <<FakeFtpServer>> is

+  probably the best choice. See the {{{./fakeftpserver-features.html}FakeFtpServer Features and Limitations}} page

+  for more information on which features and scenarios are supported.

+

+  Some reasons to use <<StubFtpServer>> include:

+

+  * If you need to simulate an FTP server scenario not supported by <<FakeFtpServer>>.

+

+  * You want to test a very specific and/or limited FTP scenario. In this case, the setup of the

+    <<StubFtpServer>> might be simpler -- you don't have to setup fake files and directories and user accounts.

+

+  * You are more comfortable with configuring and using the lower-level FTP server command reply codes and behavior.

diff --git a/tags/2.5/src/site/apt/index.apt b/tags/2.5/src/site/apt/index.apt
new file mode 100644
index 0000000..12c4c94
--- /dev/null
+++ b/tags/2.5/src/site/apt/index.apt
@@ -0,0 +1,60 @@
+		--------------------------------------------------

+								Home

+		--------------------------------------------------

+

+MockFtpServer - Providing a Fake/Stub FTP Server

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  The <<MockFtpServer>> project provides mock/dummy FTP server implementations that can be very

+  useful for testing of FTP client code. Two FTP Server implementations are provided, each at a different

+  level of abstraction.

+

+  <<FakeFtpServer>> provides a higher-level abstraction for an FTP server and is suitable for most testing

+  and simulation scenarios. You define a filesystem (virtual, in-memory) containing an arbitrary set of

+  files and directories. These files and directories can (optionally) have associated access permissions.

+  You also configure a set of one or more user accounts that control which users can login to the FTP server,

+  and their home (default) directories. The user account is also used when assigning file and directory

+  ownership for new files. See {{{./fakeftpserver-features.html}FakeFtpServer Features and Limitations}}.

+

+  <<StubFtpServer>> is a "stub" implementation of an FTP server. It supports the main FTP commands by

+  implementing command handlers for each of the corresponding low-level FTP server commands (e.g. RETR,

+  DELE, LIST). These <CommandHandler>s can be individually configured to return custom data or reply codes,

+  allowing simulation of a complete range of both success and failure scenarios. The <CommandHandler>s can

+  also be interrogated to verify command invocation data such as command parameters and timestamps.

+  See {{{./stubftpserver-features.html}StubFtpServer Features and Limitations}}.

+

+  See the {{{./fakeftpserver-versus-stubftpserver.html}FakeFtpServer or StubFtpServer?}} page for more

+  information on deciding whether to use <<FakeFtpServer>> or <<StubFtpServer>>.

+

+  The <<MockFtpServer>> project is written in Java, and is ideally suited to testing Java code. But because

+  communication with the FTP server is across the network using sockets, it can be used to test FTP client 

+  code written in any language.

+

+  NOTE: Starting with <<MockFtpServer>> 2.4, the <<Log4J>> dependency has been replaced with {{{http://www.slf4j.org/}SLF4J}}.

+

+

+* Requirements

+~~~~~~~~~~~~~~

+

+  The <<MockFtpServer>> project requires:

+

+  *  Java (JDK) version 1.4 or later

+

+  * The {{{http://www.slf4j.org/}SLF4J}} API jar, accessible on the CLASSPATH. An SLF4J binding (logging

+    framework-specific jar) is optional.

+

+

+* Maven Support

+~~~~~~~~~~~~~~~

+

+  For projects built using {{{http://maven.apache.org/}Maven}}, <<MockFtpServer>> is now available

+  from the <<Maven Central Repository>>. Add a dependency to your POM like this:

+

+--------------------

+  <dependency>

+    <groupId>org.mockftpserver</groupId>

+    <artifactId>MockFtpServer</artifactId>

+    <version>2.4</version>

+    <scope>test</scope>

+  </dependency>

+--------------------
\ No newline at end of file
diff --git a/tags/2.5/src/site/apt/stubftpserver-commandhandlers.apt b/tags/2.5/src/site/apt/stubftpserver-commandhandlers.apt
new file mode 100644
index 0000000..e24a7e5
--- /dev/null
+++ b/tags/2.5/src/site/apt/stubftpserver-commandhandlers.apt
@@ -0,0 +1,98 @@
+		------------------------------------------------------

+			StubFtpServer FTP Commands and CommandHandlers

+		------------------------------------------------------

+

+StubFtpServer - FTP Commands and CommandHandlers

+

+  The following table lists the main FTP server commands with their corresponding FTP client commands,

+  and the <<StubFtpServer>> <CommandHandler> classes that implements support for the FTP server command.

+  See the Javadoc for each <CommandHandler> class for information on how to customize its behavior

+  through configuration, as well as what command invocation data is available.

+

+*------------------------*------------------------*------------------------------------------*

+| <<FTP Server Command>> | <<FTP Client Command>> | <<CommandHandler Class(es)>>             |

+*------------------------*------------------------*------------------------------------------*

+| ABOR                   | --                     | AborCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| ACCT                   | --                     | AcctCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| ALLO                   | --                     | AlloCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| APPE                   | APPEND                 | AppeCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| CDUP                   | --                     | CdupCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| CWD                    | CD                     | CwdCommandHandler                        |

+*------------------------*------------------------*------------------------------------------*

+| DELE                   | DELETE                 | DeleCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| EPRT                   | --                     | EprtCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| EPSV                   | --                     | EpsvCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| HELP                   | REMOTEHELP             | HelpCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| LIST                   | DIR / LS               | ListCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| MKD                    | MKDIR                  | MkdCommandHandler                        |

+*------------------------*------------------------*------------------------------------------*

+| MODE                   | --                     | ModeCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| NLST                   | --                     | NlstCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| NOOP                   | --                     | NoopCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| PASS                   | USER                   | PassCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| PASV                   | --                     | PasvCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| PORT                   | --                     | PortCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| PWD                    | PWD                    | PwdCommandHandler                        |

+*------------------------*------------------------*------------------------------------------*

+| QUIT                   | QUIT / BYE             | QuitCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| REIN                   | --                     | ReinCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| REST                   | --                     | RestCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| RETR                   | GET / RECV             | RetrCommandHandler                       |

+|                        |                        | FileRetrCommandHandler (1)               |

+*------------------------*------------------------*------------------------------------------*

+| RMD                    | RMDIR                  | RmdCommandHandler                        |

+*------------------------*------------------------*------------------------------------------*

+| RNFR                   | RENAME                 | RnfrCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| RNTO                   | RENAME                 | RntoCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| SITE                   | --                     | SiteCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| SMNT                   | --                     | SmntCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| STAT                   | STATUS                 | StatCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| STOR                   | PUT / SEND             | StorCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| STOU                   | --                     | StouCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| STRU                   | --                     | StruCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| SYST                   | --                     | SystCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| TYPE                   | ASCII / BINARY / TYPE  | TypeCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+| USER                   | USER                   | UserCommandHandler                       |

+*------------------------*------------------------*------------------------------------------*

+

+  (1) An alternative to the default <CommandHandler> implementation. See its class Javadoc.

+

+

+* Special Command Handlers

+~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  There are also <special> <CommandHandler> classes defined (in the <<core>> package).

+

+  * <<ConnectCommandHandler>> - Sends a 220 reply code after the initial connection to the server.

+     

+  * <<UnsupportedCommandHandler>> - Sends a 502 reply when an unrecognized/unsupported

+    command name is sent from a client.
\ No newline at end of file
diff --git a/tags/2.5/src/site/apt/stubftpserver-features.apt b/tags/2.5/src/site/apt/stubftpserver-features.apt
new file mode 100644
index 0000000..758019d
--- /dev/null
+++ b/tags/2.5/src/site/apt/stubftpserver-features.apt
@@ -0,0 +1,35 @@
+		--------------------------------------------------

+				StubFtpServer Features and Limitations

+		--------------------------------------------------

+

+StubFtpServer Features

+

+  * Standalone dummy FTP server. Run either within the same JVM as test code or in a different JVM.

+

+  * Implements common FTP server commands. See {{{./stubftpserver-commandhandlers.html}FTP Commands and CommandHandlers}}.

+  

+  * Supports active and passive mode data transfers.

+

+  * Works out of the box with reasonable defaults: success reply codes and empty data.

+  

+  * Easy to configure command handlers for individual FTP server commands to return success/failure reply codes and custom data.

+  

+  * Can verify expected FTP server command invocations.

+  

+  * Easy to implement command handlers for other commands or replace existing command handlers.

+

+  * Use a dynamically chosen free port number for the server control port instead of using the default (21)

+    or hard-coding some other value (set the serverControlPort property of the server to 0).

+

+  * Fully supports configuration within the <<Spring Framework>>.

+  

+  * Can be used to test FTP client code written in any language

+  

+

+StubFtpServer Limitations

+

+  * Using <<StubFtpServer>> for testing and simulation of non-default scenarios requires

+    some understanding of the FTP Specification and a configuration of the low-level

+    FTP Server commands.

+  

+  
\ No newline at end of file
diff --git a/tags/2.5/src/site/apt/stubftpserver-getting-started.apt b/tags/2.5/src/site/apt/stubftpserver-getting-started.apt
new file mode 100644
index 0000000..cbe2339
--- /dev/null
+++ b/tags/2.5/src/site/apt/stubftpserver-getting-started.apt
@@ -0,0 +1,341 @@
+		--------------------------------------------------

+					StubFtpServer Getting Started

+		--------------------------------------------------

+

+StubFtpServer - Getting Started

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  <<StubFtpServer>> is a "stub" implementation of an FTP server. It supports the main FTP commands by 

+  implementing command handlers for each of the corresponding low-level FTP server commands (e.g. RETR, 

+  DELE, LIST). These <CommandHandler>s can be individually configured to return custom data or reply codes, 

+  allowing simulation of a complete range of both success and failure scenarios. The <CommandHandler>s can 

+  also be interrogated to verify command invocation data such as command parameters and timestamps.

+

+  <<StubFtpServer>> works out of the box with reasonable defaults, but can be fully configured 

+  programmatically or within a {{{http://www.springframework.org/}Spring Framework}} (or similar) container.

+

+  Here is how to start the <<StubFtpServer>> with the default configuration. This will return 

+  success reply codes, and return empty data (for retrieved files, directory listings, etc.).

+

++------------------------------------------------------------------------------  

+StubFtpServer stubFtpServer = new StubFtpServer();

+stubFtpServer.start();

++------------------------------------------------------------------------------  

+

+  If you are running on a system where the default port (21) is already in use or cannot be bound

+  from a user process (such as Unix), you will need to use a different server control port. Use the

+  <<<StubFtpServer.setServerControlPort(int serverControlPort)>>> method to use a different port

+  number. If you specify a value of <<<0>>>, then the server will use a free port number. Then call

+  <<<getServerControlPort()>>> AFTER calling <<<start()>>> has been called to determine the actual port

+  number being used. Or, you can pass in a specific port number, such as 9187.

+

+* CommandHandlers

+~~~~~~~~~~~~~~~~~

+

+  <CommandHandler>s are the heart of the <<StubFtpServer>>.

+

+  <<StubFtpServer>> creates an appropriate default <CommandHandler> for each (supported) FTP server 

+  command. See the list of <CommandHandler> classes associated with FTP server commands in 

+  {{{./stubftpserver-commandhandlers.html}FTP Commands and CommandHandlers}}.

+

+  You can retrieve the existing <CommandHandler> defined for an FTP server command by calling the

+  <<<StubFtpServer.getCommandHandler(String name)>>> method, passing in the FTP server command

+  name. For example:

+  

++------------------------------------------------------------------------------  

+PwdCommandHandler pwdCommandHandler = (PwdCommandHandler) stubFtpServer.getCommandHandler("PWD");

++------------------------------------------------------------------------------  

+

+  You can replace the existing <CommandHandler> defined for an FTP server command by calling the

+  <<<StubFtpServer.setCommandHandler(String name, CommandHandler commandHandler)>>> method, passing 

+  in the FTP server command name, such as <<<"STOR">>> or <<<"USER">>>, and the 

+  <<<CommandHandler>>> instance. For example:

+  

++------------------------------------------------------------------------------  

+PwdCommandHandler pwdCommandHandler = new PwdCommandHandler();

+pwdCommandHandler.setDirectory("some/dir");

+stubFtpServer.setCommandHandler("PWD", pwdCommandHandler);

++------------------------------------------------------------------------------  

+

+

+** Generic CommandHandlers

+~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  <<StubFtpServer>> includes a couple generic <CommandHandler> classes that can be used to replace

+  the default command handler for an FTP command. See the Javadoc for more information.

+  

+  * <<StaticReplyCommadHandler>>

+

+    <<<StaticReplyCommadHandler>>> is a <CommandHandler> that always sends back the configured reply 

+    code and text. This can be a useful replacement for a default <CommandHandler> if you want a 

+    certain FTP command to always send back an error reply code.

+  

+  * <<SimpleCompositeCommandHandler>>

+

+    <<<SimpleCompositeCommandHandler>>> is a composite <CommandHandler> that manages an internal 

+    ordered list of <CommandHandler>s to which it delegates. Starting with the first 

+    <CommandHandler> in the list, each invocation of this composite handler will invoke (delegate to) 

+    the current internal <CommandHander>. Then it moves on the next <CommandHandler> in the internal list.

+

+

+** Configuring CommandHandler for a New (Unsupported) Command

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  If you want to add support for a command that is not provided out of the box by <<StubFtpServer>>,

+  you can create a <CommandHandler> instance and set it within the <<StubFtpServer>> using the

+  <<<StubFtpServer.setCommandHandler(String name, CommandHandler commandHandler)>>> method in the

+  same way that you replace an existing <CommandHandler> (see above). The following example uses

+  the <<<StaticReplyCommandHandler>>> to add support for the FEAT command.

+

++------------------------------------------------------------------------------

+final String FEAT_TEXT = "Extensions supported:\n" +

+        "MLST size*;create;modify*;perm;media-type\n" +

+        "SIZE\n" +

+        "COMPRESSION\n" +

+        "END";

+StaticReplyCommandHandler featCommandHandler = new StaticReplyCommandHandler(211, FEAT_TEXT);

+stubFtpServer.setCommandHandler("FEAT", featCommandHandler);

++------------------------------------------------------------------------------

+

+

+** Creating Your Own Custom CommandHandler Class

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  If one of the existing <CommandHandler> classes does not fulfill your needs, you can alternately create

+  your own custom <CommandHandler> class. The only requirement is that it implement the

+  <<<org.mockftpserver.core.command.CommandHandler>>> interface. You would, however, likely benefit from

+  inheriting from one of the existing abstract <CommandHandler> superclasses, such as

+  <<<org.mockftpserver.stub.command.AbstractStubCommandHandler>>> or

+  <<<org.mockftpserver.core.command.AbstractCommandHandler>>>. See the javadoc of these classes for

+  more information.

+

+

+* Retrieving Command Invocation Data

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  Each predefined <<StubFtpServer>> <CommandHandler> manages a List of <<<InvocationRecord>>> objects -- one

+  for each time the <CommandHandler> is invoked. An <<<InvocationRecord>>> contains the <<<Command>>> 

+  that triggered the invocation (containing the command name and parameters), as well as the invocation

+  timestamp and client host address. The <<<InvocationRecord>>> also contains a <<<Map>>>, with optional

+  <CommandHandler>-specific data. See the Javadoc for more information.

+  

+  You can retrieve the <<<InvocationRecord>>> from a <CommandHandler> by calling the

+  <<<getInvocation(int index)>>> method, passing in the (zero-based) index of the desired

+  invocation. You can get the number of invocations for a <CommandHandler> by calling

+  <<<numberOfInvocations()>>>. The {{{#Example}Example Test Using Stub Ftp Server}} below illustrates 

+  retrieving and interrogating an <<<InvocationRecord>>> from a <CommandHandler>.

+  

+

+* {Example} Test Using StubFtpServer

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  This section includes a simplified example of FTP client code to be tested, and a JUnit 

+  test for it that uses <<StubFtpServer>>.

+

+** FTP Client Code

+~~~~~~~~~~~~~~~~~~

+

+  The following <<<RemoteFile>>> class includes a <<<readFile()>>> method that retrieves a remote 

+  ASCII file and returns its contents as a String. This class uses the <<<FTPClient>>> from the

+  {{{http://commons.apache.org/net/}Apache Commons Net}} framework.

+

++------------------------------------------------------------------------------  

+public class RemoteFile {

+

+    private String server;

+

+    public String readFile(String filename) throws SocketException, IOException {

+

+        FTPClient ftpClient = new FTPClient();

+        ftpClient.connect(server);

+

+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

+        boolean success = ftpClient.retrieveFile(filename, outputStream);

+        ftpClient.disconnect();

+

+        if (!success) {

+            throw new IOException("Retrieve file failed: " + filename);

+        }

+        return outputStream.toString();

+    }

+    

+    public void setServer(String server) {

+        this.server = server;

+    }

+    

+    // Other methods ...

+}

++------------------------------------------------------------------------------  

+

+** JUnit Test For FTP Client Code Using StubFtpServer

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  The following <<<RemoteFileTest>>> class includes a couple of JUnit tests that use 

+  <<StubFtpServer>>. The test illustrates replacing the default <CommandHandler> with

+  a customized handler.

+

++------------------------------------------------------------------------------  

+import java.io.IOException;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.stub.StubFtpServer;

+import org.mockftpserver.stub.command.RetrCommandHandler;

+import org.mockftpserver.test.AbstractTest;

+

+public class RemoteFileTest extends AbstractTest {

+

+    private static final String FILENAME = "dir/sample.txt";

+

+    private RemoteFile remoteFile;

+    private StubFtpServer stubFtpServer;

+    

+    public void testReadFile() throws Exception {

+

+        final String CONTENTS = "abcdef 1234567890";

+

+        // Replace the default RETR CommandHandler; customize returned file contents

+        RetrCommandHandler retrCommandHandler = new RetrCommandHandler();

+        retrCommandHandler.setFileContents(CONTENTS);

+        stubFtpServer.setCommandHandler("RETR", retrCommandHandler);

+        

+        stubFtpServer.start();

+        

+        String contents = remoteFile.readFile(FILENAME);

+

+        // Verify returned file contents

+        assertEquals("contents", CONTENTS, contents);

+        

+        // Verify the submitted filename

+        InvocationRecord invocationRecord = retrCommandHandler.getInvocation(0);

+        String filename = invocationRecord.getString(RetrCommandHandler.PATHNAME_KEY);

+        assertEquals("filename", FILENAME, filename);

+    }

+

+    /**

+     * Test the readFile() method when the FTP transfer fails (returns a non-success reply code) 

+     */

+    public void testReadFileThrowsException() {

+

+        // Replace the default RETR CommandHandler; return failure reply code

+        RetrCommandHandler retrCommandHandler = new RetrCommandHandler();

+        retrCommandHandler.setFinalReplyCode(550);

+        stubFtpServer.setCommandHandler("RETR", retrCommandHandler);

+        

+        stubFtpServer.start();

+

+        try {

+            remoteFile.readFile(FILENAME);

+            fail("Expected IOException");

+        }

+        catch (IOException expected) {

+            // Expected this

+        }

+    }

+    

+    protected void setUp() throws Exception {

+        super.setUp();

+        remoteFile = new RemoteFile();

+        remoteFile.setServer("localhost");

+        stubFtpServer = new StubFtpServer();

+    }

+

+    protected void tearDown() throws Exception {

+        super.tearDown();

+        stubFtpServer.stop();

+    }

+}

++------------------------------------------------------------------------------  

+

+  Things to note about the above test:

+  

+  * The <<<StubFtpServer>>> instance is created in the <<<setUp()>>> method, but is not started

+    there because it must be configured differently for each test. The <<<StubFtpServer>>> instance 

+    is stopped in the <<<tearDown()>>> method, to ensure that it is stopped, even if the test fails.

+  

+

+* Spring Configuration

+~~~~~~~~~~~~~~~~~~~~~~

+

+  You can easily configure a <<StubFtpServer>> instance in the

+  {{{http://www.springframework.org/}Spring Framework}}. The following example shows a <Spring>

+  configuration file.

+

++------------------------------------------------------------------------------

+<?xml version="1.0" encoding="UTF-8"?>

+

+<beans xmlns="http://www.springframework.org/schema/beans"

+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

+       xsi:schemaLocation="http://www.springframework.org/schema/beans

+           http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

+

+  <bean id="stubFtpServer" class="org.mockftpserver.stub.StubFtpServer">

+

+    <property name="commandHandlers">

+      <map>

+        <entry key="LIST">

+          <bean class="org.mockftpserver.stub.command.ListCommandHandler">

+            <property name="directoryListing">

+              <value>

+                11-09-01 12:30PM  406348 File2350.log

+                11-01-01 1:30PM &lt;DIR&gt; 0 archive

+              </value>

+            </property>

+          </bean>

+        </entry>

+

+        <entry key="PWD">

+          <bean class="org.mockftpserver.stub.command.PwdCommandHandler">

+            <property name="directory" value="foo/bar" />

+          </bean>

+        </entry>

+

+        <entry key="DELE">

+          <bean class="org.mockftpserver.stub.command.DeleCommandHandler">

+            <property name="replyCode" value="450" />

+          </bean>

+        </entry>

+

+        <entry key="RETR">

+          <bean class="org.mockftpserver.stub.command.RetrCommandHandler">

+            <property name="fileContents"

+              value="Sample file contents line 1&#10;Line 2&#10;Line 3"/>

+          </bean>

+        </entry>

+

+      </map>

+    </property>

+  </bean>

+

+</beans>

++------------------------------------------------------------------------------

+

+  This example overrides the default handlers for the following FTP commands:

+

+  * LIST - replies with a predefined directory listing

+

+  * PWD - replies with a predefined directory pathname

+

+  * DELE - replies with an error reply code (450)

+

+  * RETR - replies with predefined contents for a retrieved file

+

+  []

+

+  And here is the Java code to load the above <Spring> configuration file and start the

+  configured <<StubFtpServer>>.

+

++------------------------------------------------------------------------------

+ApplicationContext context = new ClassPathXmlApplicationContext("stubftpserver-beans.xml");

+stubFtpServer = (StubFtpServer) context.getBean("stubFtpServer");

+stubFtpServer.start();

++------------------------------------------------------------------------------

+

+

+* FTP Command Reply Text ResourceBundle

+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+

+  The default text asociated with each FTP command reply code is contained within the

+  "ReplyText.properties" ResourceBundle file. You can customize these messages by providing a

+  locale-specific ResourceBundle file on the CLASSPATH, according to the normal lookup rules of 

+  the ResourceBundle class (e.g., "ReplyText_de.properties"). Alternatively, you can 

+  completely replace the ResourceBundle file by calling the calling the 

+  <<<StubFtpServer.setReplyTextBaseName(String)>>> method. 

diff --git a/tags/2.5/src/site/fml/faq.fml b/tags/2.5/src/site/fml/faq.fml
new file mode 100644
index 0000000..c4d1495
--- /dev/null
+++ b/tags/2.5/src/site/fml/faq.fml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>

+

+<faqs title="Frequently Asked Questions" toplink="false">

+

+  <part id="general">

+    <title>General</title>

+

+    <faq id="whats-foo">

+      <question>

+        What is Foo?

+      </question>

+      <answer>

+        <p>some markup goes here</p>

+

+        <source>some source code</source>

+

+        <p>some markup goes here</p>

+      </answer>

+    </faq>

+

+	<!--

+    <faq id="whats-bar">

+      <question>

+        What is Bar?

+      </question>

+      <answer>

+        <p>some markup goes here</p>

+      </answer>

+    </faq>

+  -->

+

+  </part>

+

+	<!--

+  <part id="install">

+

+    <title>Installation</title>

+

+    <faq id="how-install">

+      <question>

+        How do I install Foo?

+      </question>

+      <answer>

+        <p>some markup goes here</p>

+      </answer>

+    </faq>

+

+  </part>

+	-->

+

+</faqs>

diff --git a/tags/2.5/src/site/resources/css/site.css b/tags/2.5/src/site/resources/css/site.css
new file mode 100644
index 0000000..4f24aa3
--- /dev/null
+++ b/tags/2.5/src/site/resources/css/site.css
@@ -0,0 +1,4 @@
+tt { 

+    font-size: 110%;

+    font-weight: bolder;

+}
\ No newline at end of file
diff --git a/tags/2.5/src/site/resources/images/mockftpserver-logo.png b/tags/2.5/src/site/resources/images/mockftpserver-logo.png
new file mode 100644
index 0000000..a41e069
--- /dev/null
+++ b/tags/2.5/src/site/resources/images/mockftpserver-logo.png
Binary files differ
diff --git a/tags/2.5/src/site/site.xml b/tags/2.5/src/site/site.xml
new file mode 100644
index 0000000..d778777
--- /dev/null
+++ b/tags/2.5/src/site/site.xml
@@ -0,0 +1,51 @@
+<project name="MockFtpServer">

+

+    <bannerLeft>

+        <name>MockFtpServer</name>

+        <src>images/mockftpserver-logo.png</src>

+        <href>/</href>

+    </bannerLeft>

+

+    <publishDate format="dd MMM yyyy"/>

+

+    <poweredBy>

+        <logo

+                name="Hosted on SourceForge.net"

+                href="http://sourceforge.net"

+                img="http://sflogo.sourceforge.net/sflogo.php?group_id=208647&amp;type=2"/>

+        <logo

+                name="Build with Maven 2"

+                href="http://maven.apache.org"

+                img="http://maven.apache.org/images/logos/maven-feather.png"/>

+    </poweredBy>

+

+    <body>

+        <links>

+        </links>

+        <head>

+            <meta name="faq" content="mockftpserver"/>

+        </head>

+        <menu name="General">

+            <item name="Home" href="/index.html"/>

+            <item name="FakeFtpServer or StubFtpServer?" href="/fakeftpserver-versus-stubftpserver.html"/>

+            <!-- <item name="FAQs" href="/faq.html"/> -->

+            <item name="Javadocs" href="/apidocs/index.html"/>

+            <item name="Downloads" href="http://sourceforge.net/project/showfiles.php?group_id=208647"/>

+            <item name="SourceForge Project Page" href="http://sourceforge.net/projects/mockftpserver"/>

+        </menu>

+

+        <menu name="FakeFtpServer">

+            <item name="Features and Limitations" href="/fakeftpserver-features.html"/>

+            <item name="Getting Started" href="/fakeftpserver-getting-started.html"/>

+            <item name="File Systems" href="/fakeftpserver-filesystems.html"/>

+        </menu>

+

+        <menu name="StubFtpServer">

+            <item name="Features and Limitations" href="/stubftpserver-features.html"/>

+            <item name="Getting Started" href="/stubftpserver-getting-started.html"/>

+            <item name="CommandHandlers" href="/stubftpserver-commandhandlers.html"/>

+        </menu>

+

+        ${reports}

+    </body>

+</project>
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/core/server/AbstractFtpServer_MultipleStartAndStopTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/core/server/AbstractFtpServer_MultipleStartAndStopTest.groovy
new file mode 100644
index 0000000..9ab39ad
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/core/server/AbstractFtpServer_MultipleStartAndStopTest.groovy
@@ -0,0 +1,69 @@
+/*

+ * Copyright 2011 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.server

+

+import org.mockftpserver.fake.FakeFtpServer

+import org.mockftpserver.test.AbstractGroovyTestCase

+import org.mockftpserver.test.PortTestUtil

+

+/**

+ * Test starting and stopping Abstract(Fake)FtpServer multiple times. 

+ *

+ * @version $Revision: 242 $ - $Date: 2010-03-21 07:41:01 -0400 (Sun, 21 Mar 2010) $

+ *

+ * @author Chris Mair

+ */

+class AbstractFtpServer_MultipleStartAndStopTest extends AbstractGroovyTestCase {

+

+    private FakeFtpServer ftpServer = new FakeFtpServer()

+

+    // Takes ~ 500ms per start/stop

+

+    void testStartAndStop() {

+        10.times {

+            final def port = PortTestUtil.getFtpServerControlPort()

+            ftpServer.setServerControlPort(port);

+

+            ftpServer.start();

+            assert ftpServer.getServerControlPort() == port

+            Thread.sleep(100L);     // give it some time to get started

+            assertEquals("started - after start()", true, ftpServer.isStarted());

+            assertEquals("shutdown - after start()", false, ftpServer.isShutdown());

+

+            ftpServer.stop();

+

+            assertEquals("shutdown - after stop()", true, ftpServer.isShutdown());

+        }

+    }

+

+    void testStartAndStop_UseDynamicFreePort() {

+        5.times {

+            ftpServer.setServerControlPort(0);

+            assert ftpServer.getServerControlPort() == 0

+

+            ftpServer.start();

+            log("Using port ${ftpServer.getServerControlPort()}")

+            assert ftpServer.getServerControlPort() != 0

+

+            ftpServer.stop();

+        }

+    }

+

+    void tearDown() {

+        super.tearDown()

+        ftpServer.stop();   // just to be sure

+    }

+}

diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/core/session/StubSession.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/core/session/StubSession.groovy
new file mode 100644
index 0000000..139fcf9
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/core/session/StubSession.groovy
@@ -0,0 +1,185 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.session

+

+/**

+ * Stub implementation of the         {@link Session}         interface for testing

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class StubSession implements Session {

+

+    Map attributes = [:]

+    private List sentReplies = []

+    List sentData = []

+    //byte[] dataToRead

+    Object dataToRead

+    boolean closed

+    InetAddress clientDataHost

+    int clientDataPort

+    boolean dataConnectionOpen = false

+    int switchToPassiveModeReturnValue

+    boolean switchedToPassiveMode = false

+    InetAddress serverHost

+

+    /**

+     * @see org.mockftpserver.core.session.Session#close()

+     */

+    public void close() {

+        closed = true

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#closeDataConnection()

+     */

+    public void closeDataConnection() {

+        dataConnectionOpen = false

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#getAttribute(java.lang.String)

+     */

+    public Object getAttribute(String name) {

+        return attributes[name]

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#getAttributeNames()

+     */

+    public Set getAttributeNames() {

+        return attributes.keySet()

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#getClientHost()

+     */

+    public InetAddress getClientHost() {

+        return null

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#getServerHost()

+     */

+    public InetAddress getServerHost() {

+        return serverHost

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#openDataConnection()

+     */

+    public void openDataConnection() {

+        dataConnectionOpen = true

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#readData()

+     */

+    public byte[] readData() {

+        assert dataConnectionOpen, "The data connection must be OPEN"

+        return dataToRead

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#readData()

+     */

+    public byte[] readData(int numBytes) {

+        assert dataConnectionOpen, "The data connection must be OPEN"

+        return dataToRead

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#removeAttribute(java.lang.String)

+     */

+    public void removeAttribute(String name) {

+        attributes.remove(name)

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#sendData(byte [], int)

+     */

+    public void sendData(byte[] data, int numBytes) {

+        assert dataConnectionOpen, "The data connection must be OPEN"

+        sentData << new String(data, 0, numBytes)

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#sendReply(int, java.lang.String)

+     */

+    public void sendReply(int replyCode, String replyText) {

+        sentReplies << [replyCode, replyText]

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#setAttribute(java.lang.String, java.lang.Object)

+     */

+    public void setAttribute(String name, Object value) {

+        attributes[name] = value

+    }

+

+    /**

+     * @see org.mockftpserver.core.session.Session#switchToPassiveMode()

+     */

+    public int switchToPassiveMode() {

+        switchedToPassiveMode = true

+        return switchToPassiveModeReturnValue

+    }

+

+    /**

+     * @see java.lang.Runnable#run()

+     */

+    public void run() {

+

+    }

+

+    //-------------------------------------------------------------------------

+    // Stub-specific API - Helper methods not part of Session interface

+    //-------------------------------------------------------------------------

+

+    /**

+     * @return the reply code for the session reply at the specified index

+     */

+    int getReplyCode(int replyIndex) {

+        return getReply(replyIndex)[0]

+    }

+

+    /**

+     * @return the reply message for the session reply at the specified index

+     */

+    String getReplyMessage(int replyIndex) {

+        return getReply(replyIndex)[1]

+    }

+

+    /**

+     * @return the String representation of this object, including property names and values of interest

+     */

+    String toString() {

+        "StubSession[sentReplies=$sentReplies  sentData=$sentData  attributes=$attributes  closed=$closed  " +

+                "clientDataHost=$clientDataHost  clientDataPort=$clientDataPort]"

+    }

+

+    //-------------------------------------------------------------------------

+    // Internal Helper Methods

+    //-------------------------------------------------------------------------

+

+    private List getReply(int replyIndex) {

+        def reply = sentReplies[replyIndex]

+        assert reply, "No reply for index [$replyIndex] sent for ${this}"

+        return reply

+    }

+

+}

diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/core/util/IoUtilTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/core/util/IoUtilTest.groovy
new file mode 100644
index 0000000..7151322
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/core/util/IoUtilTest.groovy
@@ -0,0 +1,45 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.util

+

+import org.mockftpserver.test.AbstractGroovyTestCase

+

+/**

+ * Tests for the IoUtil class

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+public class IoUtilTest extends AbstractGroovyTestCase {

+

+    /**

+     * Test the readBytes() method 

+     */

+    void testReadBytes() {

+        final byte[] BYTES = "abc 123 %^&".getBytes()

+        InputStream input = new ByteArrayInputStream(BYTES)

+        assert IoUtil.readBytes(input) == BYTES

+    }

+

+    /**

+     * Test the readBytes() method, passing in a null 

+     */

+    void testReadBytes_Null() {

+        shouldFailWithMessageContaining("input") { IoUtil.readBytes(null) }

+    }

+

+}

diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/core/util/PatternUtilTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/core/util/PatternUtilTest.groovy
new file mode 100644
index 0000000..894d5c8
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/core/util/PatternUtilTest.groovy
@@ -0,0 +1,51 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.util

+

+import org.mockftpserver.test.AbstractGroovyTestCase

+

+/**

+ * Tests for the PatternUtil class

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+public class PatternUtilTest extends AbstractGroovyTestCase {

+

+    void testConvertStringWithWildcardsToRegex() {

+        assert PatternUtil.convertStringWithWildcardsToRegex('abc') == /abc/

+        assert PatternUtil.convertStringWithWildcardsToRegex('abc.def') == /abc\.def/

+        assert PatternUtil.convertStringWithWildcardsToRegex('(abc):{def}') == /\(abc\)\:\{def\}/

+        assert PatternUtil.convertStringWithWildcardsToRegex('|[23]^a+$b') == /\|\[23\]\^a\+/ + '\\$b'

+

+        assert PatternUtil.convertStringWithWildcardsToRegex('*.txt') == /.*\.txt/

+        assert PatternUtil.convertStringWithWildcardsToRegex('abc*') == /abc.*/

+        assert PatternUtil.convertStringWithWildcardsToRegex('??x?.*') == /..x.\..*/

+    }

+

+    void testContainsWildcards() {

+        assert !PatternUtil.containsWildcards('')

+        assert !PatternUtil.containsWildcards('abc')

+        assert !PatternUtil.containsWildcards('abc.def')

+

+        assert PatternUtil.containsWildcards('*.txt')

+        assert PatternUtil.containsWildcards('abc.*_OLD')

+        assert PatternUtil.containsWildcards('a??.txt')

+        assert PatternUtil.containsWildcards('?a*.*HH???')

+    }

+

+}

diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/core/util/PortParserTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/core/util/PortParserTest.groovy
new file mode 100644
index 0000000..1bf5ed8
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/core/util/PortParserTest.groovy
@@ -0,0 +1,106 @@
+/*

+ * Copyright 2009 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.util

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.CommandSyntaxException

+import org.mockftpserver.test.AbstractGroovyTestCase

+

+/**

+ * Tests for the PortParser class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+class PortParserTest extends AbstractGroovyTestCase {

+

+    static final Logger LOG = LoggerFactory.getLogger(PortParserTest.class)

+    static final String[] PARAMETERS = ["192", "22", "250", "44", "1", "206"]

+    static final String[] PARAMETERS_INSUFFICIENT = ["7", "29", "99", "11", "77"]

+    static final int PORT = (1 << 8) + 206

+    static final InetAddress HOST = inetAddress("192.22.250.44")

+

+    static final PARAMETER_IPV4 = "|1|132.235.1.2|6275|"

+    static final HOST_IPV4 = InetAddress.getByName("132.235.1.2")

+    static final PARAMETER_IPV6 = "|2|1080::8:800:200C:417A|6275|"

+    static final HOST_IPV6 = InetAddress.getByName("1080::8:800:200C:417A")

+    static final E_PORT = 6275

+

+    void testParseExtendedAddressHostAndPort_IPv4() {

+        def client = PortParser.parseExtendedAddressHostAndPort(PARAMETER_IPV4)

+        assert client.host == HOST_IPV4

+        assert client.port == E_PORT

+    }

+

+    void testParseExtendedAddressHostAndPort_IPv6() {

+        def client = PortParser.parseExtendedAddressHostAndPort(PARAMETER_IPV6)

+        assert client.host == HOST_IPV6

+        assert client.port == E_PORT

+    }

+

+    void testParseExtendedAddressHostAndPort_IPv6_CustomDelimiter() {

+        def client = PortParser.parseExtendedAddressHostAndPort("@2@1080::8:800:200C:417A@6275@")

+        assert client.host == HOST_IPV6

+        assert client.port == E_PORT

+    }

+

+    void testParseExtendedAddressHostAndPort_IllegalParameterFormat() {

+        final PARM = 'abcdef'

+        shouldFail(CommandSyntaxException) { PortParser.parseExtendedAddressHostAndPort(PARM) }

+    }

+

+    void testParseExtendedAddressHostAndPort_PortMissing() {

+        final PARM = '|1|132.235.1.2|'

+        shouldFail(CommandSyntaxException) { PortParser.parseExtendedAddressHostAndPort(PARM) }

+    }

+

+    void testParseExtendedAddressHostAndPort_IllegalHostName() {

+        final PARM = '|1|132.@|6275|'

+        shouldFail(CommandSyntaxException) { PortParser.parseExtendedAddressHostAndPort(PARM) }

+    }

+

+    void testParseExtendedAddressHostAndPort_Null() {

+        shouldFail(CommandSyntaxException) { PortParser.parseExtendedAddressHostAndPort(null) }

+    }

+

+    void testParseExtendedAddressHostAndPort_Empty() {

+        shouldFail(CommandSyntaxException) { PortParser.parseExtendedAddressHostAndPort('') }

+    }

+

+    void testParseHostAndPort() {

+        def client = PortParser.parseHostAndPort(PARAMETERS)

+        assert client.host == HOST

+        assert client.port == PORT

+    }

+

+    void testParseHostAndPort_Null() {

+        shouldFail(CommandSyntaxException) { PortParser.parseHostAndPort(null) }

+    }

+

+    void testParseHostAndPort_InsufficientParameters() throws UnknownHostException {

+        shouldFail(CommandSyntaxException) { PortParser.parseHostAndPort(PARAMETERS_INSUFFICIENT) }

+    }

+

+    void testConvertHostAndPortToStringOfBytes() {

+        int port = (23 << 8) + 77

+        InetAddress host = InetAddress.getByName("196.168.44.55")

+        String result = PortParser.convertHostAndPortToCommaDelimitedBytes(host, port)

+        LOG.info("result=" + result)

+        assertEquals("result", "196,168,44,55,23,77", result)

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/core/util/StringUtilTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/core/util/StringUtilTest.groovy
new file mode 100644
index 0000000..cf4800b
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/core/util/StringUtilTest.groovy
@@ -0,0 +1,61 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.util

+

+import org.mockftpserver.test.AbstractGroovyTestCase

+

+/**

+ * Tests for the IoUtil class

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class StringUtilTest extends AbstractGroovyTestCase {

+

+    void testPadRight() {

+        assert StringUtil.padRight('', 0) == ''

+        assert StringUtil.padRight('', 1) == ' '

+        assert StringUtil.padRight('z', 1) == 'z'

+        assert StringUtil.padRight(' z', 3) == ' z '

+        assert StringUtil.padRight('z', 1) == 'z'

+        assert StringUtil.padRight('zzz', 1) == 'zzz'

+        assert StringUtil.padRight('z', 5) == 'z    '

+    }

+

+    void testPadLeft() {

+        assert StringUtil.padLeft('', 0) == ''

+        assert StringUtil.padLeft('', 1) == ' '

+        assert StringUtil.padLeft('z', 1) == 'z'

+        assert StringUtil.padLeft(' z', 3) == '  z'

+        assert StringUtil.padLeft('z', 1) == 'z'

+        assert StringUtil.padLeft('zzz', 1) == 'zzz'

+        assert StringUtil.padLeft('z', 5) == '    z'

+    }

+

+    void testJoin() {

+        assert StringUtil.join([], ' ') == ''

+        assert StringUtil.join([], 'x') == ''

+        assert StringUtil.join(['a'], 'x') == 'a'

+        assert StringUtil.join(['a', 'b'], '') == 'ab'

+        assert StringUtil.join(['a', 'b'], ',') == 'a,b'

+        assert StringUtil.join(['a', 'b', 'c'], ':') == 'a:b:c'

+

+        shouldFailWithMessageContaining('parts') { StringUtil.join(null, '') }

+        shouldFailWithMessageContaining('delimiter') { StringUtil.join([], null) }

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/CustomUserAccount.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/CustomUserAccount.groovy
new file mode 100644
index 0000000..3a38551
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/CustomUserAccount.groovy
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake
+
+import org.mockftpserver.fake.UserAccount
+
+/**
+ * Test-only subclass of UserAccount tha provides a custom implementation of password comparison
+ */
+class CustomUserAccount extends UserAccount {
+    protected boolean comparePassword(String password) {
+        return password == this.password + "123"
+    }
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/FakeFtpServerIntegrationTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/FakeFtpServerIntegrationTest.groovy
new file mode 100644
index 0000000..7c5349f
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/FakeFtpServerIntegrationTest.groovy
@@ -0,0 +1,510 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake

+

+import org.apache.commons.net.ftp.FTP

+import org.apache.commons.net.ftp.FTPClient

+import org.apache.commons.net.ftp.FTPFile

+import org.mockftpserver.core.command.CommandNames

+import org.mockftpserver.core.command.StaticReplyCommandHandler

+

+import org.mockftpserver.fake.filesystem.DirectoryEntry

+import org.mockftpserver.fake.filesystem.FileEntry

+import org.mockftpserver.fake.filesystem.FileSystem

+import org.mockftpserver.fake.filesystem.UnixFakeFileSystem

+import org.mockftpserver.fake.filesystem.WindowsFakeFileSystem

+import org.mockftpserver.stub.command.CwdCommandHandler

+import org.mockftpserver.test.AbstractGroovyTestCase

+import org.mockftpserver.test.PortTestUtil

+

+/**

+ * Integration tests for FakeFtpServer.

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class FakeFtpServerIntegrationTest extends AbstractGroovyTestCase {

+

+    static final SERVER = "localhost"

+    static final USERNAME = "user123"

+    static final PASSWORD = "password"

+    static final ACCOUNT = "account123"

+    static final ASCII_DATA = "abcdef\tghijklmnopqr"

+    static final BINARY_DATA = new byte[256]

+    static final ROOT_DIR = "c:/"

+    static final HOME_DIR = p(ROOT_DIR, "home")

+    static final SUBDIR_NAME = 'sub'

+    static final SUBDIR_NAME2 = "archive"

+    static final SUBDIR = p(HOME_DIR, SUBDIR_NAME)

+    static final FILENAME1 = "abc.txt"

+    static final FILENAME2 = "SomeOtherFile.xml"

+    static final FILE1 = p(HOME_DIR, FILENAME1)

+    static final SYSTEM_NAME = "WINDOWS"

+

+    private FakeFtpServer ftpServer

+    private FTPClient ftpClient

+    private FileSystem fileSystem

+    private UserAccount userAccount

+

+    //-------------------------------------------------------------------------

+    // Tests

+    //-------------------------------------------------------------------------

+

+    void testAbor() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.abort()

+        verifyReplyCode("ABOR", 226)

+    }

+

+    void testAcct() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.acct(ACCOUNT) == 230

+    }

+

+    void testAllo() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.allocate(99)

+        verifyReplyCode("ALLO", 200)

+    }

+

+    void testAppe() {

+        def ORIGINAL_CONTENTS = '123 456 789'

+        fileSystem.add(new FileEntry(path: FILE1, contents: ORIGINAL_CONTENTS))

+

+        ftpClientConnectAndLogin()

+

+        LOG.info("Put File for local path [$FILE1]")

+        def inputStream = new ByteArrayInputStream(ASCII_DATA.getBytes())

+        assert ftpClient.appendFile(FILE1, inputStream)

+        def contents = fileSystem.getEntry(FILE1).createInputStream().text

+        LOG.info("File contents=[" + contents + "]")

+        assert contents == ORIGINAL_CONTENTS + ASCII_DATA

+    }

+

+    void testCdup() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.changeToParentDirectory()

+        verifyReplyCode("changeToParentDirectory", 200)

+    }

+

+    void testCwd() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.changeWorkingDirectory(SUBDIR_NAME)

+        verifyReplyCode("changeWorkingDirectory", 250)

+    }

+

+    /**

+     * Test that a CWD to ".." properly resolves the current dir (without the "..") so that PWD returns the parent 

+     */

+    void testCwd_DotDot_Pwd() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.changeWorkingDirectory("..")

+        verifyReplyCode("changeWorkingDirectory", 250)

+        assert p(ftpClient.printWorkingDirectory()) == p(ROOT_DIR)

+        assert ftpClient.changeWorkingDirectory("home")

+        assert p(ftpClient.printWorkingDirectory()) == p(HOME_DIR)

+    }

+

+    /**

+     * Test that a CWD to "." properly resolves the current dir (without the ".") so that PWD returns the parent

+     */

+    void testCwd_Dot_Pwd() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.changeWorkingDirectory(".")

+        verifyReplyCode("changeWorkingDirectory", 250)

+        assert p(ftpClient.printWorkingDirectory()) == p(HOME_DIR)

+    }

+

+    void testCwd_UseStaticReplyCommandHandler() {

+        final int REPLY_CODE = 500;

+        StaticReplyCommandHandler cwdCommandHandler = new StaticReplyCommandHandler(REPLY_CODE);

+        ftpServer.setCommandHandler(CommandNames.CWD, cwdCommandHandler);

+

+        ftpClientConnectAndLogin()

+        assert !ftpClient.changeWorkingDirectory(SUBDIR_NAME)

+        verifyReplyCode("changeWorkingDirectory", REPLY_CODE)

+    }

+

+    void testCwd_UseStubCommandHandler() {

+        final int REPLY_CODE = 502;

+        CwdCommandHandler cwdCommandHandler = new CwdCommandHandler();     // Stub command handler

+        cwdCommandHandler.setReplyCode(REPLY_CODE);

+        ftpServer.setCommandHandler(CommandNames.CWD, cwdCommandHandler);

+

+        ftpClientConnectAndLogin()

+        assert !ftpClient.changeWorkingDirectory(SUBDIR_NAME)

+        verifyReplyCode("changeWorkingDirectory", REPLY_CODE)

+        assert cwdCommandHandler.getInvocation(0)

+    }

+

+    void testDele() {

+        fileSystem.add(new FileEntry(FILE1))

+

+        ftpClientConnectAndLogin()

+        assert ftpClient.deleteFile(FILENAME1)

+        verifyReplyCode("deleteFile", 250)

+        assert !fileSystem.exists(FILENAME1)

+    }

+

+    void testEprt() {

+        log("Skipping...")

+//        ftpClientConnectAndLogin()

+//        assert ftpClient.sendCommand("EPRT", "|2|1080::8:800:200C:417A|5282|") == 200

+    }

+

+    void testEpsv() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.sendCommand("EPSV") == 229

+    }

+

+    void testFeat_UseStaticReplyCommandHandler() {

+        // The FEAT command is not supported out of the box

+        StaticReplyCommandHandler featCommandHandler = new StaticReplyCommandHandler(211, "No Features");

+        ftpServer.setCommandHandler("FEAT", featCommandHandler);

+

+        ftpClientConnectAndLogin()

+        assert ftpClient.sendCommand("FEAT") == 211

+    }

+

+    void testHelp() {

+        ftpServer.helpText = [a: 'aaa', '': 'default']

+        ftpClientConnect()

+

+        String help = ftpClient.listHelp()

+        assert help.contains('default')

+        verifyReplyCode("listHelp", 214)

+

+        help = ftpClient.listHelp('a')

+        assert help.contains('aaa')

+        verifyReplyCode("listHelp", 214)

+

+        help = ftpClient.listHelp('bad')

+        assert help.contains('bad')

+        verifyReplyCode("listHelp", 214)

+    }

+

+    void testList() {

+        def LAST_MODIFIED = new Date()

+        fileSystem.add(new FileEntry(path: p(SUBDIR, FILENAME1), lastModified: LAST_MODIFIED, contents: ASCII_DATA))

+        fileSystem.add(new DirectoryEntry(path: p(SUBDIR, SUBDIR_NAME2), lastModified: LAST_MODIFIED))

+

+        ftpClientConnectAndLogin()

+

+        FTPFile[] files = ftpClient.listFiles(SUBDIR)

+        assert files.length == 2

+

+        // Can't be sure of order

+        FTPFile fileEntry = (files[0].getType() == FTPFile.FILE_TYPE) ? files[0] : files[1]

+        FTPFile dirEntry = (files[0].getType() == FTPFile.FILE_TYPE) ? files[1] : files[0]

+        verifyFTPFile(fileEntry, FTPFile.FILE_TYPE, FILENAME1, ASCII_DATA.size())

+        verifyFTPFile(dirEntry, FTPFile.DIRECTORY_TYPE, SUBDIR_NAME2, 0)

+

+        verifyReplyCode("list", 226)

+    }

+

+    void testList_Unix() {

+        ftpServer.systemName = 'UNIX'

+        userAccount.homeDirectory = '/'

+

+        def unixFileSystem = new UnixFakeFileSystem()

+        unixFileSystem.createParentDirectoriesAutomatically = true

+        unixFileSystem.add(new DirectoryEntry('/'))

+        ftpServer.fileSystem = unixFileSystem

+

+        def LAST_MODIFIED = new Date()

+        unixFileSystem.add(new FileEntry(path: p('/', FILENAME1), lastModified: LAST_MODIFIED, contents: ASCII_DATA))

+        unixFileSystem.add(new DirectoryEntry(path: p('/', SUBDIR_NAME2), lastModified: LAST_MODIFIED))

+

+        ftpClientConnectAndLogin()

+

+        FTPFile[] files = ftpClient.listFiles('/')

+        assert files.length == 2

+

+        // Can't be sure of order

+        FTPFile fileEntry = (files[0].getType() == FTPFile.FILE_TYPE) ? files[0] : files[1]

+        FTPFile dirEntry = (files[0].getType() == FTPFile.FILE_TYPE) ? files[1] : files[0]

+

+        verifyFTPFile(dirEntry, FTPFile.DIRECTORY_TYPE, SUBDIR_NAME2, 0)

+        verifyFTPFile(fileEntry, FTPFile.FILE_TYPE, FILENAME1, ASCII_DATA.size())

+        verifyReplyCode("list", 226)

+    }

+

+    void testLogin() {

+        ftpClientConnect()

+        LOG.info("Logging in as $USERNAME/$PASSWORD")

+        assert ftpClient.login(USERNAME, PASSWORD)

+        verifyReplyCode("login with $USERNAME/$PASSWORD", 230)

+

+        assertTrue("isStarted", ftpServer.isStarted());

+        assertFalse("isShutdown", ftpServer.isShutdown());

+    }

+

+    void testLogin_WithAccount() {

+        userAccount.accountRequiredForLogin = true

+        ftpClientConnect()

+        LOG.info("Logging in as $USERNAME/$PASSWORD with $ACCOUNT")

+        assert ftpClient.login(USERNAME, PASSWORD, ACCOUNT)

+        verifyReplyCode("login with $USERNAME/$PASSWORD with $ACCOUNT", 230)

+    }

+

+    void testMkd() {

+        ftpClientConnectAndLogin()

+

+        def DIR = p(HOME_DIR, 'NewDir')

+        assert ftpClient.makeDirectory(DIR)

+        verifyReplyCode("makeDirectory", 257)

+        assert fileSystem.isDirectory(DIR)

+    }

+

+    void testMode() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.setFileTransferMode(FTP.STREAM_TRANSFER_MODE);

+        verifyReplyCode("MODE", 200)

+    }

+

+    void testNlst() {

+        fileSystem.add(new FileEntry(path: p(SUBDIR, FILENAME1)))

+        fileSystem.add(new DirectoryEntry(path: p(SUBDIR, SUBDIR_NAME2)))

+

+        ftpClientConnectAndLogin()

+

+        String[] filenames = ftpClient.listNames(SUBDIR)

+        assert filenames as Set == [FILENAME1, SUBDIR_NAME2] as Set

+        verifyReplyCode("listNames", 226)

+    }

+

+    void testNoop() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.sendNoOp()

+        verifyReplyCode("NOOP", 200)

+    }

+

+    void testPasv_Nlst() {

+        fileSystem.add(new FileEntry(path: p(SUBDIR, FILENAME1)))

+        fileSystem.add(new FileEntry(path: p(SUBDIR, FILENAME2)))

+

+        ftpClientConnectAndLogin()

+        ftpClient.enterLocalPassiveMode();

+

+        String[] filenames = ftpClient.listNames(SUBDIR)

+        assert filenames == [FILENAME1, FILENAME2]

+        verifyReplyCode("listNames", 226)

+    }

+

+    void testPwd() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.printWorkingDirectory() == HOME_DIR

+        verifyReplyCode("printWorkingDirectory", 257)

+    }

+

+    void testQuit() {

+        ftpClientConnect()

+        ftpClient.quit()

+        verifyReplyCode("quit", 221)

+    }

+

+    void testRein() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.rein() == 220

+        assert ftpClient.cdup() == 530      // now logged out

+    }

+

+    void testRest() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.rest("marker") == 350

+    }

+

+    void testRetr() {

+        fileSystem.add(new FileEntry(path: FILE1, contents: ASCII_DATA))

+

+        ftpClientConnectAndLogin()

+

+        LOG.info("Get File for remotePath [$FILE1]")

+        def outputStream = new ByteArrayOutputStream()

+        assert ftpClient.retrieveFile(FILE1, outputStream)

+        LOG.info("File contents=[${outputStream.toString()}]")

+        assert outputStream.toString() == ASCII_DATA

+    }

+

+    void testRmd() {

+        ftpClientConnectAndLogin()

+

+        assert ftpClient.removeDirectory(SUBDIR)

+        verifyReplyCode("removeDirectory", 250)

+        assert !fileSystem.exists(SUBDIR)

+    }

+

+    void testRename() {                 // RNFR and RNTO

+        fileSystem.add(new FileEntry(FILE1))

+

+        ftpClientConnectAndLogin()

+

+        assert ftpClient.rename(FILE1, FILE1 + "NEW")

+        verifyReplyCode("rename", 250)

+        assert !fileSystem.exists(FILE1)

+        assert fileSystem.exists(FILE1 + "NEW")

+    }

+

+    void testSite() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.site("parameters,1,2,3") == 200

+    }

+

+    void testSmnt() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.smnt("dir") == 250

+    }

+

+    void testStat() {

+        ftpClientConnectAndLogin()

+        def status = ftpClient.getStatus()

+        assert status.contains('Connected')

+        verifyReplyCode("stat", 211)

+    }

+

+    void testStor() {

+        ftpClientConnectAndLogin()

+

+        LOG.info("Put File for local path [$FILE1]")

+        def inputStream = new ByteArrayInputStream(ASCII_DATA.getBytes())

+        assert ftpClient.storeFile(FILENAME1, inputStream)      // relative to homeDirectory

+        def contents = fileSystem.getEntry(FILE1).createInputStream().text

+        LOG.info("File contents=[" + contents + "]")

+        assert contents == ASCII_DATA

+    }

+

+    void testStou() {

+        ftpClientConnectAndLogin()

+

+        def inputStream = new ByteArrayInputStream(ASCII_DATA.getBytes())

+        assert ftpClient.storeUniqueFile(FILENAME1, inputStream)

+

+        def names = fileSystem.listNames(HOME_DIR)

+        def filename = names.find {name -> name.startsWith(FILENAME1) }

+        assert filename

+

+        def contents = fileSystem.getEntry(p(HOME_DIR, filename)).createInputStream().text

+        LOG.info("File contents=[" + contents + "]")

+        assert contents == ASCII_DATA

+    }

+

+    void testStru() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.setFileStructure(FTP.FILE_STRUCTURE);

+        verifyReplyCode("STRU", 200)

+    }

+

+    void testSyst() {

+        ftpClientConnectAndLogin()

+

+        def systemName = ftpClient.getSystemName()

+        LOG.info("system name = [$systemName]")

+        assert systemName.contains('"' + SYSTEM_NAME + '"')

+        verifyReplyCode("getSystemName", 215)

+    }

+

+    void testType() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.type(FTP.ASCII_FILE_TYPE)

+        verifyReplyCode("TYPE", 200)

+    }

+

+    void testUnrecognizedCommand() {

+        ftpClientConnectAndLogin()

+        assert ftpClient.sendCommand("XXX") == 502

+        verifyReplyCode("XXX", 502)

+    }

+

+    // -------------------------------------------------------------------------

+    // Test setup and tear-down

+    // -------------------------------------------------------------------------

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    void setUp() {

+        super.setUp()

+

+        for (int i = 0; i < BINARY_DATA.length; i++) {

+            BINARY_DATA[i] = (byte) i

+        }

+

+        ftpServer = new FakeFtpServer()

+        ftpServer.serverControlPort = PortTestUtil.getFtpServerControlPort()

+        ftpServer.systemName = SYSTEM_NAME

+

+        fileSystem = new WindowsFakeFileSystem()

+        fileSystem.createParentDirectoriesAutomatically = true

+        fileSystem.add(new DirectoryEntry(SUBDIR))

+        ftpServer.fileSystem = fileSystem

+

+        userAccount = new UserAccount(USERNAME, PASSWORD, HOME_DIR)

+        ftpServer.addUserAccount(userAccount)

+

+        ftpServer.start()

+        ftpClient = new FTPClient()

+    }

+

+    /**

+     * Perform cleanup after each test

+     * @see org.mockftpserver.test.AbstractTestCase#tearDown()

+     */

+    void tearDown() {

+        super.tearDown()

+        ftpServer.stop()

+    }

+

+    // -------------------------------------------------------------------------

+    // Internal Helper Methods

+    // -------------------------------------------------------------------------

+

+    private ftpClientConnectAndLogin() {

+        ftpClientConnect()

+        assert ftpClient.login(USERNAME, PASSWORD)

+    }

+

+    /**

+     * Connect to the server from the FTPClient

+     */

+    private void ftpClientConnect() {

+        def port = PortTestUtil.getFtpServerControlPort()

+        LOG.info("Conecting to $SERVER on port $port")

+        ftpClient.connect(SERVER, port)

+        verifyReplyCode("connect", 220)

+    }

+

+    /**

+     * Assert that the FtpClient reply code is equal to the expected value

+     *

+     * @param operation - the description of the operation performed used in the error message

+     * @param expectedReplyCode - the expected FtpClient reply code

+     */

+    private void verifyReplyCode(String operation, int expectedReplyCode) {

+        int replyCode = ftpClient.getReplyCode()

+        LOG.info("Reply: operation=\"" + operation + "\" replyCode=" + replyCode)

+        assertEquals("Unexpected replyCode for " + operation, expectedReplyCode, replyCode)

+    }

+

+    private void verifyFTPFile(FTPFile ftpFile, int type, String name, long size) {

+        LOG.info(ftpFile.toString())

+        assertEquals("type: " + ftpFile, type, ftpFile.getType())

+        assertEquals("name: " + ftpFile, name, ftpFile.getName())

+        assertEquals("size: " + ftpFile, size, ftpFile.getSize())

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/FakeFtpServerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/FakeFtpServerTest.groovy
new file mode 100644
index 0000000..e7e2597
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/FakeFtpServerTest.groovy
@@ -0,0 +1,132 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake

+

+import org.mockftpserver.core.command.Command

+import org.mockftpserver.core.command.CommandHandler

+import org.mockftpserver.core.command.ReplyTextBundleAware

+import org.mockftpserver.core.server.AbstractFtpServer

+import org.mockftpserver.core.server.AbstractFtpServerTestCase

+import org.mockftpserver.core.session.Session

+

+/**

+ * Tests for FakeFtpServer.

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class FakeFtpServerTest extends AbstractFtpServerTestCase {

+

+    def commandHandler

+    def commandHandler_NotServerConfigurationAware

+

+    //-------------------------------------------------------------------------

+    // Extra tests  (Standard tests defined in superclass)

+    //-------------------------------------------------------------------------

+

+    void testSetCommandHandler_NotServerConfigurationAware() {

+        ftpServer.setCommandHandler("ZZZ", commandHandler_NotServerConfigurationAware)

+        assert ftpServer.getCommandHandler("ZZZ") == commandHandler_NotServerConfigurationAware

+    }

+

+    void testSetCommandHandler_ServerConfigurationAware() {

+        ftpServer.setCommandHandler("ZZZ", commandHandler)

+        assert ftpServer.getCommandHandler("ZZZ") == commandHandler

+        assert ftpServer == commandHandler.serverConfiguration

+    }

+

+    void testSetCommandHandler_ReplyTextBundleAware() {

+        def cmdHandler = new TestCommandHandlerReplyTextBundleAware()

+        ftpServer.setCommandHandler("ZZZ", cmdHandler)

+        assert ftpServer.getCommandHandler("ZZZ") == cmdHandler

+        assert ftpServer.replyTextBundle == cmdHandler.replyTextBundle

+    }

+

+    void testUserAccounts() {

+        def userAccount = new UserAccount(username: 'abc')

+

+        // addUserAccount()

+        ftpServer.addUserAccount(userAccount)

+        assert ftpServer.getUserAccount("abc") == userAccount

+

+        // setUserAccounts

+        def userAccounts = [userAccount]

+        ftpServer.userAccounts = userAccounts

+        assert ftpServer.getUserAccount("abc") == userAccount

+    }

+

+    void testHelpText() {

+        ftpServer.helpText = [a: 'aaaaa', b: 'bbbbb', '': 'default']

+        assert ftpServer.getHelpText('a') == 'aaaaa'

+        assert ftpServer.getHelpText('b') == 'bbbbb'

+        assert ftpServer.getHelpText('') == 'default'

+        assert ftpServer.getHelpText('unrecognized') == null

+    }

+

+    void testSystemName() {

+        assert ftpServer.systemName == "WINDOWS"

+        ftpServer.systemName = "abc"

+        assert ftpServer.systemName == "abc"

+    }

+

+    void testSystemStatus() {

+        assert ftpServer.systemStatus == "Connected"

+        ftpServer.systemStatus = "abc"

+        assert ftpServer.systemStatus == "abc"

+    }

+

+    void testReplyText() {

+        ftpServer.replyTextBaseName = "SampleReplyText"

+

+        ResourceBundle resourceBundle = ftpServer.replyTextBundle

+        assert resourceBundle.getString("110") == "Testing123"

+    }

+

+    //-------------------------------------------------------------------------

+    // Test set up

+    //-------------------------------------------------------------------------

+

+    void setUp() {

+        super.setUp();

+        commandHandler = new TestCommandHandler()

+        commandHandler_NotServerConfigurationAware = new TestCommandHandlerNotServerConfigurationAware()

+    }

+

+    //-------------------------------------------------------------------------

+    // Abstract method implementations

+    //-------------------------------------------------------------------------

+

+    protected AbstractFtpServer createFtpServer() {

+        return new FakeFtpServer();

+    }

+

+    protected CommandHandler createCommandHandler() {

+        return new TestCommandHandler();

+    }

+

+    protected void verifyCommandHandlerInitialized(CommandHandler commandHandler) {

+        //To change body of implemented methods use File | Settings | File Templates.

+    }

+

+}

+class TestCommandHandlerReplyTextBundleAware implements CommandHandler, ReplyTextBundleAware {

+    ResourceBundle replyTextBundle

+

+    public void handleCommand(Command command, Session session) {

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/FakeFtpServer_AlreadyStartedTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/FakeFtpServer_AlreadyStartedTest.groovy
new file mode 100644
index 0000000..be918c4
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/FakeFtpServer_AlreadyStartedTest.groovy
@@ -0,0 +1,43 @@
+/*

+ * Copyright 2010 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake

+

+import org.mockftpserver.test.AbstractGroovyTestCase

+import org.mockftpserver.test.PortTestUtil

+

+class FakeFtpServer_AlreadyStartedTest extends AbstractGroovyTestCase {

+    private FakeFtpServer ftpServer1 = new FakeFtpServer()

+    private FakeFtpServer ftpServer2 = new FakeFtpServer()

+

+    void testStartServer_WhenAlreadyStarted() {

+        ftpServer1.setServerControlPort(PortTestUtil.getFtpServerControlPort())

+        ftpServer1.start();

+        Thread.sleep(200L);     // give it some time to get started

+        assertEquals("started - after start()", true, ftpServer1.isStarted());

+

+        ftpServer2.setServerControlPort(PortTestUtil.getFtpServerControlPort())

+        ftpServer2.start();

+        log("started ftpServer2")

+        sleep(200L)      // give it a chance to start and terminate

+        assert !ftpServer2.isStarted()

+    }

+

+    void tearDown() {

+        super.tearDown()

+        ftpServer1.stop();

+        ftpServer2.stop();

+    }

+}

diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/FakeFtpServer_StartTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/FakeFtpServer_StartTest.groovy
new file mode 100644
index 0000000..2140ca4
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/FakeFtpServer_StartTest.groovy
@@ -0,0 +1,34 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake

+

+import org.mockftpserver.core.server.AbstractFtpServer

+import org.mockftpserver.core.server.AbstractFtpServer_StartTestCase

+

+/**

+ * Tests for FakeFtpServer that require the server thread to be started.

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class FakeFtpServer_StartTest extends AbstractFtpServer_StartTestCase {

+

+    protected AbstractFtpServer createFtpServer() {

+        return new FakeFtpServer();

+    }

+

+}

diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/RunFakeFtpServer.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/RunFakeFtpServer.groovy
new file mode 100644
index 0000000..49b5b17
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/RunFakeFtpServer.groovy
@@ -0,0 +1,53 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake

+

+import org.mockftpserver.fake.FakeFtpServer

+import org.mockftpserver.fake.UserAccount

+import org.mockftpserver.fake.filesystem.DirectoryEntry

+import org.mockftpserver.fake.filesystem.FileEntry

+import org.mockftpserver.fake.filesystem.UnixFakeFileSystem

+

+/**

+ * Run the FakeFtpServer with a minimal configuration for interactive testing and exploration.

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class RunFakeFtpServer {

+

+    static final ANONYMOUS = 'anonymous'

+    static final HOME_DIR = '/home'

+

+    static main(args) {

+        def fileSystem = new UnixFakeFileSystem()

+        fileSystem.createParentDirectoriesAutomatically = true

+        fileSystem.add(new DirectoryEntry(HOME_DIR))

+        fileSystem.add(new DirectoryEntry("$HOME_DIR/subdir"))

+        fileSystem.add(new DirectoryEntry("$HOME_DIR/subdir2"))

+        fileSystem.add(new FileEntry(path: "$HOME_DIR/abc.txt", contents: '1234567890'))

+        fileSystem.add(new FileEntry(path: "$HOME_DIR/def.txt", contents: '1234567890'))

+        fileSystem.add(new FileEntry(path: "$HOME_DIR/subdir/xyz.txt", contents: '1234567890'))

+

+        def userAccount = new UserAccount(username: ANONYMOUS, passwordRequiredForLogin: false, homeDirectory: HOME_DIR)

+

+        def ftpServer = new FakeFtpServer()

+        ftpServer.fileSystem = fileSystem

+        ftpServer.userAccounts = [userAccount]

+        ftpServer.run()

+    }

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/StubServerConfiguration.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/StubServerConfiguration.groovy
new file mode 100644
index 0000000..09d39e4
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/StubServerConfiguration.groovy
@@ -0,0 +1,46 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake

+

+import org.mockftpserver.fake.ServerConfiguration

+import org.mockftpserver.fake.UserAccount

+import org.mockftpserver.fake.filesystem.FileSystem

+

+/**

+ * Stub implementation of the   {@link org.mockftpserver.fake.ServerConfiguration}   interface for testing

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class StubServerConfiguration implements ServerConfiguration {

+

+    Map userAccounts = [:]

+    Map helpText = [:]

+    FileSystem fileSystem

+    String systemName = "WINDOWS"

+    String systemStatus

+

+    UserAccount getUserAccount(String username) {

+        (UserAccount) userAccounts[username]

+    }

+

+    public String getHelpText(String name) {

+        def key = name == null ? '' : name

+        return helpText[key]

+    }

+

+}

diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/TestCommandHandler.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/TestCommandHandler.groovy
new file mode 100644
index 0000000..af055ef
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/TestCommandHandler.groovy
@@ -0,0 +1,34 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake

+

+import org.mockftpserver.core.command.Command

+import org.mockftpserver.core.session.Session

+import org.mockftpserver.fake.command.AbstractFakeCommandHandler

+

+/**

+ * Test CommandHandler - subclass of AbstractFakeCommandHandler

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class TestCommandHandler extends AbstractFakeCommandHandler {

+

+    protected void handle(Command command, Session session) {

+        // Do nothing

+    }

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/TestCommandHandlerNotServerConfigurationAware.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/TestCommandHandlerNotServerConfigurationAware.groovy
new file mode 100644
index 0000000..8932394
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/TestCommandHandlerNotServerConfigurationAware.groovy
@@ -0,0 +1,34 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake

+

+import org.mockftpserver.core.command.Command

+import org.mockftpserver.core.command.CommandHandler

+import org.mockftpserver.core.session.Session

+

+/**

+ * Test CommandHandler - does not implement ServerConfigurationAware

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class TestCommandHandlerNotServerConfigurationAware implements CommandHandler {

+

+    public void handleCommand(Command command, Session session) {

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/UserAccountTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/UserAccountTest.groovy
new file mode 100644
index 0000000..93683a3
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/UserAccountTest.groovy
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake
+
+import org.mockftpserver.fake.filesystem.FileEntry
+import org.mockftpserver.fake.filesystem.FileSystemEntry
+import org.mockftpserver.fake.filesystem.Permissions
+import org.mockftpserver.test.AbstractGroovyTestCase
+
+/**
+ * Tests for UserAccount
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class UserAccountTest extends AbstractGroovyTestCase {
+
+    private static final USERNAME = "user123"
+    private static final PASSWORD = "password123"
+    private static final HOME_DIR = "/usr/user123"
+    private static final GROUP = 'group'
+
+    private UserAccount userAccount
+
+    void testConstructor() {
+        def acct = new UserAccount(USERNAME, PASSWORD, HOME_DIR)
+        assert acct.username == USERNAME
+        assert acct.password == PASSWORD
+        assert acct.homeDirectory == HOME_DIR
+    }
+
+    void testGetPrimaryGroup() {
+        assert userAccount.primaryGroup == UserAccount.DEFAULT_GROUP
+
+        userAccount.groups = ['abc']
+        assert userAccount.primaryGroup == 'abc'
+
+        userAccount.groups.add('def')
+        assert userAccount.primaryGroup == 'abc'
+
+        userAccount.groups = []
+        assert userAccount.primaryGroup == UserAccount.DEFAULT_GROUP
+    }
+
+    void testIsValidPassword() {
+        userAccount.username = USERNAME
+        userAccount.password = PASSWORD
+        assert userAccount.isValidPassword(PASSWORD)
+
+        assert !userAccount.isValidPassword("")
+        assert !userAccount.isValidPassword("wrong")
+        assert !userAccount.isValidPassword(null)
+    }
+
+    void testIsValidPassword_UsernameNullOrEmpty() {
+        userAccount.password = PASSWORD
+        shouldFailWithMessageContaining('username') { userAccount.isValidPassword(PASSWORD) }
+
+        userAccount.username = ''
+        shouldFailWithMessageContaining('username') { userAccount.isValidPassword(PASSWORD) }
+    }
+
+    void testIsValidPassword_OverrideComparePassword() {
+        def customUserAccount = new CustomUserAccount()
+        customUserAccount.username = USERNAME
+        customUserAccount.password = PASSWORD
+        println customUserAccount
+        assert customUserAccount.isValidPassword(PASSWORD) == false
+        assert customUserAccount.isValidPassword(PASSWORD + "123")
+    }
+
+    void testIsValidPassword_PasswordNotCheckedDuringValidation() {
+        userAccount.username = USERNAME
+        userAccount.password = PASSWORD
+        userAccount.passwordCheckedDuringValidation = false
+        assert userAccount.isValidPassword("wrong")
+    }
+
+    void testIsValid() {
+        assert !userAccount.valid
+        userAccount.homeDirectory = ""
+        assert !userAccount.valid
+        userAccount.homeDirectory = "/abc"
+        assert userAccount.valid
+    }
+
+    void testCanRead() {
+        // No file permissions - readable by all
+        testCanRead(USERNAME, GROUP, null, true)
+
+        // UserAccount has no username or group; use World permissions
+        testCanRead(USERNAME, GROUP, '------r--', true)
+        testCanRead(USERNAME, GROUP, 'rwxrwx-wx', false)
+
+        userAccount.username = USERNAME
+        userAccount.groups = [GROUP]
+
+        testCanRead(USERNAME, GROUP, 'rwxrwxrwx', true)     // ALL
+        testCanRead(USERNAME, GROUP, '---------', false)    // NONE
+
+        testCanRead(USERNAME, null, 'r--------', true)      // User
+        testCanRead(USERNAME, null, '-wxrwxrwx', false)
+
+        testCanRead(null, GROUP, '---r-----', true)         // Group
+        testCanRead(null, GROUP, 'rwx-wxrwx', false)
+
+        testCanRead(null, null, '------r--', true)          // World
+        testCanRead(null, null, 'rwxrwx-wx', false)
+    }
+
+    void testCanWrite() {
+        // No file permissions - writable by all
+        testCanWrite(USERNAME, GROUP, null, true)
+
+        // UserAccount has no username or group; use World permissions
+        testCanWrite(USERNAME, GROUP, '-------w-', true)
+        testCanWrite(USERNAME, GROUP, 'rwxrwxr-x', false)
+
+        userAccount.username = USERNAME
+        userAccount.groups = [GROUP]
+
+        testCanWrite(USERNAME, GROUP, 'rwxrwxrwx', true)     // ALL
+        testCanWrite(USERNAME, GROUP, '---------', false)    // NONE
+
+        testCanWrite(USERNAME, null, '-w-------', true)      // User
+        testCanWrite(USERNAME, null, 'r-xrwxrwx', false)
+
+        testCanWrite(null, GROUP, '----w----', true)         // Group
+        testCanWrite(null, GROUP, 'rwxr-xrwx', false)
+
+        testCanWrite(null, null, '-------w-', true)          // World
+        testCanWrite(null, null, 'rwxrwxr-x', false)
+    }
+
+    void testCanExecute() {
+        // No file permissions - executable by all
+        testCanExecute(USERNAME, GROUP, null, true)
+
+        // UserAccount has no username or group; use World permissions
+        testCanExecute(USERNAME, GROUP, '--------x', true)
+        testCanExecute(USERNAME, GROUP, 'rwxrwxrw-', false)
+
+        userAccount.username = USERNAME
+        userAccount.groups = [GROUP]
+
+        testCanExecute(USERNAME, GROUP, 'rwxrwxrwx', true)     // ALL
+        testCanExecute(USERNAME, GROUP, '---------', false)    // NONE
+
+        testCanExecute(USERNAME, null, '--x------', true)      // User
+        testCanExecute(USERNAME, null, 'rw-rwxrwx', false)
+
+        testCanExecute(null, GROUP, '-----x---', true)         // Group
+        testCanExecute(null, GROUP, 'rwxrw-rwx', false)
+
+        testCanExecute(null, null, '--------x', true)          // World
+        testCanExecute(null, null, 'rwxrwxrw-', false)
+    }
+
+    void testDefaultPermissions() {
+        assert userAccount.defaultPermissionsForNewFile == new Permissions('rw-rw-rw-')
+        assert userAccount.defaultPermissionsForNewDirectory == Permissions.ALL
+    }
+
+    //--------------------------------------------------------------------------
+    // Helper Methods
+    //--------------------------------------------------------------------------
+
+    private void testCanRead(owner, group, permissionsString, expectedResult) {
+        def file = createFileEntry(owner, permissionsString, group)
+        assert userAccount.canRead(file) == expectedResult, file
+    }
+
+    private void testCanWrite(owner, group, permissionsString, expectedResult) {
+        def file = createFileEntry(owner, permissionsString, group)
+        assert userAccount.canWrite(file) == expectedResult, file
+    }
+
+    private void testCanExecute(owner, group, permissionsString, expectedResult) {
+        def file = createFileEntry(owner, permissionsString, group)
+        assert userAccount.canExecute(file) == expectedResult, file
+    }
+
+    private FileSystemEntry createFileEntry(owner, permissionsString, group) {
+        def permissions = permissionsString ? new Permissions(permissionsString) : null
+        return new FileEntry(path: '', owner: owner, group: group, permissions: permissions)
+    }
+
+    void setUp() {
+        super.setUp()
+        userAccount = new UserAccount()
+    }
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AborCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AborCommandHandlerTest.groovy
new file mode 100644
index 0000000..95f0201
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AborCommandHandlerTest.groovy
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for AborCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class AborCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    void testHandleCommand() {
+        handleCommand([])
+        assertSessionReply(ReplyCodes.ABOR_OK, 'abor')
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new AborCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.ABOR, [])
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AbstractFakeCommandHandlerTestCase.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AbstractFakeCommandHandlerTestCase.groovy
new file mode 100644
index 0000000..2423aec
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AbstractFakeCommandHandlerTestCase.groovy
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.core.session.StubSession
+import org.mockftpserver.fake.StubServerConfiguration
+import org.mockftpserver.fake.UserAccount
+import org.mockftpserver.fake.filesystem.DirectoryEntry
+import org.mockftpserver.fake.filesystem.FileEntry
+import org.mockftpserver.fake.filesystem.FileSystemException
+import org.mockftpserver.fake.filesystem.TestUnixFakeFileSystem
+import org.mockftpserver.test.AbstractGroovyTestCase
+import org.mockftpserver.test.StubResourceBundle
+
+/**
+ * Abstract superclass for CommandHandler tests
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+abstract class AbstractFakeCommandHandlerTestCase extends AbstractGroovyTestCase {
+
+    protected static final ERROR_MESSAGE_KEY = 'msgkey'
+
+    protected session
+    protected serverConfiguration
+    protected replyTextBundle
+    protected commandHandler
+    protected fileSystem
+    protected userAccount
+
+    /** Set this to false to skip the test that verifies that the CommandHandler requires a logged in user              */
+    boolean testNotLoggedIn = true
+
+    //-------------------------------------------------------------------------
+    // Tests (common to all subclasses)
+    //-------------------------------------------------------------------------
+
+    void testHandleCommand_ServerConfigurationIsNull() {
+        commandHandler.serverConfiguration = null
+        def command = createValidCommand()
+        shouldFailWithMessageContaining("serverConfiguration") { commandHandler.handleCommand(command, session) }
+    }
+
+    void testHandleCommand_CommandIsNull() {
+        shouldFailWithMessageContaining("command") { commandHandler.handleCommand(null, session) }
+    }
+
+    void testHandleCommand_SessionIsNull() {
+        def command = createValidCommand()
+        shouldFailWithMessageContaining("session") { commandHandler.handleCommand(command, null) }
+    }
+
+    void testHandleCommand_NotLoggedIn() {
+        if (getProperty('testNotLoggedIn')) {
+            def command = createValidCommand()
+            session.removeAttribute(SessionKeys.USER_ACCOUNT)
+            commandHandler.handleCommand(command, session)
+            assertSessionReply(ReplyCodes.NOT_LOGGED_IN)
+        }
+    }
+
+    //-------------------------------------------------------------------------
+    // Abstract Method Declarations (must be implemented by all subclasses)
+    //-------------------------------------------------------------------------
+
+    /**
+     * Create and return a new instance of the CommandHandler class under test. Concrete subclasses must implement.
+     */
+    abstract CommandHandler createCommandHandler()
+
+    /**
+     * Create and return a valid instance of the Command for the CommandHandler class 
+     * under test. Concrete subclasses must implement.
+     */
+    abstract Command createValidCommand()
+
+    //-------------------------------------------------------------------------
+    // Test Setup
+    //-------------------------------------------------------------------------
+
+    void setUp() {
+        super.setUp()
+        session = new StubSession()
+        serverConfiguration = new StubServerConfiguration()
+        replyTextBundle = new StubResourceBundle()
+        fileSystem = new TestUnixFakeFileSystem()
+        fileSystem.createParentDirectoriesAutomatically = true
+        serverConfiguration.setFileSystem(fileSystem)
+
+        userAccount = new UserAccount()
+        session.setAttribute(SessionKeys.USER_ACCOUNT, userAccount)
+
+        commandHandler = createCommandHandler()
+        commandHandler.serverConfiguration = serverConfiguration
+        commandHandler.replyTextBundle = replyTextBundle
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    /**
+     * Perform a test of the handleCommand() method on the specified command
+     * parameters, which are missing a required parameter for this CommandHandler.
+     */
+    protected void testHandleCommand_MissingRequiredParameter(List commandParameters) {
+        commandHandler.handleCommand(createCommand(commandParameters), session)
+        assertSessionReply(ReplyCodes.COMMAND_SYNTAX_ERROR)
+    }
+
+    /**
+     * Perform a test of the handleCommand() method on the specified command
+     * parameters, which are missing a required parameter for this CommandHandler.
+     */
+    protected testHandleCommand_MissingRequiredSessionAttribute() {
+        def command = createValidCommand()
+        commandHandler.handleCommand(command, session)
+        assertSessionReply(ReplyCodes.ILLEGAL_STATE)
+    }
+
+    /**
+     * @return a new Command with the specified parameters for this CommandHandler
+     */
+    protected Command createCommand(List commandParameters) {
+        new Command(createValidCommand().name, commandParameters)
+    }
+
+    /**
+     * Invoke the handleCommand() method for the current CommandHandler, passing in
+     * the specified parameters
+     * @param parameters - the List of command parameters; may be empty, but not null
+     */
+    protected void handleCommand(List parameters) {
+        commandHandler.handleCommand(createCommand(parameters), session)
+    }
+
+    /**
+     * Assert that the specified reply code and message containing text was sent through the session.
+     * @param expectedReplyCode - the expected reply code
+     * @param text - the text expected within the reply message; defaults to the reply code as a String
+     */
+    protected assertSessionReply(int expectedReplyCode, text = expectedReplyCode as String) {
+        assertSessionReply(0, expectedReplyCode, text)
+    }
+
+    /**
+     * Assert that the specified reply code and message containing text was sent through the session.
+     * @param replyIndex - the index of the reply to compare
+     * @param expectedReplyCode - the expected reply code
+     * @param text - the text expected within the reply message; defaults to the reply code as a String
+     */
+    protected assertSessionReply(int replyIndex, int expectedReplyCode, text = expectedReplyCode as String) {
+        LOG.info(session.toString())
+        String actualMessage = session.getReplyMessage(replyIndex)
+        def actualReplyCode = session.getReplyCode(replyIndex)
+        assert actualReplyCode == expectedReplyCode
+        if (text instanceof List) {
+            text.each { assert actualMessage.contains(it), "[$actualMessage] does not contain [$it]" }
+        }
+        else {
+            assert actualMessage.contains(text), "[$actualMessage] does not contain [$text]"
+        }
+    }
+
+    /**
+     * Assert that the specified reply codes were sent through the session.
+     * @param replyCodes - the List of expected sent reply codes
+     */
+    protected assertSessionReplies(List replyCodes) {
+        LOG.info(session.toString())
+        replyCodes.eachWithIndex {replyCode, replyIndex ->
+            assertSessionReply(replyIndex, replyCode)
+        }
+    }
+
+    /**
+     * Assert that the specified data was sent through the session.
+     * @param expectedData - the expected data
+     */
+    protected assertSessionData(String expectedData) {
+        def actual = session.sentData[0]
+        assert actual != null, "No data for index [0] sent for $session"
+        assert actual == expectedData
+    }
+
+    /**
+     * Assert that the specified data was sent through the session, terminated by an end-of-line.
+     * @param expectedData - the expected data
+     */
+    protected assertSessionDataWithEndOfLine(String expectedData) {
+        assertSessionData(expectedData + endOfLine())
+    }
+
+    /**
+     * Assert that the data sent through the session terminated with an end-of-line.
+     */
+    protected assertSessionDataEndsWithEndOfLine() {
+        assert session.sentData[0].endsWith(endOfLine())
+    }
+
+    /**
+     * Execute the handleCommand() method with the specified parameters and 
+     * assert that the standard SEND DATA replies were sent through the session.
+     * @param parameters - the command parameters to use; defaults to []
+     * @param finalReplyCode - the expected final reply code; defaults to ReplyCodes.TRANSFER_DATA_FINAL_OK
+     */
+    protected handleCommandAndVerifySendDataReplies(parameters = [], int finalReplyCode = ReplyCodes.TRANSFER_DATA_FINAL_OK) {
+        handleCommand(parameters)
+        assertSessionReplies([ReplyCodes.TRANSFER_DATA_INITIAL_OK, finalReplyCode])
+    }
+
+    /**
+     * Execute the handleCommand() method with the specified parameters and
+     * assert that the standard SEND DATA replies were sent through the session.
+     * @param parameters - the command parameters to use
+     * @param initialReplyMessageKey - the expected reply message key for the initial reply
+     * @param finalReplyMessageKey -  the expected reply message key for the final reply
+     * @param finalReplyCode - the expected final reply code; defaults to ReplyCodes.TRANSFER_DATA_FINAL_OK
+     */
+    protected handleCommandAndVerifySendDataReplies(parameters, String initialReplyMessageKey, String finalReplyMessageKey, int finalReplyCode = ReplyCodes.TRANSFER_DATA_FINAL_OK) {
+        handleCommand(parameters)
+        assertSessionReply(0, ReplyCodes.TRANSFER_DATA_INITIAL_OK, initialReplyMessageKey)
+        assertSessionReply(1, finalReplyCode, finalReplyMessageKey)
+    }
+
+    /**
+     * Override the named method for the specified object instance
+     * @param object - the object instance
+     * @param methodName - the name of the method to override
+     * @param newMethod - the Closure representing the new method for this single instance
+     */
+    protected void overrideMethod(object, String methodName, Closure newMethod) {
+        LOG.info("Overriding method [$methodName] for class [${object.class}]")
+        def emc = new ExpandoMetaClass(object.class, false)
+        emc."$methodName" = newMethod
+        emc.initialize()
+        object.metaClass = emc
+    }
+
+    /**
+     * Override the named method (that takes a single String arg) of the fileSystem object to throw a (generic) FileSystemException
+     * @param methodName - the name of the fileSystem method to override
+     */
+    protected void overrideMethodToThrowFileSystemException(String methodName) {
+        def newMethod = {String path -> throw new FileSystemException("Error thrown by method [$methodName]", ERROR_MESSAGE_KEY) }
+        overrideMethod(fileSystem, methodName, newMethod)
+    }
+
+    /**
+     * Set the current directory within the session
+     * @param path - the new path value for the current directory
+     */
+    protected void setCurrentDirectory(String path) {
+        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, path)
+    }
+
+    /**
+     * Convenience method to return the end-of-line character(s) for the current CommandHandler.
+     */
+    protected endOfLine() {
+        commandHandler.endOfLine()
+    }
+
+    /**
+     * Create a new directory entry with the specified path in the file system
+     * @param path - the path of the new directory entry
+     * @return the newly created DirectoryEntry
+     */
+    protected DirectoryEntry createDirectory(String path) {
+        DirectoryEntry entry = new DirectoryEntry(path)
+        fileSystem.add(entry)
+        return entry
+    }
+
+    /**
+     * Create a new file entry with the specified path in the file system
+     * @param path - the path of the new file entry
+     * @param contents - the contents for the file; defaults to null
+     * @return the newly created FileEntry
+     */
+    protected FileEntry createFile(String path, contents = null) {
+        FileEntry entry = new FileEntry(path: path, contents: contents)
+        fileSystem.add(entry)
+        return entry
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AbstractStoreFileCommandHandlerTestCase.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AbstractStoreFileCommandHandlerTestCase.groovy
new file mode 100644
index 0000000..62fe519
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AbstractStoreFileCommandHandlerTestCase.groovy
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.fake.filesystem.FileEntry
+import org.mockftpserver.fake.filesystem.FileSystemEntry
+import org.mockftpserver.fake.filesystem.FileSystemException
+import org.mockftpserver.fake.filesystem.Permissions
+
+/**
+ * Abstract superclass for tests of Fake CommandHandlers that store a file (STOR, STOU, APPE)
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+abstract class AbstractStoreFileCommandHandlerTestCase extends AbstractFakeCommandHandlerTestCase {
+
+    protected static final DIR = "/"
+    protected static final FILENAME = "file.txt"
+    protected static final FILE = p(DIR, FILENAME)
+    protected static final CONTENTS = "abc"
+
+    //-------------------------------------------------------------------------
+    // Tests Common to All Subclasses
+    //-------------------------------------------------------------------------
+
+    void testHandleCommand_NoWriteAccessToExistingFile() {
+        fileSystem.add(new FileEntry(path: FILE))
+        fileSystem.getEntry(FILE).permissions = Permissions.NONE
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.WRITE_FILE_ERROR, ['filesystem.cannotWrite', FILE])
+    }
+
+    void testHandleCommand_NoWriteAccessToDirectoryForNewFile() {
+        fileSystem.getEntry(DIR).permissions = new Permissions('r-xr-xr-x')
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.WRITE_FILE_ERROR, ['filesystem.cannotWrite', DIR])
+    }
+
+    void testHandleCommand_NoExecuteAccessToDirectory() {
+        fileSystem.add(new FileEntry(path: FILE))
+        fileSystem.getEntry(DIR).permissions = new Permissions('rw-rw-rw-')
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.WRITE_FILE_ERROR, ['filesystem.cannotExecute', DIR])
+    }
+
+    void testHandleCommand_ThrowsFileSystemException() {
+        fileSystem.addMethodException = new FileSystemException("bad", ERROR_MESSAGE_KEY)
+
+        handleCommand([FILE])
+        assertSessionReply(0, ReplyCodes.TRANSFER_DATA_INITIAL_OK)
+        assertSessionReply(1, ReplyCodes.WRITE_FILE_ERROR, ERROR_MESSAGE_KEY)
+    }
+
+    //-------------------------------------------------------------------------
+    // Abstract Method Declarations
+    //-------------------------------------------------------------------------
+
+    /**
+     * Verify the created output file and return its full path
+     * @return the full path to the created output file; the path may be absolute or relative
+     */
+    protected abstract String verifyOutputFile()
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    protected void testHandleCommand(List parameters, String messageKey, String contents) {
+        session.dataToRead = CONTENTS.bytes
+        handleCommand(parameters)
+        assertSessionReply(0, ReplyCodes.TRANSFER_DATA_INITIAL_OK)
+        assertSessionReply(1, ReplyCodes.TRANSFER_DATA_FINAL_OK, messageKey)
+
+        def outputFile = verifyOutputFile()
+
+        FileSystemEntry fileEntry = fileSystem.getEntry(outputFile)
+        def actualContents = fileEntry.createInputStream().text
+        assert actualContents == contents
+        assert fileEntry.permissions == userAccount.defaultPermissionsForNewFile
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.APPE, [FILE])
+    }
+
+    void setUp() {
+        super.setUp()
+        createDirectory(DIR)
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AcctCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AcctCommandHandlerTest.groovy
new file mode 100644
index 0000000..1f92b76
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AcctCommandHandlerTest.groovy
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+
+/**
+ * Tests for AcctCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class AcctCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    def USERNAME = "user123"
+    def ACCOUNT_NAME = "account123"
+
+    boolean testNotLoggedIn = false
+
+    void testHandleCommand() {
+        handleCommand([ACCOUNT_NAME])
+        assertSessionReply(ReplyCodes.ACCT_OK, ['acct', USERNAME])
+        assertAccountNameInSession(true)
+    }
+
+    void testHandleCommand_UsernameNotSetInSession() {
+        session.removeAttribute(SessionKeys.USERNAME)
+        testHandleCommand_MissingRequiredSessionAttribute()
+        assertAccountNameInSession(false)
+    }
+
+    void testHandleCommand_MissingAccountNameParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+        assertAccountNameInSession(false)
+    }
+
+    //-------------------------------------------------------------------------
+    // Abstract and Overridden Methods
+    //-------------------------------------------------------------------------
+
+    void setUp() {
+        super.setUp()
+        session.setAttribute(SessionKeys.USERNAME, USERNAME)
+    }
+
+    CommandHandler createCommandHandler() {
+        new AcctCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.ACCT, [ACCOUNT_NAME])
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    /**
+     * Assert that the account name is stored in the session, depending on the value of isAccountNameInSession.
+     * @param isAccountNameInSession - true if the account name is expected in the session; false if it is not expected
+     */
+    private void assertAccountNameInSession(boolean isAccountNameInSession) {
+        def expectedValue = isAccountNameInSession ? ACCOUNT_NAME : null
+        assert session.getAttribute(SessionKeys.ACCOUNT_NAME) == expectedValue
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AlloCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AlloCommandHandlerTest.groovy
new file mode 100644
index 0000000..5016cc5
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AlloCommandHandlerTest.groovy
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for AlloCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class AlloCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    void testHandleCommand() {
+        handleCommand([])
+        assertSessionReply(ReplyCodes.ALLO_OK, 'allo')
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new AlloCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.ALLO, [])
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AppeCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AppeCommandHandlerTest.groovy
new file mode 100644
index 0000000..6fc4434
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/AppeCommandHandlerTest.groovy
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.fake.filesystem.FileEntry
+import org.mockftpserver.fake.filesystem.Permissions
+
+/**
+ * Tests for AppeCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class AppeCommandHandlerTest extends AbstractStoreFileCommandHandlerTestCase {
+
+    void testHandleCommand_MissingPathParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+    }
+
+    void testHandleCommand_AbsolutePath() {
+        userAccount.defaultPermissionsForNewFile = Permissions.NONE
+        testHandleCommand([FILE], 'appe', CONTENTS)
+    }
+
+    void testHandleCommand_AbsolutePath_FileAlreadyExists() {
+        def ORIGINAL_CONTENTS = '123 456 789'
+        fileSystem.add(new FileEntry(path: FILE, contents: ORIGINAL_CONTENTS))
+        testHandleCommand([FILE], 'appe', ORIGINAL_CONTENTS + CONTENTS)
+    }
+
+    void testHandleCommand_RelativePath() {
+        setCurrentDirectory(DIR)
+        testHandleCommand([FILENAME], 'appe', CONTENTS)
+    }
+
+    void testHandleCommand_PathSpecifiesAnExistingDirectory() {
+        createDirectory(FILE)
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.FILENAME_NOT_VALID, FILE)
+    }
+
+    void testHandleCommand_ParentDirectoryDoesNotExist() {
+        def NO_SUCH_DIR = "/path/DoesNotExist"
+        handleCommand([p(NO_SUCH_DIR, FILENAME)])
+        assertSessionReply(ReplyCodes.FILENAME_NOT_VALID, NO_SUCH_DIR)
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new AppeCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.APPE, [FILE])
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+    protected String verifyOutputFile() {
+        assert fileSystem.isFile(FILE)
+        assert session.getReplyMessage(1).contains(FILENAME)
+        return FILE
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/CdupCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/CdupCommandHandlerTest.groovy
new file mode 100644
index 0000000..763cc00
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/CdupCommandHandlerTest.groovy
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.fake.filesystem.Permissions
+
+/**
+ * Tests for CdupCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class CdupCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    def DIR = "/usr"
+    def SUBDIR = "${DIR}/sub"
+
+    void testHandleCommand() {
+        setCurrentDirectory(SUBDIR)
+        handleCommand([])
+        assertSessionReply(ReplyCodes.CDUP_OK, ['cdup', DIR])
+        assert session.getAttribute(SessionKeys.CURRENT_DIRECTORY) == DIR
+    }
+
+    void testHandleCommand_NoParentDirectory() {
+        setCurrentDirectory('/')
+        handleCommand([])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.parentDirectoryDoesNotExist', '/'])
+        assert session.getAttribute(SessionKeys.CURRENT_DIRECTORY) == '/'
+    }
+
+    void testHandleCommand_NoExecuteAccessToDirectory() {
+        setCurrentDirectory(SUBDIR)
+        def dir = fileSystem.getEntry(DIR)
+        dir.permissions = new Permissions('rw-rw-rw-')
+        handleCommand([])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.cannotExecute', DIR])
+        assert session.getAttribute(SessionKeys.CURRENT_DIRECTORY) == SUBDIR
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new CdupCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.CDUP, [])
+    }
+
+    void setUp() {
+        super.setUp()
+        createDirectory(SUBDIR)
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/CwdCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/CwdCommandHandlerTest.groovy
new file mode 100644
index 0000000..d63210b
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/CwdCommandHandlerTest.groovy
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.fake.filesystem.Permissions
+
+/**
+ * Tests for CwdCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class CwdCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    def DIR = "/usr"
+
+    void testHandleCommand() {
+        createDirectory(DIR)
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.CWD_OK, ['cwd', DIR])
+        assert session.getAttribute(SessionKeys.CURRENT_DIRECTORY) == DIR
+    }
+
+    void testHandleCommand_PathIsRelative() {
+        def SUB = "sub"
+        createDirectory(p(DIR, SUB))
+        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, DIR)
+        handleCommand([SUB])
+        assertSessionReply(ReplyCodes.CWD_OK, ['cwd', SUB])
+        assert session.getAttribute(SessionKeys.CURRENT_DIRECTORY) == p(DIR, SUB)
+    }
+
+    void testHandleCommand_PathDoesNotExistInFileSystem() {
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.doesNotExist', DIR])
+        assert session.getAttribute(SessionKeys.CURRENT_DIRECTORY) == null
+    }
+
+    void testHandleCommand_PathSpecifiesAFile() {
+        createFile(DIR)
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.isNotADirectory', DIR])
+        assert session.getAttribute(SessionKeys.CURRENT_DIRECTORY) == null
+    }
+
+    void testHandleCommand_NoExecuteAccessToParentDirectory() {
+        def dir = createDirectory(DIR)
+        dir.permissions = new Permissions('rw-rw-rw-')
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.cannotExecute', DIR])
+        assert session.getAttribute(SessionKeys.CURRENT_DIRECTORY) == null
+    }
+
+    void testHandleCommand_MissingPathParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new CwdCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.CWD, [DIR])
+    }
+
+    void setUp() {
+        super.setUp()
+        userAccount.username = 'user'
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/DeleCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/DeleCommandHandlerTest.groovy
new file mode 100644
index 0000000..8becd17
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/DeleCommandHandlerTest.groovy
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.fake.filesystem.FileSystemException
+import org.mockftpserver.fake.filesystem.Permissions
+
+/**
+ * Tests for DeleCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class DeleCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    static final DIR = '/'
+    static final FILENAME = "f.txt"
+    static final FILE = p(DIR, FILENAME)
+
+    void testHandleCommand() {
+        createFile(FILE)
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.DELE_OK, ['dele', FILE])
+        assert fileSystem.exists(FILE) == false
+    }
+
+    void testHandleCommand_PathIsRelative() {
+        createFile(FILE)
+        setCurrentDirectory("/")
+        handleCommand([FILENAME])
+        assertSessionReply(ReplyCodes.DELE_OK, ['dele', FILENAME])
+        assert fileSystem.exists(FILE) == false
+    }
+
+    void testHandleCommand_PathDoesNotExistInFileSystem() {
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.isNotAFile', FILE])
+    }
+
+    void testHandleCommand_PathSpecifiesADirectory() {
+        createDirectory(FILE)
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.isNotAFile', FILE])
+        assert fileSystem.exists(FILE)
+    }
+
+    void testHandleCommand_MissingPathParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+    }
+
+    void testHandleCommand_DeleteThrowsException() {
+        createFile(FILE)
+//        overrideMethodToThrowFileSystemException("delete")
+        fileSystem.deleteMethodException = new FileSystemException("bad", ERROR_MESSAGE_KEY)
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ERROR_MESSAGE_KEY)
+    }
+
+    void testHandleCommand_NoWriteAccessToParentDirectory() {
+        createFile(FILE)
+        fileSystem.getEntry(DIR).permissions = new Permissions('r-xr-xr-x')
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.cannotWrite', DIR])
+        assert fileSystem.exists(FILE)
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new DeleCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.DELE, [FILE])
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/EprtCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/EprtCommandHandlerTest.groovy
new file mode 100644
index 0000000..7ed28e5
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/EprtCommandHandlerTest.groovy
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2009 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for PortCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class EprtCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    static final PARAMETERS_IPV4 = ["|1|132.235.1.2|6275|"]
+    static final HOST_IPV4 = InetAddress.getByName("132.235.1.2")
+    static final PARAMETERS_IPV6 = ["|2|1080::8:800:200C:417A|6275|"]
+    static final HOST_IPV6 = InetAddress.getByName("1080::8:800:200C:417A")
+    static final PORT = 6275
+
+    boolean testNotLoggedIn = false
+
+    void testHandleCommand_IPv4() {
+        handleCommand(PARAMETERS_IPV4)
+        assertSessionReply(ReplyCodes.EPRT_OK, 'eprt')
+        assert session.clientDataHost == HOST_IPV4
+        assert session.clientDataPort == PORT
+    }
+
+    void testHandleCommand_IPv6() {
+        handleCommand(PARAMETERS_IPV6)
+        assertSessionReply(ReplyCodes.EPRT_OK, 'eprt')
+        assert session.clientDataHost == HOST_IPV6
+        assert session.clientDataPort == PORT
+    }
+
+    void testHandleCommand_IPv6_CustomDelimiter() {
+        handleCommand(["@2@1080::8:800:200C:417A@6275@"])
+        assertSessionReply(ReplyCodes.EPRT_OK, 'eprt')
+        assert session.clientDataHost == HOST_IPV6
+        assert session.clientDataPort == PORT
+    }
+
+    void testHandleCommand_IllegalParameterFormat() {
+        handleCommand(['abcdef'])
+        assertSessionReply(ReplyCodes.COMMAND_SYNTAX_ERROR)
+    }
+
+    void testHandleCommand_PortMissing() {
+        handleCommand(['|1|132.235.1.2|'])
+        assertSessionReply(ReplyCodes.COMMAND_SYNTAX_ERROR)
+    }
+
+    void testHandleCommand_IllegalHostName() {
+        handleCommand(['|1|132.@|6275|'])
+        assertSessionReply(ReplyCodes.COMMAND_SYNTAX_ERROR)
+    }
+
+    void testHandleCommand_MissingRequiredParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    void setUp() {
+        super.setUp()
+    }
+
+    CommandHandler createCommandHandler() {
+        new EprtCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.EPRT, PARAMETERS_IPV4)
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/EpsvCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/EpsvCommandHandlerTest.groovy
new file mode 100644
index 0000000..e43eb30
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/EpsvCommandHandlerTest.groovy
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2009 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for EpsvCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class EpsvCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    static final SERVER = InetAddress.getByName("1080::8:800:200C:417A")
+    static final PORT = 6275
+
+    void testHandleCommand() {
+        session.switchToPassiveModeReturnValue = PORT
+        session.serverHost = SERVER
+        handleCommand([])
+
+        assertSessionReply(ReplyCodes.EPSV_OK, PORT as String)
+        assert session.switchedToPassiveMode
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new EpsvCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.EPSV, [])
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/HelpCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/HelpCommandHandlerTest.groovy
new file mode 100644
index 0000000..8b84667
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/HelpCommandHandlerTest.groovy
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for HelpCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class HelpCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    boolean testNotLoggedIn = false
+
+    void testHandleCommand_Arg() {
+        serverConfiguration.helpText = [abc: '_abc']
+        handleCommand(['abc'])
+        assertSessionReply(ReplyCodes.HELP_OK, ['help', '_abc'])
+    }
+
+    void testHandleCommand_MultiWordArg() {
+        serverConfiguration.helpText = ["abc def": 'abcdef']
+        handleCommand(['abc', 'def'])
+        assertSessionReply(ReplyCodes.HELP_OK, ['help', 'abcdef'])
+    }
+
+    void testHandleCommand_NoArg_UseDefault() {
+        serverConfiguration.helpText = ['': 'default']
+        handleCommand([])
+        assertSessionReply(ReplyCodes.HELP_OK, ['help', 'default'])
+    }
+
+    void testHandleCommand_Unrecognized() {
+        serverConfiguration.helpText = ['': 'default']
+        handleCommand(['unrecognized'])
+
+        // Reply text includes the message text and the passed-in command as a message parameter 
+        assertSessionReply(ReplyCodes.HELP_OK, ['help.noHelpTextDefined', 'unrecognized'])
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new HelpCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.HELP, [])
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/ListCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/ListCommandHandlerTest.groovy
new file mode 100644
index 0000000..5596977
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/ListCommandHandlerTest.groovy
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.fake.filesystem.DirectoryEntry
+import org.mockftpserver.fake.filesystem.DirectoryListingFormatter
+import org.mockftpserver.fake.filesystem.FileEntry
+import org.mockftpserver.fake.filesystem.FileSystemEntry
+import org.mockftpserver.fake.filesystem.FileSystemException
+import org.mockftpserver.fake.filesystem.Permissions
+
+/**
+ * Tests for ListCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class ListCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    private static final DIR = "/usr"
+    private static final NAME = "abc.txt"
+    private static final LAST_MODIFIED = new Date()
+
+    void testHandleCommand_SingleFile() {
+        final entry = new FileEntry(path: p(DIR, NAME), lastModified: LAST_MODIFIED, contents: "abc")
+        fileSystem.add(entry)
+        handleCommandAndVerifySendDataReplies([DIR])
+        assertSessionDataWithEndOfLine(listingFor(entry))
+    }
+
+    void testHandleCommand_FilesAndDirectories() {
+        def DATA3 = "".padRight(1000, 'x')
+        final entry1 = new FileEntry(path: p(DIR, "abc.txt"), lastModified: LAST_MODIFIED, contents: "abc")
+        final entry2 = new DirectoryEntry(path: p(DIR, "OtherFiles"), lastModified: LAST_MODIFIED)
+        final entry3 = new FileEntry(path: p(DIR, "another_file.doc"), lastModified: LAST_MODIFIED, contents: DATA3)
+        fileSystem.add(entry1)
+        fileSystem.add(entry2)
+        fileSystem.add(entry3)
+
+        handleCommandAndVerifySendDataReplies([DIR])
+
+        def actualLines = session.sentData[0].tokenize(endOfLine()) as Set
+        LOG.info("actualLines=$actualLines")
+        def EXPECTED = [
+                listingFor(entry1),
+                listingFor(entry2),
+                listingFor(entry3)] as Set
+        assert actualLines == EXPECTED
+        assertSessionDataEndsWithEndOfLine()
+    }
+
+    void testHandleCommand_NoPath_UseCurrentDirectory() {
+        final entry = new FileEntry(path: p(DIR, NAME), lastModified: LAST_MODIFIED, contents: "abc")
+        fileSystem.add(entry)
+        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, DIR)
+        handleCommandAndVerifySendDataReplies([])
+        assertSessionDataWithEndOfLine(listingFor(entry))
+    }
+
+    void testHandleCommand_EmptyDirectory() {
+        handleCommandAndVerifySendDataReplies([DIR])
+        assertSessionData("")
+    }
+
+    void testHandleCommand_PathSpecifiesAFile() {
+        final entry = new FileEntry(path: p(DIR, NAME), lastModified: LAST_MODIFIED, contents: "abc")
+        fileSystem.add(entry)
+        handleCommandAndVerifySendDataReplies([p(DIR, NAME)])
+        assertSessionDataWithEndOfLine(listingFor(entry))
+    }
+
+    void testHandleCommand_PathDoesNotExist() {
+        handleCommandAndVerifySendDataReplies(["/DoesNotExist"])
+        assertSessionData("")
+    }
+
+    void testHandleCommand_NoReadAccessToDirectory() {
+        fileSystem.getEntry(DIR).permissions = new Permissions('-wx-wx-wx')
+        handleCommand([DIR])
+        assertSessionReply(0, ReplyCodes.TRANSFER_DATA_INITIAL_OK)
+        assertSessionReply(1, ReplyCodes.READ_FILE_ERROR, ['filesystem.cannotRead', DIR])
+    }
+
+    void testHandleCommand_ListFilesThrowsException() {
+        fileSystem.listFilesMethodException = new FileSystemException("bad", ERROR_MESSAGE_KEY)
+        handleCommand([DIR])
+        assertSessionReply(0, ReplyCodes.TRANSFER_DATA_INITIAL_OK)
+        assertSessionReply(1, ReplyCodes.SYSTEM_ERROR, ERROR_MESSAGE_KEY)
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new ListCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.LIST, [DIR])
+    }
+
+    void setUp() {
+        super.setUp()
+        createDirectory(DIR)
+        fileSystem.directoryListingFormatter = [format: {entry -> entry.toString()}] as DirectoryListingFormatter
+    }
+
+    private listingFor(FileSystemEntry fileSystemEntry) {
+        fileSystemEntry.toString()
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/MkdCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/MkdCommandHandlerTest.groovy
new file mode 100644
index 0000000..81dac95
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/MkdCommandHandlerTest.groovy
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.fake.UserAccount
+import org.mockftpserver.fake.filesystem.FileSystemException
+import org.mockftpserver.fake.filesystem.Permissions
+
+/**
+ * Tests for MkdCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class MkdCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    static final PARENT = '/'
+    static final DIRNAME = "usr"
+    static final DIR = p(PARENT, DIRNAME)
+    static final PERMISSIONS = new Permissions('rwx------')
+
+    void testHandleCommand() {
+        userAccount.defaultPermissionsForNewDirectory = PERMISSIONS
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.MKD_OK, ['mkd', DIR])
+        assert fileSystem.exists(DIR)
+        def dirEntry = fileSystem.getEntry(DIR)
+        assert dirEntry.permissions == PERMISSIONS
+    }
+
+    void testHandleCommand_PathIsRelative() {
+        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, '/')
+        handleCommand([DIRNAME])
+        assertSessionReply(ReplyCodes.MKD_OK, ['mkd', DIRNAME])
+        assert fileSystem.exists(DIR)
+        def dirEntry = fileSystem.getEntry(DIR)
+        assert dirEntry.permissions == UserAccount.DEFAULT_PERMISSIONS_FOR_NEW_DIRECTORY
+    }
+
+    void testHandleCommand_ParentDirectoryDoesNotExist() {
+        handleCommand(['/abc/def'])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.doesNotExist', '/abc'])
+    }
+
+    void testHandleCommand_PathSpecifiesAFile() {
+        createFile(DIR)
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.alreadyExists', DIR])
+        assert fileSystem.exists(DIR)
+    }
+
+    void testHandleCommand_MissingPathParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+    }
+
+    void testHandleCommand_NoWriteAccessToParentDirectory() {
+        fileSystem.getEntry(PARENT).permissions = new Permissions('r-xr-xr-x')
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.cannotWrite', PARENT])
+    }
+
+    void testHandleCommand_NoExecuteAccessToParentDirectory() {
+        fileSystem.getEntry(PARENT).permissions = new Permissions('rw-rw-rw-')
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.cannotExecute', PARENT])
+    }
+
+    void testHandleCommand_CreateDirectoryThrowsException() {
+        fileSystem.addMethodException = new FileSystemException("bad", ERROR_MESSAGE_KEY)
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ERROR_MESSAGE_KEY)
+    }
+
+    void setUp() {
+        super.setUp()
+        createDirectory(PARENT)
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new MkdCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.MKD, [DIR])
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/ModeCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/ModeCommandHandlerTest.groovy
new file mode 100644
index 0000000..a4dec34
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/ModeCommandHandlerTest.groovy
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for ModeCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class ModeCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    void testHandleCommand() {
+        handleCommand([])
+        assertSessionReply(ReplyCodes.MODE_OK, 'mode')
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new ModeCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.MODE, [])
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/NlstCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/NlstCommandHandlerTest.groovy
new file mode 100644
index 0000000..5444394
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/NlstCommandHandlerTest.groovy
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.fake.filesystem.FileSystemException
+import org.mockftpserver.fake.filesystem.Permissions
+
+/**
+ * Tests for NlstCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class NlstCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    def DIR = "/usr"
+
+    void testHandleCommand_SingleFile() {
+        createFile("/usr/f1.txt")
+        handleCommandAndVerifySendDataReplies([DIR])
+        assertSessionDataWithEndOfLine("f1.txt")
+    }
+
+    void testHandleCommand_FilesAndDirectories() {
+        createFile("/usr/f1.txt")
+        createDirectory("/usr/OtherFiles")
+        createFile("/usr/f2.txt")
+        createDirectory("/usr/Archive")
+        handleCommandAndVerifySendDataReplies([DIR])
+
+        def EXPECTED = ["f1.txt", "OtherFiles", "f2.txt", "Archive"] as Set
+        def actualLines = session.sentData[0].tokenize(endOfLine()) as Set
+        LOG.info("actualLines=$actualLines")
+        assert actualLines == EXPECTED
+        assertSessionDataEndsWithEndOfLine()
+    }
+
+    void testHandleCommand_NoPath_UseCurrentDirectory() {
+        createFile("/usr/f1.txt")
+        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, DIR)
+        handleCommandAndVerifySendDataReplies([])
+        assertSessionDataWithEndOfLine("f1.txt")
+    }
+
+    void testHandleCommand_EmptyDirectory() {
+        handleCommandAndVerifySendDataReplies([DIR])
+        assertSessionData("")
+    }
+
+    void testHandleCommand_PathSpecifiesAFile() {
+        createFile("/usr/f1.txt")
+        handleCommandAndVerifySendDataReplies(["/usr/f1.txt"])
+        assertSessionDataWithEndOfLine("f1.txt")
+    }
+
+    void testHandleCommand_PathDoesNotExist() {
+        handleCommandAndVerifySendDataReplies(["/DoesNotExist"])
+        assertSessionData("")
+    }
+
+    void testHandleCommand_NoReadAccessToDirectory() {
+        fileSystem.getEntry(DIR).permissions = new Permissions('-wx-wx-wx')
+        handleCommand([DIR])
+        assertSessionReply(0, ReplyCodes.TRANSFER_DATA_INITIAL_OK)
+        assertSessionReply(1, ReplyCodes.READ_FILE_ERROR, ['filesystem.cannotRead', DIR])
+    }
+
+    void testHandleCommand_ListNamesThrowsException() {
+        fileSystem.listNamesMethodException = new FileSystemException("bad", ERROR_MESSAGE_KEY)
+        handleCommand([DIR])
+        assertSessionReply(0, ReplyCodes.TRANSFER_DATA_INITIAL_OK)
+        assertSessionReply(1, ReplyCodes.SYSTEM_ERROR, ERROR_MESSAGE_KEY)
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new NlstCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.NLST, [DIR])
+    }
+
+    void setUp() {
+        super.setUp()
+        createDirectory(DIR)
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/NoopCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/NoopCommandHandlerTest.groovy
new file mode 100644
index 0000000..ff998e4
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/NoopCommandHandlerTest.groovy
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for NoopCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class NoopCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    boolean testNotLoggedIn = false
+
+    void testHandleCommand() {
+        handleCommand([])
+        assertSessionReply(ReplyCodes.NOOP_OK, 'noop')
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    void setUp() {
+        super.setUp()
+    }
+
+    CommandHandler createCommandHandler() {
+        new NoopCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.NOOP, [])
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/PassCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/PassCommandHandlerTest.groovy
new file mode 100644
index 0000000..2017155
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/PassCommandHandlerTest.groovy
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.fake.UserAccount
+
+/**
+ * Tests for PassCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class PassCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    def USERNAME = "user123"
+    def PASSWORD = "password123"
+    def HOME_DIRECTORY = "/"
+    UserAccount userAccount
+
+    boolean testNotLoggedIn = false
+
+    void testHandleCommand_UserExists_PasswordCorrect() {
+        serverConfiguration.userAccounts[USERNAME] = userAccount
+        handleCommand([PASSWORD])
+        assertSessionReply(ReplyCodes.PASS_OK, 'pass')
+        assertUserAccountInSession(true)
+        assertCurrentDirectory(HOME_DIRECTORY)
+    }
+
+    void testHandleCommand_UserExists_PasswordCorrect_AccountRequired() {
+        serverConfiguration.userAccounts[USERNAME] = userAccount
+        userAccount.accountRequiredForLogin = true
+        handleCommand([PASSWORD])
+        assertSessionReply(ReplyCodes.PASS_NEED_ACCOUNT, 'pass.needAccount')
+        assertUserAccountInSession(true)
+        assertCurrentDirectory(HOME_DIRECTORY)
+    }
+
+    void testHandleCommand_UserExists_PasswordIncorrect() {
+        serverConfiguration.userAccounts[USERNAME] = userAccount
+        handleCommand(["wrong"])
+        assertSessionReply(ReplyCodes.PASS_LOG_IN_FAILED, 'pass.loginFailed')
+        assertUserAccountInSession(false)
+        assertCurrentDirectory(null)
+    }
+
+    void testHandleCommand_UserExists_PasswordWrongButIgnored() {
+        userAccount.passwordCheckedDuringValidation = false
+        serverConfiguration.userAccounts[USERNAME] = userAccount
+        handleCommand(["wrong"])
+        assertSessionReply(ReplyCodes.PASS_OK, 'pass')
+        assertUserAccountInSession(true)
+        assertCurrentDirectory(HOME_DIRECTORY)
+    }
+
+    void testHandleCommand_UserExists_HomeDirectoryNotDefinedForUserAccount() {
+        userAccount.homeDirectory = ''
+        serverConfiguration.userAccounts[USERNAME] = userAccount
+        handleCommand([PASSWORD])
+        assertSessionReply(ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.userAccountNotValid")
+        assertUserAccountInSession(false)
+        assertCurrentDirectory(null)
+    }
+
+    void testHandleCommand_UserExists_HomeDirectoryDoesNotExist() {
+        userAccount.homeDirectory = '/abc/def'
+        serverConfiguration.userAccounts[USERNAME] = userAccount
+        handleCommand([PASSWORD])
+        assertSessionReply(ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.homeDirectoryNotValid")
+        assertUserAccountInSession(false)
+        assertCurrentDirectory(null)
+    }
+
+    void testHandleCommand_UserDoesNotExist() {
+        handleCommand([PASSWORD])
+        assertSessionReply(ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.userAccountNotValid")
+        assertUserAccountInSession(false)
+        assertCurrentDirectory(null)
+    }
+
+    void testHandleCommand_UsernameNotSetInSession() {
+        session.removeAttribute(SessionKeys.USERNAME)
+        testHandleCommand_MissingRequiredSessionAttribute()
+        assertUserAccountInSession(false)
+        assertCurrentDirectory(null)
+    }
+
+    void testHandleCommand_MissingPasswordParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+        assertUserAccountInSession(false)
+        assertCurrentDirectory(null)
+    }
+
+    //-------------------------------------------------------------------------
+    // Abstract and Overridden Methods
+    //-------------------------------------------------------------------------
+
+    void setUp() {
+        super.setUp()
+
+        createDirectory(HOME_DIRECTORY)
+
+        userAccount = new UserAccount(USERNAME, PASSWORD, HOME_DIRECTORY)
+
+        session.setAttribute(SessionKeys.USERNAME, USERNAME)
+        session.removeAttribute(SessionKeys.USER_ACCOUNT)
+    }
+
+    CommandHandler createCommandHandler() {
+        new PassCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.PASS, [PASSWORD])
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    /**
+     * Assert that the UserAccount object is in the session, depending on the value of isUserAccountInSession.
+     * @param isUserAccountInSession - true if the UserAccount is expected in the session; false if it is not expected
+     */
+    private void assertUserAccountInSession(boolean isUserAccountInSession) {
+        def expectedValue = isUserAccountInSession ? userAccount : null
+        assert session.getAttribute(SessionKeys.USER_ACCOUNT) == expectedValue
+    }
+
+    /**
+     * Assert that the current directory is set in the session, but only if currentDirectory is not null.
+     * @param currentDirectory - the curent directory expected in the session; null if it is not expected
+     */
+    private void assertCurrentDirectory(String currentDirectory) {
+        assert session.getAttribute(SessionKeys.CURRENT_DIRECTORY) == currentDirectory
+    }
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/PasvCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/PasvCommandHandlerTest.groovy
new file mode 100644
index 0000000..e913d57
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/PasvCommandHandlerTest.groovy
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for PasvCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class PasvCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    static final PORT = (23 << 8) + 77
+    static final InetAddress SERVER = inetAddress("192.168.0.2")
+
+    void testHandleCommand() {
+        final HOST_AND_PORT = "192,168,0,2,23,77"
+        session.switchToPassiveModeReturnValue = PORT
+        session.serverHost = SERVER
+        handleCommand([])
+
+        assertSessionReply(ReplyCodes.PASV_OK, HOST_AND_PORT)
+        assert session.switchedToPassiveMode
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new PasvCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.PASV, [])
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/PortCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/PortCommandHandlerTest.groovy
new file mode 100644
index 0000000..035652b
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/PortCommandHandlerTest.groovy
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for PortCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class PortCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    static final PARAMETERS = ["11", "22", "33", "44", "1", "206"]
+    static final PARAMETERS_INSUFFICIENT = ["7", "29", "99", "11", "77"]
+    static final PORT = (1 << 8) + 206
+    static final HOST = InetAddress.getByName("11.22.33.44")
+
+    boolean testNotLoggedIn = false
+
+    void testHandleCommand() {
+        handleCommand(PARAMETERS)
+        assertSessionReply(ReplyCodes.PORT_OK, 'port')
+        assert session.clientDataPort == PORT
+        assert session.clientDataHost == HOST
+    }
+
+    void testHandleCommand_MissingRequiredParameter() {
+        testHandleCommand_MissingRequiredParameter(PARAMETERS_INSUFFICIENT)
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    void setUp() {
+        super.setUp()
+    }
+
+    CommandHandler createCommandHandler() {
+        new PortCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.PORT, PARAMETERS)
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/PwdCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/PwdCommandHandlerTest.groovy
new file mode 100644
index 0000000..a26d451
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/PwdCommandHandlerTest.groovy
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+
+/**
+ * Tests for PwdCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class PwdCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    static final DIR = "/usr/abc"
+
+    boolean testNotLoggedIn = false
+
+    void testHandleCommand() {
+        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, DIR)
+        handleCommand([])
+        assertSessionReply(ReplyCodes.PWD_OK, ["pwd", DIR])
+    }
+
+    void testHandleCommand_CurrentDirectoryNotSet() {
+        handleCommand([])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, 'filesystem.currentDirectoryNotSet')
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new PwdCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.PWD, [])
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/QuitCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/QuitCommandHandlerTest.groovy
new file mode 100644
index 0000000..83b611d
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/QuitCommandHandlerTest.groovy
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for QuitCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class QuitCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    boolean testNotLoggedIn = false
+
+    void testHandleCommand() {
+        assert !session.closed
+        handleCommand([])
+        assertSessionReply(ReplyCodes.QUIT_OK, 'quit')
+        assert session.closed
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new QuitCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.QUIT, [])
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/ReinCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/ReinCommandHandlerTest.groovy
new file mode 100644
index 0000000..365bf9a
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/ReinCommandHandlerTest.groovy
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.fake.UserAccount
+
+/**
+ * Tests for ReinCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class ReinCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    boolean testNotLoggedIn = false
+
+    UserAccount userAccount
+
+    void testHandleCommand_AlreadyLoggedIn() {
+        session.setAttribute(SessionKeys.USER_ACCOUNT, userAccount)
+        assert isLoggedIn()
+        handleCommand([])
+        assertSessionReply(ReplyCodes.REIN_OK, 'rein')
+        assert !isLoggedIn()
+    }
+
+    void testHandleCommand_NotLoggedIn() {
+        handleCommand([])
+        assertSessionReply(ReplyCodes.REIN_OK, 'rein')
+        assert !isLoggedIn()
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new ReinCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.REIN, [])
+    }
+
+    void setUp() {
+        super.setUp()
+        userAccount = new UserAccount(username: 'user')
+    }
+
+    private boolean isLoggedIn() {
+        return session.getAttribute(SessionKeys.USER_ACCOUNT) != null
+    }
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RestCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RestCommandHandlerTest.groovy
new file mode 100644
index 0000000..263c4a6
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RestCommandHandlerTest.groovy
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for RestCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class RestCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    void testHandleCommand() {
+        handleCommand([])
+        assertSessionReply(ReplyCodes.REST_OK, 'rest')
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new RestCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.REST, [])
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RetrCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RetrCommandHandlerTest.groovy
new file mode 100644
index 0000000..7f52409
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RetrCommandHandlerTest.groovy
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.fake.filesystem.FileEntry
+import org.mockftpserver.fake.filesystem.FileSystemException
+import org.mockftpserver.fake.filesystem.Permissions
+
+/**
+ * Tests for RetrCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class RetrCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    def DIR = "/"
+    def FILENAME = "file.txt"
+    def FILE = p(DIR, FILENAME)
+    def CONTENTS = "abc\ndef\nghi"
+    def CONTENTS_ASCII = "abc\r\ndef\r\nghi"
+
+    void testHandleCommand_MissingPathParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+    }
+
+    void testHandleCommand_AbsolutePath() {
+        handleCommandAndVerifySendDataReplies([FILE])
+        assertSessionData(CONTENTS_ASCII)
+    }
+
+    void testHandleCommand_AbsolutePath_NonAsciiMode() {
+        session.setAttribute(SessionKeys.ASCII_TYPE, false)
+        handleCommandAndVerifySendDataReplies([FILE])
+        assertSessionData(CONTENTS)
+    }
+
+    void testHandleCommand_RelativePath() {
+        setCurrentDirectory(DIR)
+        handleCommandAndVerifySendDataReplies([FILENAME])
+        assertSessionData(CONTENTS_ASCII)
+    }
+
+    void testHandleCommand_PathSpecifiesAnExistingDirectory() {
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.isNotAFile', DIR])
+    }
+
+    void testHandleCommand_PathDoesNotExist() {
+        def path = FILE + "XXX"
+        handleCommand([path])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.doesNotExist', path])
+    }
+
+    void testHandleCommand_NoReadAccessToFile() {
+        fileSystem.getEntry(FILE).permissions = Permissions.NONE
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.cannotRead', FILE])
+    }
+
+    void testHandleCommand_NoExecuteAccessToDirectory() {
+        fileSystem.getEntry(DIR).permissions = Permissions.NONE
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.cannotExecute', DIR])
+    }
+
+    void testHandleCommand_ThrowsFileSystemException() {
+        fileSystem.delete(FILE)
+        def fileEntry = new BadFileEntry(FILE)
+        fileSystem.add(fileEntry)
+
+        handleCommand([FILE])
+        assertSessionReply(0, ReplyCodes.TRANSFER_DATA_INITIAL_OK)
+        assertSessionReply(1, ReplyCodes.READ_FILE_ERROR, ERROR_MESSAGE_KEY)
+    }
+
+    void testConvertLfToCrLf() {
+        // LF='\n' and CRLF='\r\n'
+        assert commandHandler.convertLfToCrLf('abc'.bytes) == 'abc'.bytes
+        assert commandHandler.convertLfToCrLf('abc\r\ndef'.bytes) == 'abc\r\ndef'.bytes
+        assert commandHandler.convertLfToCrLf('abc\ndef'.bytes) == 'abc\r\ndef'.bytes
+        assert commandHandler.convertLfToCrLf('abc\ndef\nghi'.bytes) == 'abc\r\ndef\r\nghi'.bytes
+        assert commandHandler.convertLfToCrLf('\n'.bytes) == '\r\n'.bytes
+        assert commandHandler.convertLfToCrLf('\r\nabc\n'.bytes) == '\r\nabc\r\n'.bytes
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new RetrCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.RETR, [FILE])
+    }
+
+    void setUp() {
+        super.setUp()
+        createDirectory(DIR)
+        createFile(FILE, CONTENTS)
+    }
+
+}
+
+class BadFileEntry extends FileEntry {
+
+    BadFileEntry(String path) {
+        super(path)
+    }
+
+    InputStream createInputStream() {
+        throw new FileSystemException("BAD", AbstractFakeCommandHandlerTestCase.ERROR_MESSAGE_KEY)
+    }
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RmdCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RmdCommandHandlerTest.groovy
new file mode 100644
index 0000000..968f14f
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RmdCommandHandlerTest.groovy
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.fake.filesystem.FileSystemException
+import org.mockftpserver.fake.filesystem.Permissions
+
+/**
+ * Tests for RmdCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class RmdCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    static final PARENT = '/'
+    static final DIR = p(PARENT, "usr")
+
+    void testHandleCommand() {
+        createDirectory(DIR)
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.RMD_OK, ['rmd', DIR])
+        assert fileSystem.exists(DIR) == false
+    }
+
+    void testHandleCommand_PathIsRelative() {
+        def SUB = "sub"
+        createDirectory(p(DIR, SUB))
+        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, DIR)
+        handleCommand([SUB])
+        assertSessionReply(ReplyCodes.RMD_OK, ['rmd', SUB])
+        assert fileSystem.exists(p(DIR, SUB)) == false
+    }
+
+    void testHandleCommand_PathDoesNotExistInFileSystem() {
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.doesNotExist', DIR])
+    }
+
+    void testHandleCommand_PathSpecifiesAFile() {
+        createFile(DIR)
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.isNotADirectory', DIR])
+        assert fileSystem.exists(DIR)
+    }
+
+    void testHandleCommand_DirectoryIsNotEmpty() {
+        final FILE = DIR + "/file.txt"
+        createFile(FILE)
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.directoryIsNotEmpty', DIR])
+        assert fileSystem.exists(DIR)
+        assert fileSystem.exists(FILE)
+    }
+
+    void testHandleCommand_MissingPathParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+    }
+
+    void testHandleCommand_ListNamesThrowsException() {
+        createDirectory(DIR)
+        fileSystem.listNamesMethodException = new FileSystemException("bad", ERROR_MESSAGE_KEY)
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ERROR_MESSAGE_KEY)
+    }
+
+    void testHandleCommand_DeleteThrowsException() {
+        createDirectory(DIR)
+        fileSystem.deleteMethodException = new FileSystemException("bad", ERROR_MESSAGE_KEY)
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ERROR_MESSAGE_KEY)
+    }
+
+    void testHandleCommand_NoWriteAccessToParentDirectory() {
+        createDirectory(DIR)
+        fileSystem.getEntry(PARENT).permissions = new Permissions('r-xr-xr-x')
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.cannotWrite', PARENT])
+        assert fileSystem.exists(DIR)
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new RmdCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.RMD, [DIR])
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RnfrCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RnfrCommandHandlerTest.groovy
new file mode 100644
index 0000000..1f31171
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RnfrCommandHandlerTest.groovy
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.fake.filesystem.Permissions
+
+/**
+ * Tests for RnfrCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class RnfrCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    private static final FILE = "/file.txt"
+    private static final DIR = "/subdir"
+
+    void testHandleCommand() {
+        createFile(FILE)
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.RNFR_OK, 'rnfr')
+        assert session.getAttribute(SessionKeys.RENAME_FROM) == FILE
+    }
+
+    void testHandleCommand_PathIsRelative() {
+        createFile(FILE)
+        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, "/")
+        handleCommand(["file.txt"])
+        assertSessionReply(ReplyCodes.RNFR_OK, 'rnfr')
+        assert session.getAttribute(SessionKeys.RENAME_FROM) == FILE
+    }
+
+    void testHandleCommand_PathDoesNotExistInFileSystem() {
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.doesNotExist', FILE])
+        assert session.getAttribute(SessionKeys.RENAME_FROM) == null
+    }
+
+    void testHandleCommand_PathSpecifiesADirectory() {
+        createDirectory(DIR)
+        handleCommand([DIR])
+        assertSessionReply(ReplyCodes.RNFR_OK, 'rnfr')
+        assert session.getAttribute(SessionKeys.RENAME_FROM) == DIR
+    }
+
+    void testHandleCommand_NoReadAccessToFile() {
+        createFile(FILE)
+        fileSystem.getEntry(FILE).permissions = new Permissions('-wx-wx-wx')
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.READ_FILE_ERROR, ['filesystem.cannotRead', FILE])
+    }
+
+    void testHandleCommand_MissingPathParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new RnfrCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.RNFR, [FILE])
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RntoCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RntoCommandHandlerTest.groovy
new file mode 100644
index 0000000..ebd5481
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/RntoCommandHandlerTest.groovy
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2010 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.fake.filesystem.FileSystemException
+import org.mockftpserver.fake.filesystem.Permissions
+
+/**
+ * Tests for RntoCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class RntoCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    private static final DIR = '/'
+    private static final FROM_FILE = "/from.txt"
+    private static final TO_FILE = "/file.txt"
+    private static final FROM_DIR = "/subdir"
+
+    void testHandleCommand_SingleFile() {
+        createFile(FROM_FILE)
+        handleCommand([TO_FILE])
+        assertSessionReply(ReplyCodes.RNTO_OK, ['rnto', FROM_FILE, TO_FILE])
+        assert !fileSystem.exists(FROM_FILE), FROM_FILE
+        assert fileSystem.exists(TO_FILE), TO_FILE
+        assertRenameFromSessionProperty(null)
+    }
+
+    void testHandleCommand_SingleFile_PathIsRelative() {
+        createFile(FROM_FILE)
+        handleCommand(["file.txt"])
+        assertSessionReply(ReplyCodes.RNTO_OK, ['rnto', FROM_FILE, 'file.txt'])
+        assert !fileSystem.exists(FROM_FILE), FROM_FILE
+        assert fileSystem.exists(TO_FILE), TO_FILE
+        assertRenameFromSessionProperty(null)
+    }
+
+    void testHandleCommand_FromFileNotSetInSession() {
+        session.removeAttribute(SessionKeys.RENAME_FROM)
+        testHandleCommand_MissingRequiredSessionAttribute()
+    }
+
+    void testHandleCommand_ToFilenameNotValid() {
+        createFile(FROM_FILE)
+        handleCommand([""])
+        assertSessionReply(ReplyCodes.FILENAME_NOT_VALID, "")
+        assertRenameFromSessionProperty(FROM_FILE)
+    }
+
+    void testHandleCommand_EmptyDirectory() {
+        final TO_DIR = "/newdir"
+        createDirectory(FROM_DIR)
+        setRenameFromSessionProperty(FROM_DIR)
+        handleCommand([TO_DIR])
+        assertSessionReply(ReplyCodes.RNTO_OK, ['rnto', FROM_DIR, TO_DIR])
+        assert !fileSystem.exists(FROM_DIR), FROM_DIR
+        assert fileSystem.exists(TO_DIR), TO_DIR
+        assertRenameFromSessionProperty(null)
+    }
+
+    void testHandleCommand_DirectoryContainingFilesAndSubdirectory() {
+        final TO_DIR = "/newdir"
+        createDirectory(FROM_DIR)
+        createFile(FROM_DIR + "/a.txt")
+        createFile(FROM_DIR + "/b.txt")
+        createDirectory(FROM_DIR + "/child/grandchild")
+        setRenameFromSessionProperty(FROM_DIR)
+        handleCommand([TO_DIR])
+        assertSessionReply(ReplyCodes.RNTO_OK, ['rnto', FROM_DIR, TO_DIR])
+        assert !fileSystem.exists(FROM_DIR), FROM_DIR
+        assert fileSystem.exists(TO_DIR), TO_DIR
+        assert fileSystem.isFile(TO_DIR + "/a.txt")
+        assert fileSystem.isFile(TO_DIR + "/b.txt")
+        assert fileSystem.isDirectory(TO_DIR + "/child")
+        assert fileSystem.isDirectory(TO_DIR + "/child/grandchild")
+        assertRenameFromSessionProperty(null)
+    }
+
+    void testHandleCommand_ToDirectoryIsChildOfFromDirectory() {
+        final TO_DIR = FROM_DIR + "/child"
+        createDirectory(FROM_DIR)
+        setRenameFromSessionProperty(FROM_DIR)
+        handleCommand([TO_DIR])
+        assertSessionReply(ReplyCodes.WRITE_FILE_ERROR, ['filesystem.renameFailed', TO_DIR])
+        assertRenameFromSessionProperty(FROM_DIR)
+    }
+
+    void testHandleCommand_NoWriteAccessToDirectory() {
+        createFile(FROM_FILE)
+        fileSystem.getEntry(DIR).permissions = new Permissions('r-xr-xr-x')
+        handleCommand([TO_FILE])
+        assertSessionReply(ReplyCodes.WRITE_FILE_ERROR, ['filesystem.cannotWrite', DIR])
+        assertRenameFromSessionProperty(FROM_FILE)
+    }
+
+    void testHandleCommand_FromFileDoesNotExist() {
+        createDirectory(DIR)
+        handleCommand([TO_FILE])
+        assertSessionReply(ReplyCodes.FILENAME_NOT_VALID, ['filesystem.doesNotExist', FROM_FILE])
+        assertRenameFromSessionProperty(FROM_FILE)
+    }
+
+    void testHandleCommand_ToFileParentDirectoryDoesNotExist() {
+        createFile(FROM_FILE)
+        final BAD_DIR = p(DIR, 'SUB')
+        final BAD_TO_FILE = p(BAD_DIR, 'Filename.txt')
+        handleCommand([BAD_TO_FILE])
+        assertSessionReply(ReplyCodes.FILENAME_NOT_VALID, ['filesystem.doesNotExist', BAD_DIR])
+        assertRenameFromSessionProperty(FROM_FILE)
+    }
+
+    void testHandleCommand_RenameThrowsException() {
+        createDirectory(DIR)
+        fileSystem.renameMethodException = new FileSystemException("bad", ERROR_MESSAGE_KEY)
+        handleCommand([TO_FILE])
+        assertSessionReply(ReplyCodes.WRITE_FILE_ERROR, ERROR_MESSAGE_KEY)
+        assertRenameFromSessionProperty(FROM_FILE)
+    }
+
+    void testHandleCommand_MissingPathParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new RntoCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.RNTO, [TO_FILE])
+    }
+
+    void setUp() {
+        super.setUp()
+        setCurrentDirectory(DIR)
+        setRenameFromSessionProperty(FROM_FILE)
+    }
+
+    private void setRenameFromSessionProperty(String renameFrom) {
+        session.setAttribute(SessionKeys.RENAME_FROM, renameFrom)
+    }
+
+    private void assertRenameFromSessionProperty(String value) {
+        assert session.getAttribute(SessionKeys.RENAME_FROM) == value
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/SiteCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/SiteCommandHandlerTest.groovy
new file mode 100644
index 0000000..9fbfdcd
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/SiteCommandHandlerTest.groovy
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for SiteCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class SiteCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    void testHandleCommand() {
+        handleCommand([])
+        assertSessionReply(ReplyCodes.SITE_OK, 'site')
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    void setUp() {
+        super.setUp()
+    }
+
+    CommandHandler createCommandHandler() {
+        new SiteCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.SITE, [])
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/SmntCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/SmntCommandHandlerTest.groovy
new file mode 100644
index 0000000..2f38f17
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/SmntCommandHandlerTest.groovy
@@ -0,0 +1,50 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.command

+

+import org.mockftpserver.core.command.Command

+import org.mockftpserver.core.command.CommandHandler

+import org.mockftpserver.core.command.CommandNames

+import org.mockftpserver.core.command.ReplyCodes

+

+

+/**

+ * Tests for SmntCommandHandler

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class SmntCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {

+

+    void testHandleCommand() {

+        handleCommand([])

+        assertSessionReply(ReplyCodes.SMNT_OK, 'smnt')

+    }

+

+    //-------------------------------------------------------------------------

+    // Helper Methods

+    //-------------------------------------------------------------------------

+

+    CommandHandler createCommandHandler() {

+        new SmntCommandHandler()

+    }

+

+    Command createValidCommand() {

+        return new Command(CommandNames.SMNT, [])

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/StatCommandHandlerText.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/StatCommandHandlerText.groovy
new file mode 100644
index 0000000..4ac9bc1
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/StatCommandHandlerText.groovy
@@ -0,0 +1,52 @@
+package org.mockftpserver.fake.command

+

+import org.mockftpserver.core.command.Command

+import org.mockftpserver.core.command.CommandHandler

+import org.mockftpserver.core.command.CommandNames

+import org.mockftpserver.core.command.ReplyCodes

+

+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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.

+ */

+/**

+ * Tests for StatCommandHandler

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class StatCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {

+

+    boolean testNotLoggedIn = false

+

+    void testHandleCommand() {

+        serverConfiguration.systemStatus = '12345'

+        handleCommand([])

+        assertSessionReply(ReplyCodes.STAT_SYSTEM_OK, ['12345'])

+    }

+

+    //-------------------------------------------------------------------------

+    // Helper Methods

+    //-------------------------------------------------------------------------

+

+    CommandHandler createCommandHandler() {

+        new StatCommandHandler()

+    }

+

+    Command createValidCommand() {

+        return new Command(CommandNames.STAT, [])

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/StorCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/StorCommandHandlerTest.groovy
new file mode 100644
index 0000000..25a3afe
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/StorCommandHandlerTest.groovy
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for StorCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class StorCommandHandlerTest extends AbstractStoreFileCommandHandlerTestCase {
+
+    void testHandleCommand_MissingPathParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+    }
+
+    void testHandleCommand_AbsolutePath() {
+        testHandleCommand([FILE], 'stor', CONTENTS)
+    }
+
+    void testHandleCommand_RelativePath() {
+        setCurrentDirectory(DIR)
+        testHandleCommand([FILENAME], 'stor', CONTENTS)
+    }
+
+    void testHandleCommand_PathSpecifiesAnExistingDirectory() {
+        createDirectory(FILE)
+        handleCommand([FILE])
+        assertSessionReply(ReplyCodes.FILENAME_NOT_VALID, FILE)
+    }
+
+    void testHandleCommand_ParentDirectoryDoesNotExist() {
+        def NO_SUCH_DIR = "/path/DoesNotExist"
+        handleCommand([p(NO_SUCH_DIR, FILENAME)])
+        assertSessionReply(ReplyCodes.FILENAME_NOT_VALID, NO_SUCH_DIR)
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new StorCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.STOR, [FILE])
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+    protected String verifyOutputFile() {
+        assert fileSystem.isFile(FILE)
+        assert session.getReplyMessage(1).contains(FILENAME)
+        return FILE
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/StouCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/StouCommandHandlerTest.groovy
new file mode 100644
index 0000000..44a88a3
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/StouCommandHandlerTest.groovy
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+
+/**
+ * Tests for StouCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class StouCommandHandlerTest extends AbstractStoreFileCommandHandlerTestCase {
+
+    def expectedBaseName
+
+    void testHandleCommand_SpecifyBaseFilename() {
+        setCurrentDirectory(DIR)
+        expectedBaseName = FILENAME
+        testHandleCommand([expectedBaseName], 'stou', CONTENTS)
+    }
+
+    void testHandleCommand_UseDefaultBaseFilename() {
+        setCurrentDirectory(DIR)
+        expectedBaseName = 'Temp'
+        testHandleCommand([expectedBaseName], 'stou', CONTENTS)
+    }
+
+    void testHandleCommand_AbsolutePath() {
+        expectedBaseName = FILENAME
+        testHandleCommand([FILE], 'stou', CONTENTS)
+    }
+
+    void testHandleCommand_NoWriteAccessToExistingFile() {
+        // This command always stores a new (unique) file, so this test does not apply
+    }
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new StouCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.STOU, [])
+    }
+
+    void setUp() {
+        super.setUp()
+        session.dataToRead = CONTENTS.bytes
+    }
+
+    protected String verifyOutputFile() {
+        def names = fileSystem.listNames(DIR)
+        def filename = names.find {name -> name.startsWith(expectedBaseName) }
+        assert filename
+        return p(DIR, filename)
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/StruCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/StruCommandHandlerTest.groovy
new file mode 100644
index 0000000..fced21b
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/StruCommandHandlerTest.groovy
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for StruCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class StruCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    void testHandleCommand() {
+        handleCommand([])
+        assertSessionReply(ReplyCodes.STRU_OK, 'stru')
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new StruCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.STRU, [])
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/SystCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/SystCommandHandlerTest.groovy
new file mode 100644
index 0000000..e33e164
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/SystCommandHandlerTest.groovy
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+
+/**
+ * Tests for SystCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class SystCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    static final SYSTEM_NAME = "UNIX"
+
+    boolean testNotLoggedIn = false
+
+    void testHandleCommand() {
+        serverConfiguration.systemName = SYSTEM_NAME
+        handleCommand([])
+        assertSessionReply(ReplyCodes.SYST_OK, ['syst', SYSTEM_NAME])
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    void setUp() {
+        super.setUp()
+    }
+
+    CommandHandler createCommandHandler() {
+        new SystCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.SYST, [])
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/TypeCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/TypeCommandHandlerTest.groovy
new file mode 100644
index 0000000..0a48679
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/TypeCommandHandlerTest.groovy
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+
+/**
+ * Tests for TestCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class TypeCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    void testHandleCommand_Ascii() {
+        handleCommand(['A'])
+        assertSessionReply(ReplyCodes.TYPE_OK, 'type')
+        assert session.getAttribute(SessionKeys.ASCII_TYPE) == true
+    }
+
+    void testHandleCommand_NonAscii() {
+        handleCommand(['I'])
+        assertSessionReply(ReplyCodes.TYPE_OK, 'type')
+        assert session.getAttribute(SessionKeys.ASCII_TYPE) == false
+    }
+
+    void testHandleCommand_MissingRequiredParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+        assert session.getAttribute(SessionKeys.ASCII_TYPE) == null
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    CommandHandler createCommandHandler() {
+        new TypeCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.TYPE, ['A'])
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/UserCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/UserCommandHandlerTest.groovy
new file mode 100644
index 0000000..609897e
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/UserCommandHandlerTest.groovy
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.CommandHandler
+import org.mockftpserver.core.command.CommandNames
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.fake.UserAccount
+
+/**
+ * Tests for UserCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class UserCommandHandlerTest extends AbstractFakeCommandHandlerTestCase {
+
+    static final USERNAME = "user123"
+    static final HOME_DIRECTORY = "/"
+    UserAccount userAccount
+
+    boolean testNotLoggedIn = false
+
+    void testHandleCommand_UserExists() {
+        serverConfiguration.userAccounts[USERNAME] = userAccount
+        handleCommand([USERNAME])
+        assertSessionReply(ReplyCodes.USER_NEED_PASSWORD_OK, 'user.needPassword')
+        assertUsernameInSession(true)
+        assertCurrentDirectory(null)
+    }
+
+    void testHandleCommand_NoSuchUser() {
+        handleCommand([USERNAME])
+        // Will return OK, even if username is not recognized
+        assertSessionReply(ReplyCodes.USER_NEED_PASSWORD_OK, 'user.needPassword')
+        assertUsernameInSession(true)
+        assertCurrentDirectory(null)
+    }
+
+    void testHandleCommand_PasswordNotRequiredForLogin() {
+        userAccount.passwordRequiredForLogin = false
+        serverConfiguration.userAccounts[USERNAME] = userAccount
+
+        handleCommand([USERNAME])
+        assertSessionReply(ReplyCodes.USER_LOGGED_IN_OK, 'user.loggedIn')
+        assert session.getAttribute(SessionKeys.USER_ACCOUNT) == userAccount
+        assertUsernameInSession(false)
+        assertCurrentDirectory(HOME_DIRECTORY)
+    }
+
+    void testHandleCommand_UserExists_HomeDirectoryNotDefinedForUser() {
+        userAccount.homeDirectory = ''
+        serverConfiguration.userAccounts[USERNAME] = userAccount
+        handleCommand([USERNAME])
+        assertSessionReply(ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.userAccountNotValid")
+        assertUsernameInSession(false)
+        assertCurrentDirectory(null)
+    }
+
+    void testHandleCommand_UserExists_HomeDirectoryDoesNotExist() {
+        userAccount.homeDirectory = '/abc/def'
+        serverConfiguration.userAccounts[USERNAME] = userAccount
+        handleCommand([USERNAME])
+        assertSessionReply(ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.homeDirectoryNotValid")
+        assertUsernameInSession(false)
+        assertCurrentDirectory(null)
+    }
+
+    void testHandleCommand_MissingUsernameParameter() {
+        testHandleCommand_MissingRequiredParameter([])
+        assertUsernameInSession(false)
+        assertCurrentDirectory(null)
+    }
+
+    //-------------------------------------------------------------------------
+    // Abstract and Overridden Methods
+    //-------------------------------------------------------------------------
+
+    void setUp() {
+        super.setUp()
+
+        createDirectory(HOME_DIRECTORY)
+        userAccount = new UserAccount(username: USERNAME, homeDirectory: HOME_DIRECTORY)
+    }
+
+    CommandHandler createCommandHandler() {
+        new UserCommandHandler()
+    }
+
+    Command createValidCommand() {
+        return new Command(CommandNames.USER, [USERNAME])
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    /**
+     * Assert that the Username is stored in the session, depending on the value of isUsernameInSession.
+     * @param isUsernameInSession - true if the Username is expected in the session; false if it is not expected
+     */
+    private void assertUsernameInSession(boolean isUsernameInSession) {
+        def expectedValue = isUsernameInSession ? USERNAME : null
+        assert session.getAttribute(SessionKeys.USERNAME) == expectedValue
+    }
+
+    /**
+     * Assert that the current directory is set in the session, but only if currentDirectory is not null.
+     * @param currentDirectory - the curent directory expected in the session; null if it is not expected
+     */
+    private void assertCurrentDirectory(String currentDirectory) {
+        assert session.getAttribute(SessionKeys.CURRENT_DIRECTORY) == currentDirectory
+    }
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/_AbstractFakeCommandHandlerTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/_AbstractFakeCommandHandlerTest.groovy
new file mode 100644
index 0000000..7b2400b
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/command/_AbstractFakeCommandHandlerTest.groovy
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.command
+
+import org.mockftpserver.core.CommandSyntaxException
+import org.mockftpserver.core.IllegalStateException
+import org.mockftpserver.core.NotLoggedInException
+import org.mockftpserver.core.command.Command
+import org.mockftpserver.core.command.ReplyCodes
+import org.mockftpserver.core.session.Session
+import org.mockftpserver.core.session.SessionKeys
+import org.mockftpserver.core.session.StubSession
+import org.mockftpserver.fake.StubServerConfiguration
+import org.mockftpserver.fake.UserAccount
+import org.mockftpserver.fake.filesystem.FileSystemException
+import org.mockftpserver.fake.filesystem.InvalidFilenameException
+import org.mockftpserver.fake.filesystem.UnixFakeFileSystem
+import org.mockftpserver.test.AbstractGroovyTestCase
+import org.mockftpserver.test.StubResourceBundle
+
+/**
+ * Tests for AbstractFakeCommandHandler
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class AbstractFakeCommandHandlerClassTest extends AbstractGroovyTestCase {
+
+    static PATH = "some/path"
+    static REPLY_CODE = 99
+    static MESSAGE_KEY = "99.WithFilename"
+    static ARG = "ABC"
+    static MSG = "text {0}"
+    static MSG_WITH_ARG = "text ABC"
+    static MSG_FOR_KEY = "some other message"
+    static INTERNAL_ERROR = AbstractFakeCommandHandler.INTERNAL_ERROR_KEY
+    static MSG_INTERNAL_ERROR = "internal error message {0}"
+    private AbstractFakeCommandHandler commandHandler
+    private session
+    private serverConfiguration
+    private replyTextBundle
+    private fileSystem
+    private userAccount
+
+    //-------------------------------------------------------------------------
+    // Tests
+    //-------------------------------------------------------------------------
+
+    void testHandleCommand() {
+        def command = new Command("C1", ["abc"])
+        commandHandler.handleCommand(command, session)
+        assert commandHandler.handled
+
+        assertHandleCommandReplyCode(new CommandSyntaxException(""), ReplyCodes.COMMAND_SYNTAX_ERROR)
+        assertHandleCommandReplyCode(new IllegalStateException(""), ReplyCodes.ILLEGAL_STATE)
+        assertHandleCommandReplyCode(new NotLoggedInException(""), ReplyCodes.NOT_LOGGED_IN)
+        assertHandleCommandReplyCode(new InvalidFilenameException(""), ReplyCodes.FILENAME_NOT_VALID)
+
+        shouldFail { commandHandler.handleCommand(null, session) }
+        shouldFail { commandHandler.handleCommand(command, null) }
+    }
+
+    void testHandleCommand_FileSystemException() {
+        assertHandleCommandReplyCode(new FileSystemException(PATH, ''), ReplyCodes.READ_FILE_ERROR, PATH)
+        commandHandler.replyCodeForFileSystemException = ReplyCodes.WRITE_FILE_ERROR
+        assertHandleCommandReplyCode(new FileSystemException(PATH, ''), ReplyCodes.WRITE_FILE_ERROR, PATH)
+    }
+
+    void testSendReply() {
+        commandHandler.sendReply(session, REPLY_CODE)
+        assert session.sentReplies[0] == [REPLY_CODE, MSG], session.sentReplies[0]
+
+        commandHandler.sendReply(session, REPLY_CODE, [ARG])
+        assert session.sentReplies[1] == [REPLY_CODE, MSG_WITH_ARG], session.sentReplies[0]
+
+        shouldFailWithMessageContaining('session') { commandHandler.sendReply(null, REPLY_CODE) }
+        shouldFailWithMessageContaining('reply code') { commandHandler.sendReply(session, 0) }
+    }
+
+    void testSendReply_MessageKey() {
+        commandHandler.sendReply(session, REPLY_CODE, MESSAGE_KEY)
+        assert session.sentReplies[0] == [REPLY_CODE, MSG_FOR_KEY], session.sentReplies[0]
+
+        shouldFailWithMessageContaining('session') { commandHandler.sendReply(null, REPLY_CODE, MESSAGE_KEY) }
+        shouldFailWithMessageContaining('reply code') { commandHandler.sendReply(session, 0, MESSAGE_KEY) }
+    }
+
+    void testSendReply_NullMessageKey() {
+        commandHandler.sendReply(session, REPLY_CODE, null, null)
+        assert session.sentReplies[0] == [REPLY_CODE, MSG_INTERNAL_ERROR], session.sentReplies[0]
+    }
+
+    void testAssertValidReplyCode() {
+        commandHandler.assertValidReplyCode(1)        // no exception expected
+        shouldFail { commandHandler.assertValidReplyCode(0) }
+    }
+
+    void testGetRequiredSessionAttribute() {
+        shouldFail(IllegalStateException) { commandHandler.getRequiredSessionAttribute(session, "undefined") }
+
+        session.setAttribute("abc", "not empty")
+        commandHandler.getRequiredSessionAttribute(session, "abc") // no exception
+
+        session.setAttribute("abc", "")
+        commandHandler.getRequiredSessionAttribute(session, "abc") // no exception
+    }
+
+    void testVerifyLoggedIn() {
+        shouldFail(NotLoggedInException) { commandHandler.verifyLoggedIn(session) }
+        session.setAttribute(SessionKeys.USER_ACCOUNT, userAccount)
+        commandHandler.verifyLoggedIn(session)        // no exception expected
+    }
+
+    void testGetUserAccount() {
+        assert commandHandler.getUserAccount(session) == null
+        session.setAttribute(SessionKeys.USER_ACCOUNT, userAccount)
+        assert commandHandler.getUserAccount(session)
+    }
+
+    void testVerifyFileSystemCondition() {
+        commandHandler.verifyFileSystemCondition(true, PATH, '')    // no exception expected
+        shouldFail(FileSystemException) { commandHandler.verifyFileSystemCondition(false, PATH, '') }
+    }
+
+    void testGetRealPath() {
+        assert commandHandler.getRealPath(session, "/xxx") == "/xxx"
+
+        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, "/usr/me")
+        assert commandHandler.getRealPath(session, null) == "/usr/me"
+        assert commandHandler.getRealPath(session, "/xxx") == "/xxx"
+        assert commandHandler.getRealPath(session, "xxx") == "/usr/me/xxx"
+        assert commandHandler.getRealPath(session, "../xxx") == "/usr/xxx"
+        assert commandHandler.getRealPath(session, "./xxx") == "/usr/me/xxx"
+    }
+
+    //-------------------------------------------------------------------------
+    // Test Setup
+    //-------------------------------------------------------------------------
+
+    void setUp() {
+        super.setUp()
+        commandHandler = new TestFakeCommandHandler()
+        session = new StubSession()
+        serverConfiguration = new StubServerConfiguration()
+        replyTextBundle = new StubResourceBundle()
+        userAccount = new UserAccount()
+        fileSystem = new UnixFakeFileSystem()
+        serverConfiguration.setFileSystem(fileSystem)
+
+        replyTextBundle.put(REPLY_CODE as String, MSG)
+        replyTextBundle.put(MESSAGE_KEY as String, MSG_FOR_KEY)
+        replyTextBundle.put(INTERNAL_ERROR as String, MSG_INTERNAL_ERROR)
+
+        commandHandler.serverConfiguration = serverConfiguration
+        commandHandler.replyTextBundle = replyTextBundle
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    /**
+     * Assert that when the CommandHandler handleCommand() method throws the
+     * specified exception, that the expected reply is sent through the session.
+     */
+    private void assertHandleCommandReplyCode(Throwable exception, int expected, text = null) {
+        commandHandler.exception = exception
+        def command = new Command("C1", ["abc"])
+        session.sentReplies.clear()
+        commandHandler.handleCommand(command, session)
+        def sentReply = session.sentReplies[0][0]
+        assert sentReply == expected
+        if (text) {
+            def sentMessage = session.sentReplies[0][1]
+            assert sentMessage.contains(text), "sentMessage=[$sentMessage] text=[$text]"
+        }
+    }
+
+}
+
+/**
+ * Concrete subclass of AbstractFakeCommandHandler for testing
+ */
+class TestFakeCommandHandler extends AbstractFakeCommandHandler {
+    boolean handled = false
+    def exception
+
+    protected void handle(Command command, Session session) {
+        if (exception) {
+            throw exception
+        }
+        this.handled = true
+    }
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/example/FakeFtpServerSpringConfigurationTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/example/FakeFtpServerSpringConfigurationTest.groovy
new file mode 100644
index 0000000..2d42431
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/example/FakeFtpServerSpringConfigurationTest.groovy
@@ -0,0 +1,100 @@
+package org.mockftpserver.fake.example

+

+import org.apache.commons.net.ftp.FTPClient

+import org.apache.commons.net.ftp.FTPFile

+import org.mockftpserver.fake.FakeFtpServer

+import org.mockftpserver.test.AbstractGroovyTestCase

+import org.springframework.context.ApplicationContext

+import org.springframework.context.support.ClassPathXmlApplicationContext

+

+/*

+* Copyright 2008 the original author or authors.

+*

+* 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.

+*/

+class FakeFtpServerSpringConfigurationTest extends AbstractGroovyTestCase {

+

+    static final SERVER = "localhost"

+    static final PORT = 9981

+    static final USERNAME = 'joe'           // Must match Spring config

+    static final PASSWORD = 'password'      // Must match Spring config 

+

+    private FakeFtpServer fakeFtpServer

+    private FTPClient ftpClient

+

+    void testFakeFtpServer_Unix() {

+        startFtpServer('fakeftpserver-beans.xml')

+        connectAndLogin()

+

+        // PWD

+        String dir = ftpClient.printWorkingDirectory()

+        assert dir == '/'

+

+        // LIST

+        FTPFile[] files = ftpClient.listFiles()

+        LOG.info("FTPFile[0]=" + files[0])

+        assert files.length == 1

+

+        // RETR

+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream()

+        assert ftpClient.retrieveFile("File.txt", outputStream)

+        LOG.info("File contents=[" + outputStream.toString() + "]")

+    }

+

+    void testFakeFtpServer_Windows_WithPermissions() {

+        startFtpServer('fakeftpserver-permissions-beans.xml')

+        connectAndLogin()

+

+        // PWD

+        String dir = ftpClient.printWorkingDirectory()

+        assert dir == 'c:\\'

+

+        // LIST

+        FTPFile[] files = ftpClient.listFiles()

+        assert files.length == 2

+        LOG.info("FTPFile[0]=" + files[0])

+        LOG.info("FTPFile[1]=" + files[1])

+

+        // RETR - File1.txt; we have required permissions

+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream()

+        assert ftpClient.retrieveFile("File1.txt", outputStream)

+        LOG.info("File contents=[" + outputStream.toString() + "]")

+

+        // RETR - File2.txt; we DO NOT have required permissions

+        outputStream = new ByteArrayOutputStream()

+        assert !ftpClient.retrieveFile("File2.txt", outputStream)

+        assert ftpClient.replyCode == 550

+    }

+

+    void setUp() {

+        super.setUp()

+        ftpClient = new FTPClient()

+    }

+

+    void tearDown() {

+        super.tearDown()

+        fakeFtpServer?.stop()

+    }

+

+    private void startFtpServer(String springConfigFile) {

+        ApplicationContext context = new ClassPathXmlApplicationContext(springConfigFile)

+        fakeFtpServer = (FakeFtpServer) context.getBean("fakeFtpServer")

+        fakeFtpServer.start()

+    }

+

+    private void connectAndLogin() {

+        ftpClient.connect(SERVER, PORT)

+        assert ftpClient.login(USERNAME, PASSWORD)

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/AbstractFakeFileSystemTestCase.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/AbstractFakeFileSystemTestCase.groovy
new file mode 100644
index 0000000..440c9d5
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/AbstractFakeFileSystemTestCase.groovy
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.filesystem
+
+import org.mockftpserver.core.util.IoUtil
+
+/**
+ * Tests for subclasses of AbstractFakeFileSystem. Subclasses must define
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+abstract class AbstractFakeFileSystemTestCase extends AbstractFileSystemTestCase {
+
+    // -------------------------------------------------------------------------
+    // Tests
+    // -------------------------------------------------------------------------
+
+    void testDefaultDirectoryListingFormatterClass() {
+        assert fileSystem.directoryListingFormatter.class == expectedDirectoryListingFormatterClass
+    }
+
+    void testAdd_PathLocked() {
+        def dirEntry = new DirectoryEntry(NEW_DIR)
+        fileSystem.add(dirEntry)
+        def fileEntry = new FileEntry(NEW_FILE)
+        fileSystem.add(fileEntry)
+
+        // The path should be locked for both entries
+        shouldFail { dirEntry.setPath('abc') }
+        shouldFail { fileEntry.setPath('abc') }
+    }
+
+    void testAdd_Directory_CreateParentDirectoriesAutomatically() {
+        final NEW_SUBDIR = fileSystem.path(NEW_DIR, "sub")
+        assert !fileSystem.exists(NEW_DIR), "Before createDirectory"
+        assert !fileSystem.exists(NEW_SUBDIR), "Before createDirectory"
+
+        fileSystem.createParentDirectoriesAutomatically = true
+        fileSystem.add(new DirectoryEntry(NEW_SUBDIR))
+        assert fileSystem.exists(NEW_DIR), "After createDirectory"
+        assert fileSystem.exists(NEW_SUBDIR), "$NEW_SUBDIR: After createDirectory"
+    }
+
+    void testAdd_File_CreateParentDirectoriesAutomatically() {
+        final NEW_FILE_IN_SUBDIR = fileSystem.path(NEW_DIR, "abc.txt")
+        assert !fileSystem.exists(NEW_DIR), "Before createDirectory"
+        assert !fileSystem.exists(NEW_FILE_IN_SUBDIR), "Before createDirectory"
+
+        fileSystem.createParentDirectoriesAutomatically = true
+        fileSystem.add(new FileEntry(NEW_FILE_IN_SUBDIR))
+        assert fileSystem.exists(NEW_DIR), "After createDirectory"
+        assert fileSystem.exists(NEW_FILE_IN_SUBDIR), "$NEW_FILE_IN_SUBDIR: After createDirectory"
+    }
+
+    void testAdd_File_CreateParentDirectoriesAutomatically_False() {
+        fileSystem.createParentDirectoriesAutomatically = false
+        final NEW_FILE_IN_SUBDIR = fileSystem.path(NEW_DIR, "abc.txt")
+        assert !fileSystem.exists(NEW_DIR), "Before createDirectory"
+
+        shouldFail(FileSystemException) { fileSystem.add(new FileEntry(NEW_FILE_IN_SUBDIR)) }
+        assert !fileSystem.exists(NEW_DIR), "After createDirectory"
+    }
+
+    void testSetEntries() {
+        fileSystem.createParentDirectoriesAutomatically = false
+        def entries = [new FileEntry(NEW_FILE), new DirectoryEntry(NEW_DIR)]
+        fileSystem.setEntries(entries)
+        assert fileSystem.exists(NEW_DIR)
+        assert fileSystem.exists(NEW_FILE)
+    }
+
+    void testToString() {
+        String toString = fileSystem.toString()
+        LOG.info("toString=" + toString)
+        assert toString.contains(EXISTING_DIR)
+        assert toString.contains(EXISTING_FILE)
+    }
+
+    void testFormatDirectoryListing() {
+        def fileEntry = new FileEntry(path: 'abc')
+        def formatter = [format: {f ->
+            assert f == fileEntry
+            return 'abc'
+        }] as DirectoryListingFormatter
+        fileSystem.directoryListingFormatter = formatter
+        assert fileSystem.formatDirectoryListing(fileEntry) == 'abc'
+    }
+
+    void testFormatDirectoryListing_NullDirectoryListingFormatter() {
+        fileSystem.directoryListingFormatter = null
+        def fileEntry = new FileEntry('abc')
+        shouldFailWithMessageContaining('directoryListingFormatter') { assert fileSystem.formatDirectoryListing(fileEntry) }
+    }
+
+    void testFormatDirectoryListing_NullFileSystemEntry() {
+        def formatter = [format: {f -> }] as DirectoryListingFormatter
+        fileSystem.directoryListingFormatter = formatter
+        shouldFailWithMessageContaining('fileSystemEntry') { assert fileSystem.formatDirectoryListing(null) }
+    }
+
+    void testGetEntry() {
+        assert fileSystem.getEntry(NO_SUCH_DIR) == null
+        assert fileSystem.getEntry(NO_SUCH_FILE) == null
+
+        assert fileSystem.getEntry(EXISTING_FILE).path == EXISTING_FILE
+        assert fileSystem.getEntry(EXISTING_DIR).path == EXISTING_DIR
+
+        def permissions = new Permissions('-wxrwx---')
+        def fileEntry = new FileEntry(path: NEW_FILE, lastModified: DATE, contents: 'abc', owner: 'owner',
+                group: 'group', permissions: permissions)
+        fileSystem.add(fileEntry)
+        def entry = fileSystem.getEntry(NEW_FILE)
+        LOG.info(entry.toString())
+        assert entry.path == NEW_FILE
+        assert !entry.directory
+        assert entry.size == 3
+        assert entry.owner == 'owner'
+        assert entry.group == 'group'
+        assert entry.permissions == permissions
+    }
+
+    void testNormalize_Null() {
+        shouldFailWithMessageContaining("path") { fileSystem.normalize(null) }
+    }
+
+    void testGetName_Null() {
+        shouldFailWithMessageContaining("path") { fileSystem.getName(null) }
+    }
+
+    //--------------------------------------------------------------------------
+    // Abstract Methods
+    //--------------------------------------------------------------------------
+
+    protected abstract Class getExpectedDirectoryListingFormatterClass()
+
+    //--------------------------------------------------------------------------
+    // Internal Helper Methods
+    //--------------------------------------------------------------------------
+
+    /**
+     * Verify the contents of the file at the specified path read from its InputSteam
+     *
+     * @param fileSystem - the FileSystem instance
+     * @param expectedContents - the expected contents
+     * @throws IOException
+     * @see org.mockftpserver.fake.filesystem.AbstractFileSystemTestCase#verifyFileContents(FileSystem,String,String )
+     */
+    protected void verifyFileContents(FileSystem fileSystem, String path, String expectedContents) throws IOException {
+        def fileEntry = fileSystem.getEntry(path)
+        InputStream input = fileEntry.createInputStream()
+        byte[] bytes = IoUtil.readBytes(input)
+        LOG.info("bytes=[" + new String(bytes) + "]")
+        assertEquals("contents: actual=[" + new String(bytes) + "]", expectedContents.getBytes(), bytes)
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/AbstractFileSystemEntryTestCase.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/AbstractFileSystemEntryTestCase.groovy
new file mode 100644
index 0000000..2d2580b
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/AbstractFileSystemEntryTestCase.groovy
@@ -0,0 +1,92 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.filesystem

+

+import java.lang.reflect.Constructor

+import org.mockftpserver.test.AbstractGroovyTestCase

+

+/**

+ * Abstract test superclass for subclasses of AbstractFileSystemEntry

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+public abstract class AbstractFileSystemEntryTestCase extends AbstractGroovyTestCase {

+

+    protected static final PATH = "c:/test/dir"

+    protected static final NEW_PATH = "d:/other/dir"

+    protected static final USER = 'user77'

+    protected static final GROUP = 'group88'

+    protected static final PERMISSIONS = new Permissions('rwxrwx---')

+    protected static final LAST_MODIFIED = new Date()

+

+    void testConstructor_NoArgs() {

+        AbstractFileSystemEntry entry = (AbstractFileSystemEntry) getImplementationClass().newInstance()

+        assertNull("path", entry.getPath())

+        entry.setPath(PATH)

+        assert entry.getPath() == PATH

+        assert isDirectory() == entry.isDirectory()

+    }

+

+    void testConstructor_Path() {

+        Constructor constructor = getImplementationClass().getConstructor([String.class] as Class[])

+        AbstractFileSystemEntry entry = (AbstractFileSystemEntry) constructor.newInstance([PATH] as Object[])

+        LOG.info(entry.toString())

+        assertEquals("path", PATH, entry.getPath())

+        entry.setPath("")

+        assert entry.getPath() == ""

+        assert isDirectory() == entry.isDirectory()

+    }

+

+    void testLockPath() {

+        def entry = createFileSystemEntry(PATH)

+        entry.lockPath()

+        shouldFail { entry.path = 'abc' }

+        assert entry.path == PATH

+    }

+

+    void testGetName() {

+        assert createFileSystemEntry('abc').name == 'abc'

+        assert createFileSystemEntry('/abc').name == 'abc'

+        assert createFileSystemEntry('/dir/abc').name == 'abc'

+        assert createFileSystemEntry('\\abc').name == 'abc'

+    }

+

+    void testSetPermissionsFromString() {

+        def entry = createFileSystemEntry('abc')

+        final PERM = 'rw-r---wx'

+        entry.setPermissionsFromString(PERM)

+        assert entry.permissions == new Permissions(PERM)

+    }

+

+    protected AbstractFileSystemEntry createFileSystemEntry(String path) {

+        def entry = (AbstractFileSystemEntry) getImplementationClass().newInstance()

+        entry.setPath(path)

+        return entry

+    }

+

+    /**

+     * @return the subclass of AbstractFileSystemEntry to be tested

+     */

+    protected abstract Class getImplementationClass()

+

+    /**

+     * @return true if the class being tested represents a directory entry 

+     */

+    protected abstract boolean isDirectory()

+

+}

diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/AbstractFileSystemTestCase.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/AbstractFileSystemTestCase.groovy
new file mode 100644
index 0000000..d3ceaf9
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/AbstractFileSystemTestCase.groovy
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2010 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.filesystem
+
+import org.mockftpserver.test.AbstractGroovyTestCase
+
+/**
+ * Abstract superclass for tests of FileSystem implementation classes. Contains common
+ * tests and test infrastructure. 
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+abstract class AbstractFileSystemTestCase extends AbstractGroovyTestCase {
+
+    public static final FILENAME1 = "File1.txt"
+    public static final FILENAME2 = "file2.txt"
+    public static final DIR1 = "dir1"
+    public static final NEW_DIRNAME = "testDir"
+    public static final ILLEGAL_FILE = "xx/yy////z!<>?*z.txt"
+    public static final EXISTING_FILE_CONTENTS = "abc 123 %^& xxx"
+    public static final DATE = new Date()
+
+    // These must be set by the concrete subclass (in its constructor)
+    protected String NEW_DIR = null
+    protected String NEW_FILE = null
+    protected String EXISTING_DIR = null
+    protected String EXISTING_FILE = null
+    protected NO_SUCH_DIR = null
+    protected NO_SUCH_FILE = null
+
+    protected FileSystem fileSystem
+
+    //-------------------------------------------------------------------------
+    // Common Tests
+    //-------------------------------------------------------------------------
+
+    void testExists() {
+        assert !fileSystem.exists(NEW_FILE)
+        assert !fileSystem.exists(NEW_DIR)
+        assert !fileSystem.exists(ILLEGAL_FILE)
+        assert fileSystem.exists(EXISTING_FILE)
+        assert fileSystem.exists(EXISTING_DIR)
+
+        shouldFailWithMessageContaining("path") { fileSystem.exists(null) }
+    }
+
+    void testIsDirectory() {
+        assert fileSystem.isDirectory(EXISTING_DIR)
+        assert !fileSystem.isDirectory(EXISTING_FILE)
+        assert !fileSystem.isDirectory(NO_SUCH_DIR)
+        assert !fileSystem.isDirectory(NO_SUCH_FILE)
+        assert !fileSystem.isDirectory(ILLEGAL_FILE)
+
+        shouldFailWithMessageContaining("path") { fileSystem.isDirectory(null) }
+    }
+
+    void testIsFile() {
+        assert fileSystem.isFile(EXISTING_FILE)
+        assert !fileSystem.isFile(EXISTING_DIR)
+        assert !fileSystem.isFile(NO_SUCH_DIR)
+        assert !fileSystem.isFile(NO_SUCH_FILE)
+        assert !fileSystem.isFile(ILLEGAL_FILE)
+
+        shouldFailWithMessageContaining("path") { fileSystem.isFile(null) }
+    }
+
+    void testAdd_Directory() {
+        assert !fileSystem.exists(NEW_DIR), "Before createDirectory"
+        fileSystem.add(new DirectoryEntry(NEW_DIR))
+        assert fileSystem.exists(NEW_DIR), "After createDirectory"
+
+        // Duplicate directory
+        shouldThrowFileSystemExceptionWithMessageKey('filesystem.pathAlreadyExists') {
+            fileSystem.add(new DirectoryEntry(NEW_DIR))
+        }
+
+        // The parent of the path does not exist
+        shouldThrowFileSystemExceptionWithMessageKey('filesystem.parentDirectoryDoesNotExist') {
+            fileSystem.add(new DirectoryEntry(NEW_DIR + "/abc/def"))
+        }
+
+        shouldFail(InvalidFilenameException) { fileSystem.add(new DirectoryEntry(ILLEGAL_FILE)) }
+        shouldFailWithMessageContaining("path") { fileSystem.add(new DirectoryEntry(null)) }
+    }
+
+    void testAdd_File() {
+        assert !fileSystem.exists(NEW_FILE), "Before createFile"
+        fileSystem.add(new FileEntry(NEW_FILE))
+        assert fileSystem.exists(NEW_FILE), "After createFile"
+
+        // File already exists
+        shouldThrowFileSystemExceptionWithMessageKey('filesystem.pathAlreadyExists') {
+            fileSystem.add(new FileEntry(NEW_FILE))
+        }
+
+        // The parent of the path does not exist
+        shouldThrowFileSystemExceptionWithMessageKey('filesystem.parentDirectoryDoesNotExist') {
+            fileSystem.add(new FileEntry(NEW_DIR + "/abc/def"))
+        }
+
+        shouldThrowFileSystemExceptionWithMessageKey('filesystem.parentDirectoryDoesNotExist') {
+            fileSystem.add(new FileEntry(NO_SUCH_DIR))
+        }
+
+        shouldFail(InvalidFilenameException) { fileSystem.add(new FileEntry(ILLEGAL_FILE)) }
+
+        shouldFailWithMessageContaining("path") { fileSystem.add(new FileEntry(null)) }
+    }
+
+    void testRename_NullFromPath() {
+        shouldFailWithMessageContaining("fromPath") { fileSystem.rename(null, FILENAME1) }
+    }
+
+    void testRename_NullToPath() {
+        shouldFailWithMessageContaining("toPath") { fileSystem.rename(FILENAME1, null) }
+    }
+
+    void testListNames() {
+        fileSystem.add(new DirectoryEntry(NEW_DIR))
+        assert fileSystem.listNames(NEW_DIR) == []
+
+        fileSystem.add(new FileEntry(p(NEW_DIR, FILENAME1)))
+        fileSystem.add(new FileEntry(p(NEW_DIR, FILENAME2)))
+        fileSystem.add(new DirectoryEntry(p(NEW_DIR, DIR1)))
+        fileSystem.add(new FileEntry(p(NEW_DIR, DIR1, "/abc.def")))
+
+        List filenames = fileSystem.listNames(NEW_DIR)
+        LOG.info("filenames=" + filenames)
+        assertSameIgnoringOrder(filenames, [FILENAME1, FILENAME2, DIR1])
+
+        // Specify a filename instead of a directory name
+        assert [FILENAME1] == fileSystem.listNames(p(NEW_DIR, FILENAME1))
+
+        assert [] == fileSystem.listNames(NO_SUCH_DIR)
+
+        shouldFailWithMessageContaining("path") { fileSystem.listNames(null) }
+    }
+
+    void testListNames_Wildcards() {
+        fileSystem.add(new DirectoryEntry(NEW_DIR))
+        fileSystem.add(new FileEntry(p(NEW_DIR, 'abc.txt')))
+        fileSystem.add(new FileEntry(p(NEW_DIR, 'def.txt')))
+
+        assertSameIgnoringOrder(fileSystem.listNames(p(NEW_DIR, '*.txt')), ['abc.txt', 'def.txt'])
+        assertSameIgnoringOrder(fileSystem.listNames(p(NEW_DIR, '*')), ['abc.txt', 'def.txt'])
+        assertSameIgnoringOrder(fileSystem.listNames(p(NEW_DIR, '???.???')), ['abc.txt', 'def.txt'])
+        assertSameIgnoringOrder(fileSystem.listNames(p(NEW_DIR, '*.exe')), [])
+        assertSameIgnoringOrder(fileSystem.listNames(p(NEW_DIR, 'abc.???')), ['abc.txt'])
+        assertSameIgnoringOrder(fileSystem.listNames(p(NEW_DIR, 'a?c.?xt')), ['abc.txt'])
+        assertSameIgnoringOrder(fileSystem.listNames(p(NEW_DIR, 'd?f.*')), ['def.txt'])
+    }
+
+    void testListFiles() {
+        fileSystem.add(new DirectoryEntry(NEW_DIR))
+        assert [] == fileSystem.listFiles(NEW_DIR)
+
+        def path1 = p(NEW_DIR, FILENAME1)
+        def fileEntry1 = new FileEntry(path1)
+        fileSystem.add(fileEntry1)
+        assert fileSystem.listFiles(NEW_DIR) == [fileEntry1]
+
+        // Specify a filename instead of a directory name
+        assert fileSystem.listFiles(p(NEW_DIR, FILENAME1)) == [fileEntry1]
+
+        def fileEntry2 = new FileEntry(p(NEW_DIR, FILENAME2))
+        fileSystem.add(fileEntry2)
+        assert fileSystem.listFiles(NEW_DIR) as Set == [fileEntry1, fileEntry2] as Set
+
+        // Write to the file to get a non-zero length
+        final byte[] CONTENTS = "1234567890".getBytes()
+        OutputStream out = fileEntry1.createOutputStream(false)
+        out.write(CONTENTS)
+        out.close()
+        assert fileSystem.listFiles(NEW_DIR) as Set == [fileEntry1, fileEntry2] as Set
+
+        def dirEntry3 = new DirectoryEntry(p(NEW_DIR, DIR1))
+        fileSystem.add(dirEntry3)
+        assert fileSystem.listFiles(NEW_DIR) as Set == [fileEntry1, fileEntry2, dirEntry3] as Set
+
+        assert fileSystem.listFiles(NO_SUCH_DIR) == []
+
+        shouldFailWithMessageContaining("path") { fileSystem.listFiles(null) }
+    }
+
+    void testListFiles_Wildcards() {
+        def dirEntry = new DirectoryEntry(NEW_DIR)
+        def fileEntry1 = new FileEntry(p(NEW_DIR, 'abc.txt'))
+        def fileEntry2 = new FileEntry(p(NEW_DIR, 'def.txt'))
+
+        fileSystem.add(dirEntry)
+        fileSystem.add(fileEntry1)
+        fileSystem.add(fileEntry2)
+
+        assert fileSystem.listFiles(p(NEW_DIR, '*.txt')) as Set == [fileEntry1, fileEntry2] as Set
+        assert fileSystem.listFiles(p(NEW_DIR, '*')) as Set == [fileEntry1, fileEntry2] as Set
+        assert fileSystem.listFiles(p(NEW_DIR, '???.???')) as Set == [fileEntry1, fileEntry2] as Set
+        assert fileSystem.listFiles(p(NEW_DIR, '*.exe')) as Set == [] as Set
+        assert fileSystem.listFiles(p(NEW_DIR, 'abc.???')) as Set == [fileEntry1] as Set
+        assert fileSystem.listFiles(p(NEW_DIR, 'a?c.?xt')) as Set == [fileEntry1] as Set
+        assert fileSystem.listFiles(p(NEW_DIR, 'd?f.*')) as Set == [fileEntry2] as Set
+    }
+
+    void testDelete() {
+        fileSystem.add(new FileEntry(NEW_FILE))
+        assert fileSystem.delete(NEW_FILE)
+        assert !fileSystem.exists(NEW_FILE)
+
+        assert !fileSystem.delete(NO_SUCH_FILE)
+
+        fileSystem.add(new DirectoryEntry(NEW_DIR))
+        assert fileSystem.delete(NEW_DIR)
+        assert !fileSystem.exists(NEW_DIR)
+
+        fileSystem.add(new DirectoryEntry(NEW_DIR))
+        fileSystem.add(new FileEntry(NEW_DIR + "/abc.txt"))
+
+        assert !fileSystem.delete(NEW_DIR), "Directory containing files"
+        assert fileSystem.exists(NEW_DIR)
+
+        shouldFailWithMessageContaining("path") { fileSystem.delete(null) }
+    }
+
+    void testRename() {
+        final FROM_FILE = NEW_FILE + "2"
+        fileSystem.add(new FileEntry(FROM_FILE))
+
+        fileSystem.rename(FROM_FILE, NEW_FILE)
+        assert fileSystem.exists(NEW_FILE)
+
+        fileSystem.add(new DirectoryEntry(NEW_DIR))
+
+        // Rename existing directory
+        final String TO_DIR = NEW_DIR + "2"
+        fileSystem.rename(NEW_DIR, TO_DIR)
+        assert !fileSystem.exists(NEW_DIR)
+        assert fileSystem.exists(TO_DIR)
+    }
+
+    void testRename_ToPathFileAlreadyExists() {
+        final FROM_FILE = EXISTING_FILE
+        final String TO_FILE = NEW_FILE
+        fileSystem.add(new FileEntry(TO_FILE))
+         shouldThrowFileSystemExceptionWithMessageKey('filesystem.alreadyExists') {
+             fileSystem.rename(FROM_FILE, TO_FILE) 
+         }
+    }
+
+    void testRename_FromPathDoesNotExist() {
+        final TO_FILE2 = NEW_FILE + "2"
+        shouldThrowFileSystemExceptionWithMessageKey('filesystem.doesNotExist') {
+            fileSystem.rename(NO_SUCH_FILE, TO_FILE2)
+        }
+        assert !fileSystem.exists(TO_FILE2), "After failed rename"
+    }
+
+    void testRename_ToPathIsChildOfFromPath() {
+        final FROM_DIR = NEW_DIR
+        final TO_DIR = FROM_DIR + "/child"
+        fileSystem.add(new DirectoryEntry(FROM_DIR))
+        shouldThrowFileSystemExceptionWithMessageKey('filesystem.renameFailed') {
+            fileSystem.rename(FROM_DIR, TO_DIR)
+        }
+        assert !fileSystem.exists(TO_DIR), "After failed rename"
+    }
+
+    void testRename_EmptyDirectory() {
+        final FROM_DIR = NEW_DIR
+        final TO_DIR = FROM_DIR + "2"
+        fileSystem.add(new DirectoryEntry(FROM_DIR))
+        fileSystem.rename(FROM_DIR, TO_DIR)
+        assert !fileSystem.exists(FROM_DIR)
+        assert fileSystem.exists(TO_DIR)
+    }
+
+    void testRename_DirectoryContainsFiles() {
+        fileSystem.add(new DirectoryEntry(NEW_DIR))
+        fileSystem.add(new FileEntry(NEW_DIR + "/a.txt"))
+        fileSystem.add(new FileEntry(NEW_DIR + "/b.txt"))
+        fileSystem.add(new DirectoryEntry(NEW_DIR + "/subdir"))
+
+        final String TO_DIR = NEW_DIR + "2"
+        fileSystem.rename(NEW_DIR, TO_DIR)
+        assert !fileSystem.exists(NEW_DIR)
+        assert !fileSystem.exists(NEW_DIR + "/a.txt")
+        assert !fileSystem.exists(NEW_DIR + "/b.txt")
+        assert !fileSystem.exists(NEW_DIR + "/subdir")
+
+        assert fileSystem.exists(TO_DIR)
+        assert fileSystem.exists(TO_DIR + "/a.txt")
+        assert fileSystem.exists(TO_DIR + "/b.txt")
+        assert fileSystem.exists(TO_DIR + "/subdir")
+    }
+
+    void testRename_ParentOfToPathDoesNotExist() {
+        final String FROM_FILE = NEW_FILE
+        final String TO_FILE = fileSystem.path(NO_SUCH_DIR, "abc")
+        fileSystem.add(new FileEntry(FROM_FILE))
+
+        shouldThrowFileSystemExceptionWithMessageKey('filesystem.parentDirectoryDoesNotExist') {
+            fileSystem.rename(FROM_FILE, TO_FILE)
+        }
+        assert fileSystem.exists(FROM_FILE)
+        assert !fileSystem.exists(TO_FILE)
+    }
+
+    void testGetParent_Null() {
+        shouldFailWithMessageContaining("path") { fileSystem.getParent(null) }
+    }
+
+    //-------------------------------------------------------------------------
+    // Test setup
+    //-------------------------------------------------------------------------
+
+    void setUp() {
+        super.setUp()
+        fileSystem = createFileSystem()
+    }
+
+    //-------------------------------------------------------------------------
+    // Helper Methods
+    //-------------------------------------------------------------------------
+
+    protected void shouldThrowFileSystemExceptionWithMessageKey(String messageKey, Closure closure) {
+        def e = shouldThrow(FileSystemException, closure)
+        assert e.messageKey == messageKey, "Expected message key [$messageKey], but was [${e.messageKey}]"
+    }
+    
+    private verifyEntries(List expected, List actual) {
+        expected.eachWithIndex {entry, index ->
+            def entryStr = entry.toString()
+            LOG.info("expected=$entryStr")
+            assert actual.find {actualEntry -> actualEntry.toString() == entryStr }
+        }
+    }
+
+    protected void assertSameIgnoringOrder(list1, list2) {
+        LOG.info("Comparing $list1 to $list2")
+        assert list1 as Set == list2 as Set, "list1=$list1  list2=$list2"
+    }
+
+    /**
+     * Return a new instance of the FileSystem implementation class under test
+     * @return a new FileSystem instance
+     * @throws Exception
+     */
+    protected abstract FileSystem createFileSystem()
+
+    /**
+     * Verify the contents of the file at the specified path read from its InputSteam
+     *
+     * @param fileSystem - the FileSystem instance
+     * @param expectedContents - the expected contents
+     * @throws IOException
+     */
+    protected abstract void verifyFileContents(FileSystem fileSystem, String path, String contents) throws Exception
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/DirectoryEntryTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/DirectoryEntryTest.groovy
new file mode 100644
index 0000000..ee755db
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/DirectoryEntryTest.groovy
@@ -0,0 +1,65 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.filesystem

+

+/**

+ * Tests for DirectoryEntry

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+public class DirectoryEntryTest extends AbstractFileSystemEntryTestCase {

+

+    private DirectoryEntry entry

+

+    void testCloneWithNewPath() {

+        entry.lastModified = LAST_MODIFIED

+        entry.owner = USER

+        entry.group = GROUP

+        entry.permissions = PERMISSIONS

+        def clone = entry.cloneWithNewPath(NEW_PATH)

+

+        assert !clone.is(entry)

+        assert clone.path == NEW_PATH

+        assert clone.lastModified == LAST_MODIFIED

+        assert clone.owner == USER

+        assert clone.group == GROUP

+        assert clone.permissions == PERMISSIONS

+        assert clone.size == 0

+        assert clone.directory

+    }

+

+    /**

+     * @see org.mockftpserver.fake.filesystem.AbstractFileSystemEntryTestCase#getImplementationClass()

+     */

+    protected Class getImplementationClass() {

+        return DirectoryEntry.class

+    }

+

+    /**

+     * @see org.mockftpserver.fake.filesystem.AbstractFileSystemEntryTestCase#isDirectory()

+     */

+    protected boolean isDirectory() {

+        return true

+    }

+

+    void setUp() {

+        super.setUp()

+        entry = new DirectoryEntry(PATH)

+    }

+

+}

diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/FileEntryTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/FileEntryTest.groovy
new file mode 100644
index 0000000..a58a1b7
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/FileEntryTest.groovy
@@ -0,0 +1,216 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.filesystem

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.util.IoUtil

+

+/**

+ * Tests for FileEntry

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+public class FileEntryTest extends AbstractFileSystemEntryTestCase {

+

+    private static final LOG = LoggerFactory.getLogger(FileEntryTest)

+    private static final CONTENTS = "abc 123 %^& xxx"

+

+    private FileEntry entry

+

+    void testConstructorWithStringContents() {

+        entry = new FileEntry(PATH, CONTENTS)

+        verifyContents(CONTENTS)

+    }

+

+    void testSettingContentsFromString() {

+        entry.setContents(CONTENTS)

+        verifyContents(CONTENTS)

+    }

+

+    void testSettingContentsFromBytes() {

+        byte[] contents = CONTENTS.getBytes()

+        entry.setContents(contents)

+        // Now corrupt the original byte array to make sure the file entry is not affected

+        contents[1] = (byte) '#'

+        verifyContents(CONTENTS)

+    }

+

+    void testSetContents_BytesNotInCharSet() {

+        byte[] contents = [65, -99, 91, -115] as byte[]

+        entry.setContents(contents)

+        verifyContents(contents)

+    }

+

+    void testSetContents_NullString() {

+        entry.setContents((String) null)

+        assert entry.size == 0

+    }

+

+    void testSetContents_NullBytes() {

+        entry.setContents((byte[]) null)

+        assert entry.size == 0

+    }

+

+    void testCreateOutputStream() {

+        // New, empty file

+        OutputStream out = entry.createOutputStream(false)

+        out.write(CONTENTS.getBytes())

+        verifyContents(CONTENTS)

+

+        // Another OutputStream, append=false

+        out = entry.createOutputStream(false)

+        out.write(CONTENTS.getBytes())

+        verifyContents(CONTENTS)

+

+        // Another OutputStream, append=true

+        out = entry.createOutputStream(true)

+        out.write(CONTENTS.getBytes())

+        verifyContents(CONTENTS + CONTENTS)

+

+        // Set contents directly

+        final String NEW_CONTENTS = ",./'\t\r[]-\n="

+        entry.setContents(NEW_CONTENTS)

+        verifyContents(NEW_CONTENTS)

+

+        // New OutputStream, append=true (so should append to contents we set directly)

+        out = entry.createOutputStream(true)

+        out.write(CONTENTS.getBytes())

+        verifyContents(NEW_CONTENTS + CONTENTS)

+

+        // Yet another OutputStream, append=true (so should append to accumulated contents)

+        OutputStream out2 = entry.createOutputStream(true)

+        out2.write(CONTENTS.getBytes())

+        out2.close()       // should have no effect

+        verifyContents(NEW_CONTENTS + CONTENTS + CONTENTS)

+

+        // Write with the previous OutputStream (simulate 2 OututStreams writing "concurrently")

+        out.write(NEW_CONTENTS.getBytes())

+        verifyContents(NEW_CONTENTS + CONTENTS + CONTENTS + NEW_CONTENTS)

+    }

+

+    void testCreateInputStream_NullContents() {

+        verifyContents("")

+    }

+

+    void testCloneWithNewPath() {

+        entry.lastModified = LAST_MODIFIED

+        entry.owner = USER

+        entry.group = GROUP

+        entry.permissions = PERMISSIONS

+        entry.setContents('abc')

+        def clone = entry.cloneWithNewPath(NEW_PATH)

+

+        assert !clone.is(entry)

+        assert clone.path == NEW_PATH

+        assert clone.lastModified == LAST_MODIFIED

+        assert clone.owner == USER

+        assert clone.group == GROUP

+        assert clone.permissions == PERMISSIONS

+        assert clone.createInputStream().text == 'abc'

+        assert !clone.directory

+    }

+

+    void testCloneWithNewPath_WriteToOutputStream() {

+        def outputStream = entry.createOutputStream(false)

+        outputStream.withWriter { writer -> writer.write('ABCDEF') }

+        def clone = entry.cloneWithNewPath(NEW_PATH)

+

+        assert !clone.is(entry)

+        assert clone.path == NEW_PATH

+        assert clone.createInputStream().text == 'ABCDEF' 

+        assert !clone.directory

+    }

+

+//    void testEquals() {

+//        assert entry.equals(entry)

+//        assert entry.equals(new FileEntry(path:PATH, lastModified:LAST_MODIFIED))

+//        assert entry.equals(new FileEntry(path:PATH, lastModified:new Date())) // lastModified ignored

+//

+//        assert !entry.equals(new FileEntry("xyz", lastModified:LAST_MODIFIED))

+//        assert !entry.equals(new FileEntry(path:PATH, contents:'abc', lastModified:LAST_MODIFIED))

+//        assert !entry.equals("ABC")

+//        assert !entry.equals(null)

+//    }

+//

+//    void testHashCode() {

+//        assert entry.hashCode() == entry.hashCode()

+//        assert entry.hashCode() == new FileEntry(path:PATH, contents:'abc', lastModified:LAST_MODIFIED).hashCode()

+//        assert entry.hashCode() == new FileEntry(path:PATH, contents:'abc', new Date()).hashCode()  // lastModified ignored

+//

+//        assert entry.hashCode() != new FileEntry(path:PATH, contents:'abc', lastModified:LAST_MODIFIED).hashCode()

+//        assert entry.hashCode() != new FileEntry(path:PATH, contents:'abcdef', lastModified:LAST_MODIFIED).hashCode()

+//

+//        assert entry.hashCode() == new DirectoryEntry(path:PATH, lastModified:LAST_MODIFIED).hashCode()

+//    }

+

+    //-------------------------------------------------------------------------

+    // Implementation of Required Abstract Methods

+    //-------------------------------------------------------------------------

+

+    /**

+     * @see org.mockftpserver.fake.filesystem.AbstractFileSystemEntryTestCase#getImplementationClass()

+     */

+    protected Class getImplementationClass() {

+        return FileEntry.class

+    }

+

+    /**

+     * @see org.mockftpserver.fake.filesystem.AbstractFileSystemEntryTestCase#isDirectory()

+     */

+    protected boolean isDirectory() {

+        return false

+    }

+

+    //-------------------------------------------------------------------------

+    // Test setup

+    //-------------------------------------------------------------------------

+

+    void setUp() {

+        super.setUp()

+        entry = new FileEntry(PATH)

+    }

+

+    //-------------------------------------------------------------------------

+    // Internal Helper Methods

+    //-------------------------------------------------------------------------

+

+    /**

+     * Verify the expected contents of the file entry, read from its InputSteam

+     * @param expectedContents - the expected contents, as a String

+     * @throws IOException

+     */

+    private void verifyContents(String expectedContents) {

+        LOG.info("expectedContents=$expectedContents")

+        verifyContents(expectedContents.bytes)

+    }

+

+    /**

+     * Verify the expected contents of the file entry, read from its InputSteam

+     * @param expectedContents - the expected contents, as a byte[]

+     * @throws IOException

+     */

+    private void verifyContents(byte[] expectedContents) {

+        byte[] bytes = IoUtil.readBytes(entry.createInputStream())

+        def bytesAsList = bytes as List

+        LOG.info("bytes=$bytesAsList")

+        assert bytes == expectedContents, "actual=$bytesAsList  expected=${expectedContents as byte[]}"

+        assert entry.getSize() == expectedContents.length

+    }

+

+}

diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/PermissionsTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/PermissionsTest.groovy
new file mode 100644
index 0000000..343df87
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/PermissionsTest.groovy
@@ -0,0 +1,102 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.filesystem

+

+import org.mockftpserver.test.AbstractGroovyTestCase

+

+/**

+ * Tests for the Permissions class

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class PermissionsTest extends AbstractGroovyTestCase {

+

+    void testConstructor() {

+        testConstructorWithValidString('rwxrwxrwx')

+        testConstructorWithValidString('rwxr--r--')

+        testConstructorWithValidString('---------')

+    }

+

+    void testConstructor_InvalidString() {

+        testConstructorWithInvalidString('')

+        testConstructorWithInvalidString('------')

+        testConstructorWithInvalidString('-')

+        testConstructorWithInvalidString('r')

+        testConstructorWithInvalidString('rwx')

+        testConstructorWithInvalidString('rwxrwxrw')

+        testConstructorWithInvalidString('123456789')

+        testConstructorWithInvalidString('rwxrZxrwx')

+        testConstructorWithInvalidString('--------Z')

+    }

+

+    void testCanReadWriteExecute() {

+        testCanReadWriteExecute('rwxrwxrwx', true, true, true, true, true, true, true, true, true)

+        testCanReadWriteExecute('r--r--r--', true, false, false, true, false, false, true, false, false)

+        testCanReadWriteExecute('-w-r----x', false, true, false, true, false, false, false, false, true)

+        testCanReadWriteExecute('---------', false, false, false, false, false, false, false, false, false)

+    }

+

+    void testHashCode() {

+        assert new Permissions('rwxrwxrwx').hashCode() == Permissions.DEFAULT.hashCode()

+        assert new Permissions('---------').hashCode() == Permissions.NONE.hashCode()

+    }

+

+    void testEquals() {

+        assert new Permissions('rwxrwxrwx').equals(Permissions.DEFAULT)

+        assert new Permissions('---------').equals(Permissions.NONE)

+        assert Permissions.NONE.equals(Permissions.NONE)

+

+        assert !(new Permissions('------rwx').equals(Permissions.NONE))

+        assert !Permissions.NONE.equals(null)

+        assert !Permissions.NONE.equals(123)

+    }

+

+    //--------------------------------------------------------------------------

+    // Helper Methods

+    //--------------------------------------------------------------------------

+

+    private testCanReadWriteExecute(rwxString,

+                                    canUserRead, canUserWrite, canUserExecute,

+                                    canGroupRead, canGroupWrite, canGroupExecute,

+                                    canWorldRead, canWorldWrite, canWorldExecute) {

+

+        def permissions = new Permissions(rwxString)

+        LOG.info("Testing can read/write/execute for $permissions")

+        assert permissions.canUserRead() == canUserRead

+        assert permissions.canUserWrite() == canUserWrite

+        assert permissions.canUserExecute() == canUserExecute

+        assert permissions.canGroupRead() == canGroupRead

+        assert permissions.canGroupWrite() == canGroupWrite

+        assert permissions.canGroupExecute() == canGroupExecute

+        assert permissions.canWorldRead() == canWorldRead

+        assert permissions.canWorldWrite() == canWorldWrite

+        assert permissions.canWorldExecute() == canWorldExecute

+    }

+

+    private testConstructorWithInvalidString(String string) {

+        LOG.info("Verifying invalid: [$string]")

+        shouldFail { new Permissions(string) }

+    }

+

+    private testConstructorWithValidString(String string) {

+        LOG.info("Verifying valid: [$string]")

+        def permissions = new Permissions(string)

+        LOG.info(permissions.toString())

+        assert permissions.asRwxString() == string

+    }

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/TestUnixFakeFileSystem.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/TestUnixFakeFileSystem.groovy
new file mode 100644
index 0000000..9d48cd8
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/TestUnixFakeFileSystem.groovy
@@ -0,0 +1,68 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.filesystem

+

+/**

+ * Test-only subclass of UnixFakeFileSystem. Groovy implementation enables access to metaclass.

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class TestUnixFakeFileSystem extends UnixFakeFileSystem {

+

+    Throwable addMethodException

+    Throwable renameMethodException

+    Throwable listNamesMethodException

+    Throwable listFilesMethodException

+    Throwable deleteMethodException

+

+    void add(FileSystemEntry entry) {

+        if (addMethodException) {

+            throw addMethodException

+        }

+        super.add(entry)

+    }

+

+    void rename(String fromPath, String toPath) {

+        if (renameMethodException) {

+            throw renameMethodException

+        }

+        super.rename(fromPath, toPath)

+    }

+

+    List listNames(String path) {

+        if (listNamesMethodException) {

+            throw listNamesMethodException

+        }

+        super.listNames(path)

+    }

+

+    List listFiles(String path) {

+        if (listFilesMethodException) {

+            throw listFilesMethodException

+        }

+        super.listFiles(path)

+    }

+

+    boolean delete(String path) {

+        if (deleteMethodException) {

+            throw deleteMethodException

+        }

+        super.delete(path)

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/UnixDirectoryListingFormatterTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/UnixDirectoryListingFormatterTest.groovy
new file mode 100644
index 0000000..fb10e5a
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/UnixDirectoryListingFormatterTest.groovy
@@ -0,0 +1,114 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.filesystem

+

+import java.text.SimpleDateFormat

+import org.mockftpserver.test.AbstractGroovyTestCase

+

+/**

+ * Tests for UnixDirectoryListingFormatter

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class UnixDirectoryListingFormatterTest extends AbstractGroovyTestCase {

+

+    static final FILE_NAME = "def.txt"

+    static final FILE_PATH = "/dir/$FILE_NAME"

+    static final DIR_NAME = "etc"

+    static final DIR_PATH = "/dir/$DIR_NAME"

+    static final OWNER = 'owner123'

+    static final GROUP = 'group456'

+    static final SIZE = 11L

+    static final LAST_MODIFIED = new Date()

+    static final FILE_PERMISSIONS = new Permissions('rw-r--r--')

+    static final DIR_PERMISSIONS = new Permissions('rwxr-xr-x')

+

+    private formatter

+    private lastModifiedFormatted

+    private defaultLocale

+

+    // "-rw-rw-r--    1 ftp      ftp           254 Feb 23  2007 robots.txt"

+    // "-rw-r--r--    1 ftp      ftp      30014925 Apr 15 00:19 md5.sums.gz"

+    // "-rwxr-xr-x   1 c096336  iawebgrp    5778 Dec  1  2005 FU_WyCONN_updateplanaccess.sql"

+    // "drwxr-xr-x   2 c096336  iawebgrp    8192 Nov  7  2006 tmp"

+    // "drwxr-xr-x   39 ftp      ftp          4096 Mar 19  2004 a"

+

+    void testFormat_File() {

+        def fileSystemEntry = new FileEntry(path: FILE_PATH, contents: '12345678901', lastModified: LAST_MODIFIED,

+                owner: OWNER, group: GROUP, permissions: FILE_PERMISSIONS)

+        LOG.info(fileSystemEntry.toString())

+        verifyFormat(fileSystemEntry, "-rw-r--r--  1 owner123 group456              11 $lastModifiedFormatted def.txt")

+    }

+

+    void testFormat_File_Defaults() {

+        def fileSystemEntry = new FileEntry(path: FILE_PATH, contents: '12345678901', lastModified: LAST_MODIFIED)

+        LOG.info(fileSystemEntry.toString())

+        verifyFormat(fileSystemEntry, "-rwxrwxrwx  1 none     none                  11 $lastModifiedFormatted def.txt")

+    }

+

+    void testFormat_File_NonEnglishDefaultLocale() {

+        Locale.setDefault(Locale.GERMAN)

+        def fileSystemEntry = new FileEntry(path: FILE_PATH, contents: '12345678901', lastModified: LAST_MODIFIED)

+        LOG.info(fileSystemEntry.toString())

+        verifyFormat(fileSystemEntry, "-rwxrwxrwx  1 none     none                  11 $lastModifiedFormatted def.txt")

+    }

+

+    void testFormat_File_NonEnglishLocale() {

+        formatter.setLocale(Locale.FRENCH)

+        def fileSystemEntry = new FileEntry(path: FILE_PATH, contents: '12345678901', lastModified: LAST_MODIFIED)

+        LOG.info(fileSystemEntry.toString())

+        def dateFormat = new SimpleDateFormat(UnixDirectoryListingFormatter.DATE_FORMAT, Locale.FRENCH)

+        def formattedDate = dateFormat.format(LAST_MODIFIED)

+        def result = formatter.format(fileSystemEntry)

+        assert result.contains(formattedDate)

+    }

+

+    void testFormat_Directory() {

+        def fileSystemEntry = new DirectoryEntry(path: DIR_PATH, lastModified: LAST_MODIFIED,

+                owner: OWNER, group: GROUP, permissions: DIR_PERMISSIONS)

+        LOG.info(fileSystemEntry.toString())

+        verifyFormat(fileSystemEntry, "drwxr-xr-x  1 owner123 group456               0 $lastModifiedFormatted etc")

+    }

+

+    void testFormat_Directory_Defaults() {

+        def fileSystemEntry = new DirectoryEntry(path: DIR_PATH, lastModified: LAST_MODIFIED)

+        LOG.info(fileSystemEntry.toString())

+        verifyFormat(fileSystemEntry, "drwxrwxrwx  1 none     none                   0 $lastModifiedFormatted etc")

+    }

+

+    void setUp() {

+        super.setUp()

+        formatter = new UnixDirectoryListingFormatter()

+        def dateFormat = new SimpleDateFormat(UnixDirectoryListingFormatter.DATE_FORMAT, Locale.ENGLISH)

+        lastModifiedFormatted = dateFormat.format(LAST_MODIFIED)

+        defaultLocale = Locale.default

+    }

+

+    void tearDown() {

+        super.tearDown()

+        Locale.setDefault(defaultLocale)

+    }

+

+    private void verifyFormat(FileSystemEntry fileSystemEntry, String expectedResult) {

+        def result = formatter.format(fileSystemEntry)

+        LOG.info("result=  [$result]")

+        LOG.info("expected=[$expectedResult]")

+        assert result == expectedResult

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/UnixFakeFileSystemTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/UnixFakeFileSystemTest.groovy
new file mode 100644
index 0000000..c50d988
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/UnixFakeFileSystemTest.groovy
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.filesystem
+
+/**
+ * Tests for UnixFakeFileSystem.
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class UnixFakeFileSystemTest extends AbstractFakeFileSystemTestCase {
+
+    private static final String SEP = "/"
+
+    UnixFakeFileSystemTest() {
+        // These need to be set in the constructor because these values are used in setUp()
+        NEW_DIR = SEP + NEW_DIRNAME
+        NEW_FILE = "/NewFile.txt"
+        EXISTING_DIR = "/"
+        EXISTING_FILE = "/ExistingFile.txt"
+        NO_SUCH_DIR = "/xx/yy"
+        NO_SUCH_FILE = "/xx/yy/zz.txt"
+    }
+
+
+    void testListNames_FromRoot() {
+        final DIR = '/'
+        final FILENAME = 'abc.txt'
+        final FILE = p(DIR, FILENAME)
+
+        assert !fileSystem.exists(FILE)
+        fileSystem.add(new FileEntry(FILE))
+        def names = fileSystem.listNames(DIR)
+        assert names.find { it == FILENAME }
+    }
+
+    void testPath() {
+        assert fileSystem.path(null, null) == ""
+        assert fileSystem.path(null, "abc") == "abc"
+        assert fileSystem.path("abc", null) == "abc"
+        assert fileSystem.path("", "") == ""
+        assert fileSystem.path("", "abc") == "abc"
+        assert fileSystem.path("abc", "") == "abc"
+        assert fileSystem.path("abc", "DEF") == "abc/DEF"
+        assert fileSystem.path("abc/", "def") == "abc/def"
+        assert fileSystem.path("/abc/", "def") == "/abc/def"
+        assert fileSystem.path("/ABC", "/def") == "/ABC/def"
+        assert fileSystem.path("abc", "/def") == "abc/def"
+        assert fileSystem.path("abc", "def/..") == "abc"
+        assert fileSystem.path("abc", "./def") == "abc/def"
+        assert fileSystem.path("abc/.", null) == "abc"
+    }
+
+    void testNormalize() {
+        assert fileSystem.normalize("/") == "/"
+        assert fileSystem.normalize("/aBc") == "/aBc"
+        assert fileSystem.normalize("/abc/DEF") == "/abc/DEF"
+        assert fileSystem.normalize("/Abc/def/..") == "/Abc"
+        assert fileSystem.normalize("/abc/def/../ghi") == "/abc/ghi"
+        assert fileSystem.normalize("/abc/def/.") == "/abc/def"
+        assert fileSystem.normalize("/abc/def/./gHI") == "/abc/def/gHI"
+    }
+
+    void testGetName() {
+        assert fileSystem.getName("/") == ""
+        assert fileSystem.getName("/aBC") == "aBC"
+        assert fileSystem.getName("/abc/def") == "def"
+        assert fileSystem.getName("/abc/def/../GHI") == "GHI"
+    }
+
+    public void testGetParent() {
+        assert fileSystem.getParent("/") == null
+        assert fileSystem.getParent("/abc") == "/"
+        assert fileSystem.getParent("/abc/def") == "/abc"
+    }
+
+    void testIsValidName() {
+        ["/abc",
+                "/test/",
+                "/ABC/def",
+                "/abc/d!ef",
+                "/abc/DEF/h(ij)!@#\$%^&*()-_+=~`,.<>?;:[]{}\\|abc",
+        ].each {
+            assert fileSystem.isValidName(it), "[$it]"
+        }
+
+        ["",
+                "abc",
+                "abc/def",
+                "a:/abc:",
+                "//a*bc",
+                "C:/?abc",
+        ].each {
+            assert !fileSystem.isValidName(it), "[$it]"
+        }
+    }
+
+    void testIsAbsolute() {
+        assert fileSystem.isAbsolute("/")
+        assert fileSystem.isAbsolute("/abc")
+
+        assert !fileSystem.isAbsolute("abc")
+        assert !fileSystem.isAbsolute("c:\\usr")
+
+        shouldFailWithMessageContaining("path") { fileSystem.isAbsolute(null) }
+    }
+
+    //-----------------------------------------------------------------------------------
+    // Helper Methods
+    //-----------------------------------------------------------------------------------
+
+    /**
+     * Return a new instance of the FileSystem implementation class under test
+     * @return a new FileSystem instance
+     */
+    protected FileSystem createFileSystem() {
+        UnixFakeFileSystem fs = new UnixFakeFileSystem()
+        fs.add(new DirectoryEntry(EXISTING_DIR))
+        fs.add(new FileEntry(EXISTING_FILE, EXISTING_FILE_CONTENTS))
+        assert fs.createParentDirectoriesAutomatically
+        fs.createParentDirectoriesAutomatically = false
+        return fs
+    }
+
+    protected Class getExpectedDirectoryListingFormatterClass() {
+        return UnixDirectoryListingFormatter
+    }
+
+}
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/WindowsDirectoryListingFormatterTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/WindowsDirectoryListingFormatterTest.groovy
new file mode 100644
index 0000000..04eed35
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/WindowsDirectoryListingFormatterTest.groovy
@@ -0,0 +1,81 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.filesystem

+

+import java.text.SimpleDateFormat

+import org.mockftpserver.test.AbstractGroovyTestCase

+

+/**

+ * Tests for WindowsDirectoryListingFormatter

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class WindowsDirectoryListingFormatterTest extends AbstractGroovyTestCase {

+

+    static final NAME = "def.txt"

+    static final PATH = "/dir/$NAME"

+    static final LAST_MODIFIED = new Date()

+    static final SIZE_WIDTH = WindowsDirectoryListingFormatter.SIZE_WIDTH

+

+    private formatter

+    private dateFormat

+    private lastModifiedFormatted

+    private defaultLocale

+

+    void testFormat_File() {

+        def fileEntry = new FileEntry(path: PATH, contents: 'abcd', lastModified: LAST_MODIFIED)

+        def sizeStr = 4.toString().padLeft(SIZE_WIDTH)

+        def expected = "$lastModifiedFormatted  $sizeStr  $NAME"

+        def result = formatter.format(fileEntry)

+        LOG.info("result=$result")

+        assert result == expected

+    }

+

+    void testFormat_Directory() {

+        def fileEntry = new DirectoryEntry(path: PATH, lastModified: LAST_MODIFIED)

+        def dirStr = "<DIR>".padRight(SIZE_WIDTH)

+        def expected = "$lastModifiedFormatted  $dirStr  $NAME"

+        def result = formatter.format(fileEntry)

+        LOG.info("result=$result")

+        assert result == expected

+    }

+

+    void testFormat_File_NonEnglishDefaultLocale() {

+        Locale.setDefault(Locale.GERMAN)

+        def fileEntry = new FileEntry(path: PATH, contents: 'abcd', lastModified: LAST_MODIFIED)

+        def sizeStr = 4.toString().padLeft(SIZE_WIDTH)

+        def expected = "$lastModifiedFormatted  $sizeStr  $NAME"

+        def result = formatter.format(fileEntry)

+        LOG.info("result=$result")

+        assert result == expected

+    }

+

+    void setUp() {

+        super.setUp()

+        formatter = new WindowsDirectoryListingFormatter()

+        dateFormat = new SimpleDateFormat(WindowsDirectoryListingFormatter.DATE_FORMAT)

+        lastModifiedFormatted = dateFormat.format(LAST_MODIFIED)

+        defaultLocale = Locale.default

+    }

+

+    void tearDown() {

+        super.tearDown()

+        Locale.setDefault(defaultLocale)

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/WindowsFakeFileSystemTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/WindowsFakeFileSystemTest.groovy
new file mode 100644
index 0000000..0c415d1
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/fake/filesystem/WindowsFakeFileSystemTest.groovy
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.fake.filesystem
+
+/**
+ * Tests for WindowsFakeFileSystem.
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+class WindowsFakeFileSystemTest extends AbstractFakeFileSystemTestCase {
+
+    private static final String SEP = "\\"
+
+    WindowsFakeFileSystemTest() {
+        // These need to be set in the constructor because these values are used in setUp()
+        NEW_DIR = "d:/" + NEW_DIRNAME
+        NEW_FILE = "d:/NewFile.txt"
+        EXISTING_DIR = "d:/"
+        EXISTING_FILE = "d:/ExistingFile.txt"
+        NO_SUCH_DIR = 'x:/xx/yy'
+        NO_SUCH_FILE = "x:/xx/yy/zz.txt"
+    }
+
+    // -------------------------------------------------------------------------
+    // Tests
+    // -------------------------------------------------------------------------
+
+    void testOtherRoots() {
+        final String X = "x:/"
+        final String Y = "y:\\"
+        assertFalse(X, fileSystem.exists(X))
+        assertFalse(Y, fileSystem.exists(Y))
+
+        fileSystem.add(new DirectoryEntry(X))
+        fileSystem.add(new DirectoryEntry(Y))
+
+        assertTrue(X, fileSystem.exists(X))
+        assertTrue(Y, fileSystem.exists(Y))
+    }
+
+    void testPath() {
+        assert fileSystem.path(null, null) == ""
+        assert fileSystem.path(null, "abc") == "abc"
+        assert fileSystem.path("abc", null) == "abc"
+        assert fileSystem.path("", "") == ""
+        assert fileSystem.path("", "abc") == "abc"
+        assert fileSystem.path("abc", "") == "abc"
+        assert fileSystem.path("abc", "def") == "abc" + SEP + "def"
+        assert fileSystem.path("abc\\", "def") == "abc\\def"
+        assert fileSystem.path("c:/abc/", "def") == "c:\\abc\\def"
+        assert fileSystem.path("d:\\abc", "\\def") == "d:\\abc\\def"
+        assert fileSystem.path("abc", "/def") == "abc\\def"
+        assert fileSystem.path("abc/def", "..") == "abc"
+        assert fileSystem.path("abc", "def/..") == "abc"
+        assert fileSystem.path("abc", "./def") == "abc\\def"
+        assert fileSystem.path("abc/.", null) == "abc"
+    }
+
+    void testNormalize() {
+        assert fileSystem.normalize("a:\\") == "a:\\"
+        assert fileSystem.normalize("a:/") == "a:\\"
+        assert fileSystem.normalize("b:/abc") == path("b:", "abc")
+        assert fileSystem.normalize("c:\\abc\\def") == path("c:", "abc", "def")
+        assert fileSystem.normalize("d:/abc/def") == path("d:", "abc", "def")
+        assert fileSystem.normalize("e:\\abc/def/..") == path("e:", "abc")
+        assert fileSystem.normalize("f:/abc/def/../ghi") == path("f:", "abc", "ghi")
+        assert fileSystem.normalize("g:\\abc\\def\\.") == path("g:", "abc", "def")
+        assert fileSystem.normalize("h:/abc\\def\\./ghi") == path("h:", "abc", "def", "ghi")
+        assert fileSystem.normalize("c:\\abc").toLowerCase() == path("c:", "abc")
+        assert fileSystem.normalize("c:/abc").toLowerCase() == path("c:", "abc")
+        assert fileSystem.normalize("z:/abc").toLowerCase() == path("z:", "abc")
+    }
+
+    void testGetName() {
+        assert fileSystem.getName("l:\\") == ""
+        assert fileSystem.getName("m:\\abc") == "abc"
+        assert fileSystem.getName("n:/abc\\def") == "def"
+        assert fileSystem.getName("o:/abc/def") == "def"
+    }
+
+    public void testGetParent() {
+        assert fileSystem.getParent("p:/") == null
+        assert fileSystem.getParent("q:\\abc") == "q:\\"
+        assert fileSystem.getParent("r:/abc\\def") == path("r:", "abc")
+        assert fileSystem.getParent("s:\\abc/def") == path("s:", "abc")
+    }
+
+    void testIsValidName() {
+        // \/:*?"<>|
+        ["a:\\abc",
+                "c:/abc",
+                "d:/abc\\def",
+                "e:/abc\\d!ef",
+                "f:\\abc\\def\\h(ij)",
+                "g:\\abc",
+                "z:/abc/def",
+                "\\\\shared"
+        ].each {
+            assert fileSystem.isValidName(it), "[$it]"
+        }
+
+        ["",
+                "abc",
+                "abc/def",
+                "a:/abc:",
+                "B:\\a*bc",
+                "C:/?abc",
+                "D:\\abc/<def",
+                "E:/abc/def>",
+                "aa:\\abc",
+                "X:X:/abc",
+                ":\\abc\\def",
+                "X:\\\\abc"
+        ].each {
+            assert !fileSystem.isValidName(it), "[$it]"
+        }
+    }
+
+    void testIsAbsolute() {
+        assert fileSystem.isAbsolute("c:\\")
+        assert fileSystem.isAbsolute("x:\\Documents")
+        assert fileSystem.isAbsolute("a:/")
+        assert fileSystem.isAbsolute("\\\\shared\\docs")
+
+        assert !fileSystem.isAbsolute("abc")
+        assert !fileSystem.isAbsolute("/usr")
+        assert !fileSystem.isAbsolute("c:usr")
+
+        shouldFailWithMessageContaining("path") { fileSystem.isAbsolute(null) }
+    }
+
+    void testCaseInsensitive() {
+        def fileEntry = fileSystem.getEntry(EXISTING_FILE)
+        assert fileEntry
+        assert fileEntry == fileSystem.getEntry(EXISTING_FILE.toLowerCase())
+    }
+
+    //-------------------------------------------------------------------------
+    // Test setup
+    //-------------------------------------------------------------------------
+
+    void setUp() {
+        super.setUp()
+    }
+
+    protected Class getExpectedDirectoryListingFormatterClass() {
+        return WindowsDirectoryListingFormatter
+    }
+
+    //-----------------------------------------------------------------------------------
+    // Helper Methods
+    //-----------------------------------------------------------------------------------
+
+    /**
+     * Return a new instance of the FileSystem implementation class under test
+     *
+     * @return a new FileSystem instance
+     */
+    protected FileSystem createFileSystem() {
+        WindowsFakeFileSystem fs = new WindowsFakeFileSystem()
+        fs.add(new DirectoryEntry(EXISTING_DIR))
+        fs.add(new FileEntry(EXISTING_FILE, EXISTING_FILE_CONTENTS))
+        fs.createParentDirectoriesAutomatically = false
+        return fs
+    }
+
+    /**
+     * Return the specified paths concatenated with the system-dependent separator in between
+     * @param p1 - the first path
+     * @param p2 - the second path
+     * @return p1 + SEPARATOR + p2
+     */
+    private String path(String[] paths) {
+        return paths.join(SEP)
+    }
+}
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/stub/RunStubFtpServer.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/stub/RunStubFtpServer.groovy
new file mode 100644
index 0000000..6baa4ed
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/stub/RunStubFtpServer.groovy
@@ -0,0 +1,43 @@
+/*

+ * Copyright 2009 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub

+

+import org.mockftpserver.stub.StubFtpServer

+import org.mockftpserver.test.PortTestUtil

+import org.mockftpserver.stub.command.PwdCommandHandler

+import org.mockftpserver.core.command.CommandNames

+

+/**

+ * Run the StubFtpServer with a minimal configuration for interactive testing and exploration.

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class RunStubFtpServer {

+

+    static main(args) {

+        def stubFtpServer = new StubFtpServer();

+        stubFtpServer.setServerControlPort(PortTestUtil.getFtpServerControlPort());

+

+        stubFtpServer.getCommandHandler(CommandNames.PWD).setDirectory("/MyDir");

+

+        final LISTING = "11-09-01 12:30PM  406348 File2350.log\n" + "11-01-01 1:30PM <DIR> 0 archive"

+        stubFtpServer.getCommandHandler(CommandNames.LIST).setDirectoryListing(LISTING)

+        

+        stubFtpServer.start();

+    }

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/stub/StubFtpServer_RestartTest.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/stub/StubFtpServer_RestartTest.groovy
new file mode 100644
index 0000000..af13413
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/stub/StubFtpServer_RestartTest.groovy
@@ -0,0 +1,58 @@
+/*

+ * Copyright 2009 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub

+

+import org.mockftpserver.test.AbstractGroovyTestCase

+import org.apache.commons.net.ftp.FTPClient

+import org.mockftpserver.test.PortTestUtil

+

+/**

+ * Integration tests for restart of an StubFtpServer.

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+class StubFtpServer_RestartTest extends AbstractGroovyTestCase {

+    static final SERVER = "localhost"

+    private stubFtpServer

+    private ftpClient

+

+    void testRestart() {

+        stubFtpServer.start()

+        ftpClient.connect(SERVER, PortTestUtil.getFtpServerControlPort())

+        assert ftpClient.changeWorkingDirectory("dir1")

+

+        stubFtpServer.stop()

+        LOG.info("Restarting...")

+

+        stubFtpServer.start()

+        ftpClient.connect(SERVER, PortTestUtil.getFtpServerControlPort())

+        assert ftpClient.changeWorkingDirectory("dir1")

+    }

+

+    void setUp() {

+        super.setUp()

+        stubFtpServer = new StubFtpServer()

+        stubFtpServer.setServerControlPort(PortTestUtil.getFtpServerControlPort())

+        ftpClient = new FTPClient()

+    }

+

+    void tearDown() {

+        super.tearDown()

+        stubFtpServer.stop()

+    }

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/test/AbstractGroovyTestCase.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/test/AbstractGroovyTestCase.groovy
new file mode 100644
index 0000000..27ab7f7
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/test/AbstractGroovyTestCase.groovy
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2008 the original author or authors.
+ * 
+ * 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 org.mockftpserver.test
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.mockftpserver.test.LoggingUtil
+
+/**
+ * Abstract superclass for Groovy tests
+ *
+ * @version $Revision$ - $Date$
+ *
+ * @author Chris Mair
+ */
+abstract class AbstractGroovyTestCase extends GroovyTestCase {
+
+    protected final Logger LOG = LoggerFactory.getLogger(this.class)
+    private LoggingUtil testLogger
+
+    /**
+     * Write out the specified log message, prefixing with the current class name.
+     * @param message - the message to log; toString() is applied first
+     */
+    protected void log(message) {
+        println "[${classNameNoPackage()}] ${message.toString()}"
+    }
+
+    private String classNameNoPackage() {
+        def className = getClass().name
+        def index = className.lastIndexOf('.')
+        return index > -1 ? className.substring(index+1) : className
+    }
+    
+    /**
+     * Assert that the specified code throws an exception of the specified type.
+     * @param expectedExceptionClass - the Class of exception that is expected
+     * @param code - the Closure containing the code to be executed, which is expected to throw an exception of the specified type
+     * @return the thrown Exception instance
+     *
+     * @throws AssertionError - if no exception is thrown by the code or if the thrown exception is not of the expected type
+     */
+    protected Throwable shouldThrow(Class expectedExceptionClass, Closure code) {
+        def actualException = null
+        try {
+            code.call()
+        } catch (Throwable thrown) {
+            actualException = thrown
+        }
+        assert actualException, "No exception thrown. Expected [${expectedExceptionClass.getName()}]"
+        assert actualException.class == expectedExceptionClass, "Expected [${expectedExceptionClass.getName()}] but was [${actualException.class.name}]"
+        return actualException
+    }
+
+    /**
+     * Assert that the specified code throws an exception with an error message
+     * containing the specified text.
+     * @param text - the text expected within the exception message
+     * @param code - the Closure containing the code to be executed, which is expected to throw an exception of the specified type
+     * @return the message from the thrown Exception
+     *
+     * @throws AssertionError - if no exception is thrown by the code or if the thrown
+     * 	exception message does not contain the expected text
+     */
+    protected String shouldFailWithMessageContaining(String text, Closure code) {
+        def message = shouldFail(code)
+        assert message.contains(text), "message=[$message], text=[$text]"
+        return message
+    }
+
+    /**
+     * Return the specified paths concatenated with the path separator in between
+     * @param paths - the varargs list of path components to concatenate
+     * @return p[0] + '/' + p[1] + '/' + p[2] + ...
+     */
+    protected static String p(String[] paths) {
+        return paths.join("/").replace('\\', '/').replace("//", "/")
+    }
+
+    /**
+     * Create a new InetAddress from the specified host String, using the
+     * {@link InetAddress#getByName(String)}   method.
+     * @param host
+     * @return an InetAddress for the specified host
+     */
+    protected static InetAddress inetAddress(String host) {
+        return InetAddress.getByName(host);
+    }
+
+    //------------------------------------------------------------------------------------
+    // Test Setup and Tear Down
+    //------------------------------------------------------------------------------------
+
+    void setUp() {
+        testLogger = LoggingUtil.getTestCaseLogger(this)
+        testLogger.logStartOfTest()
+
+        super.setUp()
+    }
+
+    void tearDown() {
+        super.tearDown();
+        if (testLogger) {
+            testLogger.logEndOfTest()
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/groovy/org/mockftpserver/test/StubResourceBundle.groovy b/tags/2.5/src/test/groovy/org/mockftpserver/test/StubResourceBundle.groovy
new file mode 100644
index 0000000..90ce2a0
--- /dev/null
+++ b/tags/2.5/src/test/groovy/org/mockftpserver/test/StubResourceBundle.groovy
@@ -0,0 +1,44 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.test

+

+/**

+ * Stub implementation of ResourceBundle for testing. Provide an optional Map of entries in the constructor,

+ * and allow dynamic adding or changing of map contents. Automatically define default value for key if no entry

+ * exists for the key.

+ */

+class StubResourceBundle extends ResourceBundle {

+

+    Map map

+

+    StubResourceBundle(Map map = [:]) {

+        this.map = map

+    }

+

+    void put(String key, String value) {

+        map.put(key, value)

+    }

+

+    Object handleGetObject(String key) {

+        // Return default if no entry is defined

+        return map[key] ?: "key=$key arg0={0} arg1={1}".toString()

+    }

+

+    public Enumeration getKeys() {

+        return new Vector(map.keySet()).elements()

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/command/AbstractCommandHandlerTestCase.java b/tags/2.5/src/test/java/org/mockftpserver/core/command/AbstractCommandHandlerTestCase.java
new file mode 100644
index 0000000..6694c0f
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/command/AbstractCommandHandlerTestCase.java
@@ -0,0 +1,230 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.easymock.MockControl;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.text.MessageFormat;

+import java.util.ListResourceBundle;

+import java.util.ResourceBundle;

+

+/**

+ * Abstract superclass for CommandHandler tests

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractCommandHandlerTestCase extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(AbstractCommandHandlerTestCase.class);

+

+    // Some common test constants

+    protected static final String DIR1 = "dir1";

+    protected static final String DIR2 = "dir2";

+    protected static final String FILENAME1 = "sample1.txt";

+    protected static final String FILENAME2 = "sample2.txt";

+

+    protected Session session;

+    protected ResourceBundle replyTextBundle;

+

+    /**

+     * Test the handleCommand() method, when one or more parameter is missing or invalid

+     *

+     * @param commandHandler - the CommandHandler to test

+     * @param commandName    - the name for the Command

+     * @param parameters     - the Command parameters

+     */

+    protected void testHandleCommand_InvalidParameters(AbstractTrackingCommandHandler commandHandler,

+                                                       String commandName, String[] parameters) throws Exception {

+        Command command = new Command(commandName, parameters);

+        session.sendReply(ReplyCodes.COMMAND_SYNTAX_ERROR, replyTextFor(ReplyCodes.COMMAND_SYNTAX_ERROR));

+        replay(session);

+

+        commandHandler.handleCommand(command, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+

+    /**

+     * Verify that the CommandHandler contains the specified number of invocation records

+     *

+     * @param commandHandler - the CommandHandler

+     * @param expected       - the expected number of invocations

+     */

+    protected void verifyNumberOfInvocations(InvocationHistory commandHandler, int expected) {

+        assertEquals("number of invocations", expected, commandHandler.numberOfInvocations());

+    }

+

+    /**

+     * Verify that the InvocationRecord contains no data elements

+     *

+     * @param invocationRecord - the InvocationRecord

+     */

+    protected void verifyNoDataElements(InvocationRecord invocationRecord) {

+        LOG.info("Verifying: " + invocationRecord);

+        assertEquals("number of data elements", 0, invocationRecord.keySet().size());

+    }

+

+    /**

+     * Verify that the InvocationRecord contains exactly one data element, with the specified key

+     * and value.

+     *

+     * @param invocationRecord - the InvocationRecord

+     * @param key              - the expected key

+     * @param value            - the expected value

+     */

+    protected void verifyOneDataElement(InvocationRecord invocationRecord, String key, Object value) {

+        LOG.info("Verifying: " + invocationRecord);

+        assertEquals("number of data elements", 1, invocationRecord.keySet().size());

+        assertEqualsAllTypes("value:" + value, value, invocationRecord.getObject(key));

+    }

+

+    /**

+     * Verify that the InvocationRecord contains exactly two data element, with the specified keys

+     * and values.

+     *

+     * @param invocationRecord - the InvocationRecord

+     * @param key1             - the expected key1

+     * @param value1           - the expected value1

+     * @param key2             - the expected key2

+     * @param value2-          the expected value2

+     */

+    protected void verifyTwoDataElements(InvocationRecord invocationRecord, String key1, Object value1,

+                                         String key2, Object value2) {

+

+        LOG.info("Verifying: " + invocationRecord);

+        assertEquals("number of data elements", 2, invocationRecord.keySet().size());

+        assertEqualsAllTypes("value1:" + value1, value1, invocationRecord.getObject(key1));

+        assertEqualsAllTypes("value2:" + value2, value2, invocationRecord.getObject(key2));

+    }

+

+    /**

+     * Assert that the actual is equal to the expected, using arrays equality comparison if

+     * necessary

+     *

+     * @param message  - the message, used if the comparison fails

+     * @param expected - the expected value

+     * @param actual   - the actual value

+     */

+    private void assertEqualsAllTypes(String message, Object expected, Object actual) {

+

+        if (expected instanceof byte[] || actual instanceof byte[]) {

+            assertEquals(message, (byte[]) expected, (byte[]) actual);

+        } else if (expected instanceof Object[] || actual instanceof Object[]) {

+            assertEquals(message, (Object[]) expected, (Object[]) actual);

+        } else {

+            assertEquals(message, expected, actual);

+        }

+    }

+

+    /**

+     * Perform setup before each test

+     *

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+

+        session = (Session) createMock(Session.class);

+        control(session).setDefaultMatcher(MockControl.ARRAY_MATCHER);

+        control(session).expectAndDefaultReturn(session.getClientHost(), DEFAULT_HOST);

+

+        replyTextBundle = new ListResourceBundle() {

+            protected Object[][] getContents() {

+                return new Object[][]{

+                        {"150", replyTextFor(150)},

+                        {"200", replyTextFor(200)},

+                        {"211", replyTextWithParameterFor(211)},

+                        {"213", replyTextWithParameterFor(213)},

+                        {"214", replyTextWithParameterFor(214)},

+                        {"215", replyTextWithParameterFor(215)},

+                        {"220", replyTextFor(220)},

+                        {"221", replyTextFor(221)},

+                        {"226", replyTextFor(226)},

+                        {"226.WithFilename", replyTextWithParameterFor("226.WithFilename")},

+                        {"227", replyTextWithParameterFor(227)},

+                        {"229", replyTextWithParameterFor(229)},

+                        {"230", replyTextFor(230)},

+                        {"250", replyTextFor(250)},

+                        {"257", replyTextWithParameterFor(257)},

+                        {"331", replyTextFor(331)},

+                        {"350", replyTextFor(350)},

+                        {"501", replyTextFor(501)},

+                        {"502", replyTextFor(502)},

+                };

+            }

+        };

+    }

+

+    /**

+     * Return the test-specific reply text for the specified reply code

+     *

+     * @param replyCode - the reply code

+     * @return the reply text for the specified reply code

+     */

+    protected String replyTextFor(int replyCode) {

+        return "Reply for " + replyCode;

+    }

+

+    /**

+     * Return the test-specific parameterized reply text for the specified reply code

+     *

+     * @param replyCode - the reply code

+     * @return the reply text for the specified reply code

+     */

+    protected String replyTextWithParameterFor(int replyCode) {

+        return "Reply for " + replyCode + ":{0}";

+    }

+

+    /**

+     * Return the test-specific parameterized reply text for the specified messageKey

+     *

+     * @param messageKey - the messageKey

+     * @return the reply text for the specified messageKey

+     */

+    protected String replyTextWithParameterFor(String messageKey) {

+        return "Reply for " + messageKey + ":{0}";

+    }

+

+    /**

+     * Return the test-specific reply text for the specified reply code and message parameter

+     *

+     * @param replyCode - the reply code

+     * @param parameter - the message parameter value

+     * @return the reply text for the specified reply code

+     */

+    protected String formattedReplyTextFor(int replyCode, Object parameter) {

+        return MessageFormat.format(replyTextWithParameterFor(replyCode), objArray(parameter));

+    }

+

+    /**

+     * Return the test-specific reply text for the specified message key and message parameter

+     *

+     * @param messageKey - the messageKey

+     * @param parameter  - the message parameter value

+     * @return the reply text for the specified message key and parameter

+     */

+    protected String formattedReplyTextFor(String messageKey, Object parameter) {

+        return MessageFormat.format(replyTextWithParameterFor(messageKey), objArray(parameter));

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/command/CommandTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/command/CommandTest.java
new file mode 100644
index 0000000..9182046
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/command/CommandTest.java
@@ -0,0 +1,186 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.CommandSyntaxException;

+import org.mockftpserver.core.util.AssertFailedException;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.util.List;

+

+/**

+ * Tests for the Command class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class CommandTest extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(CommandTest.class);

+

+    /**

+     * Test the Command(String,String[]) constructor

+     */

+    public void testConstructor() {

+        final String[] PARAMETERS = array("123");

+        Command command = new Command("abc", PARAMETERS);

+        assertEquals("name", "abc", command.getName());

+        assertEquals("parameters", PARAMETERS, command.getParameters());

+    }

+

+    /**

+     * Test the Command(String,List) constructor

+     */

+    public void testConstructor_List() {

+        final List PARAMETERS_LIST = list("123");

+        final String[] PARAMETERS_ARRAY = array("123");

+        Command command = new Command("abc", PARAMETERS_LIST);

+        assertEquals("name", "abc", command.getName());

+        assertEquals("parameters String[]", PARAMETERS_ARRAY, command.getParameters());

+    }

+

+    /**

+     * Test the Constructor method, passing in a null name

+     */

+    public void testConstructor_NullName() {

+        try {

+            new Command(null, EMPTY);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the Constructor method, passing in a null parameters

+     */

+    public void testConstructor_NullParameters() {

+        try {

+            new Command("OK", (String[]) null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the normalizeName() method

+     */

+    public void testNormalizeName() {

+        assertEquals("XXX", "XXX", Command.normalizeName("XXX"));

+        assertEquals("xxx", "XXX", Command.normalizeName("xxx"));

+        assertEquals("Xxx", "XXX", Command.normalizeName("Xxx"));

+    }

+

+    /**

+     * Test the getRequiredParameter method

+     */

+    public void testGetRequiredParameter() {

+        Command command = new Command("abc", array("123", "456"));

+        assertEquals("123", "123", command.getRequiredParameter(0));

+        assertEquals("456", "456", command.getRequiredParameter(1));

+    }

+

+    /**

+     * Test the getRequiredParameter method, when the index is not valid

+     */

+    public void testGetRequiredParameter_IndexNotValid() {

+        Command command = new Command("abc", array("123", "456"));

+        try {

+            command.getRequiredParameter(2);

+            fail("Expected CommandSyntaxException");

+        }

+        catch (CommandSyntaxException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the getOptionalString method

+     */

+    public void testGetOptionalString() {

+        Command command = new Command("abc", array("123", "456"));

+        assertEquals("123", "123", command.getOptionalString(0));

+        assertEquals("456", "456", command.getOptionalString(1));

+        assertEquals("null", null, command.getOptionalString(2));

+    }

+

+    /**

+     * Test the getParameter method

+     */

+    public void testGetParameter() {

+        Command command = new Command("abc", array("123", "456"));

+        assertEquals("123", "123", command.getParameter(0));

+        assertEquals("456", "456", command.getParameter(1));

+        assertEquals("null", null, command.getParameter(2));

+    }

+

+    /**

+     * Test that a Command object is immutable, changing the original parameters passed in to the constructor

+     */

+    public void testImmutable_ChangeOriginalParameters() {

+        final String[] PARAMETERS = {"a", "b", "c"};

+        final Command COMMAND = new Command("command", PARAMETERS);

+        PARAMETERS[2] = "xxx";

+        assertEquals("parameters", COMMAND.getParameters(), new String[]{"a", "b", "c"});

+    }

+

+    /**

+     * Test that a Command object is immutable, changing the parameters returned from getParameters

+     */

+    public void testImmutable_ChangeRetrievedParameters() {

+        final String[] PARAMETERS = {"a", "b", "c"};

+        final Command COMMAND = new Command("command", PARAMETERS);

+        String[] parameters = COMMAND.getParameters();

+        parameters[2] = "xxx";

+        assertEquals("parameters", PARAMETERS, COMMAND.getParameters());

+    }

+

+    /**

+     * Test the equals() method, and tests the hasCode() method implicitly

+     *

+     * @throws Exception

+     */

+    public void testEquals() throws Exception {

+        final Command COMMAND1 = new Command("a", EMPTY);

+        final Command COMMAND2 = new Command("a", EMPTY);

+        final Command COMMAND3 = new Command("b", array("1"));

+        final Command COMMAND4 = new Command("b", array("2"));

+        final Command COMMAND5 = new Command("c", array("1"));

+        _testEquals(COMMAND1, null, false);

+        _testEquals(COMMAND1, COMMAND1, true);

+        _testEquals(COMMAND1, COMMAND2, true);

+        _testEquals(COMMAND1, COMMAND3, false);

+        _testEquals(COMMAND3, COMMAND4, false);

+        _testEquals(COMMAND3, COMMAND5, false);

+    }

+

+    /**

+     * Test that command1 equals command2 if and only if expectedEqual is true

+     *

+     * @param command1      - the first command

+     * @param command2      - the second command

+     * @param expectedEqual - true if command1 is expected to equal command2

+     */

+    private void _testEquals(Command command1, Command command2, boolean expectedEqual) {

+        assertEquals(command1.toString() + " and " + command2, expectedEqual, command1.equals(command2));

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/command/ConnectCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/command/ConnectCommandHandlerTest.java
new file mode 100644
index 0000000..127b403
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/command/ConnectCommandHandlerTest.java
@@ -0,0 +1,48 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+/**

+ * Tests for the ConnectCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class ConnectCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private ConnectCommandHandler commandHandler;

+    private Command command1;

+

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(ReplyCodes.CONNECT_OK, replyTextFor(ReplyCodes.CONNECT_OK));

+        replay(session);

+

+        commandHandler.handleCommand(command1, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new ConnectCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.CONNECT, EMPTY);

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/command/InvocationRecordTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/command/InvocationRecordTest.java
new file mode 100644
index 0000000..026a7e2
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/command/InvocationRecordTest.java
@@ -0,0 +1,226 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.util.AssertFailedException;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.util.Date;

+import java.util.HashSet;

+import java.util.Set;

+

+/**

+ * Tests for InvocationRecord

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class InvocationRecordTest extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(InvocationRecordTest.class);

+    private static final Command COMMAND = new Command("command", EMPTY);

+    private static final String KEY1 = "key1";

+    private static final String KEY2 = "key2";

+    private static final String STRING = "abc123";

+    private static final Integer INT = new Integer(77);

+

+    private InvocationRecord invocationRecord;

+

+    /**

+     * Test the Constructor

+     */

+    public void testConstructor() {

+        final Command COMMAND = new Command("ABC", EMPTY);

+        long beforeTime = System.currentTimeMillis();

+        InvocationRecord commandInvocation = new InvocationRecord(COMMAND, DEFAULT_HOST);

+        long afterTime = System.currentTimeMillis();

+        LOG.info(commandInvocation.toString());

+        assertEquals("Command", COMMAND, commandInvocation.getCommand());

+        assertTrue("time", commandInvocation.getTime().getTime() >= beforeTime

+                && commandInvocation.getTime().getTime() <= afterTime);

+        assertEquals("host", DEFAULT_HOST, commandInvocation.getClientHost());

+    }

+

+    /**

+     * Test the set() method, passing in a null key

+     */

+    public void testSet_NullKey() {

+        try {

+            invocationRecord.set(null, STRING);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the set() method, passing in a null value

+     */

+    public void testSet_NullValue() {

+        invocationRecord.set(KEY1, null);

+        assertNull(KEY1, invocationRecord.getObject(KEY1));

+    }

+

+    /**

+     * Test the containsKey() method

+     */

+    public void testContainsKey() {

+        invocationRecord.set(KEY1, STRING);

+        assertTrue(KEY1, invocationRecord.containsKey(KEY1));

+        assertFalse(KEY2, invocationRecord.containsKey(KEY2));

+    }

+

+    /**

+     * Test the containsKey() method, passing in a null key

+     */

+    public void testContainsKey_Null() {

+        try {

+            invocationRecord.containsKey(null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the getString() method

+     */

+    public void testGetString() {

+        assertNull("undefined", invocationRecord.getString("UNDEFINED"));

+        invocationRecord.set(KEY1, STRING);

+        assertEquals(KEY1, STRING, invocationRecord.getString(KEY1));

+    }

+

+    /**

+     * Test the getString() method, passing in a null key

+     */

+    public void testGetString_Null() {

+        try {

+            invocationRecord.getString(null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the getString() method, when the value for the key is not a String

+     */

+    public void testGetString_NotAString() {

+

+        invocationRecord.set(KEY1, INT);

+        try {

+            invocationRecord.getString(KEY1);

+            fail("Expected ClassCastException");

+        }

+        catch (ClassCastException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the getObject() method

+     */

+    public void testGetObject() {

+        assertNull("undefined", invocationRecord.getObject("UNDEFINED"));

+        invocationRecord.set(KEY1, STRING);

+        assertEquals(KEY1, STRING, invocationRecord.getObject(KEY1));

+    }

+

+    /**

+     * Test the keySet() method

+     */

+    public void testKeySet() {

+        Set set = new HashSet();

+        assertEquals("empty", set, invocationRecord.keySet());

+        invocationRecord.set(KEY1, STRING);

+        invocationRecord.set(KEY2, STRING);

+        set.add(KEY1);

+        set.add(KEY2);

+        assertEquals("2", set, invocationRecord.keySet());

+    }

+

+    /**

+     * Test that the keySet() return value does not allow breaking immutability   

+     */

+    public void testKeySet_Immutability() {

+        Set keySet = invocationRecord.keySet();

+        try {

+            keySet.add("abc");

+            fail("Expected UnsupportedOperationException");

+        }

+        catch (UnsupportedOperationException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the getObject() method, passing in a null key

+     */

+    public void testGetObject_Null() {

+        try {

+            invocationRecord.getObject(null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the lock() method 

+     */

+    public void testLock() {

+        assertFalse("locked - before", invocationRecord.isLocked());

+        invocationRecord.set(KEY1, STRING);

+        invocationRecord.lock();

+        assertTrue("locked - after", invocationRecord.isLocked());

+        try {

+            invocationRecord.set(KEY2, "abc");

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test that the getTime() return value does not break immutability   

+     */

+    public void testGetTime_Immutability() {

+        

+        Date timestamp = invocationRecord.getTime();

+        long timeInMillis = timestamp.getTime();

+        timestamp.setTime(12345L);

+        assertEquals("time", timeInMillis, invocationRecord.getTime().getTime());

+    }

+    

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        invocationRecord = new InvocationRecord(COMMAND, DEFAULT_HOST);

+    }

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/command/ReplyTextBundleUtilTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/command/ReplyTextBundleUtilTest.java
new file mode 100644
index 0000000..edb6384
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/command/ReplyTextBundleUtilTest.java
@@ -0,0 +1,104 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.util.AssertFailedException;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.util.ListResourceBundle;

+import java.util.ResourceBundle;

+

+/**

+ * Tests for the ReplyTextBundleUtil class.

+ * 

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+public final class ReplyTextBundleUtilTest extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(ReplyTextBundleUtilTest.class);

+    

+    private ResourceBundle resourceBundle1;

+    private ResourceBundle resourceBundle2;

+    

+    /**

+     * Test the setReplyTextBundleIfAppropriate() method, when the CommandHandler implements 

+     * the ResourceBundleAware interface, and the replyTextBundle has not yet been set. 

+     */

+    public void testSetReplyTextBundleIfAppropriate_ReplyTextBundleAware_NotSetYet() {

+        AbstractTrackingCommandHandler commandHandler = new StaticReplyCommandHandler();

+        ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, resourceBundle1);

+        assertSame(resourceBundle1, commandHandler.getReplyTextBundle());

+    }

+

+    /**

+     * Test the setReplyTextBundleIfAppropriate() method, when the CommandHandler implements 

+     * the ResourceBundleAware interface, and the replyTextBundle has already been set. 

+     */

+    public void testSetReplyTextBundleIfAppropriate_ReplyTextBundleAware_AlreadySet() {

+        AbstractTrackingCommandHandler commandHandler = new StaticReplyCommandHandler();

+        commandHandler.setReplyTextBundle(resourceBundle2);

+        ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, resourceBundle1);

+        assertSame(resourceBundle2, commandHandler.getReplyTextBundle());

+    }

+

+    /**

+     * Test the setReplyTextBundleIfAppropriate() method, when the CommandHandler does not 

+     * implement the ResourceBundleAware interface. 

+     */

+    public void testSetReplyTextBundleIfAppropriate_NotReplyTextBundleAware() {

+        CommandHandler commandHandler = (CommandHandler) createMock(CommandHandler.class);

+        replay(commandHandler);

+        ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, resourceBundle1);

+        verify(commandHandler);         // expect no method calls

+    }

+    

+    /**

+     * Test the setReplyTextBundleIfAppropriate() method, when the CommandHandler is null. 

+     */

+    public void testSetReplyTextBundleIfAppropriate_NullCommandHandler() {

+        try {

+            ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(null, resourceBundle1);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+

+        resourceBundle1 = new ListResourceBundle() {

+            protected Object[][] getContents() {

+                return null;

+            }

+        };

+

+        resourceBundle2 = new ListResourceBundle() {

+            protected Object[][] getContents() {

+                return new Object[][] { { "a", "b" } };

+            }

+        };

+    }

+    

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/command/SimpleCompositeCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/command/SimpleCompositeCommandHandlerTest.java
new file mode 100644
index 0000000..6635afa
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/command/SimpleCompositeCommandHandlerTest.java
@@ -0,0 +1,269 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.AssertFailedException;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.util.ArrayList;

+import java.util.List;

+import java.util.ListResourceBundle;

+import java.util.ResourceBundle;

+

+/**

+ * Tests for SimpleCompositeCommandHandler

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public class SimpleCompositeCommandHandlerTest extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(SimpleCompositeCommandHandlerTest.class);

+    

+    private SimpleCompositeCommandHandler simpleCompositeCommandHandler;

+    private Session session;

+    private Command command;

+    private CommandHandler commandHandler1;

+    private CommandHandler commandHandler2;

+    private CommandHandler commandHandler3;

+    

+    /**

+     * Test the handleCommand() method 

+     */

+    public void testHandleCommand_OneHandler_OneInvocation() throws Exception {

+        simpleCompositeCommandHandler.addCommandHandler(commandHandler1);

+        

+        commandHandler1.handleCommand(command, session);

+        replay(commandHandler1);

+        

+        simpleCompositeCommandHandler.handleCommand(command, session);

+        verify(commandHandler1);

+    }

+    

+    /**

+     * Test the handleCommand() method, with two CommandHandler defined, but with multiple invocation 

+     */

+    public void testHandleCommand_TwoHandlers() throws Exception {

+        simpleCompositeCommandHandler.addCommandHandler(commandHandler1);

+        simpleCompositeCommandHandler.addCommandHandler(commandHandler2);

+        

+        commandHandler1.handleCommand(command, session);

+        commandHandler2.handleCommand(command, session);

+        replayAll();

+        

+        simpleCompositeCommandHandler.handleCommand(command, session);

+        simpleCompositeCommandHandler.handleCommand(command, session);

+        verifyAll();

+    }

+    

+    /**

+     * Test the handleCommand() method, with three CommandHandler defined, and multiple invocation 

+     */

+    public void testHandleCommand_ThreeHandlers() throws Exception {

+        

+        List list = new ArrayList();

+        list.add(commandHandler1);

+        list.add(commandHandler2);

+        list.add(commandHandler3);

+        simpleCompositeCommandHandler.setCommandHandlers(list);

+        

+        commandHandler1.handleCommand(command, session);

+        commandHandler2.handleCommand(command, session);

+        commandHandler3.handleCommand(command, session);

+        replayAll();

+        

+        simpleCompositeCommandHandler.handleCommand(command, session);

+        simpleCompositeCommandHandler.handleCommand(command, session);

+        simpleCompositeCommandHandler.handleCommand(command, session);

+        verifyAll();

+    }

+    

+    /**

+     * Test the handleCommand() method, with a single CommandHandler defined, but too many invocations 

+     */

+    public void testHandleCommand_OneHandler_TooManyInvocations() throws Exception {

+        simpleCompositeCommandHandler.addCommandHandler(commandHandler1);

+        

+        commandHandler1.handleCommand(command, session);

+        replay(commandHandler1);

+        

+        simpleCompositeCommandHandler.handleCommand(command, session);

+

+        // Second invocation throws an exception

+        try {

+            simpleCompositeCommandHandler.handleCommand(command, session);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the handleCommand_NoHandlersDefined() method 

+     */

+    public void testHandleCommand_NoHandlersDefined() throws Exception {

+        try {

+            simpleCompositeCommandHandler.handleCommand(command, session);

+            fail("Expected AssertFailedException");

+        }

+        catch(AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the handleCommand(Command,Session) method, passing in a null Command

+     */

+    public void testHandleCommand_NullCommand() throws Exception {

+        try {

+            simpleCompositeCommandHandler.handleCommand(null, session);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the handleCommand(Command,Session) method, passing in a null Session

+     */

+    public void testHandleCommand_NullSession() throws Exception {

+        try {

+            simpleCompositeCommandHandler.handleCommand(command, null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the addCommandHandler(CommandHandler) method, passing in a null CommandHandler

+     */

+    public void testAddCommandHandler_NullCommandHandler() throws Exception {

+        try {

+            simpleCompositeCommandHandler.addCommandHandler(null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the setCommandHandlers(List) method, passing in a null

+     */

+    public void testSetCommandHandlers_Null() throws Exception {

+        try {

+            simpleCompositeCommandHandler.setCommandHandlers(null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the getCommandHandler(int) method, passing in an index for which no CommandHandler is defined

+     */

+    public void testGetCommandHandler_UndefinedIndex() throws Exception {

+        simpleCompositeCommandHandler.addCommandHandler(commandHandler1);

+        try {

+            simpleCompositeCommandHandler.getCommandHandler(1);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the getCommandHandler(int) method

+     */

+    public void testGetCommandHandler() throws Exception {

+        simpleCompositeCommandHandler.addCommandHandler(commandHandler1);

+        simpleCompositeCommandHandler.addCommandHandler(commandHandler2);

+        assertSame("index 0", commandHandler1, simpleCompositeCommandHandler.getCommandHandler(0));

+        assertSame("index 1", commandHandler2, simpleCompositeCommandHandler.getCommandHandler(1));

+    }

+    

+    /**

+     * Test the getCommandHandler(int) method, passing in a negative index

+     */

+    public void testGetCommandHandler_NegativeIndex() throws Exception {

+        simpleCompositeCommandHandler.addCommandHandler(commandHandler1);

+        try {

+            simpleCompositeCommandHandler.getCommandHandler(-1);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the getReplyTextBundle() method

+     */

+    public void testGetReplyTextBundle() {

+        assertNull(simpleCompositeCommandHandler.getReplyTextBundle());

+    }

+    

+    /**

+     * Test the setReplyTextBundle() method

+     */

+    public void testSetReplyTextBundle() {

+        

+        AbstractTrackingCommandHandler replyTextBundleAwareCommandHandler1 = new StaticReplyCommandHandler();

+        AbstractTrackingCommandHandler replyTextBundleAwareCommandHandler2 = new StaticReplyCommandHandler();

+        simpleCompositeCommandHandler.addCommandHandler(replyTextBundleAwareCommandHandler1);

+        simpleCompositeCommandHandler.addCommandHandler(commandHandler1);

+        simpleCompositeCommandHandler.addCommandHandler(replyTextBundleAwareCommandHandler2);

+        

+        ResourceBundle resourceBundle = new ListResourceBundle() {

+            protected Object[][] getContents() {

+                return null;

+            }

+        };

+        

+        simpleCompositeCommandHandler.setReplyTextBundle(resourceBundle);

+        assertSame("1", resourceBundle, replyTextBundleAwareCommandHandler1.getReplyTextBundle());

+        assertSame("2", resourceBundle, replyTextBundleAwareCommandHandler1.getReplyTextBundle());

+    }

+    

+    //-------------------------------------------------------------------------

+    // Test setup

+    //-------------------------------------------------------------------------

+    

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        simpleCompositeCommandHandler = new SimpleCompositeCommandHandler();

+        session = (Session) createMock(Session.class);

+        command = new Command("cmd", EMPTY);

+        commandHandler1 = (CommandHandler) createMock(CommandHandler.class);

+        commandHandler2 = (CommandHandler) createMock(CommandHandler.class);

+        commandHandler3 = (CommandHandler) createMock(CommandHandler.class);

+    }

+    

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/command/StaticReplyCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/command/StaticReplyCommandHandlerTest.java
new file mode 100644
index 0000000..634f182
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/command/StaticReplyCommandHandlerTest.java
@@ -0,0 +1,140 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.util.AssertFailedException;

+

+/**

+ * Tests for the StaticReplyCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class StaticReplyCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(StaticReplyCommandHandlerTest.class);

+    private static final int REPLY_CODE = 999;

+    private static final String REPLY_TEXT = "some text 123";

+    private static final Command COMMAND = new Command("ANY", EMPTY);

+    

+    private StaticReplyCommandHandler commandHandler;

+    

+    /**

+     * Test the constructor that takes a replyCode, passing in a null

+     */

+    public void testConstructor_String_InvalidReplyCode() {

+        try {

+            new StaticReplyCommandHandler(-1);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the constructor that takes a replyCode and replyText, passing in a null replyCode

+     */

+    public void testConstructor_StringString_InvalidReplyCode() {

+        try {

+            new StaticReplyCommandHandler(-99, "text");

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the setReplyCode() method, passing in a null

+     */

+    public void testSetReplyCode_Invalid() {

+        try {

+            commandHandler.setReplyCode(-1);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the handleCommand() method when the replyText attribute has not been set.

+     * So, use whatever replyText has been configured in the replyCodeMapping

+     * @throws Exception

+     */

+    public void testHandleCommand_ReplyTextNotSet() throws Exception {

+        commandHandler.setReplyCode(250);

+        

+        session.sendReply(250, replyTextFor(250));

+        replay(session);

+        

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+    

+    /**

+     * Test the handleCommand() method, when the replyCode and replyText are both set

+     * @throws Exception

+     */

+    public void testHandleCommand_SetReplyText() throws Exception {

+        commandHandler.setReplyCode(REPLY_CODE);

+        commandHandler.setReplyText(REPLY_TEXT);

+        

+        session.sendReply(REPLY_CODE, REPLY_TEXT);

+        replay(session);

+        

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+    

+    /**

+     * Test the handleCommand() method when the replyCode attribute has not been set

+     * @throws Exception

+     */

+    public void testHandleCommand_ReplyCodeNotSet() throws Exception {

+

+        try {

+            commandHandler.handleCommand(COMMAND, session);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+        

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+    

+    /**

+     * @see AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new StaticReplyCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+    

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/command/UnsupportedCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/command/UnsupportedCommandHandlerTest.java
new file mode 100644
index 0000000..cd6d84a
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/command/UnsupportedCommandHandlerTest.java
@@ -0,0 +1,48 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+/**

+ * Tests for the UnsupportedCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class UnsupportedCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private UnsupportedCommandHandler commandHandler;

+    private Command command1;

+

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(ReplyCodes.COMMAND_NOT_SUPPORTED, replyTextFor(ReplyCodes.COMMAND_NOT_SUPPORTED));

+        replay(session);

+

+        commandHandler.handleCommand(command1, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new UnsupportedCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command("XXXX", EMPTY);

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/command/_AbstractCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/command/_AbstractCommandHandlerTest.java
new file mode 100644
index 0000000..f356c88
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/command/_AbstractCommandHandlerTest.java
@@ -0,0 +1,126 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.easymock.MockControl;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.AssertFailedException;

+import org.mockftpserver.stub.command.AbstractStubCommandHandler;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.util.ListResourceBundle;

+import java.util.ResourceBundle;

+

+/**

+ * Tests for the AbstractCommandHandler class. The class name is prefixed with an

+ * underscore so that it is not filtered out by Maven's Surefire test plugin.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class _AbstractCommandHandlerTest extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(_AbstractTrackingCommandHandlerTest.class);

+    private static final int REPLY_CODE1 = 777;

+    private static final int REPLY_CODE2 = 888;

+    private static final String REPLY_TEXT1 = "reply1 ... abcdef";

+    private static final String REPLY_TEXT2 = "abc {0} def";

+    private static final String MESSAGE_KEY = "key.123";

+    private static final String MESSAGE_TEXT = "message.123";

+

+    private AbstractCommandHandler commandHandler;

+

+    /**

+     * Test the quotes utility method

+     */

+    public void testQuotes() {

+        assertEquals("abc", "\"abc\"", AbstractStubCommandHandler.quotes("abc"));

+        assertEquals("<empty>", "\"\"", AbstractStubCommandHandler.quotes(""));

+    }

+

+    /**

+     * Test the quotes utility method, passing in a null

+     */

+    public void testQuotes_Null() {

+        try {

+            AbstractStubCommandHandler.quotes(null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the assertValidReplyCode() method

+     */

+    public void testAssertValidReplyCode() {

+        // These are valid, so expect no exceptions

+        commandHandler.assertValidReplyCode(1);

+        commandHandler.assertValidReplyCode(100);

+

+        // These are invalid

+        testAssertValidReplyCodeWithInvalid(0);

+        testAssertValidReplyCodeWithInvalid(-1);

+    }

+

+    /**

+     * Test the assertValidReplyCode() method , passing in an invalid replyCode value

+     *

+     * @param invalidReplyCode - a reply code that is expected to be invalid

+     */

+    private void testAssertValidReplyCodeWithInvalid(int invalidReplyCode) {

+        try {

+            commandHandler.assertValidReplyCode(invalidReplyCode);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    //-------------------------------------------------------------------------

+    // Test setup

+    //-------------------------------------------------------------------------

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        Session session = (Session) createMock(Session.class);

+        control(session).setDefaultMatcher(MockControl.ARRAY_MATCHER);

+        commandHandler = new AbstractCommandHandler() {

+            public void handleCommand(Command command, Session session) throws Exception {

+            }

+        };

+        ResourceBundle replyTextBundle = new ListResourceBundle() {

+            protected Object[][] getContents() {

+                return new Object[][]{

+                        {Integer.toString(REPLY_CODE1), REPLY_TEXT1},

+                        {Integer.toString(REPLY_CODE2), REPLY_TEXT2},

+                        {MESSAGE_KEY, MESSAGE_TEXT}

+                };

+            }

+        };

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/command/_AbstractStaticReplyCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/command/_AbstractStaticReplyCommandHandlerTest.java
new file mode 100644
index 0000000..c4a8e50
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/command/_AbstractStaticReplyCommandHandlerTest.java
@@ -0,0 +1,158 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.easymock.MockControl;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.AssertFailedException;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.util.ListResourceBundle;

+import java.util.ResourceBundle;

+

+/**

+ * Tests for the AbstractStaticReplyCommandHandler class. The class name is prefixed with an underscore

+ * so that it is not filtered out by Maven's Surefire test plugin.

+ *

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+public final class _AbstractStaticReplyCommandHandlerTest extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(_AbstractStaticReplyCommandHandlerTest.class);

+    private static final int REPLY_CODE1 = 777;

+    private static final int REPLY_CODE2 = 888;

+    private static final String REPLY_TEXT1 = "reply1 ... abcdef";

+    private static final String REPLY_TEXT2 = "abc {0} def";

+    private static final String REPLY_TEXT2_FORMATTED = "abc 123 def";

+    private static final String MESSAGE_KEY = "key.123";

+    private static final String MESSAGE_TEXT = "message.123";

+    private static final String OVERRIDE_REPLY_TEXT = "overridden reply ... abcdef";

+    private static final Object ARG = "123";

+

+    private AbstractStaticReplyCommandHandler commandHandler;

+    private Session session;

+

+    /**

+     * Test the sendReply(Session) method

+     */

+    public void testSendReply() {

+        session.sendReply(REPLY_CODE1, REPLY_TEXT1);

+        replay(session);

+

+        commandHandler.setReplyCode(REPLY_CODE1);

+        commandHandler.sendReply(session);

+        verify(session);

+    }

+

+    /**

+     * Test the sendReply(Session) method, when the replyText has been set

+     */

+    public void testSendReply_SetReplyText() {

+        session.sendReply(REPLY_CODE1, OVERRIDE_REPLY_TEXT);

+        replay(session);

+

+        commandHandler.setReplyCode(REPLY_CODE1);

+        commandHandler.setReplyText(OVERRIDE_REPLY_TEXT);

+        commandHandler.sendReply(session);

+        verify(session);

+    }

+

+    /**

+     * Test the sendReply(Session) method, when the replyMessageKey has been set

+     */

+    public void testSendReply_SetReplyMessageKey() {

+        session.sendReply(REPLY_CODE1, REPLY_TEXT2);

+        replay(session);

+

+        commandHandler.setReplyCode(REPLY_CODE1);

+        commandHandler.setReplyMessageKey(Integer.toString(REPLY_CODE2));

+        commandHandler.sendReply(session);

+        verify(session);

+    }

+

+    /**

+     * Test the sendReply(Session) method, when the replyCode has not been set

+     */

+    public void testSendReply_ReplyCodeNotSet() {

+        try {

+            commandHandler.sendReply(session);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the sendReply(Session,Object) method

+     */

+    public void testSendReply_MessageParameter() {

+        session.sendReply(REPLY_CODE2, REPLY_TEXT2);

+        session.sendReply(REPLY_CODE2, REPLY_TEXT2_FORMATTED);

+        replay(session);

+

+        commandHandler.setReplyCode(REPLY_CODE2);

+        commandHandler.sendReply(session);

+        commandHandler.sendReply(session, ARG);

+        verify(session);

+    }

+

+    /**

+     * Test the setReplyCode() method, passing in an invalid value

+     */

+    public void testSetReplyCode_Invalid() {

+        try {

+            commandHandler.setReplyCode(0);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    //-------------------------------------------------------------------------

+    // Test setup

+    //-------------------------------------------------------------------------

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        session = (Session) createMock(Session.class);

+        control(session).setDefaultMatcher(MockControl.ARRAY_MATCHER);

+        commandHandler = new AbstractStaticReplyCommandHandler() {

+            public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+            }

+        };

+        ResourceBundle replyTextBundle = new ListResourceBundle() {

+            protected Object[][] getContents() {

+                return new Object[][]{

+                        {Integer.toString(REPLY_CODE1), REPLY_TEXT1},

+                        {Integer.toString(REPLY_CODE2), REPLY_TEXT2},

+                        {MESSAGE_KEY, MESSAGE_TEXT}

+                };

+            }

+        };

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/command/_AbstractTrackingCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/command/_AbstractTrackingCommandHandlerTest.java
new file mode 100644
index 0000000..5cc98f2
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/command/_AbstractTrackingCommandHandlerTest.java
@@ -0,0 +1,222 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.easymock.MockControl;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.AssertFailedException;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.util.ListResourceBundle;

+import java.util.ResourceBundle;

+

+/**

+ * Tests for the AbstractTrackingCommandHandler class. The class name is prefixed with an

+ * underscore so that it is not filtered out by Maven's Surefire test plugin.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class _AbstractTrackingCommandHandlerTest extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(_AbstractTrackingCommandHandlerTest.class);

+    private static final String COMMAND_NAME = "abc";

+    private static final Object ARG = "123";

+    private static final Object[] ARGS = {ARG};

+    private static final Command COMMAND = new Command(COMMAND_NAME, EMPTY);

+    private static final Command COMMAND_WITH_ARGS = new Command(COMMAND_NAME, EMPTY);

+    private static final int REPLY_CODE1 = 777;

+    private static final int REPLY_CODE2 = 888;

+    private static final int REPLY_CODE3 = 999;

+    private static final String REPLY_TEXT1 = "reply1 ... abcdef";

+    private static final String REPLY_TEXT2 = "abc {0} def";

+    private static final String REPLY_TEXT2_FORMATTED = "abc 123 def";

+    private static final String OVERRIDE_REPLY_TEXT = "overridden reply ... abcdef";

+    private static final String MESSAGE_KEY = "key.123";

+    private static final String MESSAGE_TEXT = "message.123";

+

+    private AbstractTrackingCommandHandler commandHandler;

+    private Session session;

+

+    /**

+     * Test the handleCommand(Command,Session) method

+     */

+    public void testHandleCommand() throws Exception {

+        assertEquals("before", 0, commandHandler.numberOfInvocations());

+        commandHandler.handleCommand(COMMAND, session);

+        assertEquals("after", 1, commandHandler.numberOfInvocations());

+        assertTrue("locked", commandHandler.getInvocation(0).isLocked());

+    }

+

+    /**

+     * Test the handleCommand(Command,Session) method, passing in a null Command

+     */

+    public void testHandleCommand_NullCommand() throws Exception {

+        try {

+            commandHandler.handleCommand(null, session);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the handleCommand(Command,Session) method, passing in a null Session

+     */

+    public void testHandleCommand_NullSession() throws Exception {

+        try {

+            commandHandler.handleCommand(COMMAND, null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the numberOfInvocations(), addInvocationRecord() and clearInvocationRecord() methods

+     */

+    public void testInvocationHistory() throws Exception {

+        control(session).expectAndDefaultReturn(session.getClientHost(), DEFAULT_HOST);

+        replay(session);

+

+        assertEquals("none", 0, commandHandler.numberOfInvocations());

+        commandHandler.handleCommand(COMMAND, session);

+        assertEquals("1", 1, commandHandler.numberOfInvocations());

+        commandHandler.handleCommand(COMMAND, session);

+        assertEquals("2", 2, commandHandler.numberOfInvocations());

+        commandHandler.clearInvocations();

+        assertEquals("cleared", 0, commandHandler.numberOfInvocations());

+    }

+

+    /**

+     * Test the getInvocation() method

+     *

+     * @throws Exception

+     */

+    public void testGetInvocation() throws Exception {

+        control(session).expectAndDefaultReturn(session.getClientHost(), DEFAULT_HOST);

+        replay(session);

+

+        commandHandler.handleCommand(COMMAND, session);

+        commandHandler.handleCommand(COMMAND_WITH_ARGS, session);

+        assertSame("1", COMMAND, commandHandler.getInvocation(0).getCommand());

+        assertSame("2", COMMAND_WITH_ARGS, commandHandler.getInvocation(1).getCommand());

+    }

+

+    /**

+     * Test the getInvocation() method, passing in an invalid index

+     */

+    public void testGetInvocation_IndexOutOfBounds() throws Exception {

+        commandHandler.handleCommand(COMMAND, session);

+        try {

+            commandHandler.getInvocation(2);

+            fail("Expected IndexOutOfBoundsException");

+        }

+        catch (IndexOutOfBoundsException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the sendReply() method, when no message arguments are specified

+     */

+    public void testSendReply() {

+        session.sendReply(REPLY_CODE1, REPLY_TEXT1);

+        session.sendReply(REPLY_CODE1, MESSAGE_TEXT);

+        session.sendReply(REPLY_CODE1, OVERRIDE_REPLY_TEXT);

+        session.sendReply(REPLY_CODE3, null);

+        replay(session);

+

+        commandHandler.sendReply(session, REPLY_CODE1, null, null, null);

+        commandHandler.sendReply(session, REPLY_CODE1, MESSAGE_KEY, null, null);

+        commandHandler.sendReply(session, REPLY_CODE1, MESSAGE_KEY, OVERRIDE_REPLY_TEXT, null);

+        commandHandler.sendReply(session, REPLY_CODE3, null, null, null);

+

+        verify(session);

+    }

+

+    /**

+     * Test the sendReply() method, passing in message arguments

+     */

+    public void testSendReply_WithMessageArguments() {

+        session.sendReply(REPLY_CODE1, REPLY_TEXT2_FORMATTED);

+        replay(session);

+

+        commandHandler.sendReply(session, REPLY_CODE1, null, REPLY_TEXT2, ARGS);

+

+        verify(session);

+    }

+

+    /**

+     * Test the sendReply() method, passing in a null Session

+     */

+    public void testSendReply_NullSession() {

+        try {

+            commandHandler.sendReply(null, REPLY_CODE1, REPLY_TEXT1, null, null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the sendReply() method, passing in an invalid replyCode

+     */

+    public void testSendReply_InvalidReplyCode() {

+        try {

+            commandHandler.sendReply(session, 0, REPLY_TEXT1, null, null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    //-------------------------------------------------------------------------

+    // Test setup

+    //-------------------------------------------------------------------------

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        session = (Session) createMock(Session.class);

+        control(session).setDefaultMatcher(MockControl.ARRAY_MATCHER);

+        commandHandler = new AbstractTrackingCommandHandler() {

+            public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+            }

+        };

+        ResourceBundle replyTextBundle = new ListResourceBundle() {

+            protected Object[][] getContents() {

+                return new Object[][]{

+                        {Integer.toString(REPLY_CODE1), REPLY_TEXT1},

+                        {Integer.toString(REPLY_CODE2), REPLY_TEXT2},

+                        {MESSAGE_KEY, MESSAGE_TEXT}

+                };

+            }

+        };

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/server/AbstractFtpServerTestCase.java b/tags/2.5/src/test/java/org/mockftpserver/core/server/AbstractFtpServerTestCase.java
new file mode 100644
index 0000000..70e6f9a
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/server/AbstractFtpServerTestCase.java
@@ -0,0 +1,175 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.server;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.session.DefaultSession;

+import org.mockftpserver.core.util.AssertFailedException;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.net.Socket;

+import java.util.HashMap;

+import java.util.Map;

+

+/**

+ * Abstract superclass for tests of AbstractFtpServer subclasses.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractFtpServerTestCase extends AbstractTestCase {

+

+    protected Logger LOG = LoggerFactory.getLogger(getClass());

+

+    protected AbstractFtpServer ftpServer;

+    private CommandHandler commandHandler;

+    private CommandHandler commandHandler2;

+

+    /**

+     * Test the setCommandHandlers() method

+     */

+    public void testSetCommandHandlers() {

+        Map mapping = new HashMap();

+        mapping.put("AAA", commandHandler);

+        mapping.put("BBB", commandHandler2);

+

+        ftpServer.setCommandHandlers(mapping);

+        assertSame("commandHandler1", commandHandler, ftpServer.getCommandHandler("AAA"));

+        assertSame("commandHandler2", commandHandler2, ftpServer.getCommandHandler("BBB"));

+

+        verifyCommandHandlerInitialized(commandHandler);

+        verifyCommandHandlerInitialized(commandHandler2);

+

+        // Make sure default CommandHandlers are still set

+        assertTrue("ConnectCommandHandler", ftpServer.getCommandHandler(CommandNames.CONNECT) != null);

+    }

+

+    /**

+     * Test the setCommandHandlers() method, when the Map is null

+     */

+    public void testSetCommandHandlers_Null() {

+        try {

+            ftpServer.setCommandHandlers(null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the setCommandHandler() method

+     */

+    public void testSetCommandHandler() {

+        ftpServer.setCommandHandler("ZZZ", commandHandler2);

+        assertSame("commandHandler", commandHandler2, ftpServer.getCommandHandler("ZZZ"));

+        verifyCommandHandlerInitialized(commandHandler2);

+    }

+

+    /**

+     * Test the setCommandHandler() method, when the commandName is null

+     */

+    public void testSetCommandHandler_NullCommandName() {

+        CommandHandler commandHandler = (CommandHandler) createMock(CommandHandler.class);

+        try {

+            ftpServer.setCommandHandler(null, commandHandler);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the setCommandHandler() method, when the commandHandler is null

+     */

+    public void testSetCommandHandler_NullCommandHandler() {

+        try {

+            ftpServer.setCommandHandler("ZZZ", null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    public void testSetServerControlPort() {

+        assertEquals("default", 21, ftpServer.getServerControlPort());

+        ftpServer.setServerControlPort(99);

+        assertEquals("99", 99, ftpServer.getServerControlPort());

+    }

+

+    /**

+     * Test the setCommandHandler() and getCommandHandler() methods for commands in lower case or mixed case

+     */

+    public void testLowerCaseOrMixedCaseCommandNames() {

+        ftpServer.setCommandHandler("XXX", commandHandler);

+        assertSame("ZZZ", commandHandler, ftpServer.getCommandHandler("XXX"));

+        assertSame("Zzz", commandHandler, ftpServer.getCommandHandler("Xxx"));

+        assertSame("zzz", commandHandler, ftpServer.getCommandHandler("xxx"));

+

+        ftpServer.setCommandHandler("YyY", commandHandler);

+        assertSame("ZZZ", commandHandler, ftpServer.getCommandHandler("YYY"));

+        assertSame("Zzz", commandHandler, ftpServer.getCommandHandler("Yyy"));

+        assertSame("zzz", commandHandler, ftpServer.getCommandHandler("yyy"));

+

+        ftpServer.setCommandHandler("zzz", commandHandler);

+        assertSame("ZZZ", commandHandler, ftpServer.getCommandHandler("ZZZ"));

+        assertSame("Zzz", commandHandler, ftpServer.getCommandHandler("zzZ"));

+        assertSame("zzz", commandHandler, ftpServer.getCommandHandler("zzz"));

+    }

+

+    /**

+     * Test calling stop() for a server that was never started.

+     */

+    public void testStopWithoutStart() {

+        ftpServer.stop();

+    }

+

+    public void testCreateSession() {

+        assertEquals(ftpServer.createSession(new Socket()).getClass(), DefaultSession.class);

+    }

+

+    //-------------------------------------------------------------------------

+    // Test setup

+    //-------------------------------------------------------------------------

+

+    /**

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+

+        ftpServer = createFtpServer();

+

+        commandHandler = createCommandHandler();

+        commandHandler2 = createCommandHandler();

+    }

+

+    //-------------------------------------------------------------------------

+    // Abstract method declarations

+    //-------------------------------------------------------------------------

+

+    protected abstract AbstractFtpServer createFtpServer();

+

+    protected abstract CommandHandler createCommandHandler();

+

+    protected abstract void verifyCommandHandlerInitialized(CommandHandler commandHandler);

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/server/AbstractFtpServer_StartTestCase.java b/tags/2.5/src/test/java/org/mockftpserver/core/server/AbstractFtpServer_StartTestCase.java
new file mode 100644
index 0000000..cc5b346
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/server/AbstractFtpServer_StartTestCase.java
@@ -0,0 +1,87 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.server;

+

+import org.apache.commons.net.ftp.FTPClient;

+import org.mockftpserver.test.*;

+import org.mockftpserver.test.AbstractTestCase;

+

+/**

+ * Abstract superclass for tests of AbstractFtpServer subclasses that require the server thread to be started.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractFtpServer_StartTestCase extends AbstractTestCase {

+

+    private static final String SERVER = "localhost";

+

+    private AbstractFtpServer ftpServer;

+

+    /**

+     * Test the start() and stop() methods. Start the server and then stop it immediately.

+     */

+    public void testStartAndStop() throws Exception {

+        ftpServer.setServerControlPort(PortTestUtil.getFtpServerControlPort());

+        assertEquals("started - before", false, ftpServer.isStarted());

+

+        ftpServer.start();

+        Thread.sleep(200L);     // give it some time to get started

+        assertEquals("started - after start()", true, ftpServer.isStarted());

+        assertEquals("shutdown - after start()", false, ftpServer.isShutdown());

+

+        ftpServer.stop();

+

+        assertEquals("shutdown - after stop()", true, ftpServer.isShutdown());

+    }

+

+    /**

+     * Test setting a non-default port number for the StubFtpServer control connection socket.

+     */

+    public void testCustomServerControlPort() throws Exception {

+        final int SERVER_CONTROL_PORT = 9187;

+

+        ftpServer.setServerControlPort(SERVER_CONTROL_PORT);

+        ftpServer.start();

+

+        try {

+            FTPClient ftpClient = new FTPClient();

+            ftpClient.connect(SERVER, SERVER_CONTROL_PORT);

+        }

+        finally {

+            ftpServer.stop();

+        }

+    }

+

+    //-------------------------------------------------------------------------

+    // Test setup

+    //-------------------------------------------------------------------------

+

+    /**

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        ftpServer = createFtpServer();

+    }

+

+    //-------------------------------------------------------------------------

+    // Abstract method declarations

+    //-------------------------------------------------------------------------

+

+    protected abstract AbstractFtpServer createFtpServer();

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/session/DefaultSessionTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/session/DefaultSessionTest.java
new file mode 100644
index 0000000..5763e5e
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/session/DefaultSessionTest.java
@@ -0,0 +1,482 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.session;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.MockFtpServerException;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.socket.StubServerSocket;

+import org.mockftpserver.core.socket.StubServerSocketFactory;

+import org.mockftpserver.core.socket.StubSocket;

+import org.mockftpserver.core.socket.StubSocketFactory;

+import org.mockftpserver.core.util.AssertFailedException;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.io.ByteArrayInputStream;

+import java.io.ByteArrayOutputStream;

+import java.io.IOException;

+import java.io.InputStream;

+import java.net.InetAddress;

+import java.net.SocketTimeoutException;

+import java.util.Collections;

+import java.util.HashMap;

+import java.util.Map;

+

+/**

+ * Tests for the DefaultSession class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class DefaultSessionTest extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(DefaultSessionTest.class);

+    private static final String DATA = "sample data 123";

+    private static final int PORT = 197;

+    private static final String NAME1 = "name1";

+    private static final String NAME2 = "name2";

+    private static final Object VALUE = "value";

+

+    private DefaultSession session;

+    private ByteArrayOutputStream outputStream;

+    private Map commandHandlerMap;

+    private StubSocket stubSocket;

+    private InetAddress clientHost;

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+

+        commandHandlerMap = new HashMap();

+        outputStream = new ByteArrayOutputStream();

+        session = createDefaultSession("");

+        clientHost = InetAddress.getLocalHost();

+    }

+

+    /**

+     * @see org.mockftpserver.test.AbstractTestCase#tearDown()

+     */

+    protected void tearDown() throws Exception {

+        super.tearDown();

+    }

+

+    /**

+     * Test the Constructor when the control socket is null

+     */

+    public void testConstructor_NullControlSocket() {

+        try {

+            new DefaultSession(null, commandHandlerMap);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the Constructor when the command handler Map is null

+     */

+    public void testConstructor_NullCommandHandlerMap() {

+        try {

+            new DefaultSession(stubSocket, null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the setClientDataPort() method

+     */

+    public void testSetClientDataPort() {

+        StubSocket stubSocket = createTestSocket("");

+        StubSocketFactory stubSocketFactory = new StubSocketFactory(stubSocket);

+        session.socketFactory = stubSocketFactory;

+        session.setClientDataPort(PORT);

+        session.setClientDataHost(clientHost);

+        session.openDataConnection();

+        assertEquals("data port", PORT, stubSocketFactory.requestedDataPort);

+    }

+

+    /**

+     * Test the setClientDataPort() method after the session was in passive data mode

+     */

+    public void testSetClientDataPort_AfterPassiveConnectionMode() throws IOException {

+        StubServerSocket stubServerSocket = new StubServerSocket(PORT);

+        StubServerSocketFactory stubServerSocketFactory = new StubServerSocketFactory(stubServerSocket);

+        session.serverSocketFactory = stubServerSocketFactory;

+

+        session.switchToPassiveMode();

+        assertFalse("server socket closed", stubServerSocket.isClosed());

+        assertNotNull("passiveModeDataSocket", session.passiveModeDataSocket);

+        session.setClientDataPort(PORT);

+

+        // Make sure that any passive mode connection info is cleared out

+        assertTrue("server socket closed", stubServerSocket.isClosed());

+        assertNull("passiveModeDataSocket should be null", session.passiveModeDataSocket);

+    }

+

+    /**

+     * Test the setClientHost() method

+     */

+    public void testSetClientHost() throws Exception {

+        StubSocket stubSocket = createTestSocket("");

+        StubSocketFactory stubSocketFactory = new StubSocketFactory(stubSocket);

+        session.socketFactory = stubSocketFactory;

+        session.setClientDataHost(clientHost);

+        session.openDataConnection();

+        assertEquals("client host", clientHost, stubSocketFactory.requestedHost);

+    }

+

+    /**

+     * Test the openDataConnection(), setClientDataPort() and setClientDataHost() methods

+     */

+    public void testOpenDataConnection() {

+        StubSocket stubSocket = createTestSocket("");

+        StubSocketFactory stubSocketFactory = new StubSocketFactory(stubSocket);

+        session.socketFactory = stubSocketFactory;

+

+        // Use default client data port

+        session.setClientDataHost(clientHost);

+        session.openDataConnection();

+        assertEquals("data port", DefaultSession.DEFAULT_CLIENT_DATA_PORT, stubSocketFactory.requestedDataPort);

+        assertEquals("client host", clientHost, stubSocketFactory.requestedHost);

+

+        // Set client data port explicitly

+        session.setClientDataPort(PORT);

+        session.setClientDataHost(clientHost);

+        session.openDataConnection();

+        assertEquals("data port", PORT, stubSocketFactory.requestedDataPort);

+        assertEquals("client host", clientHost, stubSocketFactory.requestedHost);

+    }

+

+    /**

+     * Test the OpenDataConnection method, when in passive mode and no incoming connection is

+     * initiated

+     */

+    public void testOpenDataConnection_PassiveMode_NoConnection() throws IOException {

+

+        StubServerSocket stubServerSocket = new StubServerSocket(PORT);

+        StubServerSocketFactory stubServerSocketFactory = new StubServerSocketFactory(stubServerSocket);

+        session.serverSocketFactory = stubServerSocketFactory;

+

+        session.switchToPassiveMode();

+

+        try {

+            session.openDataConnection();

+            fail("Expected MockFtpServerException");

+        }

+        catch (MockFtpServerException expected) {

+            LOG.info("Expected: " + expected);

+            assertSame("cause", SocketTimeoutException.class, expected.getCause().getClass());

+        }

+    }

+

+    /**

+     * Test the OpenDataConnection method, when the clientHost has not been set

+     */

+    public void testOpenDataConnection_NullClientHost() {

+        try {

+            session.openDataConnection();

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the readData() method

+     */

+    public void testReadData() {

+        StubSocket stubSocket = createTestSocket(DATA);

+        session.socketFactory = new StubSocketFactory(stubSocket);

+        session.setClientDataHost(clientHost);

+

+        session.openDataConnection();

+        byte[] data = session.readData();

+        LOG.info("data=[" + new String(data) + "]");

+        assertEquals("data", DATA.getBytes(), data);

+    }

+

+    /**

+     * Test the readData() method after switching to passive mode

+     */

+    public void testReadData_PassiveMode() throws IOException {

+        StubSocket stubSocket = createTestSocket(DATA);

+        StubServerSocket stubServerSocket = new StubServerSocket(PORT, stubSocket);

+        StubServerSocketFactory stubServerSocketFactory = new StubServerSocketFactory(stubServerSocket);

+        session.serverSocketFactory = stubServerSocketFactory;

+

+        session.switchToPassiveMode();

+        session.openDataConnection();

+        byte[] data = session.readData();

+        LOG.info("data=[" + new String(data) + "]");

+        assertEquals("data", DATA.getBytes(), data);

+    }

+

+    /**

+     * Test the readData(int) method

+     */

+    public void testReadData_NumBytes() {

+        final int NUM_BYTES = 5;

+        final String EXPECTED_DATA = DATA.substring(0, NUM_BYTES);

+        StubSocket stubSocket = createTestSocket(DATA);

+        session.socketFactory = new StubSocketFactory(stubSocket);

+        session.setClientDataHost(clientHost);

+

+        session.openDataConnection();

+        byte[] data = session.readData(NUM_BYTES);

+        LOG.info("data=[" + new String(data) + "]");

+        assertEquals("data", EXPECTED_DATA.getBytes(), data);

+    }

+

+    public void testReadData_NumBytes_AskForMoreBytesThanThereAre() {

+        StubSocket stubSocket = createTestSocket(DATA);

+        session.socketFactory = new StubSocketFactory(stubSocket);

+        session.setClientDataHost(clientHost);

+

+        session.openDataConnection();

+        byte[] data = session.readData(10000);

+        LOG.info("data=[" + new String(data) + "]");

+        assertEquals("data", DATA.getBytes(), data);

+    }

+

+    /**

+     * Test the closeDataConnection() method

+     */

+    public void testCloseDataConnection() {

+        StubSocket stubSocket = createTestSocket(DATA);

+        session.socketFactory = new StubSocketFactory(stubSocket);

+

+        session.setClientDataHost(clientHost);

+        session.openDataConnection();

+        session.closeDataConnection();

+        assertTrue("client data socket should be closed", stubSocket.isClosed());

+    }

+

+    /**

+     * Test the switchToPassiveMode() method

+     */

+    public void testSwitchToPassiveMode() throws IOException {

+        StubServerSocket stubServerSocket = new StubServerSocket(PORT);

+        StubServerSocketFactory stubServerSocketFactory = new StubServerSocketFactory(stubServerSocket);

+        session.serverSocketFactory = stubServerSocketFactory;

+

+        assertNull("passiveModeDataSocket starts out null", session.passiveModeDataSocket);

+        int port = session.switchToPassiveMode();

+        assertSame("passiveModeDataSocket", stubServerSocket, session.passiveModeDataSocket);

+        assertEquals("port", PORT, port);

+    }

+

+    /**

+     * Test the getServerHost() method

+     */

+    public void testGetServerHost() {

+        assertEquals("host", DEFAULT_HOST, session.getServerHost());

+    }

+

+    /**

+     * Test the getClientHost() method when the session is not yet started

+     */

+    public void testGetClientHost_NotRunning() {

+        assertNull("null", session.getClientHost());

+    }

+

+    /**

+     * Test the parseCommand() method

+     */

+    public void testParseCommand() {

+        Command command = session.parseCommand("LIST");

+        assertEquals("command name", "LIST", command.getName());

+        assertEquals("command parameters", EMPTY, command.getParameters());

+

+        command = session.parseCommand("USER user123");

+        assertEquals("command name", "USER", command.getName());

+        assertEquals("command parameters", array("user123"), command.getParameters());

+

+        command = session.parseCommand("PORT 127,0,0,1,17,37");

+        assertEquals("command name", "PORT", command.getName());

+        assertEquals("command parameters", new String[] { "127", "0", "0", "1", "17", "37" }, command

+                .getParameters());

+    }

+

+    /**

+     * Test the parseCommand() method, passing in an empty command String

+     */

+    public void testParseCommand_EmptyCommandString() {

+        try {

+            session.parseCommand("");

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the sendData() method, as well as the openDataConnection() and closeDataConnection()

+     */

+    public void testSendData() {

+        StubSocket stubSocket = createTestSocket("1234567890 abcdef");

+        session.socketFactory = new StubSocketFactory(stubSocket);

+

+        session.setClientDataHost(clientHost);

+        session.openDataConnection();

+        session.sendData(DATA.getBytes(), DATA.length());

+        LOG.info("output=[" + outputStream.toString() + "]");

+        assertEquals("output", DATA, outputStream.toString());

+    }

+

+    /**

+     * Test the SendData() method, passing in a null byte[]

+     */

+    public void testSendData_Null() {

+

+        try {

+            session.sendData(null, 1);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the SendReply(int,String) method, passing in an invalid reply code

+     */

+    public void testSendReply_InvalidReplyCode() {

+

+        try {

+            session.sendReply(-66, "text");

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the getAttribute() and setAttribute() methods 

+     */

+    public void testGetAndSetAttribute() {

+        assertNull("name does not exist yet", session.getAttribute(NAME1));

+        session.setAttribute(NAME1, VALUE);

+        session.setAttribute(NAME2, null);

+        assertEquals("NAME1", VALUE, session.getAttribute(NAME1));

+        assertNull("NAME2", session.getAttribute(NAME2));

+        assertNull("no such name", session.getAttribute("noSuchName"));

+    }

+    

+    /**

+     * Test the getAttribute() method, passing in a null name

+     */

+    public void testGetAttribute_Null() {

+        try {

+            session.getAttribute(null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the setAttribute() method, passing in a null name

+     */

+    public void testSetAttribute_NullName() {

+        try {

+            session.setAttribute(null, VALUE);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the removeAttribute() 

+     */

+    public void testRemoveAttribute() {

+        session.removeAttribute("noSuchName");      // do nothing

+        session.setAttribute(NAME1, VALUE);

+        session.removeAttribute(NAME1);

+        assertNull("NAME1", session.getAttribute(NAME1));

+    }

+    

+    /**

+     * Test the removeAttribute() method, passing in a null name

+     */

+    public void testRemoveAttribute_Null() {

+        try {

+            session.removeAttribute(null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Test the getAttributeNames() 

+     */

+    public void testGetAttributeNames() {

+        assertEquals("No names yet", Collections.EMPTY_SET, session.getAttributeNames());

+        session.setAttribute(NAME1, VALUE);

+        assertEquals("1", Collections.singleton(NAME1), session.getAttributeNames());

+        session.setAttribute(NAME2, VALUE);

+        assertEquals("2", set(NAME1, NAME2), session.getAttributeNames());

+    }

+    

+    // -------------------------------------------------------------------------

+    // Internal Helper Methods

+    // -------------------------------------------------------------------------

+

+    /**

+     * Create and return a DefaultSession object that reads from an InputStream with the specified

+     * contents and writes to the predefined outputStrean ByteArrayOutputStream. Also, save the

+     * StubSocket being used in the stubSocket attribute.

+     * 

+     * @param inputStreamContents - the contents of the input stream

+     * @return the DefaultSession

+     */

+    private DefaultSession createDefaultSession(String inputStreamContents) {

+        stubSocket = createTestSocket(inputStreamContents);

+        return new DefaultSession(stubSocket, commandHandlerMap);

+    }

+

+    /**

+     * Create and return a StubSocket that reads from an InputStream with the specified contents and

+     * writes to the predefined outputStrean ByteArrayOutputStream.

+     * 

+     * @param inputStreamContents - the contents of the input stream

+     * @return the StubSocket

+     */

+    private StubSocket createTestSocket(String inputStreamContents) {

+        InputStream inputStream = new ByteArrayInputStream(inputStreamContents.getBytes());

+        StubSocket stubSocket = new StubSocket(inputStream, outputStream);

+        stubSocket._setLocalAddress(DEFAULT_HOST);

+        return stubSocket;

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/session/DefaultSession_RunTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/session/DefaultSession_RunTest.java
new file mode 100644
index 0000000..5c64877
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/session/DefaultSession_RunTest.java
@@ -0,0 +1,244 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.session;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ConnectCommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.socket.StubSocket;

+import org.mockftpserver.stub.command.AbstractStubCommandHandler;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.io.*;

+import java.util.HashMap;

+import java.util.ListResourceBundle;

+import java.util.Map;

+import java.util.ResourceBundle;

+

+/**

+ * Tests for the DefaultSession class that require the session (thread) to be running/active.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class DefaultSession_RunTest extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(DefaultSession_RunTest.class);

+    private static final Command COMMAND = new Command("USER", EMPTY);

+    private static final int REPLY_CODE = 100;

+    private static final String REPLY_TEXT = "sample text description";

+

+    private DefaultSession session;

+    private ByteArrayOutputStream outputStream;

+    private Map commandHandlerMap;

+    private StubSocket stubSocket;

+    private boolean commandHandled = false;

+    private String commandToRegister = COMMAND.getName();

+

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandlerMap = new HashMap();

+        outputStream = new ByteArrayOutputStream();

+    }

+

+    public void testInvocationOfCommandHandler() throws Exception {

+        AbstractStubCommandHandler commandHandler = new AbstractStubCommandHandler() {

+            public void handleCommand(Command command, Session cmdSession, InvocationRecord invocationRecord) {

+                assertEquals("command", COMMAND, command);

+                assertSame("session", session, cmdSession);

+                assertEquals("InvocationRecord: command", COMMAND, invocationRecord.getCommand());

+                assertEquals("InvocationRecord: clientHost", DEFAULT_HOST, invocationRecord.getClientHost());

+                commandHandled = true;

+            }

+        };

+        runCommandAndVerifyOutput(commandHandler, "");

+    }

+

+    public void testClose() throws Exception {

+        CommandHandler commandHandler = new AbstractStubCommandHandler() {

+            public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+                session.close();

+                commandHandled = true;

+            }

+        };

+        runCommandAndVerifyOutput(commandHandler, "");

+        assertFalse("socket should not be closed", stubSocket.isClosed());

+    }

+

+    public void testClose_WithoutCommand() throws Exception {

+        PipedOutputStream pipedOutputStream = new PipedOutputStream();

+        PipedInputStream inputStream = new PipedInputStream(pipedOutputStream);

+        stubSocket = new StubSocket(DEFAULT_HOST, inputStream, outputStream);

+        session = new DefaultSession(stubSocket, commandHandlerMap);

+

+        initializeConnectCommandHandler();

+

+        Thread thread = new Thread(session);

+        thread.start();

+        Thread.sleep(1000L);

+

+        session.close();

+        thread.join();

+    }

+

+    public void testGetClientHost() throws Exception {

+        CommandHandler commandHandler = new AbstractStubCommandHandler() {

+            public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+                commandHandled = true;

+            }

+        };

+        runCommandAndVerifyOutput(commandHandler, "");

+        LOG.info("clientHost=" + session.getClientHost());

+        assertEquals("clientHost", DEFAULT_HOST, session.getClientHost());

+    }

+

+    public void testSendReply_NullReplyText() throws Exception {

+        CommandHandler commandHandler = new AbstractStubCommandHandler() {

+            public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+                session.sendReply(REPLY_CODE, null);

+                commandHandled = true;

+            }

+        };

+        runCommandAndVerifyOutput(commandHandler, Integer.toString(REPLY_CODE));

+    }

+

+    public void testSendReply_TrimReplyText() throws Exception {

+        CommandHandler commandHandler = new AbstractStubCommandHandler() {

+            public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+                session.sendReply(REPLY_CODE, " " + REPLY_TEXT + " ");

+                commandHandled = true;

+            }

+        };

+        runCommandAndVerifyOutput(commandHandler, REPLY_CODE + " " + REPLY_TEXT);

+    }

+

+    public void testSendReply_MultiLineText() throws Exception {

+        final String MULTILINE_REPLY_TEXT = "abc\ndef\nghi\njkl";

+        final String FORMATTED_MULTILINE_REPLY_TEXT = "123-abc\ndef\nghi\n123 jkl";

+

+        CommandHandler commandHandler = new AbstractStubCommandHandler() {

+            public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+                session.sendReply(123, MULTILINE_REPLY_TEXT);

+                commandHandled = true;

+            }

+        };

+        runCommandAndVerifyOutput(commandHandler, FORMATTED_MULTILINE_REPLY_TEXT);

+    }

+

+    public void testSendReply_ReplyText() throws Exception {

+        CommandHandler commandHandler = new AbstractStubCommandHandler() {

+            public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+                session.sendReply(REPLY_CODE, REPLY_TEXT);

+                commandHandled = true;

+            }

+        };

+        runCommandAndVerifyOutput(commandHandler, REPLY_CODE + " " + REPLY_TEXT);

+    }

+

+    public void testUnrecognizedCommand() throws Exception {

+        // Register a handler for unsupported commands

+        CommandHandler commandHandler = new AbstractStubCommandHandler() {

+            public void handleCommand(Command command, Session session, InvocationRecord invocationRecord) {

+                session.sendReply(502, "Unsupported");

+                commandHandled = true;

+            }

+        };

+        // Register the UNSUPPORTED command handler instead of the command that will be sent. So when we

+        // send the regular command, it will trigger the handling for unsupported/unrecognized commands.

+        commandToRegister = CommandNames.UNSUPPORTED;

+        runCommandAndVerifyOutput(commandHandler, "502 Unsupported");

+    }

+

+    // -------------------------------------------------------------------------

+    // Internal Helper Methods

+    // -------------------------------------------------------------------------

+

+    /**

+     * Create and return a DefaultSession and define the specified CommandHandler. Also, save the

+     * StubSocket being used in the stubSocket attribute.

+     *

+     * @param commandHandler - define this CommandHandler within the commandHandlerMap

+     * @return the DefaultSession

+     */

+    private DefaultSession createDefaultSession(CommandHandler commandHandler) {

+        stubSocket = createTestSocket(COMMAND.getName());

+        commandHandlerMap.put(commandToRegister, commandHandler);

+        initializeConnectCommandHandler();

+        return new DefaultSession(stubSocket, commandHandlerMap);

+    }

+

+    private void initializeConnectCommandHandler() {

+        ConnectCommandHandler connectCommandHandler = new ConnectCommandHandler();

+

+        ResourceBundle replyTextBundle = new ListResourceBundle() {

+            protected Object[][] getContents() {

+                return new Object[][]{

+                        {"220", "Reply for 220"},

+                };

+            }

+        };

+        connectCommandHandler.setReplyTextBundle(replyTextBundle);

+        commandHandlerMap.put(CommandNames.CONNECT, connectCommandHandler);

+    }

+

+    /**

+     * Create and return a StubSocket that reads from an InputStream with the specified contents and

+     * writes to the predefined outputStrean ByteArrayOutputStream.

+     *

+     * @param inputStreamContents - the contents of the input stream

+     * @return the StubSocket

+     */

+    private StubSocket createTestSocket(String inputStreamContents) {

+        InputStream inputStream = new ByteArrayInputStream(inputStreamContents.getBytes());

+        return new StubSocket(DEFAULT_HOST, inputStream, outputStream);

+    }

+

+    /**

+     * Run the command represented by the CommandHandler and verify that the session output from the

+     * control socket contains the expected output text.

+     *

+     * @param commandHandler - the CommandHandler to invoke

+     * @param expectedOutput - the text expected within the session output

+     * @throws InterruptedException - if the thread sleep is interrupted

+     */

+    private void runCommandAndVerifyOutput(CommandHandler commandHandler, String expectedOutput)

+            throws InterruptedException {

+        session = createDefaultSession(commandHandler);

+

+        Thread thread = new Thread(session);

+        thread.start();

+

+        for (int i = 0; !commandHandled && i < 10; i++) {

+            Thread.sleep(50L);

+        }

+

+        session.close();

+        thread.join();

+

+        assertEquals("commandHandled", true, commandHandled);

+

+        String output = outputStream.toString();

+        LOG.info("output=[" + output.trim() + "]");

+        assertTrue("line ends with \\r\\n",

+                output.charAt(output.length() - 2) == '\r' && output.charAt(output.length() - 1) == '\n');

+        assertTrue("output: expected [" + expectedOutput + "]", output.indexOf(expectedOutput) != -1);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/socket/StubServerSocket.java b/tags/2.5/src/test/java/org/mockftpserver/core/socket/StubServerSocket.java
new file mode 100644
index 0000000..e3a9686
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/socket/StubServerSocket.java
@@ -0,0 +1,74 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.socket;

+

+import java.io.IOException;

+import java.net.ServerSocket;

+import java.net.Socket;

+import java.net.SocketTimeoutException;

+

+/**

+ * Test (fake) subclass of ServerSocket that performs no network access and allows setting the 

+ * Socket returned by accept(), and the local port for the ServerSocket.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public class StubServerSocket extends ServerSocket {

+    private int localPort;

+    private Socket socket;

+

+    /**

+     * Construct a new instance with the specified local port.

+     * @param localPort - the local port to be returned from getLocalPort()

+     * @throws IOException

+     */

+    public StubServerSocket(int localPort) throws IOException {

+        this(localPort, null);

+    }

+

+    /**

+     * Construct a new instance with specified local port and accept() socket. 

+     * @param localPort - the local port to be returned from getLocalPort()

+     * @param socket - the socket to be returned from accept(); if null, then accept() throws SocketTimeoutException. 

+     * @throws IOException

+     */

+    public StubServerSocket(int localPort, Socket socket) throws IOException {

+        super(0);

+        this.localPort = localPort;

+        this.socket = socket;

+    }

+    

+    /**

+     * Return the predefined local port 

+     * @see java.net.ServerSocket#getLocalPort()

+     */

+    public int getLocalPort() {

+        return localPort;

+    }

+    

+    /**

+     * If a socket was specified on the constructor, then return that; otherwise, throw a SocketTimeoutException. 

+     * @see java.net.ServerSocket#accept()

+     */

+    public Socket accept() throws IOException {

+        if (socket != null) {

+            return socket;

+        }

+        throw new SocketTimeoutException();

+    }

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/socket/StubServerSocketFactory.java b/tags/2.5/src/test/java/org/mockftpserver/core/socket/StubServerSocketFactory.java
new file mode 100644
index 0000000..c58caa4
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/socket/StubServerSocketFactory.java
@@ -0,0 +1,48 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.socket;

+

+import java.io.IOException;

+import java.net.ServerSocket;

+

+/**

+ * Test-only implementation of ServerSocketFactory. It always returns the predefined

+ * ServerSocket instance specified on the constructor.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public class StubServerSocketFactory implements ServerSocketFactory {

+    private StubServerSocket stubServerSocket;

+

+    /**

+     * Construct a new factory instance that always returns the specified

+     * ServerSocket instance. 

+     * @param serverSocket - the ServerSocket instance to be returned by this factory

+     */

+    public StubServerSocketFactory(StubServerSocket serverSocket) {

+        this.stubServerSocket = serverSocket;

+    }

+

+    /**

+     * Return the predefined ServerSocket instance.

+     * @see org.mockftpserver.core.socket.ServerSocketFactory#createServerSocket(int)

+     */

+    public ServerSocket createServerSocket(int port) throws IOException {

+        return stubServerSocket;

+    }

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/socket/StubSocket.java b/tags/2.5/src/test/java/org/mockftpserver/core/socket/StubSocket.java
new file mode 100644
index 0000000..3816bf0
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/socket/StubSocket.java
@@ -0,0 +1,101 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.socket;

+

+import java.io.IOException;

+import java.io.InputStream;

+import java.io.OutputStream;

+import java.net.InetAddress;

+import java.net.Socket;

+

+/**

+ * Test (fake) subclass of Socket that performs no network access and allows setting the 

+ * inputStream and OutputStream for the socket.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class StubSocket extends Socket {

+

+    private InetAddress inetAddress;

+    private InetAddress localAddress;

+    private InputStream inputStream;

+    private OutputStream outputStream;

+    

+    /**

+     * Construct a new instance using the specified InputStream and OutputStream

+     * @param inputStream - the InputStream to use

+     * @param outputStream - the OutputStream to use

+     */

+    public StubSocket(InputStream inputStream, OutputStream outputStream) {

+        this(null, inputStream, outputStream);

+    }

+    

+    /**

+     * Construct a new instance using the specified host, InputStream and OutputStream

+     * @param inetAddress - the InetAddress for this socket

+     * @param inputStream - the InputStream to use

+     * @param outputStream - the OutputStream to use

+     */

+    public StubSocket(InetAddress inetAddress, InputStream inputStream, OutputStream outputStream) {

+        this.inetAddress = inetAddress;

+        this.inputStream = inputStream;

+        this.outputStream = outputStream;

+    }

+    

+    /**

+     * Override the superclass implementation. If the local inetAddress is not null, 

+     * return that. Otherwise return super.getInetAddress().

+     * @see java.net.Socket#getInetAddress()

+     */

+    public InetAddress getInetAddress() {

+        return (inetAddress != null) ? inetAddress : super.getInetAddress();

+    }

+    

+    /**

+     * Override the superclass implementation. If the local localAddress is not

+     * null, return that. Otherwise return super.getLocalAddress();

+     * @see java.net.Socket#getLocalAddress()

+     */

+    public InetAddress getLocalAddress() {

+        return (localAddress != null) ? localAddress : super.getLocalAddress();

+    }

+    

+    /**

+     * Override the superclass implementation to provide the predefined InputStream

+     * @see java.net.Socket#getInputStream()

+     */

+    public InputStream getInputStream() throws IOException {

+        return inputStream;

+    }

+    

+    /**

+     * Override the superclass implementation to provide the predefined OutputStream

+     * @see java.net.Socket#getOutputStream()

+     */

+    public OutputStream getOutputStream() throws IOException {

+        return outputStream;

+    }

+    

+    //-------------------------------------------------------------------------

+    // Test-specific helper methods

+    //-------------------------------------------------------------------------

+

+    public void _setLocalAddress(InetAddress localAddress) {

+        this.localAddress = localAddress;

+    }

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/socket/StubSocketFactory.java b/tags/2.5/src/test/java/org/mockftpserver/core/socket/StubSocketFactory.java
new file mode 100644
index 0000000..724399c
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/socket/StubSocketFactory.java
@@ -0,0 +1,53 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.socket;

+

+import java.io.IOException;

+import java.net.InetAddress;

+import java.net.Socket;

+

+/**

+ * Test-only implementation of SocketFactory. It always returns the predefined

+ * StubSocket instance specified on the constructor. It allows direct access to the

+ * requested host address and port number.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public class StubSocketFactory implements SocketFactory {

+    private StubSocket stubSocket;

+    public int requestedDataPort;

+    public InetAddress requestedHost;

+

+    /**

+     * Create a new instance that always returns the specified StubSocket instance.

+     * @param stubSocket - the StubSocket to be returned by this factory

+     */

+    public StubSocketFactory(StubSocket stubSocket) {

+        this.stubSocket = stubSocket;

+    }

+

+    /**

+     * Return the predefined StubSocket instance

+     * @see org.mockftpserver.core.socket.SocketFactory#createSocket(java.net.InetAddress, int)

+     */

+    public Socket createSocket(InetAddress host, int port) throws IOException {

+        this.requestedHost = host;

+        this.requestedDataPort = port;

+        return stubSocket;

+    }

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/java/org/mockftpserver/core/util/AssertTest.java b/tags/2.5/src/test/java/org/mockftpserver/core/util/AssertTest.java
new file mode 100644
index 0000000..0cdf72f
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/core/util/AssertTest.java
@@ -0,0 +1,229 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.core.util;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.util.ArrayList;

+import java.util.Collection;

+import java.util.Collections;

+import java.util.HashMap;

+import java.util.Map;

+

+/**

+ * Tests for the Assert class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class AssertTest extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(AssertTest.class);

+

+    /**

+     * This interface defines a generic closure (a generic wrapper for a block of code).

+     */

+    private static interface ExceptionClosure {

+        /**

+         * Execute arbitrary logic that can throw any type of Exception

+         *

+         * @throws Exception

+         */

+        public void execute() throws Exception;

+    }

+

+

+    private static final String MESSAGE = "exception message";

+

+    /**

+     * Test the assertNull() method

+     */

+    public void testAssertNull() {

+

+        Assert.isNull(null, MESSAGE);

+

+        try {

+            Assert.isNull("OK", MESSAGE);

+            fail("Expected IllegalArumentException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+            assertExceptionMessageContains(expected, MESSAGE);

+        }

+    }

+

+

+    /**

+     * Test the assertNotNull() method

+     */

+    public void testAssertNotNull() {

+

+        Assert.notNull("OK", MESSAGE);

+

+        try {

+            Assert.notNull(null, MESSAGE);

+            fail("Expected IllegalArumentException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+            assertExceptionMessageContains(expected, MESSAGE);

+        }

+    }

+

+    /**

+     * Test the assertTrue() method

+     */

+    public void testAssertTrue() throws Exception {

+

+        Assert.isTrue(true, MESSAGE);

+

+        verifyThrowsAssertFailedException(true, new ExceptionClosure() {

+            public void execute() throws Exception {

+                Assert.isTrue(false, MESSAGE);

+            }

+        });

+    }

+

+    /**

+     * Test the assertFalse() method

+     */

+    public void testAssertFalse() throws Exception {

+

+        Assert.isFalse(false, MESSAGE);

+

+        verifyThrowsAssertFailedException(true, new ExceptionClosure() {

+            public void execute() throws Exception {

+                Assert.isFalse(true, MESSAGE);

+            }

+        });

+    }

+

+    /**

+     * Test the assertNotEmpty(Collection,String) method

+     */

+    public void testAssertNotNullOrEmpty_Collection() throws Exception {

+

+        final Collection COLLECTION = Collections.singletonList("item");

+        Assert.notNullOrEmpty(COLLECTION, MESSAGE);

+

+        verifyThrowsAssertFailedException(true, new ExceptionClosure() {

+            public void execute() throws Exception {

+                Assert.notNullOrEmpty((Collection) null, MESSAGE);

+            }

+        });

+

+        verifyThrowsAssertFailedException(true, new ExceptionClosure() {

+            public void execute() throws Exception {

+                Assert.notNullOrEmpty(new ArrayList(), MESSAGE);

+            }

+        });

+    }

+

+    /**

+     * Test the assertNotEmpty(Map,String) method

+     */

+    public void testAssertNotNullOrEmpty_Map() throws Exception {

+

+        final Map MAP = Collections.singletonMap("key", "value");

+        Assert.notNullOrEmpty(MAP, MESSAGE);

+

+        verifyThrowsAssertFailedException(true, new ExceptionClosure() {

+            public void execute() throws Exception {

+                Assert.notNullOrEmpty((Map) null, MESSAGE);

+            }

+        });

+

+        verifyThrowsAssertFailedException(true, new ExceptionClosure() {

+            public void execute() throws Exception {

+                Assert.notNullOrEmpty(new HashMap(), MESSAGE);

+            }

+        });

+    }

+

+    /**

+     * Test the assertNotEmpty(Objecct[],String) method

+     */

+    public void testAssertNotNullOrEmpty_array() throws Exception {

+

+        final Object[] ARRAY = {"1", "2"};

+        Assert.notNullOrEmpty(ARRAY, MESSAGE);

+

+        verifyThrowsAssertFailedException(true, new ExceptionClosure() {

+            public void execute() throws Exception {

+                Assert.notNullOrEmpty((Object[]) null, MESSAGE);

+            }

+        });

+

+        verifyThrowsAssertFailedException(true, new ExceptionClosure() {

+            public void execute() throws Exception {

+                Assert.notNullOrEmpty(new String[]{}, MESSAGE);

+            }

+        });

+    }

+

+    /**

+     * Test the assertNotEmpty(String,String) method

+     */

+    public void testAssertNotNullOrEmpty_String() throws Exception {

+

+        Assert.notNullOrEmpty("OK", MESSAGE);

+

+        verifyThrowsAssertFailedException(true, new ExceptionClosure() {

+            public void execute() throws Exception {

+                Assert.notNullOrEmpty((String) null, MESSAGE);

+            }

+        });

+

+        verifyThrowsAssertFailedException(true, new ExceptionClosure() {

+            public void execute() throws Exception {

+                Assert.notNullOrEmpty("", MESSAGE);

+            }

+        });

+    }

+

+    //-------------------------------------------------------------------------

+    // Helper Methods

+    //-------------------------------------------------------------------------

+

+    private void assertExceptionMessageContains(Throwable exception, String text) {

+        String message = exception.getMessage();

+        assertTrue("Exception message [" + message + "] does not contain [" + text + "]", message.indexOf(text) != -1);

+    }

+

+    /**

+     * Verify that execution of the ExceptionClosure (code block) results in an

+     * AssertFailedException being thrown with the constant MESSAGE as its message.

+     *

+     * @param closure - the ExceptionClosure encapsulating the code to execute

+     */

+    private void verifyThrowsAssertFailedException(boolean checkMessage, ExceptionClosure closure)

+            throws Exception {

+

+        try {

+            closure.execute();

+            fail("Expected IllegalArumentException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+            if (checkMessage) {

+                assertExceptionMessageContains(expected, MESSAGE);

+            }

+		}

+	}

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/fake/example/RemoteFileTest.java b/tags/2.5/src/test/java/org/mockftpserver/fake/example/RemoteFileTest.java
new file mode 100644
index 0000000..8c7e5ea
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/fake/example/RemoteFileTest.java
@@ -0,0 +1,81 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.example;

+

+import org.mockftpserver.fake.FakeFtpServer;

+import org.mockftpserver.fake.UserAccount;

+import org.mockftpserver.fake.filesystem.FileEntry;

+import org.mockftpserver.fake.filesystem.FileSystem;

+import org.mockftpserver.fake.filesystem.UnixFakeFileSystem;

+import org.mockftpserver.stub.example.RemoteFile;

+import org.mockftpserver.test.*;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.io.IOException;

+

+/**

+ * Example test using FakeFtpServer, with programmatic configuration.

+ */

+public class RemoteFileTest extends AbstractTestCase implements IntegrationTest {

+

+    private static final String HOME_DIR = "/";

+    private static final String FILE = "/dir/sample.txt";

+    private static final String CONTENTS = "abcdef 1234567890";

+

+    private RemoteFile remoteFile;

+    private FakeFtpServer fakeFtpServer;

+

+    public void testReadFile() throws Exception {

+        String contents = remoteFile.readFile(FILE);

+        assertEquals("contents", CONTENTS, contents);

+    }

+

+    public void testReadFileThrowsException() {

+        try {

+            remoteFile.readFile("NoSuchFile.txt");

+            fail("Expected IOException");

+        }

+        catch (IOException expected) {

+            // Expected this

+        }

+    }

+

+    protected void setUp() throws Exception {

+        super.setUp();

+        fakeFtpServer = new FakeFtpServer();

+        fakeFtpServer.setServerControlPort(0);  // use any free port

+

+        FileSystem fileSystem = new UnixFakeFileSystem();

+        fileSystem.add(new FileEntry(FILE, CONTENTS));

+        fakeFtpServer.setFileSystem(fileSystem);

+

+        UserAccount userAccount = new UserAccount(RemoteFile.USERNAME, RemoteFile.PASSWORD, HOME_DIR);

+        fakeFtpServer.addUserAccount(userAccount);

+

+        fakeFtpServer.start();

+        int port = fakeFtpServer.getServerControlPort();

+

+        remoteFile = new RemoteFile();

+        remoteFile.setServer("localhost");

+        remoteFile.setPort(port);

+    }

+

+    protected void tearDown() throws Exception {

+        super.tearDown();

+        fakeFtpServer.stop();

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/java/org/mockftpserver/fake/example/SimpleUnixFakeFtpServerTest.java b/tags/2.5/src/test/java/org/mockftpserver/fake/example/SimpleUnixFakeFtpServerTest.java
new file mode 100644
index 0000000..5f04957
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/fake/example/SimpleUnixFakeFtpServerTest.java
@@ -0,0 +1,49 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.example;

+

+import org.mockftpserver.fake.FakeFtpServer;

+import org.mockftpserver.fake.UserAccount;

+import org.mockftpserver.fake.filesystem.DirectoryEntry;

+import org.mockftpserver.fake.filesystem.FileEntry;

+import org.mockftpserver.fake.filesystem.FileSystem;

+import org.mockftpserver.fake.filesystem.UnixFakeFileSystem;

+import org.mockftpserver.test.AbstractTestCase;

+import org.mockftpserver.test.IntegrationTest;

+

+/**

+ * Example code illustrating how to programmatically configure a FakeFtpServer with a (simulated) Unix

+ * filesystem.

+ */

+public class SimpleUnixFakeFtpServerTest extends AbstractTestCase implements IntegrationTest {

+

+    public void testConfigureAndStart() throws Exception {

+        FakeFtpServer fakeFtpServer = new FakeFtpServer();

+        fakeFtpServer.setServerControlPort(9981);

+        fakeFtpServer.addUserAccount(new UserAccount("user", "password", "c:\\data"));

+

+        FileSystem fileSystem = new UnixFakeFileSystem();

+        fileSystem.add(new DirectoryEntry("/data"));

+        fileSystem.add(new FileEntry("/data/file1.txt", "abcdef 1234567890"));

+        fileSystem.add(new FileEntry("/data/run.exe"));

+        fakeFtpServer.setFileSystem(fileSystem);

+

+        fakeFtpServer.start();

+

+        fakeFtpServer.stop();

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/java/org/mockftpserver/fake/example/SimpleWindowsFakeFtpServerTest.java b/tags/2.5/src/test/java/org/mockftpserver/fake/example/SimpleWindowsFakeFtpServerTest.java
new file mode 100644
index 0000000..e1a595f
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/fake/example/SimpleWindowsFakeFtpServerTest.java
@@ -0,0 +1,49 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.example;

+

+import org.mockftpserver.fake.FakeFtpServer;

+import org.mockftpserver.fake.UserAccount;

+import org.mockftpserver.fake.filesystem.DirectoryEntry;

+import org.mockftpserver.fake.filesystem.FileEntry;

+import org.mockftpserver.fake.filesystem.FileSystem;

+import org.mockftpserver.fake.filesystem.WindowsFakeFileSystem;

+import org.mockftpserver.test.*;

+import org.mockftpserver.test.AbstractTestCase;

+

+/**

+ * Example code illustrating how to programmatically configure a FakeFtpServer with a (simulated) Windows

+ * filesystem.

+ */

+public class SimpleWindowsFakeFtpServerTest extends AbstractTestCase implements IntegrationTest {

+

+    public void testConfigureAndStart() throws Exception {

+        FakeFtpServer fakeFtpServer = new FakeFtpServer();

+        fakeFtpServer.setServerControlPort(9981);

+        fakeFtpServer.addUserAccount(new UserAccount("user", "password", "c:\\data"));

+

+        FileSystem fileSystem = new WindowsFakeFileSystem();

+        fileSystem.add(new DirectoryEntry("c:\\data"));

+        fileSystem.add(new FileEntry("c:\\data\\file1.txt", "abcdef 1234567890"));

+        fileSystem.add(new FileEntry("c:\\data\\run.exe"));

+        fakeFtpServer.setFileSystem(fileSystem);

+

+        fakeFtpServer.start();

+

+        fakeFtpServer.stop();

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/java/org/mockftpserver/fake/example/WindowsFakeFileSystemPermissionsTest.java b/tags/2.5/src/test/java/org/mockftpserver/fake/example/WindowsFakeFileSystemPermissionsTest.java
new file mode 100644
index 0000000..54630d9
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/fake/example/WindowsFakeFileSystemPermissionsTest.java
@@ -0,0 +1,76 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.fake.example;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.fake.FakeFtpServer;

+import org.mockftpserver.fake.filesystem.DirectoryEntry;

+import org.mockftpserver.fake.filesystem.FileEntry;

+import org.mockftpserver.fake.filesystem.FileSystem;

+import org.mockftpserver.fake.filesystem.Permissions;

+import org.mockftpserver.fake.filesystem.WindowsFakeFileSystem;

+import org.mockftpserver.test.*;

+import org.mockftpserver.test.AbstractTestCase;

+

+/**

+ * Example code illustrating how to programmatically configure a FakeFtpServer with a (simulated) Windows

+ * filesystem, and including file/directory permissions.

+ */

+public class WindowsFakeFileSystemPermissionsTest extends AbstractTestCase implements IntegrationTest {

+

+    private static final Logger LOG = LoggerFactory.getLogger(WindowsFakeFileSystemPermissionsTest.class);

+

+    public void testFilesystemWithPermissions() throws Exception {

+

+        final String USER1 = "joe";

+        final String USER2 = "mary";

+        final String GROUP = "dev";

+        final String CONTENTS = "abcdef 1234567890";

+

+        FileSystem fileSystem = new WindowsFakeFileSystem();

+        DirectoryEntry directoryEntry1 = new DirectoryEntry("c:\\");

+        directoryEntry1.setPermissions(new Permissions("rwxrwx---"));

+        directoryEntry1.setOwner(USER1);

+        directoryEntry1.setGroup(GROUP);

+

+        DirectoryEntry directoryEntry2 = new DirectoryEntry("c:\\data");

+        directoryEntry2.setPermissions(Permissions.ALL);

+        directoryEntry2.setOwner(USER1);

+        directoryEntry2.setGroup(GROUP);

+

+        FileEntry fileEntry1 = new FileEntry("c:\\data\\file1.txt", CONTENTS);

+        fileEntry1.setPermissionsFromString("rw-rw-rw-");

+        fileEntry1.setOwner(USER1);

+        fileEntry1.setGroup(GROUP);

+

+        FileEntry fileEntry2 = new FileEntry("c:\\data\\run.exe");

+        fileEntry2.setPermissionsFromString("rwxrwx---");

+        fileEntry2.setOwner(USER2);

+        fileEntry2.setGroup(GROUP);

+

+        fileSystem.add(directoryEntry1);

+        fileSystem.add(directoryEntry2);

+        fileSystem.add(fileEntry1);

+        fileSystem.add(fileEntry2);

+

+        FakeFtpServer fakeFtpServer = new FakeFtpServer();

+        fakeFtpServer.setFileSystem(fileSystem);

+

+        LOG.info(fileSystem.toString());

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/StubFtpServerIntegrationTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/StubFtpServerIntegrationTest.java
new file mode 100644
index 0000000..2949a14
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/StubFtpServerIntegrationTest.java
@@ -0,0 +1,591 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub;

+

+import org.apache.commons.net.ftp.FTP;

+import org.apache.commons.net.ftp.FTPClient;

+import org.apache.commons.net.ftp.FTPFile;

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.command.SimpleCompositeCommandHandler;

+import org.mockftpserver.core.command.StaticReplyCommandHandler;

+import org.mockftpserver.stub.command.*;

+import org.mockftpserver.test.*;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.io.ByteArrayInputStream;

+import java.io.ByteArrayOutputStream;

+import java.io.IOException;

+

+/**

+ * Tests for StubFtpServer using the Apache Jakarta Commons Net FTP client.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class StubFtpServerIntegrationTest extends AbstractTestCase implements IntegrationTest {

+

+    private static final Logger LOG = LoggerFactory.getLogger(StubFtpServerIntegrationTest.class);

+    private static final String SERVER = "localhost";

+    private static final String USERNAME = "user123";

+    private static final String PASSWORD = "password";

+    private static final String FILENAME = "abc.txt";

+    private static final String ASCII_CONTENTS = "abcdef\tghijklmnopqr";

+    private static final byte[] BINARY_CONTENTS = new byte[256];

+

+    private StubFtpServer stubFtpServer;

+    private FTPClient ftpClient;

+    private RetrCommandHandler retrCommandHandler;

+    private StorCommandHandler storCommandHandler;

+

+    //-------------------------------------------------------------------------

+    // Tests

+    //-------------------------------------------------------------------------

+

+    public void testLogin() throws Exception {

+        // Connect

+        LOG.info("Conecting to " + SERVER);

+        ftpClientConnect();

+        verifyReplyCode("connect", 220);

+

+        // Login

+        String userAndPassword = USERNAME + "/" + PASSWORD;

+        LOG.info("Logging in as " + userAndPassword);

+        boolean success = ftpClient.login(USERNAME, PASSWORD);

+        assertTrue("Unable to login with " + userAndPassword, success);

+        verifyReplyCode("login with " + userAndPassword, 230);

+

+        assertTrue("isStarted", stubFtpServer.isStarted());

+        assertFalse("isShutdown", stubFtpServer.isShutdown());

+

+        // Quit

+        LOG.info("Quit");

+        ftpClient.quit();

+        verifyReplyCode("quit", 221);

+    }

+

+    public void testAcct() throws Exception {

+        ftpClientConnect();

+

+        // ACCT

+        int replyCode = ftpClient.acct("123456");

+        assertEquals("acct", 230, replyCode);

+    }

+

+    /**

+     * Test the stop() method when no session has ever been started

+     */

+    public void testStop_NoSessionEverStarted() throws Exception {

+        LOG.info("Testing a stop() when no session has ever been started");

+    }

+

+    public void testHelp() throws Exception {

+        // Modify HELP CommandHandler to return a predefined help message

+        final String HELP = "help message";

+        HelpCommandHandler helpCommandHandler = (HelpCommandHandler) stubFtpServer.getCommandHandler(CommandNames.HELP);

+        helpCommandHandler.setHelpMessage(HELP);

+

+        ftpClientConnect();

+

+        // HELP

+        String help = ftpClient.listHelp();

+        assertTrue("Wrong response", help.indexOf(HELP) != -1);

+        verifyReplyCode("listHelp", 214);

+    }

+

+    /**

+     * Test the LIST and SYST commands.

+     */

+    public void testList() throws Exception {

+        ftpClientConnect();

+

+        // Set directory listing

+        ListCommandHandler listCommandHandler = (ListCommandHandler) stubFtpServer.getCommandHandler(CommandNames.LIST);

+        listCommandHandler.setDirectoryListing("11-09-01 12:30PM  406348 File2350.log\n"

+                + "11-01-01 1:30PM <DIR>  archive");

+

+        // LIST

+        FTPFile[] files = ftpClient.listFiles();

+        assertEquals("number of files", 2, files.length);

+        verifyFTPFile(files[0], FTPFile.FILE_TYPE, "File2350.log", 406348L);

+        verifyFTPFile(files[1], FTPFile.DIRECTORY_TYPE, "archive", 0L);

+        verifyReplyCode("list", 226);

+    }

+

+    /**

+     * Test the LIST, PASV and SYST commands, transferring a directory listing in passive mode

+     */

+    public void testList_PassiveMode() throws Exception {

+        ftpClientConnect();

+

+        ftpClient.enterLocalPassiveMode();

+

+        // Set directory listing

+        ListCommandHandler listCommandHandler = (ListCommandHandler) stubFtpServer.getCommandHandler(CommandNames.LIST);

+        listCommandHandler.setDirectoryListing("11-09-01 12:30PM  406348 File2350.log");

+

+        // LIST

+        FTPFile[] files = ftpClient.listFiles();

+        assertEquals("number of files", 1, files.length);

+        verifyReplyCode("list", 226);

+    }

+

+    public void testNlst() throws Exception {

+        ftpClientConnect();

+

+        // Set directory listing

+        NlstCommandHandler nlstCommandHandler = (NlstCommandHandler) stubFtpServer.getCommandHandler(CommandNames.NLST);

+        nlstCommandHandler.setDirectoryListing("File1.txt\nfile2.data");

+

+        // NLST

+        String[] filenames = ftpClient.listNames();

+        assertEquals("number of files", 2, filenames.length);

+        assertEquals(filenames[0], "File1.txt");

+        assertEquals(filenames[1], "file2.data");

+        verifyReplyCode("listNames", 226);

+    }

+

+    /**

+     * Test printing the current working directory (PWD)

+     */

+    public void testPwd() throws Exception {

+        // Modify PWD CommandHandler to return a predefined directory

+        final String DIR = "some/dir";

+        PwdCommandHandler pwdCommandHandler = (PwdCommandHandler) stubFtpServer.getCommandHandler(CommandNames.PWD);

+        pwdCommandHandler.setDirectory(DIR);

+

+        ftpClientConnect();

+

+        // PWD

+        String dir = ftpClient.printWorkingDirectory();

+        assertEquals("Unable to PWD", DIR, dir);

+        verifyReplyCode("printWorkingDirectory", 257);

+    }

+

+    public void testStat() throws Exception {

+        // Modify Stat CommandHandler to return predefined text

+        final String STATUS = "some information 123";

+        StatCommandHandler statCommandHandler = (StatCommandHandler) stubFtpServer.getCommandHandler(CommandNames.STAT);

+        statCommandHandler.setStatus(STATUS);

+

+        ftpClientConnect();

+

+        // STAT

+        String status = ftpClient.getStatus();

+        assertEquals("STAT reply", "211 " + STATUS + ".", status.trim());

+        verifyReplyCode("getStatus", 211);

+    }

+

+    /**

+     * Test getting the status (STAT), when the reply text contains multiple lines

+     */

+    public void testStat_MultilineReplyText() throws Exception {

+        // Modify Stat CommandHandler to return predefined text

+        final String STATUS = "System name: abc.def\nVersion 3.5.7\nNumber of failed logins: 2";

+        final String FORMATTED_REPLY_STATUS = "211-System name: abc.def\r\nVersion 3.5.7\r\n211 Number of failed logins: 2.";

+        StatCommandHandler statCommandHandler = (StatCommandHandler) stubFtpServer.getCommandHandler(CommandNames.STAT);

+        statCommandHandler.setStatus(STATUS);

+

+        ftpClientConnect();

+

+        // STAT

+        String status = ftpClient.getStatus();

+        assertEquals("STAT reply", FORMATTED_REPLY_STATUS, status.trim());

+        verifyReplyCode("getStatus", 211);

+    }

+

+    public void testSyst() throws Exception {

+        ftpClientConnect();

+

+        // SYST

+        assertEquals("getSystemName()", "\"WINDOWS\" system type.", ftpClient.getSystemName());

+        verifyReplyCode("syst", 215);

+    }

+

+    public void testCwd() throws Exception {

+        // Connect

+        LOG.info("Conecting to " + SERVER);

+        ftpClientConnect();

+        verifyReplyCode("connect", 220);

+

+        // CWD

+        boolean success = ftpClient.changeWorkingDirectory("dir1/dir2");

+        assertTrue("Unable to CWD", success);

+        verifyReplyCode("changeWorkingDirectory", 250);

+    }

+

+    /**

+     * Test changing the current working directory (CWD), when it causes a remote error

+     */

+    public void testCwd_Error() throws Exception {

+        // Override CWD CommandHandler to return error reply code

+        final int REPLY_CODE = 500;

+        StaticReplyCommandHandler cwdCommandHandler = new StaticReplyCommandHandler(REPLY_CODE);

+        stubFtpServer.setCommandHandler("CWD", cwdCommandHandler);

+

+        ftpClientConnect();

+

+        // CWD

+        boolean success = ftpClient.changeWorkingDirectory("dir1/dir2");

+        assertFalse("Expected failure", success);

+        verifyReplyCode("changeWorkingDirectory", REPLY_CODE);

+    }

+

+    public void testCdup() throws Exception {

+        ftpClientConnect();

+

+        // CDUP

+        boolean success = ftpClient.changeToParentDirectory();

+        assertTrue("Unable to CDUP", success);

+        verifyReplyCode("changeToParentDirectory", 200);

+    }

+

+    public void testDele() throws Exception {

+        ftpClientConnect();

+

+        // DELE

+        boolean success = ftpClient.deleteFile(FILENAME);

+        assertTrue("Unable to DELE", success);

+        verifyReplyCode("deleteFile", 250);

+    }

+

+    public void testEprt() throws Exception {

+        LOG.info("Skipping...");

+//        ftpClientConnect();

+//        ftpClient.sendCommand("EPRT", "|2|1080::8:800:200C:417A|5282|");

+//        verifyReplyCode("EPRT", 200);

+    }

+

+    public void testEpsv() throws Exception {

+        ftpClientConnect();

+        ftpClient.sendCommand("EPSV");

+        verifyReplyCode("EPSV", 229);

+    }

+    

+    public void testFeat_UseStaticReplyCommandHandler() throws IOException {

+        // The FEAT command is not supported out of the box

+        final String FEAT_TEXT = "Extensions supported:\n" +

+                "MLST size*;create;modify*;perm;media-type\n" +

+                "SIZE\n" +

+                "COMPRESSION\n" +

+                "END";

+        StaticReplyCommandHandler featCommandHandler = new StaticReplyCommandHandler(211, FEAT_TEXT);

+        stubFtpServer.setCommandHandler("FEAT", featCommandHandler);

+

+        ftpClientConnect();

+        assertEquals(ftpClient.sendCommand("FEAT"), 211);

+        LOG.info(ftpClient.getReplyString());

+    }

+

+    public void testMkd() throws Exception {

+        ftpClientConnect();

+

+        // MKD

+        boolean success = ftpClient.makeDirectory("dir1/dir2");

+        assertTrue("Unable to CWD", success);

+        verifyReplyCode("makeDirectory", 257);

+    }

+

+    public void testNoop() throws Exception {

+        ftpClientConnect();

+

+        // NOOP

+        boolean success = ftpClient.sendNoOp();

+        assertTrue("Unable to NOOP", success);

+        verifyReplyCode("NOOP", 200);

+    }

+

+    public void testRest() throws Exception {

+        ftpClientConnect();

+

+        // REST

+        int replyCode = ftpClient.rest("marker");

+        assertEquals("Unable to REST", 350, replyCode);

+    }

+

+    public void testRmd() throws Exception {

+        ftpClientConnect();

+

+        // RMD

+        boolean success = ftpClient.removeDirectory("dir1/dir2");

+        assertTrue("Unable to RMD", success);

+        verifyReplyCode("removeDirectory", 250);

+    }

+

+    public void testRename() throws Exception {

+        ftpClientConnect();

+

+        // Rename (RNFR, RNTO)

+        boolean success = ftpClient.rename(FILENAME, "new_" + FILENAME);

+        assertTrue("Unable to RENAME", success);

+        verifyReplyCode("rename", 250);

+    }

+

+    public void testAllo() throws Exception {

+        ftpClientConnect();

+

+        // ALLO

+        assertTrue("ALLO", ftpClient.allocate(1024));

+        assertTrue("ALLO with recordSize", ftpClient.allocate(1024, 64));

+    }

+

+    /**

+     * Test GET and PUT of ASCII files

+     */

+    public void testTransferAsciiFile() throws Exception {

+        retrCommandHandler.setFileContents(ASCII_CONTENTS);

+

+        ftpClientConnect();

+

+        // Get File

+        LOG.info("Get File for remotePath [" + FILENAME + "]");

+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

+        assertTrue(ftpClient.retrieveFile(FILENAME, outputStream));

+        LOG.info("File contents=[" + outputStream.toString());

+        assertEquals("File contents", ASCII_CONTENTS, outputStream.toString());

+

+        // Put File

+        LOG.info("Put File for local path [" + FILENAME + "]");

+        ByteArrayInputStream inputStream = new ByteArrayInputStream(ASCII_CONTENTS.getBytes());

+        assertTrue(ftpClient.storeFile(FILENAME, inputStream));

+        InvocationRecord invocationRecord = storCommandHandler.getInvocation(0);

+        byte[] contents = (byte[]) invocationRecord.getObject(StorCommandHandler.FILE_CONTENTS_KEY);

+        LOG.info("File contents=[" + contents + "]");

+        assertEquals("File contents", ASCII_CONTENTS.getBytes(), contents);

+    }

+

+    /**

+     * Test GET and PUT of binary files

+     */

+    public void testTransferBinaryFiles() throws Exception {

+        retrCommandHandler.setFileContents(BINARY_CONTENTS);

+

+        ftpClientConnect();

+        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);

+

+        // Get File

+        LOG.info("Get File for remotePath [" + FILENAME + "]");

+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

+        assertTrue("GET", ftpClient.retrieveFile(FILENAME, outputStream));

+        LOG.info("GET File length=" + outputStream.size());

+        assertEquals("File contents", BINARY_CONTENTS, outputStream.toByteArray());

+

+        // Put File

+        LOG.info("Put File for local path [" + FILENAME + "]");

+        ByteArrayInputStream inputStream = new ByteArrayInputStream(BINARY_CONTENTS);

+        assertTrue("PUT", ftpClient.storeFile(FILENAME, inputStream));

+        InvocationRecord invocationRecord = storCommandHandler.getInvocation(0);

+        byte[] contents = (byte[]) invocationRecord.getObject(StorCommandHandler.FILE_CONTENTS_KEY);

+        LOG.info("PUT File length=" + contents.length);

+        assertEquals("File contents", BINARY_CONTENTS, contents);

+    }

+

+    public void testStou() throws Exception {

+        StouCommandHandler stouCommandHandler = (StouCommandHandler) stubFtpServer.getCommandHandler(CommandNames.STOU);

+        stouCommandHandler.setFilename(FILENAME);

+

+        ftpClientConnect();

+

+        // Stor a File (STOU)

+        ByteArrayInputStream inputStream = new ByteArrayInputStream(ASCII_CONTENTS.getBytes());

+        assertTrue(ftpClient.storeUniqueFile(FILENAME, inputStream));

+        InvocationRecord invocationRecord = stouCommandHandler.getInvocation(0);

+        byte[] contents = (byte[]) invocationRecord.getObject(StorCommandHandler.FILE_CONTENTS_KEY);

+        LOG.info("File contents=[" + contents + "]");

+        assertEquals("File contents", ASCII_CONTENTS.getBytes(), contents);

+    }

+

+    public void testAppe() throws Exception {

+        AppeCommandHandler appeCommandHandler = (AppeCommandHandler) stubFtpServer.getCommandHandler(CommandNames.APPE);

+

+        ftpClientConnect();

+

+        // Append a File (APPE)

+        ByteArrayInputStream inputStream = new ByteArrayInputStream(ASCII_CONTENTS.getBytes());

+        assertTrue(ftpClient.appendFile(FILENAME, inputStream));

+        InvocationRecord invocationRecord = appeCommandHandler.getInvocation(0);

+        byte[] contents = (byte[]) invocationRecord.getObject(AppeCommandHandler.FILE_CONTENTS_KEY);

+        LOG.info("File contents=[" + contents + "]");

+        assertEquals("File contents", ASCII_CONTENTS.getBytes(), contents);

+    }

+

+    public void testAbor() throws Exception {

+        ftpClientConnect();

+

+        // ABOR

+        assertTrue("ABOR", ftpClient.abort());

+    }

+

+    public void testPasv() throws Exception {

+        ftpClientConnect();

+

+        // PASV

+        ftpClient.enterLocalPassiveMode();

+        // no reply code; the PASV command is sent only when the data connection is opened 

+    }

+

+    public void testMode() throws Exception {

+        ftpClientConnect();

+

+        // MODE

+        boolean success = ftpClient.setFileTransferMode(FTP.STREAM_TRANSFER_MODE);

+        assertTrue("Unable to MODE", success);

+        verifyReplyCode("setFileTransferMode", 200);

+    }

+

+    public void testStru() throws Exception {

+        ftpClientConnect();

+

+        // STRU

+        boolean success = ftpClient.setFileStructure(FTP.FILE_STRUCTURE);

+        assertTrue("Unable to STRU", success);

+        verifyReplyCode("setFileStructure", 200);

+    }

+

+    public void testSimpleCompositeCommandHandler() throws Exception {

+        // Replace CWD CommandHandler with a SimpleCompositeCommandHandler

+        CommandHandler commandHandler1 = new StaticReplyCommandHandler(500);

+        CommandHandler commandHandler2 = new CwdCommandHandler();

+        SimpleCompositeCommandHandler simpleCompositeCommandHandler = new SimpleCompositeCommandHandler();

+        simpleCompositeCommandHandler.addCommandHandler(commandHandler1);

+        simpleCompositeCommandHandler.addCommandHandler(commandHandler2);

+        stubFtpServer.setCommandHandler("CWD", simpleCompositeCommandHandler);

+

+        // Connect

+        ftpClientConnect();

+

+        // CWD

+        assertFalse("first", ftpClient.changeWorkingDirectory("dir1/dir2"));

+        assertTrue("first", ftpClient.changeWorkingDirectory("dir1/dir2"));

+    }

+

+    public void testSite() throws Exception {

+        ftpClientConnect();

+

+        // SITE

+        int replyCode = ftpClient.site("parameters,1,2,3");

+        assertEquals("SITE", 200, replyCode);

+    }

+

+    public void testSmnt() throws Exception {

+        ftpClientConnect();

+

+        // SMNT

+        assertTrue("SMNT", ftpClient.structureMount("dir1/dir2"));

+        verifyReplyCode("structureMount", 250);

+    }

+

+    public void testRein() throws Exception {

+        ftpClientConnect();

+

+        // REIN

+        assertEquals("REIN", 220, ftpClient.rein());

+    }

+

+    /**

+     * Test that command names in lowercase or mixed upper/lower case are accepted

+     */

+    public void testCommandNamesInLowerOrMixedCase() throws Exception {

+        ftpClientConnect();

+

+        assertEquals("rein", 220, ftpClient.sendCommand("rein"));

+        assertEquals("rEIn", 220, ftpClient.sendCommand("rEIn"));

+        assertEquals("reiN", 220, ftpClient.sendCommand("reiN"));

+        assertEquals("Rein", 220, ftpClient.sendCommand("Rein"));

+    }

+

+    public void testUnrecognizedCommand() throws Exception {

+        ftpClientConnect();

+

+        assertEquals("Unrecognized:XXXX", 502, ftpClient.sendCommand("XXXX"));

+    }

+

+    // -------------------------------------------------------------------------

+    // Test setup and tear-down

+    // -------------------------------------------------------------------------

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+

+        for (int i = 0; i < BINARY_CONTENTS.length; i++) {

+            BINARY_CONTENTS[i] = (byte) i;

+        }

+

+        stubFtpServer = new StubFtpServer();

+        stubFtpServer.setServerControlPort(PortTestUtil.getFtpServerControlPort());

+        stubFtpServer.start();

+        ftpClient = new FTPClient();

+        retrCommandHandler = (RetrCommandHandler) stubFtpServer.getCommandHandler(CommandNames.RETR);

+        storCommandHandler = (StorCommandHandler) stubFtpServer.getCommandHandler(CommandNames.STOR);

+    }

+

+    /**

+     * Perform cleanup after each test

+     *

+     * @see org.mockftpserver.test.AbstractTestCase#tearDown()

+     */

+    protected void tearDown() throws Exception {

+        super.tearDown();

+        stubFtpServer.stop();

+    }

+

+    // -------------------------------------------------------------------------

+    // Internal Helper Methods

+    // -------------------------------------------------------------------------

+

+    /**

+     * Connect to the server from the FTPClient

+     */

+    private void ftpClientConnect() throws IOException {

+        ftpClient.connect(SERVER, PortTestUtil.getFtpServerControlPort());

+    }

+

+    /**

+     * Assert that the FtpClient reply code is equal to the expected value

+     *

+     * @param operation         - the description of the operation performed; used in the error message

+     * @param expectedReplyCode - the expected FtpClient reply code

+     */

+    private void verifyReplyCode(String operation, int expectedReplyCode) {

+        int replyCode = ftpClient.getReplyCode();

+        LOG.info("Reply: operation=\"" + operation + "\" replyCode=" + replyCode);

+        assertEquals("Unexpected replyCode for " + operation, expectedReplyCode, replyCode);

+    }

+

+    /**

+     * Verify that the FTPFile has the specified properties

+     *

+     * @param ftpFile - the FTPFile to verify

+     * @param type    - the expected file type

+     * @param name    - the expected file name

+     * @param size    - the expected file size (will be zero for a directory)

+     */

+    private void verifyFTPFile(FTPFile ftpFile, int type, String name, long size) {

+        LOG.info(ftpFile.toString());

+        assertEquals("type: " + ftpFile, type, ftpFile.getType());

+        assertEquals("name: " + ftpFile, name, ftpFile.getName());

+        assertEquals("size: " + ftpFile, size, ftpFile.getSize());

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/StubFtpServerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/StubFtpServerTest.java
new file mode 100644
index 0000000..797d806
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/StubFtpServerTest.java
@@ -0,0 +1,122 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub;

+

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandHandler;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.server.*;

+import org.mockftpserver.core.server.AbstractFtpServerTestCase;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.stub.command.AbstractStubCommandHandler;

+import org.mockftpserver.stub.command.CwdCommandHandler;

+

+import java.util.ResourceBundle;

+

+/**

+ * Unit tests for StubFtpServer. Also see {@link StubFtpServer_StartTest}

+ * and {@link StubFtpServerIntegrationTest}.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class StubFtpServerTest extends AbstractFtpServerTestCase {

+

+    private StubFtpServer stubFtpServer;

+    private AbstractStubCommandHandler commandHandler;

+    private CommandHandler commandHandler_NoReplyTextBundle;

+

+    //-------------------------------------------------------------------------

+    // Extra tests  (Standard tests defined in superclass)

+    //-------------------------------------------------------------------------

+

+    /**

+     * Test the setCommandHandler() method, for a CommandHandler that does not implement ResourceBundleAware

+     */

+    public void testSetCommandHandler_NotReplyTextBundleAware() {

+        stubFtpServer.setCommandHandler("ZZZ", commandHandler_NoReplyTextBundle);

+        assertSame("commandHandler", commandHandler_NoReplyTextBundle, stubFtpServer.getCommandHandler("ZZZ"));

+    }

+

+    /**

+     * Test the setCommandHandler() method, for a CommandHandler that implements ReplyTextBundleAware,

+     * and whose replyTextBundle attribute is null.

+     */

+    public void testSetCommandHandler_NullReplyTextBundle() {

+        stubFtpServer.setCommandHandler("ZZZ", commandHandler);

+        assertSame("commandHandler", commandHandler, stubFtpServer.getCommandHandler("ZZZ"));

+        assertSame("replyTextBundle", stubFtpServer.getReplyTextBundle(), commandHandler.getReplyTextBundle());

+    }

+

+    /**

+     * Test setReplyTextBaseName() method

+     */

+    public void testSetReplyTextBaseName() {

+        stubFtpServer.setReplyTextBaseName("SampleReplyText");

+        CwdCommandHandler commandHandler = new CwdCommandHandler();

+

+        // The resource bundle is passed along to new CommandHandlers (if they don't already have one) 

+        stubFtpServer.setCommandHandler("CWD", commandHandler);

+        ResourceBundle resourceBundle = commandHandler.getReplyTextBundle();

+        assertEquals("110", "Testing123", resourceBundle.getString("110"));

+    }

+

+    //-------------------------------------------------------------------------

+    // Test setup

+    //-------------------------------------------------------------------------

+

+    /**

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+

+        stubFtpServer = (StubFtpServer) ftpServer;

+

+        // Create a CommandHandler instance that also implements ResourceBundleAware

+        commandHandler = new AbstractStubCommandHandler() {

+            protected void handleCommand(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+            }

+        };

+

+        // Create a CommandHandler instance that does NOT implement ResourceBundleAware

+        commandHandler_NoReplyTextBundle = new CommandHandler() {

+            public void handleCommand(Command command, Session session) throws Exception {

+            }

+        };

+    }

+

+    //-------------------------------------------------------------------------

+    // Abstract method implementations

+    //-------------------------------------------------------------------------

+

+    protected AbstractFtpServer createFtpServer() {

+        return new StubFtpServer();

+    }

+

+    protected CommandHandler createCommandHandler() {

+        return new AbstractStubCommandHandler() {

+            protected void handleCommand(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+            }

+        };

+    }

+

+    protected void verifyCommandHandlerInitialized(CommandHandler commandHandler) {

+        AbstractStubCommandHandler stubCommandHandler = (AbstractStubCommandHandler) commandHandler;

+        assertSame("replyTextBundle", stubFtpServer.getReplyTextBundle(), stubCommandHandler.getReplyTextBundle());

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/StubFtpServer_MultipleClientsIntegrationTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/StubFtpServer_MultipleClientsIntegrationTest.java
new file mode 100644
index 0000000..87ad2dd
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/StubFtpServer_MultipleClientsIntegrationTest.java
@@ -0,0 +1,137 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub;

+

+import org.apache.commons.net.ftp.FTPClient;

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.stub.command.AbstractStubCommandHandler;

+import org.mockftpserver.test.AbstractTestCase;

+import org.mockftpserver.test.IntegrationTest;

+import org.mockftpserver.test.PortTestUtil;

+

+/**

+ * StubFtpServer tests for multiple FTP clients using the Apache Jakarta Commons Net FTP client.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class StubFtpServer_MultipleClientsIntegrationTest extends AbstractTestCase implements

+        IntegrationTest {

+

+    private static final Logger LOG = LoggerFactory.getLogger(StubFtpServer_MultipleClientsIntegrationTest.class);

+    private static final String SERVER = "localhost";

+

+    // Custom CommandHandler for PWD so that we can verify unique session-specific responses.

+    // Send back the hashCode for the Session as the reply text.

+    private static class CustomPwdCommandHandler extends AbstractStubCommandHandler {

+        protected void handleCommand(Command command, Session session, InvocationRecord invocationRecord) throws Exception {

+            String replyText = quotes(Integer.toString(session.hashCode()));

+            sendReply(session, 257, null, replyText, null);

+        }

+    }

+    

+    private StubFtpServer stubFtpServer;

+    private FTPClient ftpClient1;

+    private FTPClient ftpClient2;

+    private FTPClient ftpClient3;

+

+    /**

+     * Test that multiple simultaneous clients can connect and establish sessions. 

+     */

+    public void testMultipleClients() throws Exception {

+

+        // Connect from client 1

+        LOG.info("connect() to ftpClient1");

+        ftpClient1.connect(SERVER, PortTestUtil.getFtpServerControlPort());

+        String sessionId1 = ftpClient1.printWorkingDirectory();

+        LOG.info("PWD(1) reply =[" + sessionId1 + "]");

+

+        // Connect from client 2

+        LOG.info("connect() to ftpClient2");

+        ftpClient2.connect(SERVER, PortTestUtil.getFtpServerControlPort());

+        String sessionId2 = ftpClient2.printWorkingDirectory();

+        LOG.info("PWD(2) reply =[" + sessionId2 + "]");

+

+        // Connect from client 3

+        LOG.info("connect() to ftpClient3");

+        ftpClient3.connect(SERVER, PortTestUtil.getFtpServerControlPort());

+        String sessionId3 = ftpClient3.printWorkingDirectory();

+        LOG.info("PWD(3) reply =[" + sessionId3 + "]");

+        

+        // Make sure all session ids are unique

+        assertNotSame("sessionId1 vs sessionId2", sessionId1, sessionId2);

+        assertNotSame("sessionId2 vs sessionId3", sessionId2, sessionId3);

+        assertNotSame("sessionId1 vs sessionId3", sessionId1, sessionId3);

+

+        // Now make sure that the replies from the existing sessions remain consistent

+        assertEquals("reply from session1", sessionId1, ftpClient1.printWorkingDirectory());

+        assertEquals("reply from session2", sessionId2, ftpClient2.printWorkingDirectory());

+        assertEquals("reply from session3", sessionId3, ftpClient3.printWorkingDirectory());

+    }

+    

+    // -------------------------------------------------------------------------

+    // Test setup and tear-down

+    // -------------------------------------------------------------------------

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+

+        stubFtpServer = new StubFtpServer();

+        stubFtpServer.setServerControlPort(PortTestUtil.getFtpServerControlPort());

+        stubFtpServer.setCommandHandler(CommandNames.PWD, new CustomPwdCommandHandler());

+        stubFtpServer.start();

+        

+        ftpClient1 = new FTPClient();

+        ftpClient2 = new FTPClient();

+        ftpClient3 = new FTPClient();

+        

+        ftpClient1.setDefaultTimeout(1000);

+        ftpClient2.setDefaultTimeout(1000);

+        ftpClient3.setDefaultTimeout(1000);

+    }

+

+    /**

+     * Perform cleanup after each test

+     * @see org.mockftpserver.test.AbstractTestCase#tearDown()

+     */

+    protected void tearDown() throws Exception {

+        super.tearDown();

+

+        LOG.info("Cleaning up...");

+        if (ftpClient1.isConnected()) {

+            ftpClient1.disconnect();

+        }

+        if (ftpClient2.isConnected()) {

+            ftpClient2.disconnect();

+        }

+        if (ftpClient3.isConnected()) {

+            ftpClient3.disconnect();

+        }

+

+        stubFtpServer.stop();

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/StubFtpServer_StartTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/StubFtpServer_StartTest.java
new file mode 100644
index 0000000..e67b289
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/StubFtpServer_StartTest.java
@@ -0,0 +1,37 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub;

+

+import org.mockftpserver.core.server.AbstractFtpServer;

+import org.mockftpserver.core.server.AbstractFtpServer_StartTestCase;

+

+/**

+ * Tests for StubFtpServer that require the StubFtpServer thread to be started.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class StubFtpServer_StartTest extends AbstractFtpServer_StartTestCase {

+

+    //-------------------------------------------------------------------------

+    // Abstract method implementations

+    //-------------------------------------------------------------------------

+

+    protected AbstractFtpServer createFtpServer() {

+        return new StubFtpServer();

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/AborCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/AborCommandHandlerTest.java
new file mode 100644
index 0000000..6aab009
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/AborCommandHandlerTest.java
@@ -0,0 +1,59 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the AborCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class AborCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private AborCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+        final Command COMMAND = new Command(CommandNames.ABOR, EMPTY);

+

+        session.sendReply(ReplyCodes.ABOR_OK, replyTextFor(ReplyCodes.ABOR_OK));

+        replay(session);

+

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new AborCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/AcctCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/AcctCommandHandlerTest.java
new file mode 100644
index 0000000..0960f97
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/AcctCommandHandlerTest.java
@@ -0,0 +1,75 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the AcctCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class AcctCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final String ACCOUNT1 = "account1";

+    private static final String ACCOUNT2 = "account2";

+

+    private AcctCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+

+    /**

+     * Test the handleCommand() method

+     * @throws Exception

+     */

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(ReplyCodes.ACCT_OK, replyTextFor(ReplyCodes.ACCT_OK));

+        session.sendReply(ReplyCodes.ACCT_OK, replyTextFor(ReplyCodes.ACCT_OK));

+        replay(session);

+        

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), AcctCommandHandler.ACCOUNT_KEY, ACCOUNT1);

+        verifyOneDataElement(commandHandler.getInvocation(1), AcctCommandHandler.ACCOUNT_KEY, ACCOUNT2);

+    }

+    

+    /**

+     * Test the handleCommand() method, when no password parameter has been specified

+     */

+    public void testHandleCommand_MissingPasswordParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.ACCT, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new AcctCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.ACCT, array(ACCOUNT1));

+        command2 = new Command(CommandNames.ACCT, array(ACCOUNT2));

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/AlloCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/AlloCommandHandlerTest.java
new file mode 100644
index 0000000..3d555b0
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/AlloCommandHandlerTest.java
@@ -0,0 +1,123 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.util.AssertFailedException;

+

+/**

+ * Tests for the AlloCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class AlloCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(AlloCommandHandlerTest.class);

+    private static final int BYTES1 = 64;

+    private static final int BYTES2 = 555;

+    private static final int RECORD_SIZE = 77;

+

+    private AlloCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(ReplyCodes.ALLO_OK, replyTextFor(ReplyCodes.ALLO_OK));

+        session.sendReply(ReplyCodes.ALLO_OK, replyTextFor(ReplyCodes.ALLO_OK));

+        replay(session);

+

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), AlloCommandHandler.NUMBER_OF_BYTES_KEY, new Integer(

+                BYTES1));

+        verifyTwoDataElements(commandHandler.getInvocation(1), AlloCommandHandler.NUMBER_OF_BYTES_KEY, new Integer(

+                BYTES2), AlloCommandHandler.RECORD_SIZE_KEY, new Integer(RECORD_SIZE));

+    }

+

+    /**

+     * Test the handleCommand() method, when no numberOfBytes parameter has been specified

+     */

+    public void testHandleCommand_MissingNumberOfBytesParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.ALLO, EMPTY);

+    }

+

+    /**

+     * Test the handleCommand() method, when the recordSize delimiter ("R") parameter is specified,

+     * but is not followed by the recordSize value.

+     */

+    public void testHandleCommand_RecordSizeDelimiterWithoutValue() throws Exception {

+        try {

+            commandHandler.handleCommand(new Command(CommandNames.ALLO, array("123 R ")), session);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the handleCommand() method, when a non-numeric numberOfBytes parameter has been

+     * specified

+     */

+    public void testHandleCommand_InvalidNumberOfBytesParameter() throws Exception {

+        try {

+            commandHandler.handleCommand(new Command(CommandNames.ALLO, array("xx")), session);

+            fail("Expected NumberFormatException");

+        }

+        catch (NumberFormatException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the handleCommand() method, when a non-numeric recordSize parameter has been specified

+     */

+    public void testHandleCommand_InvalidRecordSizeParameter() throws Exception {

+        try {

+            commandHandler.handleCommand(new Command(CommandNames.ALLO, array("123 R xx")), session);

+            fail("Expected NumberFormatException");

+        }

+        catch (NumberFormatException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new AlloCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.ALLO, array(Integer.toString(BYTES1)));

+        command2 = new Command(CommandNames.ALLO, array(Integer.toString(BYTES2) + " R " + Integer.toString(RECORD_SIZE)));

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/AppeCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/AppeCommandHandlerTest.java
new file mode 100644
index 0000000..8b288d4
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/AppeCommandHandlerTest.java
@@ -0,0 +1,72 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the AppeCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class AppeCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private AppeCommandHandler commandHandler;

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new AppeCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+    /**

+     * Test the handleCommand() method, as well as the getFileContents() and clearFileContents() methods

+     */

+    public void testHandleCommand() throws Exception {

+        final String DATA = "ABC";

+

+        session.sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK));

+        session.openDataConnection();

+        session.readData();

+        control(session).setReturnValue(DATA.getBytes());

+        session.closeDataConnection();

+        session.sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_FINAL_OK));

+        replay(session);

+

+        Command command = new Command(CommandNames.APPE, array(FILENAME1));

+        commandHandler.handleCommand(command, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyTwoDataElements(commandHandler.getInvocation(0), AppeCommandHandler.PATHNAME_KEY, FILENAME1,

+                AppeCommandHandler.FILE_CONTENTS_KEY, DATA.getBytes());

+    }

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.APPE, EMPTY);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/CdupCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/CdupCommandHandlerTest.java
new file mode 100644
index 0000000..ea449dd
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/CdupCommandHandlerTest.java
@@ -0,0 +1,65 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the CdupCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class CdupCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private CdupCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+    

+    /**

+     * Test the handleCommand(Command,Session) method

+     * @throws Exception

+     */

+    public void testHandleCommand() throws Exception {

+        session.sendReply(ReplyCodes.CDUP_OK, replyTextFor(ReplyCodes.CDUP_OK));

+        session.sendReply(ReplyCodes.CDUP_OK, replyTextFor(ReplyCodes.CDUP_OK));

+        replay(session);

+        

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+        verifyNoDataElements(commandHandler.getInvocation(1));

+    }

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new CdupCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.CDUP, EMPTY);

+        command2 = new Command(CommandNames.CDUP, EMPTY);

+    }

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/CwdCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/CwdCommandHandlerTest.java
new file mode 100644
index 0000000..1ca9707
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/CwdCommandHandlerTest.java
@@ -0,0 +1,72 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the CwdCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class CwdCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private CwdCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+    

+    /**

+     * Test the handleCommand(Command,Session) method

+     * @throws Exception

+     */

+    public void testHandleCommand() throws Exception {

+        session.sendReply(ReplyCodes.CWD_OK, replyTextFor(ReplyCodes.CWD_OK));

+        session.sendReply(ReplyCodes.CWD_OK, replyTextFor(ReplyCodes.CWD_OK));

+        replay(session);

+        

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), CwdCommandHandler.PATHNAME_KEY, DIR1);

+        verifyOneDataElement(commandHandler.getInvocation(1), CwdCommandHandler.PATHNAME_KEY, DIR2);

+    }

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.CWD, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new CwdCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.CWD, array(DIR1));

+        command2 = new Command(CommandNames.CWD, array(DIR2));

+    }

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/DeleCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/DeleCommandHandlerTest.java
new file mode 100644
index 0000000..f7fcad0
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/DeleCommandHandlerTest.java
@@ -0,0 +1,72 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the DeleCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class DeleCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private DeleCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+    

+    /**

+     * Test the handleCommand(Command,Session) method

+     * @throws Exception

+     */

+    public void testHandleCommand() throws Exception {

+        session.sendReply(ReplyCodes.DELE_OK, replyTextFor(ReplyCodes.DELE_OK));

+        session.sendReply(ReplyCodes.DELE_OK, replyTextFor(ReplyCodes.DELE_OK));

+        replay(session);

+        

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), DeleCommandHandler.PATHNAME_KEY, FILENAME1);

+        verifyOneDataElement(commandHandler.getInvocation(1), DeleCommandHandler.PATHNAME_KEY, FILENAME2);

+    }

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.DELE, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new DeleCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.DELE, array(FILENAME1));

+        command2 = new Command(CommandNames.DELE, array(FILENAME2));

+    }

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/EprtCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/EprtCommandHandlerTest.java
new file mode 100644
index 0000000..632fa59
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/EprtCommandHandlerTest.java
@@ -0,0 +1,89 @@
+/*

+ * Copyright 2009 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+import java.net.InetAddress;

+

+/**

+ * Tests for the EprtCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class EprtCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final String[] PARAMETERS_INSUFFICIENT = EMPTY;

+    private static final String[] PARAMETERS_IPV4 = {"|1|132.235.1.2|6275|"};

+    private static final InetAddress HOST_IPV4 = inetAddress("132.235.1.2");

+    private static final String[] PARAMETERS_IPV6 = {"|2|1080::8:800:200C:417A|6275|"};

+    private static final InetAddress HOST_IPV6 = inetAddress("1080::8:800:200C:417A");

+    private static final int PORT = 6275;

+

+    private EprtCommandHandler commandHandler;

+

+    public void testHandleCommand_IPv4() throws Exception {

+        final Command COMMAND = new Command(CommandNames.EPRT, PARAMETERS_IPV4);

+

+        session.setClientDataPort(PORT);

+        session.setClientDataHost(HOST_IPV4);

+        session.sendReply(ReplyCodes.EPRT_OK, replyTextFor(ReplyCodes.EPRT_OK));

+        replay(session);

+

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyTwoDataElements(commandHandler.getInvocation(0),

+                PortCommandHandler.HOST_KEY, HOST_IPV4,

+                PortCommandHandler.PORT_KEY, new Integer(PORT));

+    }

+

+    public void testHandleCommand_IPv6() throws Exception {

+        final Command COMMAND = new Command(CommandNames.EPRT, PARAMETERS_IPV6);

+

+        session.setClientDataPort(PORT);

+        session.setClientDataHost(HOST_IPV6);

+        session.sendReply(ReplyCodes.EPRT_OK, replyTextFor(ReplyCodes.EPRT_OK));

+        replay(session);

+

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyTwoDataElements(commandHandler.getInvocation(0),

+                PortCommandHandler.HOST_KEY, HOST_IPV6,

+                PortCommandHandler.PORT_KEY, new Integer(PORT));

+    }

+

+    public void testHandleCommand_MissingRequiredParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.EPRT, PARAMETERS_INSUFFICIENT);

+    }

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new EprtCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/EpsvCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/EpsvCommandHandlerTest.java
new file mode 100644
index 0000000..e995a8f
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/EpsvCommandHandlerTest.java
@@ -0,0 +1,67 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+import java.net.InetAddress;

+

+/**

+ * Tests for the EpsvCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class EpsvCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final InetAddress SERVER = inetAddress("1080::8:800:200C:417A");

+    private static final int PORT = 6275;

+

+    private EpsvCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+        session.switchToPassiveMode();

+        control(session).setReturnValue(PORT);

+        session.getServerHost();

+        control(session).setReturnValue(SERVER);

+        session.sendReply(ReplyCodes.EPSV_OK, formattedReplyTextFor(ReplyCodes.EPSV_OK, Integer.toString(PORT)));

+        replay(session);

+

+        final Command COMMAND = new Command(CommandNames.EPSV, EMPTY);

+

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new EpsvCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}
\ No newline at end of file
diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/FileRetrCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/FileRetrCommandHandlerTest.java
new file mode 100644
index 0000000..2e02f6e
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/FileRetrCommandHandlerTest.java
@@ -0,0 +1,179 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.easymock.ArgumentsMatcher;

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.util.AssertFailedException;

+

+import java.util.Arrays;

+

+/**

+ * Tests for the FileRetrCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class FileRetrCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(FileRetrCommandHandlerTest.class);

+    private static final byte BYTE1 = (byte) 7;

+    private static final byte BYTE2 = (byte) 21;

+

+    private FileRetrCommandHandler commandHandler;

+

+    /**

+     * Test the constructor that takes a String, passing in a null

+     */

+    public void testConstructor_String_Null() {

+        try {

+            new FileRetrCommandHandler(null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the setFile(String) method, passing in a null

+     */

+    public void testSetFile_Null() {

+        try {

+            commandHandler.setFile(null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the handleCommand(Command,Session) method. Create a temporary (binary) file, and

+     * make sure its contents are written back

+     *

+     * @throws Exception

+     */

+    public void testHandleCommand() throws Exception {

+

+        final byte[] BUFFER = new byte[FileRetrCommandHandler.BUFFER_SIZE];

+        Arrays.fill(BUFFER, BYTE1);

+

+        session.sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK));

+        session.openDataConnection();

+

+        ArgumentsMatcher matcher = new ArgumentsMatcher() {

+            int counter = -1;   // will increment for each invocation

+

+            public boolean matches(Object[] expected, Object[] actual) {

+                counter++;

+                byte[] buffer = (byte[]) actual[0];

+                int expectedLength = ((Integer) expected[1]).intValue();

+                int actualLength = ((Integer) actual[1]).intValue();

+                LOG.info("invocation #" + counter + " expected=" + expectedLength + " actualLength=" + actualLength);

+                if (counter < 5) {

+                    assertEquals("buffer for invocation #" + counter, BUFFER, buffer);

+                } else {

+                    // TODO Got two invocations here; only expected one

+                    //assertEquals("length for invocation #" + counter, expectedLength, actualLength);

+                    assertEquals("buffer[0]", BYTE2, buffer[0]);

+                    assertEquals("buffer[1]", BYTE2, buffer[1]);

+                    assertEquals("buffer[2]", BYTE2, buffer[2]);

+                }

+                return true;

+            }

+

+            public String toString(Object[] args) {

+                return args[0].getClass().getName() + " " + args[1].toString();

+            }

+        };

+

+        session.sendData(BUFFER, 512);

+        control(session).setMatcher(matcher);

+        session.sendData(BUFFER, 512);

+        session.sendData(BUFFER, 512);

+        session.sendData(BUFFER, 512);

+        session.sendData(BUFFER, 512);

+        session.sendData(BUFFER, 3);

+

+        session.closeDataConnection();

+        session.sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_FINAL_OK));

+        replay(session);

+

+        commandHandler.setFile("Sample.jpg");

+        Command command = new Command(CommandNames.RETR, array(FILENAME1));

+        commandHandler.handleCommand(command, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyOneDataElement(commandHandler.getInvocation(0), FileRetrCommandHandler.PATHNAME_KEY, FILENAME1);

+    }

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        commandHandler.setFile("abc.txt");      // this property must be set

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.RETR, EMPTY);

+    }

+

+    /**

+     * Test the HandleCommand method, when the file property has not been set

+     */

+    public void testHandleCommand_FileNotSet() throws Exception {

+        try {

+            commandHandler.handleCommand(new Command(CommandNames.RETR, EMPTY), session);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new FileRetrCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+//    /**

+//     * Create a sample binary file; 5 buffers full plus 3 extra bytes

+//     */

+//    private void createSampleFile() {

+//        final String FILE_PATH = "test/org.mockftpserver/command/Sample.jpg";

+//        final byte[] BUFFER = new byte[FileRetrCommandHandler.BUFFER_SIZE];

+//        Arrays.fill(BUFFER, BYTE1);

+//

+//        File file = new File(FILE_PATH);

+//        FileOutputStream out = new FileOutputStream(file);

+//        for (int i = 0; i < 5; i++) {

+//            out.write(BUFFER);

+//        }

+//        Arrays.fill(BUFFER, BYTE2);

+//        out.write(BUFFER, 0, 3);

+//        out.close();

+//        LOG.info("Created temporary file [" + FILE_PATH + "]: length=" + file.length());

+//    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/HelpCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/HelpCommandHandlerTest.java
new file mode 100644
index 0000000..3605fdb
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/HelpCommandHandlerTest.java
@@ -0,0 +1,69 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the HelpCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class HelpCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private HelpCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        final String RESPONSE_DATA = "help for ABC...";

+        commandHandler.setHelpMessage(RESPONSE_DATA);

+

+        session.sendReply(ReplyCodes.HELP_OK, formattedReplyTextFor(ReplyCodes.HELP_OK, RESPONSE_DATA));

+        session.sendReply(ReplyCodes.HELP_OK, formattedReplyTextFor(ReplyCodes.HELP_OK, RESPONSE_DATA));

+        replay(session);

+

+        final Command COMMAND1 = new Command(CommandNames.HELP, EMPTY);

+        final Command COMMAND2 = new Command(CommandNames.HELP, array("abc"));

+

+        commandHandler.handleCommand(COMMAND1, session);

+        commandHandler.handleCommand(COMMAND2, session);

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), HelpCommandHandler.COMMAND_NAME_KEY, null);

+        verifyOneDataElement(commandHandler.getInvocation(1), HelpCommandHandler.COMMAND_NAME_KEY, "abc");

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new HelpCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/ListCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/ListCommandHandlerTest.java
new file mode 100644
index 0000000..7301798
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/ListCommandHandlerTest.java
@@ -0,0 +1,75 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.easymock.MockControl;

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the ListCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class ListCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private ListCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     *

+     * @throws Exception

+     */

+    public void testHandleCommand() throws Exception {

+        final String DIR_LISTING = " directory listing\nabc.txt\ndef.log\n";

+        final String DIR_LISTING_TRIMMED = DIR_LISTING.trim();

+        ((ListCommandHandler) commandHandler).setDirectoryListing(DIR_LISTING);

+

+        for (int i = 0; i < 2; i++) {

+            session.sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK));

+            session.openDataConnection();

+            byte[] bytes = DIR_LISTING_TRIMMED.getBytes();

+            session.sendData(bytes, bytes.length);

+            control(session).setMatcher(MockControl.ARRAY_MATCHER);

+            session.closeDataConnection();

+            session.sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_FINAL_OK));

+        }

+        replay(session);

+

+        Command command1 = new Command(CommandNames.LIST, array(DIR1));

+        Command command2 = new Command(CommandNames.LIST, EMPTY);

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), ListCommandHandler.PATHNAME_KEY, DIR1);

+        verifyOneDataElement(commandHandler.getInvocation(1), ListCommandHandler.PATHNAME_KEY, null);

+    }

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new ListCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/MkdCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/MkdCommandHandlerTest.java
new file mode 100644
index 0000000..acb432c
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/MkdCommandHandlerTest.java
@@ -0,0 +1,73 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the MkdCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class MkdCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private MkdCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(ReplyCodes.MKD_OK, formattedReplyTextFor(ReplyCodes.MKD_OK, DIR1));

+        session.sendReply(ReplyCodes.MKD_OK, formattedReplyTextFor(ReplyCodes.MKD_OK, DIR2));

+        replay(session);

+

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), MkdCommandHandler.PATHNAME_KEY, DIR1);

+        verifyOneDataElement(commandHandler.getInvocation(1), MkdCommandHandler.PATHNAME_KEY, DIR2);

+    }

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.MKD, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new MkdCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.MKD, array(DIR1));

+        command2 = new Command(CommandNames.MKD, array(DIR2));

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/ModeCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/ModeCommandHandlerTest.java
new file mode 100644
index 0000000..bc38036
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/ModeCommandHandlerTest.java
@@ -0,0 +1,75 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the ModeCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class ModeCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final String CODE1 = "S";

+    private static final String CODE2 = "B";

+

+    private ModeCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(ReplyCodes.MODE_OK, replyTextFor(ReplyCodes.MODE_OK));

+        session.sendReply(ReplyCodes.MODE_OK, replyTextFor(ReplyCodes.MODE_OK));

+        replay(session);

+

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), ModeCommandHandler.MODE_KEY, CODE1);

+        verifyOneDataElement(commandHandler.getInvocation(1), ModeCommandHandler.MODE_KEY, CODE2);

+    }

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.MODE, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new ModeCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.MODE, array(CODE1));

+        command2 = new Command(CommandNames.MODE, array(CODE2));

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/NlstCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/NlstCommandHandlerTest.java
new file mode 100644
index 0000000..3cc6b3c
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/NlstCommandHandlerTest.java
@@ -0,0 +1,75 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.easymock.MockControl;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the NlstCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class NlstCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private NlstCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+        final String DIR_LISTING = " directory listing\nabc.txt\ndef.log\n";

+        final String DIR_LISTING_TRIMMED = DIR_LISTING.trim();

+        ((NlstCommandHandler) commandHandler).setDirectoryListing(DIR_LISTING);

+

+        for (int i = 0; i < 2; i++) {

+            session.sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK));

+            session.openDataConnection();

+            byte[] bytes = DIR_LISTING_TRIMMED.getBytes();

+            session.sendData(bytes, bytes.length);

+            control(session).setMatcher(MockControl.ARRAY_MATCHER);

+            session.closeDataConnection();

+            session.sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_FINAL_OK));

+        }

+        replay(session);

+

+        Command command1 = new Command(CommandNames.LIST, array(DIR1));

+        Command command2 = new Command(CommandNames.LIST, EMPTY);

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), NlstCommandHandler.PATHNAME_KEY, DIR1);

+        verifyOneDataElement(commandHandler.getInvocation(1), NlstCommandHandler.PATHNAME_KEY, null);

+    }

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new NlstCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/NoopCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/NoopCommandHandlerTest.java
new file mode 100644
index 0000000..65422bf
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/NoopCommandHandlerTest.java
@@ -0,0 +1,61 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the NoopCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class NoopCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private NoopCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+        final Command COMMAND = new Command(CommandNames.NOOP, EMPTY);

+

+        session.sendReply(ReplyCodes.NOOP_OK, replyTextFor(ReplyCodes.NOOP_OK));

+        replay(session);

+

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new NoopCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/PassCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/PassCommandHandlerTest.java
new file mode 100644
index 0000000..750d259
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/PassCommandHandlerTest.java
@@ -0,0 +1,76 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the PassCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class PassCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final String PASSWORD1 = "password1";

+    private static final String PASSWORD2 = "password2";

+

+    private PassCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(ReplyCodes.PASS_OK, replyTextFor(ReplyCodes.PASS_OK));

+        session.sendReply(ReplyCodes.PASS_OK, replyTextFor(ReplyCodes.PASS_OK));

+        replay(session);

+        

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), PassCommandHandler.PASSWORD_KEY, PASSWORD1);

+        verifyOneDataElement(commandHandler.getInvocation(1), PassCommandHandler.PASSWORD_KEY, PASSWORD2);

+    }

+    

+    /**

+     * Test the handleCommand() method, when no password parameter has been specified

+     */

+    public void testHandleCommand_MissingPasswordParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.PASS, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new PassCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.PASS, array(PASSWORD1));

+        command2 = new Command(CommandNames.PASS, array(PASSWORD2));

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/PasvCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/PasvCommandHandlerTest.java
new file mode 100644
index 0000000..82ee289
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/PasvCommandHandlerTest.java
@@ -0,0 +1,73 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+import java.net.InetAddress;

+

+/**

+ * Tests for the PasvCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class PasvCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(PasvCommandHandlerTest.class);

+    private static final int PORT = (23 << 8) + 77;

+

+    private PasvCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        final InetAddress SERVER = inetAddress("192.168.0.2");

+        session.switchToPassiveMode();

+        control(session).setReturnValue(PORT);

+        session.getServerHost();

+        control(session).setReturnValue(SERVER);

+        session.sendReply(ReplyCodes.PASV_OK, formattedReplyTextFor(227, "(192,168,0,2,23,77)"));

+        replay(session);

+

+        final Command COMMAND = new Command(CommandNames.PASV, EMPTY);

+

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new PasvCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/PortCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/PortCommandHandlerTest.java
new file mode 100644
index 0000000..6f5f4e0
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/PortCommandHandlerTest.java
@@ -0,0 +1,78 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+import java.net.InetAddress;

+

+/**

+ * Tests for the PortCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class PortCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final String[] PARAMETERS = new String[]{"11", "22", "33", "44", "1", "206"};

+    private static final String[] PARAMETERS_INSUFFICIENT = new String[]{"7", "29", "99", "11", "77"};

+    private static final int PORT = (1 << 8) + 206;

+    private static final InetAddress HOST = inetAddress("11.22.33.44");

+

+    private PortCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+        final Command COMMAND = new Command(CommandNames.PORT, PARAMETERS);

+

+        session.setClientDataPort(PORT);

+        session.setClientDataHost(HOST);

+        session.sendReply(ReplyCodes.PORT_OK, replyTextFor(ReplyCodes.PORT_OK));

+        replay(session);

+

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyTwoDataElements(commandHandler.getInvocation(0),

+                PortCommandHandler.HOST_KEY, HOST,

+                PortCommandHandler.PORT_KEY, new Integer(PORT));

+    }

+

+    /**

+     * Test the handleCommand() method, when not enough parameters have been specified

+     */

+    public void testHandleCommand_InsufficientParameters() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.PORT, PARAMETERS_INSUFFICIENT);

+    }

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new PortCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/PwdCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/PwdCommandHandlerTest.java
new file mode 100644
index 0000000..92d8cd4
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/PwdCommandHandlerTest.java
@@ -0,0 +1,65 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the PwdCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class PwdCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private PwdCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        final String RESPONSE_DATA = "current dir 1";

+        commandHandler.setDirectory(RESPONSE_DATA);

+

+        session.sendReply(ReplyCodes.PWD_OK, formattedReplyTextFor(ReplyCodes.PWD_OK, "\"" + RESPONSE_DATA + "\""));

+        replay(session);

+

+        final Command COMMAND = new Command(CommandNames.PWD, EMPTY);

+

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new PwdCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/QuitCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/QuitCommandHandlerTest.java
new file mode 100644
index 0000000..8392dcc
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/QuitCommandHandlerTest.java
@@ -0,0 +1,60 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the QuitCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class QuitCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private QuitCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+        final Command COMMAND = new Command(CommandNames.QUIT, EMPTY);

+

+        session.sendReply(ReplyCodes.QUIT_OK, replyTextFor(ReplyCodes.QUIT_OK));

+        session.close();

+        replay(session);

+

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new QuitCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/ReinCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/ReinCommandHandlerTest.java
new file mode 100644
index 0000000..9fd6501
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/ReinCommandHandlerTest.java
@@ -0,0 +1,65 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the ReinCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class ReinCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private ReinCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+    

+    /**

+     * Test the handleCommand(Command,Session) method

+     * @throws Exception

+     */

+    public void testHandleCommand() throws Exception {

+        session.sendReply(ReplyCodes.REIN_OK, replyTextFor(ReplyCodes.REIN_OK));

+        session.sendReply(ReplyCodes.REIN_OK, replyTextFor(ReplyCodes.REIN_OK));

+        replay(session);

+        

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+        verifyNoDataElements(commandHandler.getInvocation(1));

+    }

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new ReinCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.REIN, EMPTY);

+        command2 = new Command(CommandNames.REIN, EMPTY);

+    }

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/RestCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/RestCommandHandlerTest.java
new file mode 100644
index 0000000..09d378c
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/RestCommandHandlerTest.java
@@ -0,0 +1,78 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the RestCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class RestCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final String MARKER1 = "marker1";

+    private static final String MARKER2 = "marker2";

+    

+    private RestCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(ReplyCodes.REST_OK, replyTextFor(ReplyCodes.REST_OK));

+        session.sendReply(ReplyCodes.REST_OK, replyTextFor(ReplyCodes.REST_OK));

+        replay(session);

+

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), RestCommandHandler.MARKER_KEY, MARKER1);

+        verifyOneDataElement(commandHandler.getInvocation(1), RestCommandHandler.MARKER_KEY, MARKER2);

+    }

+

+    /**

+     * Test the handleCommand() method, when no marker parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.REST, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new RestCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.REST, array(MARKER1));

+        command2 = new Command(CommandNames.REST, array(MARKER2));

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/RetrCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/RetrCommandHandlerTest.java
new file mode 100644
index 0000000..c3e6c2d
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/RetrCommandHandlerTest.java
@@ -0,0 +1,134 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.easymock.MockControl;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+import org.mockftpserver.core.util.AssertFailedException;

+

+/**

+ * Tests for the RetrCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class RetrCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(RetrCommandHandlerTest.class);

+

+    private RetrCommandHandler commandHandler;

+

+    /**

+     * Test the constructor that takes a String, passing in a null

+     */

+    public void testConstructor_String_Null() {

+        try {

+            new RetrCommandHandler((String) null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the constructor that takes a byte[], passing in a null

+     */

+    public void testConstructor_ByteArray_Null() {

+        try {

+            new RetrCommandHandler((byte[]) null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the setFileContents(String) method, passing in a null

+     */

+    public void testSetFileContents_String_Null() {

+        try {

+            commandHandler.setFileContents((String) null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the setFileContents(byte[]) method, passing in a null

+     */

+    public void testSetFileContents_ByteArray_Null() {

+        try {

+            commandHandler.setFileContents((byte[]) null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the handleCommand() method

+     *

+     * @throws Exception

+     */

+    public void testHandleCommand() throws Exception {

+        final String FILE_CONTENTS = "abc_123 456";

+        commandHandler.setFileContents(FILE_CONTENTS);

+

+        session.sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK));

+        session.openDataConnection();

+        session.sendData(FILE_CONTENTS.getBytes(), FILE_CONTENTS.length());

+        control(session).setMatcher(MockControl.ARRAY_MATCHER);

+        session.closeDataConnection();

+        session.sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_FINAL_OK));

+        replay(session);

+

+        Command command = new Command(CommandNames.RETR, array(FILENAME1));

+        commandHandler.handleCommand(command, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyOneDataElement(commandHandler.getInvocation(0), RetrCommandHandler.PATHNAME_KEY, FILENAME1);

+    }

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.RETR, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new RetrCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/RmdCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/RmdCommandHandlerTest.java
new file mode 100644
index 0000000..a549514
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/RmdCommandHandlerTest.java
@@ -0,0 +1,72 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the RmdCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class RmdCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private RmdCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+    

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+        session.sendReply(ReplyCodes.RMD_OK, replyTextFor(ReplyCodes.RMD_OK));

+        session.sendReply(ReplyCodes.RMD_OK, replyTextFor(ReplyCodes.RMD_OK));

+        replay(session);

+        

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), RmdCommandHandler.PATHNAME_KEY, DIR1);

+        verifyOneDataElement(commandHandler.getInvocation(1), RmdCommandHandler.PATHNAME_KEY, DIR2);

+    }

+    

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.RMD, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new RmdCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.RMD, array(DIR1));

+        command2 = new Command(CommandNames.RMD, array(DIR2));

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/RnfrCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/RnfrCommandHandlerTest.java
new file mode 100644
index 0000000..f494dd5
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/RnfrCommandHandlerTest.java
@@ -0,0 +1,72 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the RnfrCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class RnfrCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private RnfrCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(ReplyCodes.RNFR_OK, replyTextFor(ReplyCodes.RNFR_OK));

+        session.sendReply(ReplyCodes.RNFR_OK, replyTextFor(ReplyCodes.RNFR_OK));

+        replay(session);

+

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), RnfrCommandHandler.PATHNAME_KEY, FILENAME1);

+        verifyOneDataElement(commandHandler.getInvocation(1), RnfrCommandHandler.PATHNAME_KEY, FILENAME2);

+    }

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.RNFR, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new RnfrCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.RNFR, array(FILENAME1));

+        command2 = new Command(CommandNames.RNFR, array(FILENAME2));

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/RntoCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/RntoCommandHandlerTest.java
new file mode 100644
index 0000000..12217d1
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/RntoCommandHandlerTest.java
@@ -0,0 +1,72 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the RntoCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class RntoCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private RntoCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(ReplyCodes.RNTO_OK, replyTextFor(ReplyCodes.RNTO_OK));

+        session.sendReply(ReplyCodes.RNTO_OK, replyTextFor(ReplyCodes.RNTO_OK));

+        replay(session);

+

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), RntoCommandHandler.PATHNAME_KEY, FILENAME1);

+        verifyOneDataElement(commandHandler.getInvocation(1), RntoCommandHandler.PATHNAME_KEY, FILENAME2);

+    }

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.RNTO, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new RntoCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.RNTO, array(FILENAME1));

+        command2 = new Command(CommandNames.RNTO, array(FILENAME2));

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/SiteCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/SiteCommandHandlerTest.java
new file mode 100644
index 0000000..6351cd5
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/SiteCommandHandlerTest.java
@@ -0,0 +1,73 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the SiteCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class SiteCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final String PARAMETERS1 = "abc def";

+    private static final String PARAMETERS2 = "abc,23,def";

+    

+    private SiteCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+    

+    /**

+     * Test the handleCommand(Command,Session) method

+     * @throws Exception

+     */

+    public void testHandleCommand() throws Exception {

+        session.sendReply(ReplyCodes.SITE_OK, replyTextFor(ReplyCodes.SITE_OK));

+        session.sendReply(ReplyCodes.SITE_OK, replyTextFor(ReplyCodes.SITE_OK));

+        replay(session);

+        

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), SiteCommandHandler.PARAMETERS_KEY, PARAMETERS1);

+        verifyOneDataElement(commandHandler.getInvocation(1), SiteCommandHandler.PARAMETERS_KEY, PARAMETERS2);

+    }

+

+    /**

+     * Test the handleCommand() method, when no "parameters" parameter has been specified

+     */

+    public void testHandleCommand_MissingParameters() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.SITE, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new SiteCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.SITE, array(PARAMETERS1));

+        command2 = new Command(CommandNames.SITE, array(PARAMETERS2));

+    }

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/SmntCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/SmntCommandHandlerTest.java
new file mode 100644
index 0000000..65cea91
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/SmntCommandHandlerTest.java
@@ -0,0 +1,70 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the SmntCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class SmntCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private SmntCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+    

+    /**

+     * Test the handleCommand(Command,Session) method

+     * @throws Exception

+     */

+    public void testHandleCommand() throws Exception {

+        session.sendReply(ReplyCodes.SMNT_OK, replyTextFor(ReplyCodes.SMNT_OK));

+        session.sendReply(ReplyCodes.SMNT_OK, replyTextFor(ReplyCodes.SMNT_OK));

+        replay(session);

+        

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), SmntCommandHandler.PATHNAME_KEY, DIR1);

+        verifyOneDataElement(commandHandler.getInvocation(1), SmntCommandHandler.PATHNAME_KEY, DIR2);

+    }

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.SMNT, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new SmntCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.SMNT, array(DIR1));

+        command2 = new Command(CommandNames.SMNT, array(DIR2));

+    }

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/StatCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/StatCommandHandlerTest.java
new file mode 100644
index 0000000..5596685
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/StatCommandHandlerTest.java
@@ -0,0 +1,101 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the StatCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class StatCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final String RESPONSE_DATA = "status info 123.456";

+    private static final String PATHNAME = "dir/file";

+

+    private StatCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter is specified

+     */

+    public void testHandleCommand_NoPathname() throws Exception {

+

+        session.sendReply(ReplyCodes.STAT_SYSTEM_OK, formattedReplyTextFor(ReplyCodes.STAT_SYSTEM_OK, RESPONSE_DATA));

+        replay(session);

+

+        final Command COMMAND = new Command(CommandNames.STAT, EMPTY);

+        commandHandler.setStatus(RESPONSE_DATA);

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyOneDataElement(commandHandler.getInvocation(0), StatCommandHandler.PATHNAME_KEY, null);

+    }

+

+    /**

+     * Test the handleCommand() method, specifying a pathname parameter

+     * @throws Exception

+     */

+    public void testHandleCommand_Pathname() throws Exception {

+

+        session.sendReply(ReplyCodes.STAT_FILE_OK, formattedReplyTextFor(ReplyCodes.STAT_FILE_OK, RESPONSE_DATA));

+        replay(session);

+

+        final Command COMMAND = new Command(CommandNames.STAT, array(PATHNAME));

+

+        commandHandler.setStatus(RESPONSE_DATA);

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyOneDataElement(commandHandler.getInvocation(0), StatCommandHandler.PATHNAME_KEY, PATHNAME);

+    }

+

+    /**

+     * Test the handleCommand() method, when the replyCode is explicitly set

+     */

+    public void testHandleCommand_OverrideReplyCode() throws Exception {

+

+        session.sendReply(200, replyTextFor(200));

+        replay(session);

+

+        final Command COMMAND = new Command(CommandNames.STAT, EMPTY);

+        commandHandler.setStatus(RESPONSE_DATA);

+        commandHandler.setReplyCode(200);

+        commandHandler.handleCommand(COMMAND, session);

+

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyOneDataElement(commandHandler.getInvocation(0), StatCommandHandler.PATHNAME_KEY, null);

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new StatCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/StorCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/StorCommandHandlerTest.java
new file mode 100644
index 0000000..8f9c51f
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/StorCommandHandlerTest.java
@@ -0,0 +1,72 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the StorCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class StorCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private StorCommandHandler commandHandler;

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new StorCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+    /**

+     * Test the handleCommand() method, as well as the getFileContents() and clearFileContents() methods

+     */

+    public void testHandleCommand() throws Exception {

+        final String DATA = "ABC";

+

+        session.sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK));

+        session.openDataConnection();

+        session.readData();

+        control(session).setReturnValue(DATA.getBytes());

+        session.closeDataConnection();

+        session.sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_FINAL_OK));

+        replay(session);

+

+        Command command = new Command(CommandNames.STOR, array(FILENAME1));

+        commandHandler.handleCommand(command, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyTwoDataElements(commandHandler.getInvocation(0), StorCommandHandler.PATHNAME_KEY, FILENAME1,

+                StorCommandHandler.FILE_CONTENTS_KEY, DATA.getBytes());

+    }

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.STOR, EMPTY);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/StouCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/StouCommandHandlerTest.java
new file mode 100644
index 0000000..4aeef23
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/StouCommandHandlerTest.java
@@ -0,0 +1,68 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the StouCommandHandler class

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public final class StouCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private StouCommandHandler commandHandler;

+

+    /**

+     * Perform initialization before each test

+     *

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new StouCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+    /**

+     * Test the handleCommand() method, as well as the getFileContents() and clearFileContents() methods

+     */

+    public void testHandleCommand() throws Exception {

+        final String DATA = "ABC";

+        final String FILENAME = "abc.txt";

+

+        session.sendReply(ReplyCodes.TRANSFER_DATA_INITIAL_OK, replyTextFor(ReplyCodes.TRANSFER_DATA_INITIAL_OK));

+        session.openDataConnection();

+        session.readData();

+        control(session).setReturnValue(DATA.getBytes());

+        session.closeDataConnection();

+        session.sendReply(ReplyCodes.TRANSFER_DATA_FINAL_OK, formattedReplyTextFor("226.WithFilename", FILENAME));

+        replay(session);

+

+        Command command = new Command(CommandNames.STOU, array(FILENAME1));

+        commandHandler.setFilename(FILENAME);

+        commandHandler.handleCommand(command, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyOneDataElement(commandHandler.getInvocation(0), StouCommandHandler.FILE_CONTENTS_KEY, DATA.getBytes());

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/StruCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/StruCommandHandlerTest.java
new file mode 100644
index 0000000..2c5ba1e
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/StruCommandHandlerTest.java
@@ -0,0 +1,75 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the StruCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class StruCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final String CODE1 = "F";

+    private static final String CODE2 = "R";

+

+    private StruCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(ReplyCodes.STRU_OK, replyTextFor(ReplyCodes.STRU_OK));

+        session.sendReply(ReplyCodes.STRU_OK, replyTextFor(ReplyCodes.STRU_OK));

+        replay(session);

+

+        commandHandler.handleCommand(command1, session);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), StruCommandHandler.FILE_STRUCTURE_KEY, CODE1);

+        verifyOneDataElement(commandHandler.getInvocation(1), StruCommandHandler.FILE_STRUCTURE_KEY, CODE2);

+    }

+

+    /**

+     * Test the handleCommand() method, when no pathname parameter has been specified

+     */

+    public void testHandleCommand_MissingPathnameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.STRU, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new StruCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        command1 = new Command(CommandNames.STRU, array(CODE1));

+        command2 = new Command(CommandNames.STRU, array(CODE2));

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/SystCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/SystCommandHandlerTest.java
new file mode 100644
index 0000000..e7991c0
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/SystCommandHandlerTest.java
@@ -0,0 +1,80 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.util.AssertFailedException;

+

+/**

+ * Tests for the SystCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class SystCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(SystCommandHandlerTest.class);

+    

+    private SystCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        final String SYSTEM_NAME = "UNIX";

+        commandHandler.setSystemName(SYSTEM_NAME);

+

+        session.sendReply(ReplyCodes.SYST_OK, formattedReplyTextFor(ReplyCodes.SYST_OK, "\"" + SYSTEM_NAME + "\""));

+        replay(session);

+        

+        final Command COMMAND = new Command(CommandNames.SYST, EMPTY);

+

+        commandHandler.handleCommand(COMMAND, session);

+        verify(session);

+        

+        verifyNumberOfInvocations(commandHandler, 1);

+        verifyNoDataElements(commandHandler.getInvocation(0));

+    }

+    

+    /**

+     * Test the SetSystemName method, passing in a null

+     */

+    public void testSetSystemName_Null() {

+        try {

+            commandHandler.setSystemName(null);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+    

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new SystCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/TypeCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/TypeCommandHandlerTest.java
new file mode 100644
index 0000000..3a1e0f2
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/TypeCommandHandlerTest.java
@@ -0,0 +1,75 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.ReplyCodes;

+

+/**

+ * Tests for the TypeCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class TypeCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private TypeCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+        final Command COMMAND1 = new Command("TYPE", array("A"));

+        final Command COMMAND2 = new Command("TYPE", array("B"));

+        final Command COMMAND3 = new Command("TYPE", array("L", "8"));

+

+        session.sendReply(ReplyCodes.TYPE_OK, replyTextFor(ReplyCodes.TYPE_OK));

+        session.sendReply(ReplyCodes.TYPE_OK, replyTextFor(ReplyCodes.TYPE_OK));

+        session.sendReply(ReplyCodes.TYPE_OK, replyTextFor(ReplyCodes.TYPE_OK));

+        replay(session);

+        

+        commandHandler.handleCommand(COMMAND1, session);

+        commandHandler.handleCommand(COMMAND2, session);

+        commandHandler.handleCommand(COMMAND3, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 3);

+        verifyOneDataElement(commandHandler.getInvocation(0), TypeCommandHandler.TYPE_INFO_KEY, new String[] {"A", null});

+        verifyOneDataElement(commandHandler.getInvocation(1), TypeCommandHandler.TYPE_INFO_KEY, new String[] {"B", null});

+        verifyOneDataElement(commandHandler.getInvocation(2), TypeCommandHandler.TYPE_INFO_KEY, new String[] {"L", "8"});

+    }

+    

+    /**

+     * Test the handleCommand() method, when no type parameter has been specified

+     */

+    public void testHandleCommand_MissingTypeParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.TYPE, EMPTY);

+    }

+

+    /**

+     * Perform initialization before each test

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new TypeCommandHandler();

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/UserCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/UserCommandHandlerTest.java
new file mode 100644
index 0000000..f63b847
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/UserCommandHandlerTest.java
@@ -0,0 +1,85 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.mockftpserver.core.command.*;

+import org.mockftpserver.core.command.AbstractCommandHandlerTestCase;

+

+/**

+ * Tests for the UserCommandHandler class

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class UserCommandHandlerTest extends AbstractCommandHandlerTestCase {

+

+    private static final String USERNAME1 = "user1";

+    private static final String USERNAME2 = "user2";

+

+    private UserCommandHandler commandHandler;

+    private Command command1;

+    private Command command2;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(ReplyCodes.USER_NEED_PASSWORD_OK, replyTextFor(ReplyCodes.USER_NEED_PASSWORD_OK));

+        session.sendReply(ReplyCodes.USER_LOGGED_IN_OK, replyTextFor(ReplyCodes.USER_LOGGED_IN_OK));

+        replay(session);

+

+        commandHandler.handleCommand(command1, session);

+        commandHandler.setPasswordRequired(false);

+        commandHandler.handleCommand(command2, session);

+        verify(session);

+

+        verifyNumberOfInvocations(commandHandler, 2);

+        verifyOneDataElement(commandHandler.getInvocation(0), UserCommandHandler.USERNAME_KEY, USERNAME1);

+        verifyOneDataElement(commandHandler.getInvocation(1), UserCommandHandler.USERNAME_KEY, USERNAME2);

+    }

+

+    /**

+     * Test the handleCommand() method, when no username parameter has been specified

+     */

+    public void testHandleCommand_MissingUsernameParameter() throws Exception {

+        testHandleCommand_InvalidParameters(commandHandler, CommandNames.USER, EMPTY);

+    }

+

+    /**

+     * Test the setPasswordRequired() and isPasswordRequired() methods 

+     */

+    public void testSetPasswordRequired() {

+        assertTrue("initial state", commandHandler.isPasswordRequired());

+        commandHandler.setPasswordRequired(false);

+        assertFalse("after set false", commandHandler.isPasswordRequired());

+    }

+    

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.core.command.AbstractCommandHandlerTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        commandHandler = new UserCommandHandler();

+        command1 = new Command(CommandNames.USER, array(USERNAME1));

+        command2 = new Command(CommandNames.USER, array(USERNAME2));

+        commandHandler.setReplyTextBundle(replyTextBundle);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/command/_AbstractStubDataCommandHandlerTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/command/_AbstractStubDataCommandHandlerTest.java
new file mode 100644
index 0000000..0d2e979
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/command/_AbstractStubDataCommandHandlerTest.java
@@ -0,0 +1,177 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.command;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.core.command.Command;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.core.session.Session;

+import org.mockftpserver.core.util.AssertFailedException;

+import org.mockftpserver.test.AbstractTestCase;

+

+import java.util.ListResourceBundle;

+import java.util.ResourceBundle;

+

+/**

+ * Tests for AbstractStubDataCommandHandler. The class name is prefixed with an underscore

+ * so that it is not filtered out by Maven's Surefire test plugin.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class _AbstractStubDataCommandHandlerTest extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(_AbstractStubDataCommandHandlerTest.class);

+    private static final Command COMMAND = new Command("command", EMPTY);

+    private static final InvocationRecord INVOCATION_RECORD = new InvocationRecord(COMMAND, DEFAULT_HOST);

+

+    private static final String REPLY_TEXT150 = "reply 150 ... abcdef";

+    private static final String REPLY_TEXT226 = "reply 226 ... abcdef";

+    private static final String REPLY_TEXT222 = "reply 222 ... abcdef";

+    private static final String REPLY_TEXT333 = "reply 333 ... abcdef";

+    private static final String REPLY_TEXT444 = "reply 444 ... abcdef";

+    

+    private Session session;

+    private ResourceBundle replyTextBundle;

+    private AbstractStubDataCommandHandler commandHandler;

+

+    /**

+     * Test the handleCommand() method

+     */

+    public void testHandleCommand() throws Exception {

+

+        session.sendReply(150, REPLY_TEXT150);

+        session.openDataConnection();

+        session.sendReply(222, REPLY_TEXT222);

+        session.sendReply(333, REPLY_TEXT333);

+        session.sendReply(444, REPLY_TEXT444);

+        session.closeDataConnection();

+        session.sendReply(226, REPLY_TEXT226);

+        replay(session);

+        

+        // Define CommandHandler test subclass

+        commandHandler = new AbstractStubDataCommandHandler() {

+            protected void beforeProcessData(Command c, Session s, InvocationRecord ir) {

+                verifyParameters(c, s, ir);

+                // Send unique reply code so that we can verify proper method invocation and ordering

+                session.sendReply(222, REPLY_TEXT222);

+            }

+

+            protected void processData(Command c, Session s, InvocationRecord ir) {

+                verifyParameters(c, s, ir);

+                // Send unique reply code so that we can verify proper method invocation and ordering

+                session.sendReply(333, REPLY_TEXT333);

+            }

+

+            protected void afterProcessData(Command c, Session s, InvocationRecord ir) {

+                verifyParameters(c, s, ir);

+                // Send unique reply code so that we can verify proper method invocation and ordering

+                session.sendReply(444, REPLY_TEXT444);

+            }

+

+            private void verifyParameters(Command c, Session s, InvocationRecord ir) {

+                assertSame("command", COMMAND, c);

+                assertSame("session", session, s);

+                assertSame("invocationRecord", INVOCATION_RECORD, ir);

+            }

+        };

+

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        commandHandler.handleCommand(COMMAND, session, INVOCATION_RECORD);

+        

+        verify(session);

+    }

+

+    /**

+     * Test the handleCommand() method, overriding the initial reply code and text

+     */

+    public void testHandleCommand_OverrideInitialReplyCodeAndText() throws Exception {

+

+        final int OVERRIDE_REPLY_CODE = 333;

+        final String OVERRIDE_REPLY_TEXT = "reply text";

+        

+        session.sendReply(OVERRIDE_REPLY_CODE, OVERRIDE_REPLY_TEXT);

+        session.openDataConnection();

+        session.closeDataConnection();

+        session.sendReply(226, REPLY_TEXT226);

+        replay(session);

+        

+        commandHandler.setPreliminaryReplyCode(OVERRIDE_REPLY_CODE);

+        commandHandler.setPreliminaryReplyText(OVERRIDE_REPLY_TEXT);

+        commandHandler.setReplyTextBundle(replyTextBundle);

+        commandHandler.handleCommand(COMMAND, session, INVOCATION_RECORD);

+        

+        verify(session);

+    }

+

+    /**

+     * Test the setPreliminaryReplyCode() method, passing in an invalid value 

+     */

+    public void testSetPreliminaryReplyCode_Invalid() {

+        try {

+            commandHandler.setPreliminaryReplyCode(0);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    /**

+     * Test the setFinalReplyCode() method, passing in an invalid value 

+     */

+    public void testSetFinalReplyCode_Invalid() {

+        try {

+            commandHandler.setFinalReplyCode(0);

+            fail("Expected AssertFailedException");

+        }

+        catch (AssertFailedException expected) {

+            LOG.info("Expected: " + expected);

+        }

+    }

+

+    //-------------------------------------------------------------------------

+    // Test setup

+    //-------------------------------------------------------------------------

+    

+    /**

+     * Perform initialization before each test

+     * 

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        session = (Session) createMock(Session.class);

+        replyTextBundle = new ListResourceBundle() {

+            protected Object[][] getContents() {

+                return new Object[][] { 

+                        { Integer.toString(150), REPLY_TEXT150 }, 

+                        { Integer.toString(222), REPLY_TEXT222 }, 

+                        { Integer.toString(226), REPLY_TEXT226 }, 

+                        { Integer.toString(333), REPLY_TEXT333 }, 

+                        { Integer.toString(444), REPLY_TEXT444 }, 

+                };

+            }

+        };

+        commandHandler = new AbstractStubDataCommandHandler() {

+            protected void processData(Command c, Session s, InvocationRecord ir) {

+            }

+        };

+    }

+    

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/example/FtpWorkingDirectory.java b/tags/2.5/src/test/java/org/mockftpserver/stub/example/FtpWorkingDirectory.java
new file mode 100644
index 0000000..dab841f
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/example/FtpWorkingDirectory.java
@@ -0,0 +1,63 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.example;

+

+import org.apache.commons.net.ftp.FTPClient;

+

+import java.io.IOException;

+import java.net.SocketException;

+

+/**

+ * Simple FTP client code example.

+ * 

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+public class FtpWorkingDirectory {

+

+    private String server;

+    private int port;

+

+    /**

+     * Return the current working directory for the FTP account on the server

+     * @return the current working directory

+     * @throws SocketException

+     * @throws IOException

+     */

+    public String getWorkingDirectory() throws SocketException, IOException {

+        FTPClient ftpClient = new FTPClient();

+        ftpClient.connect(server, port);

+        return ftpClient.printWorkingDirectory();

+    }

+

+    /**

+     * Set the hostname of the FTP server

+     * @param server - the hostname of the FTP server

+     */

+    public void setServer(String server) {

+        this.server = server;

+    }

+    

+    /**

+     * Set the port number for the FTP server

+     * @param port - the port number

+     */

+    public void setPort(int port) {

+        this.port = port;

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/example/FtpWorkingDirectoryTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/example/FtpWorkingDirectoryTest.java
new file mode 100644
index 0000000..b3a7d47
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/example/FtpWorkingDirectoryTest.java
@@ -0,0 +1,70 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.example;

+

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.stub.StubFtpServer;

+import org.mockftpserver.stub.command.PwdCommandHandler;

+import org.mockftpserver.test.AbstractTestCase;

+import org.mockftpserver.test.IntegrationTest;

+

+/**

+ * Example test using StubFtpServer, with programmatic configuration.

+ */

+public class FtpWorkingDirectoryTest extends AbstractTestCase implements IntegrationTest {

+

+    private static final int PORT = 9981;

+    private FtpWorkingDirectory ftpWorkingDirectory;

+    private StubFtpServer stubFtpServer;

+    

+    /**

+     * Test FtpWorkingDirectory getWorkingDirectory() method 

+     */

+    public void testGetWorkingDirectory() throws Exception {

+        

+        // Replace the existing (default) CommandHandler; customize returned directory pathname

+        final String DIR = "some/dir";

+        PwdCommandHandler pwdCommandHandler = new PwdCommandHandler();

+        pwdCommandHandler.setDirectory(DIR);

+        stubFtpServer.setCommandHandler(CommandNames.PWD, pwdCommandHandler);

+        

+        stubFtpServer.start();

+        

+        String workingDir = ftpWorkingDirectory.getWorkingDirectory();

+

+        assertEquals("workingDirectory", DIR, workingDir);

+    }

+

+    /**

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        ftpWorkingDirectory = new FtpWorkingDirectory();

+        ftpWorkingDirectory.setPort(PORT);

+        stubFtpServer = new StubFtpServer();

+        stubFtpServer.setServerControlPort(PORT);

+    }

+

+    /**

+     * @see org.mockftpserver.test.AbstractTestCase#tearDown()

+     */

+    protected void tearDown() throws Exception {

+        super.tearDown();

+        stubFtpServer.stop();

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/example/RemoteFile.java b/tags/2.5/src/test/java/org/mockftpserver/stub/example/RemoteFile.java
new file mode 100644
index 0000000..833cf56
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/example/RemoteFile.java
@@ -0,0 +1,72 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.example;

+

+import org.apache.commons.net.ftp.FTPClient;

+

+import java.io.ByteArrayOutputStream;

+import java.io.IOException;

+

+/**

+ * Simple FTP client code example.

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public class RemoteFile {

+

+    public static final String USERNAME = "user";

+    public static final String PASSWORD = "password";

+

+    private String server;

+    private int port;

+

+    public String readFile(String filename) throws IOException {

+

+        FTPClient ftpClient = new FTPClient();

+        ftpClient.connect(server, port);

+        ftpClient.login(USERNAME, PASSWORD);

+

+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

+        boolean success = ftpClient.retrieveFile(filename, outputStream);

+        ftpClient.disconnect();

+

+        if (!success) {

+            throw new IOException("Retrieve file failed: " + filename);

+        }

+        return outputStream.toString();

+    }

+

+    /**

+     * Set the hostname of the FTP server

+     *

+     * @param server - the hostname of the FTP server

+     */

+    public void setServer(String server) {

+        this.server = server;

+    }

+

+    /**

+     * Set the port number for the FTP server

+     *

+     * @param port - the port number

+     */

+    public void setPort(int port) {

+        this.port = port;

+    }

+

+    // Other methods ...

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/example/RemoteFileTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/example/RemoteFileTest.java
new file mode 100644
index 0000000..e7e5769
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/example/RemoteFileTest.java
@@ -0,0 +1,104 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.example;

+

+import org.mockftpserver.core.command.CommandNames;

+import org.mockftpserver.core.command.InvocationRecord;

+import org.mockftpserver.stub.StubFtpServer;

+import org.mockftpserver.stub.command.RetrCommandHandler;

+import org.mockftpserver.test.AbstractTestCase;

+import org.mockftpserver.test.IntegrationTest;

+

+import java.io.IOException;

+

+/**

+ * Example test using StubFtpServer, with programmatic configuration.

+ */

+public class RemoteFileTest extends AbstractTestCase implements IntegrationTest {

+

+    private static final int PORT = 9981;

+    private static final String FILENAME = "dir/sample.txt";

+

+    private RemoteFile remoteFile;

+    private StubFtpServer stubFtpServer;

+    

+    /**

+     * Test readFile() method 

+     */

+    public void testReadFile() throws Exception {

+

+        final String CONTENTS = "abcdef 1234567890";

+

+        // Replace the default RETR CommandHandler; customize returned file contents

+        RetrCommandHandler retrCommandHandler = new RetrCommandHandler();

+        retrCommandHandler.setFileContents(CONTENTS);

+        stubFtpServer.setCommandHandler(CommandNames.RETR, retrCommandHandler);

+        

+        stubFtpServer.start();

+        

+        String contents = remoteFile.readFile(FILENAME);

+

+        // Verify returned file contents

+        assertEquals("contents", CONTENTS, contents);

+        

+        // Verify the submitted filename

+        InvocationRecord invocationRecord = retrCommandHandler.getInvocation(0);

+        String filename = invocationRecord.getString(RetrCommandHandler.PATHNAME_KEY);

+        assertEquals("filename", FILENAME, filename);

+    }

+

+    /**

+     * Test the readFile() method when the FTP transfer fails (returns a non-success reply code) 

+     */

+    public void testReadFileThrowsException() {

+

+        // Replace the default RETR CommandHandler; return failure reply code

+        RetrCommandHandler retrCommandHandler = new RetrCommandHandler();

+        retrCommandHandler.setFinalReplyCode(550);

+        stubFtpServer.setCommandHandler(CommandNames.RETR, retrCommandHandler);

+        

+        stubFtpServer.start();

+

+        try {

+            remoteFile.readFile(FILENAME);

+            fail("Expected IOException");

+        }

+        catch (IOException expected) {

+            // Expected this

+        }

+    }

+    

+    /**

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        remoteFile = new RemoteFile();

+        remoteFile.setServer("localhost");

+        remoteFile.setPort(PORT);

+        stubFtpServer = new StubFtpServer();

+        stubFtpServer.setServerControlPort(PORT);

+    }

+

+    /**

+     * @see org.mockftpserver.test.AbstractTestCase#tearDown()

+     */

+    protected void tearDown() throws Exception {

+        super.tearDown();

+        stubFtpServer.stop();

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/stub/example/SpringConfigurationTest.java b/tags/2.5/src/test/java/org/mockftpserver/stub/example/SpringConfigurationTest.java
new file mode 100644
index 0000000..acb10c1
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/stub/example/SpringConfigurationTest.java
@@ -0,0 +1,89 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.stub.example;

+

+import org.apache.commons.net.ftp.FTPClient;

+import org.apache.commons.net.ftp.FTPFile;

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.mockftpserver.stub.StubFtpServer;

+import org.mockftpserver.test.AbstractTestCase;

+import org.springframework.context.ApplicationContext;

+import org.springframework.context.support.ClassPathXmlApplicationContext;

+

+import java.io.ByteArrayOutputStream;

+

+/**

+ * Example test for StubFtpServer, using the Spring Framework ({@link http://www.springframework.org/}) 

+ * for configuration.

+ */

+public class SpringConfigurationTest extends AbstractTestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(SpringConfigurationTest.class);

+    private static final String SERVER = "localhost";

+    private static final int PORT = 9981;

+

+    private StubFtpServer stubFtpServer;

+    private FTPClient ftpClient;

+    

+    /**

+     * Test starting the StubFtpServer configured within the example Spring configuration file 

+     */

+    public void testStubFtpServer() throws Exception {

+        stubFtpServer.start();

+        

+        ftpClient.connect(SERVER, PORT);

+

+        // PWD

+        String dir = ftpClient.printWorkingDirectory();

+        assertEquals("PWD", "foo/bar", dir);

+        

+        // LIST

+        FTPFile[] files = ftpClient.listFiles();

+        LOG.info("FTPFile[0]=" + files[0]);

+        LOG.info("FTPFile[1]=" + files[1]);

+        assertEquals("number of files from LIST", 2, files.length);

+        

+        // DELE

+        assertFalse("DELE", ftpClient.deleteFile("AnyFile.txt"));

+        

+        // RETR

+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

+        assertTrue(ftpClient.retrieveFile("SomeFile.txt", outputStream));

+        LOG.info("File contents=[" + outputStream.toString() + "]");

+    }

+

+    /**

+     * @see org.mockftpserver.test.AbstractTestCase#setUp()

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+        

+        ApplicationContext context = new ClassPathXmlApplicationContext("stubftpserver-beans.xml");

+        stubFtpServer = (StubFtpServer) context.getBean("stubFtpServer");

+

+        ftpClient = new FTPClient();

+    }

+

+    /**

+     * @see org.mockftpserver.test.AbstractTestCase#tearDown()

+     */

+    protected void tearDown() throws Exception {

+        super.tearDown();

+        stubFtpServer.stop();

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/test/AbstractTestCase.java b/tags/2.5/src/test/java/org/mockftpserver/test/AbstractTestCase.java
new file mode 100644
index 0000000..a3e5110
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/test/AbstractTestCase.java
@@ -0,0 +1,369 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.test;

+

+import junit.framework.TestCase;

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+import org.easymock.MockControl;

+import org.mockftpserver.core.MockFtpServerException;

+import org.mockftpserver.core.util.Assert;

+import org.mockftpserver.core.util.AssertFailedException;

+

+import java.io.File;

+import java.net.InetAddress;

+import java.net.UnknownHostException;

+import java.util.ArrayList;

+import java.util.Arrays;

+import java.util.Collections;

+import java.util.HashMap;

+import java.util.HashSet;

+import java.util.Iterator;

+import java.util.List;

+import java.util.Map;

+import java.util.Set;

+

+/**

+ * Abstract superclass for all project test classes

+ *

+ * @author Chris Mair

+ * @version $Revision$ - $Date$

+ */

+public abstract class AbstractTestCase extends TestCase {

+

+    private static final Logger LOG = LoggerFactory.getLogger(AbstractTestCase.class);

+    protected static final List EMPTY_LIST = Collections.EMPTY_LIST;

+    protected static final String[] EMPTY = new String[0];

+    protected static final InetAddress DEFAULT_HOST = inetAddress(null);

+

+    /**

+     * Constructor

+     */

+    public AbstractTestCase() {

+        super();

+    }

+

+    //-------------------------------------------------------------------------

+    // Manage EasyMock Control objects under the covers, and provide a syntax

+    // somewhat similar to EasyMock 2.2 for createMock, verify and replay.

+    //-------------------------------------------------------------------------

+

+    private Map mocks = new HashMap();

+

+    /**

+     * Create a new mock for the specified interface. Keep track of the associated control object

+     * under the covers to support the associated  method.

+     *

+     * @param interfaceToMock - the Class of the interface to be mocked

+     * @return the new mock

+     */

+    protected Object createMock(Class interfaceToMock) {

+        MockControl control = MockControl.createControl(interfaceToMock);

+        Object mock = control.getMock();

+        mocks.put(mock, control);

+        return mock;

+    }

+

+    /**

+     * Put the mock object into replay mode

+     *

+     * @param mock - the mock to set in replay mode

+     * @throws AssertFailedException - if mock is null

+     * @throws AssertFailedException - if mock is not a mock object created using {@link #createMock(Class)}

+     */

+    protected void replay(Object mock) {

+        control(mock).replay();

+    }

+

+    /**

+     * Put all mocks created with createMock() into replay mode.

+     */

+    protected void replayAll() {

+        for (Iterator iter = mocks.keySet().iterator(); iter.hasNext();) {

+            Object mock = iter.next();

+            replay(mock);

+        }

+    }

+

+    /**

+     * Verify the mock object

+     *

+     * @param mock - the mock to verify

+     * @throws AssertFailedException - if mock is null

+     * @throws AssertFailedException - if mock is not a mock object created using {@link #createMock(Class)}

+     */

+    protected void verify(Object mock) {

+        control(mock).verify();

+    }

+

+    /**

+     * Verify all mocks created with createMock() into replay mode.

+     */

+    protected void verifyAll() {

+        for (Iterator iter = mocks.keySet().iterator(); iter.hasNext();) {

+            Object mock = iter.next();

+            verify(mock);

+        }

+    }

+

+    /**

+     * Return the mock control associated with the mock

+     *

+     * @param mock - the mock

+     * @return the associated MockControl

+     * @throws AssertFailedException - if mock is null

+     * @throws AssertFailedException - if mock is not a mock object created using {@link #createMock(Class)}

+     */

+    protected MockControl control(Object mock) {

+        Assert.notNull(mock, "mock");

+        MockControl control = (MockControl) mocks.get(mock);

+        Assert.notNull(control, "control");

+        return control;

+    }

+

+    //-------------------------------------------------------------------------

+    // Other Helper Methods

+    //-------------------------------------------------------------------------

+

+    /**

+     * Assert that the two objects are not equal

+     *

+     * @param object1 - the first object

+     * @param object2 - the second object

+     */

+    protected void assertNotEquals(String message, Object object1, Object object2) {

+        assertFalse(message, object1.equals(object2));

+    }

+

+    /**

+     * Assert that the two byte arrays have the same length and content

+     *

+     * @param array1 - the first array

+     * @param array2 - the second array

+     */

+    protected void assertEquals(String message, byte[] array1, byte[] array2) {

+        assertTrue("Arrays not equal: " + message, Arrays.equals(array1, array2));

+    }

+

+    /**

+     * Assert that the two Object arrays have the same length and content

+     *

+     * @param array1 - the first array

+     * @param array2 - the second array

+     */

+    protected void assertEquals(String message, Object[] array1, Object[] array2) {

+        assertTrue("Arrays not equal: " + message, Arrays.equals(array1, array2));

+    }

+

+    /**

+     * Create and return a one-element Object[] containing the specified Object

+     *

+     * @param o - the object

+     * @return the Object array, of length 1, containing o

+     */

+    protected static Object[] objArray(Object o) {

+        return new Object[]{o};

+    }

+

+    /**

+     * Create and return a one-element String[] containing the specified String

+     *

+     * @param s - the String

+     * @return the String array, of length 1, containing s

+     */

+    protected static String[] array(String s) {

+        return new String[]{s};

+    }

+

+    /**

+     * Create and return a two-element String[] containing the specified Strings

+     *

+     * @param s1 - the first String

+     * @param s2 - the second String

+     * @return the String array, of length 2, containing s1 and s2

+     */

+    protected static String[] array(String s1, String s2) {

+        return new String[]{s1, s2};

+    }

+

+    /**

+     * Create a new InetAddress from the specified host String, using the

+     * {@link InetAddress#getByName(String)} method, wrapping any checked

+     * exception within a unchecked MockFtpServerException.

+     *

+     * @param host

+     * @return an InetAddress for the specified host

+     * @throws MockFtpServerException - if an UnknownHostException is thrown

+     */

+    protected static InetAddress inetAddress(String host) {

+        try {

+            return InetAddress.getByName(host);

+        }

+        catch (UnknownHostException e) {

+            throw new MockFtpServerException(e);

+        }

+    }

+

+    /**

+     * Create and return a List containing the Objects passed as arguments to this method

+     *

+     * @param e1- the first element to add

+     * @param e2- the second element to add

+     * @return the List containing the specified elements

+     */

+    protected static List list(Object e1, Object e2) {

+        List list = new ArrayList();

+        list.add(e1);

+        list.add(e2);

+        return list;

+    }

+

+    /**

+     * Create and return a List containing the single Object passed as an argument to this method

+     *

+     * @param element- the element to add

+     * @return the List containing the specified element

+     */

+    protected static List list(Object element) {

+        return Collections.singletonList(element);

+    }

+

+    /**

+     * Create and return a Set containing the Objects passed as arguments to this method

+     *

+     * @param e1 - the first element to add

+     * @param e2 - the second element to add

+     * @return the Set containing the specified elements

+     */

+    protected static Set set(Object e1, Object e2) {

+        Set set = new HashSet();

+        set.add(e1);

+        set.add(e2);

+        return set;

+    }

+

+    /**

+     * Create and return a Set containing the Objects passed as arguments to this method

+     *

+     * @param e1 - the first element to add

+     * @param e2 - the second element to add

+     * @param e3 - the third element to add

+     * @return the Set containing the specified elements

+     */

+    protected static Set set(Object e1, Object e2, Object e3) {

+        Set set = set(e1, e2);

+        set.add(e3);

+        return set;

+    }

+

+    /**

+     * Override the default test run behavior to write out the current test name

+     * and handle Errors and Exceptions in a standard way.

+     *

+     * @see junit.framework.TestCase#runBare()

+     */

+    public void runBare() throws Throwable {

+

+        LoggingUtil loggingUtil = null;

+        try {

+            loggingUtil = LoggingUtil.getTestCaseLogger(this);

+            loggingUtil.logStartOfTest();

+            super.runBare();

+        }

+        catch (Exception e) {

+            handleException(e);

+        }

+        catch (Error e) {

+            handleError(e);

+        }

+        finally {

+            if (loggingUtil != null) {

+                loggingUtil.logEndOfTest();

+            }

+        }

+    }

+

+    /**

+     * Setup before each test.

+     */

+    protected void setUp() throws Exception {

+        super.setUp();

+    }

+

+    /**

+     * Cleanup after each test.

+     */

+    protected void tearDown() throws Exception {

+        super.tearDown();

+    }

+

+    //-----------------------------------------------------------

+    // Private Internal Methods

+    //-----------------------------------------------------------

+

+    /**

+     * Handle an exception

+     *

+     * @param e the Exception

+     * @throws Exception

+     */

+    private void handleException(Exception e) throws Exception {

+

+        LOG.error("EXCEPTION: ", e);

+        throw e;

+    }

+

+    /**

+     * Handle an Error

+     *

+     * @param e the Error

+     * @throws Exception

+     */

+    private void handleError(Error e) throws Exception {

+        LOG.error("ERROR: ", e);

+        throw e;

+    }

+

+    //-------------------------------------------------------------------------

+    // Helper methods

+    //-------------------------------------------------------------------------

+

+    /**

+     * Delete the named file if it exists

+     *

+     * @param filename - the full pathname of the file

+     */

+    protected void deleteFile(String filename) {

+        File keyFile = new File(filename);

+        boolean deleted = keyFile.delete();

+        LOG.info("Deleted [" + filename + "]: " + deleted);

+    }

+

+    //-------------------------------------------------------------------------

+    // Common validation helper methods

+    //-------------------------------------------------------------------------

+

+    /**

+     * Verify that the named file exists

+     *

+     * @param filename - the full pathname of the file

+     */

+    protected void verifyFileExists(String filename) {

+        File keyFile = new File(filename);

+        assertTrue("File does not exist [" + filename + "]", keyFile.exists());

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/test/IntegrationTest.java b/tags/2.5/src/test/java/org/mockftpserver/test/IntegrationTest.java
new file mode 100644
index 0000000..0f6e3ae
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/test/IntegrationTest.java
@@ -0,0 +1,27 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.test;

+

+/**

+ * Marker interface for integration test

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public interface IntegrationTest {

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/test/LoggingUtil.java b/tags/2.5/src/test/java/org/mockftpserver/test/LoggingUtil.java
new file mode 100644
index 0000000..d7347ca
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/test/LoggingUtil.java
@@ -0,0 +1,147 @@
+/*

+ * Copyright 2007 the original author or authors.

+ * 

+ * 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 org.mockftpserver.test;

+

+import junit.framework.TestCase;

+import junit.framework.TestSuite;

+

+/**

+ * Provides facilities to log the start and end of a test run.

+ * 

+ * May want to refactor this and create two subclasses: TestCaseLogger

+ * and TestSuiteLogger.

+ * 

+ * @version $Revision$ - $Date$

+ * 

+ * @author Chris Mair

+ */

+public final class LoggingUtil {

+

+    private static final String TEST_CASE_SEPARATOR = "---------------";

+    private static final String TEST_SUITE_SEPARATOR = "####################";

+

+    private String testTitle;

+    private String separator;

+    private long startTime; 

+

+    

+    //-------------------------------------------------------------------------

+    // General-purpose API to log messages

+    //-------------------------------------------------------------------------

+    

+    /**

+     * Log the specified message from the caller object.

+     * @param caller the calling object

+     * @param message the message to log

+     */

+    public static void log(Object caller, Object message) {

+        

+        String classNameNoPackage = getClassName(caller);

+        String messageStr = (message==null) ? "null" : message.toString();

+        String formattedMessage = "[" + classNameNoPackage + "] " + messageStr;

+        writeLogMessage(formattedMessage);

+    }

+    

+    

+    //-------------------------------------------------------------------------

+    // Factory Methods to get instance for TestCase or TestSuite

+    //-------------------------------------------------------------------------

+    

+    /**

+     * Return a LoggingUtil instance suitable for logging TestCase start and end

+     * @param testCase the TestCase

+     * @return a LoggingUtil

+     */

+    public static LoggingUtil getTestCaseLogger(TestCase testCase) {

+        

+        String title = getClassName(testCase) + "." + testCase.getName();

+        return new LoggingUtil(title, TEST_CASE_SEPARATOR);

+    }

+

+

+    /**

+     * Return a LoggingUtil instance suitable for logging TestSuite start and end

+     * @param testSuite the TestSuite

+     * @return a LoggingUtil

+     */

+    public static LoggingUtil getTestSuiteLogger(TestSuite testCase) {

+        

+        String title = "SUITE " + getClassName(testCase);

+        return new LoggingUtil(title, TEST_SUITE_SEPARATOR);

+    }

+

+

+    /**

+     * Constructor. Private to force access through the factory method(s) 

+     */

+    private LoggingUtil(String title, String separator) {

+        this.startTime = System.currentTimeMillis();

+        this.testTitle = title;

+        this.separator = separator;

+    }

+

+

+    /**

+     * Write out the the name of the test class and test name to the log

+     */

+    public void logStartOfTest() {

+        

+        writeLogMessage(separator + " [ START: " + testTitle + " ] " + separator);

+    }

+

+

+    /**

+     * Write out the the name of the test class and test name to the log

+     */

+    public void logEndOfTest() {

+        

+        long elapsedTime = System.currentTimeMillis() - startTime;

+        writeLogMessage(separator + " [ END: " 

+            + testTitle

+            + "   Time=" + elapsedTime

+            + "ms ] "+ separator + "\n");

+    }

+

+

+    /**

+     * Return the name of the class for the specified object, stripping off the package name

+     * @return the name of the class, stripping off the package name

+     */

+    private static String getClassName(Object object) {

+        

+        // If it's already a class, then use as is

+        Class theClass = (object instanceof Class) ? ((Class)object) : object.getClass();

+        String className =  theClass.getName();

+        

+        int index = className.lastIndexOf(".");

+        if (index != -1) {

+            className = className.substring(index+1);

+        }

+        return className;

+    }

+

+

+    /**

+     * Write the specified message out to the log

+     * @param message the message to write

+     */

+    private static void writeLogMessage(String message) {

+        // Don't want to use Trace -- it requires initialization of the system configuration

+        //Trace.trace(message);

+        System.out.println(message);

+    }

+

+}

diff --git a/tags/2.5/src/test/java/org/mockftpserver/test/PortTestUtil.java b/tags/2.5/src/test/java/org/mockftpserver/test/PortTestUtil.java
new file mode 100644
index 0000000..72017bf
--- /dev/null
+++ b/tags/2.5/src/test/java/org/mockftpserver/test/PortTestUtil.java
@@ -0,0 +1,43 @@
+/*

+ * Copyright 2008 the original author or authors.

+ * 

+ * 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 org.mockftpserver.test;

+

+/**

+ * Contains static test utility method to determine FTP server port number to use for tests  

+ * 

+ * @version $Revision$ - $Date$

+ *

+ * @author Chris Mair

+ */

+public final class PortTestUtil {

+

+    private static final int DEFAULT_SERVER_CONTROL_PORT = 21;

+    private static final String FTP_SERVER_PORT_PROPERTY = "ftp.server.port";

+    

+    /**

+     * Return the port number to use for the FTP server control port. If the "ftp.server.port"

+     * system property is defined, then use that value (converted to an integer), otherwise

+     * return the default port number of 21.

+     * 

+     * @return the port number to use for the FTP server control port

+     */

+    public static int getFtpServerControlPort() {

+        String systemProperty = System.getProperty(FTP_SERVER_PORT_PROPERTY);

+        return (systemProperty == null) ? DEFAULT_SERVER_CONTROL_PORT : Integer.parseInt(systemProperty);

+    }

+    

+}

diff --git a/tags/2.5/src/test/resources/Sample.jpg b/tags/2.5/src/test/resources/Sample.jpg
new file mode 100644
index 0000000..628a3cd
--- /dev/null
+++ b/tags/2.5/src/test/resources/Sample.jpg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tags/2.5/src/test/resources/SampleReplyText.properties b/tags/2.5/src/test/resources/SampleReplyText.properties
new file mode 100644
index 0000000..7a19a33
--- /dev/null
+++ b/tags/2.5/src/test/resources/SampleReplyText.properties
@@ -0,0 +1,17 @@
+# Copyright 2007 the original author or authors.

+# 

+# 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.

+#

+# Test-specific mapping of reply code -> reply text

+# Tests are dependent on one or more values within this file

+110=Testing123

diff --git a/tags/2.5/src/test/resources/fakeftpserver-beans.xml b/tags/2.5/src/test/resources/fakeftpserver-beans.xml
new file mode 100644
index 0000000..e5d8793
--- /dev/null
+++ b/tags/2.5/src/test/resources/fakeftpserver-beans.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!--

+ Copyright 2008 the original author or authors.

+ 

+ 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.

+-->

+

+<!-- Spring Framework configuration for FakeFtpServer -->

+<!-- The FakeFtpServerSpringCofigurationTest class has dependencies on

+		several of the bean names and values configured within this file -->

+

+<beans xmlns="http://www.springframework.org/schema/beans"

+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

+       xsi:schemaLocation="http://www.springframework.org/schema/beans 

+       		http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

+

+    <bean id="fakeFtpServer" class="org.mockftpserver.fake.FakeFtpServer">

+        <property name="serverControlPort" value="9981"/>

+        <property name="systemName" value="UNIX"/>

+        <property name="userAccounts">

+            <list>

+                <bean class="org.mockftpserver.fake.UserAccount">

+                    <property name="username" value="joe"/>

+                    <property name="password" value="password"/>

+                    <property name="homeDirectory" value="/"/>

+                </bean>

+            </list>

+        </property>

+

+        <property name="fileSystem">

+            <bean class="org.mockftpserver.fake.filesystem.UnixFakeFileSystem">

+                <property name="createParentDirectoriesAutomatically" value="false"/>

+                <property name="entries">

+                    <list>

+                        <bean class="org.mockftpserver.fake.filesystem.DirectoryEntry">

+                            <property name="path" value="/"/>

+                        </bean>

+                        <bean class="org.mockftpserver.fake.filesystem.FileEntry">

+                            <property name="path" value="/File.txt"/>

+                            <property name="contents" value="abcdefghijklmnopqrstuvwxyz"/>

+                        </bean>

+                    </list>

+                </property>

+            </bean>

+        </property>

+

+    </bean>

+

+</beans>
\ No newline at end of file
diff --git a/tags/2.5/src/test/resources/fakeftpserver-permissions-beans.xml b/tags/2.5/src/test/resources/fakeftpserver-permissions-beans.xml
new file mode 100644
index 0000000..a506a95
--- /dev/null
+++ b/tags/2.5/src/test/resources/fakeftpserver-permissions-beans.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!--

+ Copyright 2008 the original author or authors.

+ 

+ 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.

+-->

+

+<!-- Spring Framework configuration for FakeFtpServer -->

+<!-- The FakeFtpServerSpringCofigurationTest class has dependencies on

+		several of the bean names and values configured within this file -->

+

+<beans xmlns="http://www.springframework.org/schema/beans"

+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

+       xsi:schemaLocation="http://www.springframework.org/schema/beans 

+       		http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

+

+    <bean id="fakeFtpServer" class="org.mockftpserver.fake.FakeFtpServer">

+        <property name="serverControlPort" value="9981"/>

+        <property name="userAccounts">

+            <list>

+                <bean class="org.mockftpserver.fake.UserAccount">

+                    <property name="username" value="joe"/>

+                    <property name="password" value="password"/>

+                    <property name="homeDirectory" value="c:\"/>

+                </bean>

+            </list>

+        </property>

+

+        <property name="fileSystem">

+            <bean class="org.mockftpserver.fake.filesystem.WindowsFakeFileSystem">

+                <property name="createParentDirectoriesAutomatically" value="false"/>

+                <property name="entries">

+                    <list>

+                        <bean class="org.mockftpserver.fake.filesystem.DirectoryEntry">

+                            <property name="path" value="c:\"/>

+                            <property name="permissionsFromString" value="rwxrwxrwx"/>

+                            <property name="owner" value="joe"/>

+                            <property name="group" value="users"/>

+                        </bean>

+                        <bean class="org.mockftpserver.fake.filesystem.FileEntry">

+                            <property name="path" value="c:\File1.txt"/>

+                            <property name="contents" value="1234567890"/>

+                            <property name="permissionsFromString" value="rwxrwxrwx"/>

+                            <property name="owner" value="peter"/>

+                            <property name="group" value="users"/>

+                        </bean>

+                        <bean class="org.mockftpserver.fake.filesystem.FileEntry">

+                            <property name="path" value="c:\File2.txt"/>

+                            <property name="contents" value="abcdefghijklmnopqrstuvwxyz"/>

+                            <property name="permissions">

+                                <bean class="org.mockftpserver.fake.filesystem.Permissions">

+                                    <constructor-arg value="rwx------"/>

+                                </bean>

+                            </property>

+                            <property name="owner" value="peter"/>

+                            <property name="group" value="users"/>

+                        </bean>

+                    </list>

+                </property>

+            </bean>

+        </property>

+

+    </bean>

+

+</beans>
\ No newline at end of file
diff --git a/tags/2.5/src/test/resources/log4j.properties b/tags/2.5/src/test/resources/log4j.properties
new file mode 100644
index 0000000..b7465ac
--- /dev/null
+++ b/tags/2.5/src/test/resources/log4j.properties
@@ -0,0 +1,6 @@
+# Set root category priority to INFO and set its only appender to CONSOLE

+log4j.rootCategory=INFO, CONSOLE

+

+log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender

+log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout

+log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %c{1} [%t] %p - %m%n

diff --git a/tags/2.5/src/test/resources/stubftpserver-beans.xml b/tags/2.5/src/test/resources/stubftpserver-beans.xml
new file mode 100644
index 0000000..89918f5
--- /dev/null
+++ b/tags/2.5/src/test/resources/stubftpserver-beans.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!--

+ Copyright 2007 the original author or authors.

+ 

+ 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.

+-->

+

+<!-- Spring Framework configuration for StubFtpServer -->

+<!-- The SpringConfigurationTest class has dependencies on 

+		several of the bean names and values configured within this file -->

+

+<beans xmlns="http://www.springframework.org/schema/beans"

+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

+       xsi:schemaLocation="http://www.springframework.org/schema/beans 

+       		http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

+

+	<bean id="stubFtpServer" class="org.mockftpserver.stub.StubFtpServer">

+		<property name="serverControlPort" value="9981" />

+		<property name="commandHandlers">

+			<map>

+

+				<entry key="LIST">

+					<bean class="org.mockftpserver.stub.command.ListCommandHandler">

+						<property name="directoryListing">

+							<value>

+							11-09-01 12:30PM  406348 File2350.log

+                			11-01-01 1:30PM &lt;DIR&gt; 0 archive

+                			</value>

+                		</property>

+					</bean>

+				</entry>

+

+				<entry key="PWD">

+					<bean class="org.mockftpserver.stub.command.PwdCommandHandler">

+						<property name="directory" value="foo/bar" />

+					</bean>

+				</entry>

+

+				<entry key="DELE">

+					<bean class="org.mockftpserver.stub.command.DeleCommandHandler">

+						<property name="replyCode" value="450" />

+					</bean>

+				</entry>

+

+				<entry key="RETR">

+					<bean class="org.mockftpserver.stub.command.RetrCommandHandler">

+						<property name="fileContents" 

+							value="Sample file contents line 1&#10;Line 2&#10;Line 3"/>

+					</bean>

+				</entry>

+

+			</map>

+		</property>

+	</bean>

+

+</beans>
\ No newline at end of file