1*876b9d75SXin Li -------------------------------------------------- 2*876b9d75SXin Li StubFtpServer Getting Started 3*876b9d75SXin Li -------------------------------------------------- 4*876b9d75SXin Li 5*876b9d75SXin LiStubFtpServer - Getting Started 6*876b9d75SXin Li~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 7*876b9d75SXin Li 8*876b9d75SXin Li <<StubFtpServer>> is a "stub" implementation of an FTP server. It supports the main FTP commands by 9*876b9d75SXin Li implementing command handlers for each of the corresponding low-level FTP server commands (e.g. RETR, 10*876b9d75SXin Li DELE, LIST). These <CommandHandler>s can be individually configured to return custom data or reply codes, 11*876b9d75SXin Li allowing simulation of a complete range of both success and failure scenarios. The <CommandHandler>s can 12*876b9d75SXin Li also be interrogated to verify command invocation data such as command parameters and timestamps. 13*876b9d75SXin Li 14*876b9d75SXin Li <<StubFtpServer>> works out of the box with reasonable defaults, but can be fully configured 15*876b9d75SXin Li programmatically or within a {{{http://www.springframework.org/}Spring Framework}} (or similar) container. 16*876b9d75SXin Li 17*876b9d75SXin Li Here is how to start the <<StubFtpServer>> with the default configuration. This will return 18*876b9d75SXin Li success reply codes, and return empty data (for retrieved files, directory listings, etc.). 19*876b9d75SXin Li 20*876b9d75SXin Li+------------------------------------------------------------------------------ 21*876b9d75SXin LiStubFtpServer stubFtpServer = new StubFtpServer(); 22*876b9d75SXin ListubFtpServer.start(); 23*876b9d75SXin Li+------------------------------------------------------------------------------ 24*876b9d75SXin Li 25*876b9d75SXin Li If you are running on a system where the default port (21) is already in use or cannot be bound 26*876b9d75SXin Li from a user process (such as Unix), you will need to use a different server control port. Use the 27*876b9d75SXin Li <<<StubFtpServer.setServerControlPort(int serverControlPort)>>> method to use a different port 28*876b9d75SXin Li number. If you specify a value of <<<0>>>, then the server will use a free port number. Then call 29*876b9d75SXin Li <<<getServerControlPort()>>> AFTER calling <<<start()>>> has been called to determine the actual port 30*876b9d75SXin Li number being used. Or, you can pass in a specific port number, such as 9187. 31*876b9d75SXin Li 32*876b9d75SXin Li* CommandHandlers 33*876b9d75SXin Li~~~~~~~~~~~~~~~~~ 34*876b9d75SXin Li 35*876b9d75SXin Li <CommandHandler>s are the heart of the <<StubFtpServer>>. 36*876b9d75SXin Li 37*876b9d75SXin Li <<StubFtpServer>> creates an appropriate default <CommandHandler> for each (supported) FTP server 38*876b9d75SXin Li command. See the list of <CommandHandler> classes associated with FTP server commands in 39*876b9d75SXin Li {{{./stubftpserver-commandhandlers.html}FTP Commands and CommandHandlers}}. 40*876b9d75SXin Li 41*876b9d75SXin Li You can retrieve the existing <CommandHandler> defined for an FTP server command by calling the 42*876b9d75SXin Li <<<StubFtpServer.getCommandHandler(String name)>>> method, passing in the FTP server command 43*876b9d75SXin Li name. For example: 44*876b9d75SXin Li 45*876b9d75SXin Li+------------------------------------------------------------------------------ 46*876b9d75SXin LiPwdCommandHandler pwdCommandHandler = (PwdCommandHandler) stubFtpServer.getCommandHandler("PWD"); 47*876b9d75SXin Li+------------------------------------------------------------------------------ 48*876b9d75SXin Li 49*876b9d75SXin Li You can replace the existing <CommandHandler> defined for an FTP server command by calling the 50*876b9d75SXin Li <<<StubFtpServer.setCommandHandler(String name, CommandHandler commandHandler)>>> method, passing 51*876b9d75SXin Li in the FTP server command name, such as <<<"STOR">>> or <<<"USER">>>, and the 52*876b9d75SXin Li <<<CommandHandler>>> instance. For example: 53*876b9d75SXin Li 54*876b9d75SXin Li+------------------------------------------------------------------------------ 55*876b9d75SXin LiPwdCommandHandler pwdCommandHandler = new PwdCommandHandler(); 56*876b9d75SXin LipwdCommandHandler.setDirectory("some/dir"); 57*876b9d75SXin ListubFtpServer.setCommandHandler("PWD", pwdCommandHandler); 58*876b9d75SXin Li+------------------------------------------------------------------------------ 59*876b9d75SXin Li 60*876b9d75SXin Li 61*876b9d75SXin Li** Generic CommandHandlers 62*876b9d75SXin Li~~~~~~~~~~~~~~~~~~~~~~~~~~ 63*876b9d75SXin Li 64*876b9d75SXin Li <<StubFtpServer>> includes a couple generic <CommandHandler> classes that can be used to replace 65*876b9d75SXin Li the default command handler for an FTP command. See the Javadoc for more information. 66*876b9d75SXin Li 67*876b9d75SXin Li * <<StaticReplyCommadHandler>> 68*876b9d75SXin Li 69*876b9d75SXin Li <<<StaticReplyCommadHandler>>> is a <CommandHandler> that always sends back the configured reply 70*876b9d75SXin Li code and text. This can be a useful replacement for a default <CommandHandler> if you want a 71*876b9d75SXin Li certain FTP command to always send back an error reply code. 72*876b9d75SXin Li 73*876b9d75SXin Li * <<SimpleCompositeCommandHandler>> 74*876b9d75SXin Li 75*876b9d75SXin Li <<<SimpleCompositeCommandHandler>>> is a composite <CommandHandler> that manages an internal 76*876b9d75SXin Li ordered list of <CommandHandler>s to which it delegates. Starting with the first 77*876b9d75SXin Li <CommandHandler> in the list, each invocation of this composite handler will invoke (delegate to) 78*876b9d75SXin Li the current internal <CommandHander>. Then it moves on the next <CommandHandler> in the internal list. 79*876b9d75SXin Li 80*876b9d75SXin Li 81*876b9d75SXin Li** Configuring CommandHandler for a New (Unsupported) Command 82*876b9d75SXin Li~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 83*876b9d75SXin Li 84*876b9d75SXin Li If you want to add support for a command that is not provided out of the box by <<StubFtpServer>>, 85*876b9d75SXin Li you can create a <CommandHandler> instance and set it within the <<StubFtpServer>> using the 86*876b9d75SXin Li <<<StubFtpServer.setCommandHandler(String name, CommandHandler commandHandler)>>> method in the 87*876b9d75SXin Li same way that you replace an existing <CommandHandler> (see above). The following example uses 88*876b9d75SXin Li the <<<StaticReplyCommandHandler>>> to add support for the FEAT command. 89*876b9d75SXin Li 90*876b9d75SXin Li+------------------------------------------------------------------------------ 91*876b9d75SXin Lifinal String FEAT_TEXT = "Extensions supported:\n" + 92*876b9d75SXin Li "MLST size*;create;modify*;perm;media-type\n" + 93*876b9d75SXin Li "SIZE\n" + 94*876b9d75SXin Li "COMPRESSION\n" + 95*876b9d75SXin Li "END"; 96*876b9d75SXin LiStaticReplyCommandHandler featCommandHandler = new StaticReplyCommandHandler(211, FEAT_TEXT); 97*876b9d75SXin ListubFtpServer.setCommandHandler("FEAT", featCommandHandler); 98*876b9d75SXin Li+------------------------------------------------------------------------------ 99*876b9d75SXin Li 100*876b9d75SXin Li 101*876b9d75SXin Li** Creating Your Own Custom CommandHandler Class 102*876b9d75SXin Li~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 103*876b9d75SXin Li 104*876b9d75SXin Li If one of the existing <CommandHandler> classes does not fulfill your needs, you can alternately create 105*876b9d75SXin Li your own custom <CommandHandler> class. The only requirement is that it implement the 106*876b9d75SXin Li <<<org.mockftpserver.core.command.CommandHandler>>> interface. You would, however, likely benefit from 107*876b9d75SXin Li inheriting from one of the existing abstract <CommandHandler> superclasses, such as 108*876b9d75SXin Li <<<org.mockftpserver.stub.command.AbstractStubCommandHandler>>> or 109*876b9d75SXin Li <<<org.mockftpserver.core.command.AbstractCommandHandler>>>. See the javadoc of these classes for 110*876b9d75SXin Li more information. 111*876b9d75SXin Li 112*876b9d75SXin Li 113*876b9d75SXin Li* Retrieving Command Invocation Data 114*876b9d75SXin Li~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 115*876b9d75SXin Li 116*876b9d75SXin Li Each predefined <<StubFtpServer>> <CommandHandler> manages a List of <<<InvocationRecord>>> objects -- one 117*876b9d75SXin Li for each time the <CommandHandler> is invoked. An <<<InvocationRecord>>> contains the <<<Command>>> 118*876b9d75SXin Li that triggered the invocation (containing the command name and parameters), as well as the invocation 119*876b9d75SXin Li timestamp and client host address. The <<<InvocationRecord>>> also contains a <<<Map>>>, with optional 120*876b9d75SXin Li <CommandHandler>-specific data. See the Javadoc for more information. 121*876b9d75SXin Li 122*876b9d75SXin Li You can retrieve the <<<InvocationRecord>>> from a <CommandHandler> by calling the 123*876b9d75SXin Li <<<getInvocation(int index)>>> method, passing in the (zero-based) index of the desired 124*876b9d75SXin Li invocation. You can get the number of invocations for a <CommandHandler> by calling 125*876b9d75SXin Li <<<numberOfInvocations()>>>. The {{{#Example}Example Test Using Stub Ftp Server}} below illustrates 126*876b9d75SXin Li retrieving and interrogating an <<<InvocationRecord>>> from a <CommandHandler>. 127*876b9d75SXin Li 128*876b9d75SXin Li 129*876b9d75SXin Li* {Example} Test Using StubFtpServer 130*876b9d75SXin Li~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 131*876b9d75SXin Li 132*876b9d75SXin Li This section includes a simplified example of FTP client code to be tested, and a JUnit 133*876b9d75SXin Li test for it that uses <<StubFtpServer>>. 134*876b9d75SXin Li 135*876b9d75SXin Li** FTP Client Code 136*876b9d75SXin Li~~~~~~~~~~~~~~~~~~ 137*876b9d75SXin Li 138*876b9d75SXin Li The following <<<RemoteFile>>> class includes a <<<readFile()>>> method that retrieves a remote 139*876b9d75SXin Li ASCII file and returns its contents as a String. This class uses the <<<FTPClient>>> from the 140*876b9d75SXin Li {{{http://commons.apache.org/net/}Apache Commons Net}} framework. 141*876b9d75SXin Li 142*876b9d75SXin Li+------------------------------------------------------------------------------ 143*876b9d75SXin Lipublic class RemoteFile { 144*876b9d75SXin Li 145*876b9d75SXin Li private String server; 146*876b9d75SXin Li 147*876b9d75SXin Li public String readFile(String filename) throws SocketException, IOException { 148*876b9d75SXin Li 149*876b9d75SXin Li FTPClient ftpClient = new FTPClient(); 150*876b9d75SXin Li ftpClient.connect(server); 151*876b9d75SXin Li 152*876b9d75SXin Li ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 153*876b9d75SXin Li boolean success = ftpClient.retrieveFile(filename, outputStream); 154*876b9d75SXin Li ftpClient.disconnect(); 155*876b9d75SXin Li 156*876b9d75SXin Li if (!success) { 157*876b9d75SXin Li throw new IOException("Retrieve file failed: " + filename); 158*876b9d75SXin Li } 159*876b9d75SXin Li return outputStream.toString(); 160*876b9d75SXin Li } 161*876b9d75SXin Li 162*876b9d75SXin Li public void setServer(String server) { 163*876b9d75SXin Li this.server = server; 164*876b9d75SXin Li } 165*876b9d75SXin Li 166*876b9d75SXin Li // Other methods ... 167*876b9d75SXin Li} 168*876b9d75SXin Li+------------------------------------------------------------------------------ 169*876b9d75SXin Li 170*876b9d75SXin Li** JUnit Test For FTP Client Code Using StubFtpServer 171*876b9d75SXin Li~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 172*876b9d75SXin Li 173*876b9d75SXin Li The following <<<RemoteFileTest>>> class includes a couple of JUnit tests that use 174*876b9d75SXin Li <<StubFtpServer>>. The test illustrates replacing the default <CommandHandler> with 175*876b9d75SXin Li a customized handler. 176*876b9d75SXin Li 177*876b9d75SXin Li+------------------------------------------------------------------------------ 178*876b9d75SXin Liimport java.io.IOException; 179*876b9d75SXin Liimport org.mockftpserver.core.command.InvocationRecord; 180*876b9d75SXin Liimport org.mockftpserver.stub.StubFtpServer; 181*876b9d75SXin Liimport org.mockftpserver.stub.command.RetrCommandHandler; 182*876b9d75SXin Liimport org.mockftpserver.test.AbstractTest; 183*876b9d75SXin Li 184*876b9d75SXin Lipublic class RemoteFileTest extends AbstractTest { 185*876b9d75SXin Li 186*876b9d75SXin Li private static final String FILENAME = "dir/sample.txt"; 187*876b9d75SXin Li 188*876b9d75SXin Li private RemoteFile remoteFile; 189*876b9d75SXin Li private StubFtpServer stubFtpServer; 190*876b9d75SXin Li 191*876b9d75SXin Li public void testReadFile() throws Exception { 192*876b9d75SXin Li 193*876b9d75SXin Li final String CONTENTS = "abcdef 1234567890"; 194*876b9d75SXin Li 195*876b9d75SXin Li // Replace the default RETR CommandHandler; customize returned file contents 196*876b9d75SXin Li RetrCommandHandler retrCommandHandler = new RetrCommandHandler(); 197*876b9d75SXin Li retrCommandHandler.setFileContents(CONTENTS); 198*876b9d75SXin Li stubFtpServer.setCommandHandler("RETR", retrCommandHandler); 199*876b9d75SXin Li 200*876b9d75SXin Li stubFtpServer.start(); 201*876b9d75SXin Li 202*876b9d75SXin Li String contents = remoteFile.readFile(FILENAME); 203*876b9d75SXin Li 204*876b9d75SXin Li // Verify returned file contents 205*876b9d75SXin Li assertEquals("contents", CONTENTS, contents); 206*876b9d75SXin Li 207*876b9d75SXin Li // Verify the submitted filename 208*876b9d75SXin Li InvocationRecord invocationRecord = retrCommandHandler.getInvocation(0); 209*876b9d75SXin Li String filename = invocationRecord.getString(RetrCommandHandler.PATHNAME_KEY); 210*876b9d75SXin Li assertEquals("filename", FILENAME, filename); 211*876b9d75SXin Li } 212*876b9d75SXin Li 213*876b9d75SXin Li /** 214*876b9d75SXin Li * Test the readFile() method when the FTP transfer fails (returns a non-success reply code) 215*876b9d75SXin Li */ 216*876b9d75SXin Li public void testReadFileThrowsException() { 217*876b9d75SXin Li 218*876b9d75SXin Li // Replace the default RETR CommandHandler; return failure reply code 219*876b9d75SXin Li RetrCommandHandler retrCommandHandler = new RetrCommandHandler(); 220*876b9d75SXin Li retrCommandHandler.setFinalReplyCode(550); 221*876b9d75SXin Li stubFtpServer.setCommandHandler("RETR", retrCommandHandler); 222*876b9d75SXin Li 223*876b9d75SXin Li stubFtpServer.start(); 224*876b9d75SXin Li 225*876b9d75SXin Li try { 226*876b9d75SXin Li remoteFile.readFile(FILENAME); 227*876b9d75SXin Li fail("Expected IOException"); 228*876b9d75SXin Li } 229*876b9d75SXin Li catch (IOException expected) { 230*876b9d75SXin Li // Expected this 231*876b9d75SXin Li } 232*876b9d75SXin Li } 233*876b9d75SXin Li 234*876b9d75SXin Li protected void setUp() throws Exception { 235*876b9d75SXin Li super.setUp(); 236*876b9d75SXin Li remoteFile = new RemoteFile(); 237*876b9d75SXin Li remoteFile.setServer("localhost"); 238*876b9d75SXin Li stubFtpServer = new StubFtpServer(); 239*876b9d75SXin Li } 240*876b9d75SXin Li 241*876b9d75SXin Li protected void tearDown() throws Exception { 242*876b9d75SXin Li super.tearDown(); 243*876b9d75SXin Li stubFtpServer.stop(); 244*876b9d75SXin Li } 245*876b9d75SXin Li} 246*876b9d75SXin Li+------------------------------------------------------------------------------ 247*876b9d75SXin Li 248*876b9d75SXin Li Things to note about the above test: 249*876b9d75SXin Li 250*876b9d75SXin Li * The <<<StubFtpServer>>> instance is created in the <<<setUp()>>> method, but is not started 251*876b9d75SXin Li there because it must be configured differently for each test. The <<<StubFtpServer>>> instance 252*876b9d75SXin Li is stopped in the <<<tearDown()>>> method, to ensure that it is stopped, even if the test fails. 253*876b9d75SXin Li 254*876b9d75SXin Li 255*876b9d75SXin Li* Spring Configuration 256*876b9d75SXin Li~~~~~~~~~~~~~~~~~~~~~~ 257*876b9d75SXin Li 258*876b9d75SXin Li You can easily configure a <<StubFtpServer>> instance in the 259*876b9d75SXin Li {{{http://www.springframework.org/}Spring Framework}}. The following example shows a <Spring> 260*876b9d75SXin Li configuration file. 261*876b9d75SXin Li 262*876b9d75SXin Li+------------------------------------------------------------------------------ 263*876b9d75SXin Li<?xml version="1.0" encoding="UTF-8"?> 264*876b9d75SXin Li 265*876b9d75SXin Li<beans xmlns="http://www.springframework.org/schema/beans" 266*876b9d75SXin Li xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 267*876b9d75SXin Li xsi:schemaLocation="http://www.springframework.org/schema/beans 268*876b9d75SXin Li http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"> 269*876b9d75SXin Li 270*876b9d75SXin Li <bean id="stubFtpServer" class="org.mockftpserver.stub.StubFtpServer"> 271*876b9d75SXin Li 272*876b9d75SXin Li <property name="commandHandlers"> 273*876b9d75SXin Li <map> 274*876b9d75SXin Li <entry key="LIST"> 275*876b9d75SXin Li <bean class="org.mockftpserver.stub.command.ListCommandHandler"> 276*876b9d75SXin Li <property name="directoryListing"> 277*876b9d75SXin Li <value> 278*876b9d75SXin Li 11-09-01 12:30PM 406348 File2350.log 279*876b9d75SXin Li 11-01-01 1:30PM <DIR> 0 archive 280*876b9d75SXin Li </value> 281*876b9d75SXin Li </property> 282*876b9d75SXin Li </bean> 283*876b9d75SXin Li </entry> 284*876b9d75SXin Li 285*876b9d75SXin Li <entry key="PWD"> 286*876b9d75SXin Li <bean class="org.mockftpserver.stub.command.PwdCommandHandler"> 287*876b9d75SXin Li <property name="directory" value="foo/bar" /> 288*876b9d75SXin Li </bean> 289*876b9d75SXin Li </entry> 290*876b9d75SXin Li 291*876b9d75SXin Li <entry key="DELE"> 292*876b9d75SXin Li <bean class="org.mockftpserver.stub.command.DeleCommandHandler"> 293*876b9d75SXin Li <property name="replyCode" value="450" /> 294*876b9d75SXin Li </bean> 295*876b9d75SXin Li </entry> 296*876b9d75SXin Li 297*876b9d75SXin Li <entry key="RETR"> 298*876b9d75SXin Li <bean class="org.mockftpserver.stub.command.RetrCommandHandler"> 299*876b9d75SXin Li <property name="fileContents" 300*876b9d75SXin Li value="Sample file contents line 1 Line 2 Line 3"/> 301*876b9d75SXin Li </bean> 302*876b9d75SXin Li </entry> 303*876b9d75SXin Li 304*876b9d75SXin Li </map> 305*876b9d75SXin Li </property> 306*876b9d75SXin Li </bean> 307*876b9d75SXin Li 308*876b9d75SXin Li</beans> 309*876b9d75SXin Li+------------------------------------------------------------------------------ 310*876b9d75SXin Li 311*876b9d75SXin Li This example overrides the default handlers for the following FTP commands: 312*876b9d75SXin Li 313*876b9d75SXin Li * LIST - replies with a predefined directory listing 314*876b9d75SXin Li 315*876b9d75SXin Li * PWD - replies with a predefined directory pathname 316*876b9d75SXin Li 317*876b9d75SXin Li * DELE - replies with an error reply code (450) 318*876b9d75SXin Li 319*876b9d75SXin Li * RETR - replies with predefined contents for a retrieved file 320*876b9d75SXin Li 321*876b9d75SXin Li [] 322*876b9d75SXin Li 323*876b9d75SXin Li And here is the Java code to load the above <Spring> configuration file and start the 324*876b9d75SXin Li configured <<StubFtpServer>>. 325*876b9d75SXin Li 326*876b9d75SXin Li+------------------------------------------------------------------------------ 327*876b9d75SXin LiApplicationContext context = new ClassPathXmlApplicationContext("stubftpserver-beans.xml"); 328*876b9d75SXin ListubFtpServer = (StubFtpServer) context.getBean("stubFtpServer"); 329*876b9d75SXin ListubFtpServer.start(); 330*876b9d75SXin Li+------------------------------------------------------------------------------ 331*876b9d75SXin Li 332*876b9d75SXin Li 333*876b9d75SXin Li* FTP Command Reply Text ResourceBundle 334*876b9d75SXin Li~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 335*876b9d75SXin Li 336*876b9d75SXin Li The default text asociated with each FTP command reply code is contained within the 337*876b9d75SXin Li "ReplyText.properties" ResourceBundle file. You can customize these messages by providing a 338*876b9d75SXin Li locale-specific ResourceBundle file on the CLASSPATH, according to the normal lookup rules of 339*876b9d75SXin Li the ResourceBundle class (e.g., "ReplyText_de.properties"). Alternatively, you can 340*876b9d75SXin Li completely replace the ResourceBundle file by calling the calling the 341*876b9d75SXin Li <<<StubFtpServer.setReplyTextBaseName(String)>>> method. 342