StubFtpServer.java revision 394607f12da7a0da1a3f315017b8df80924dade3
1/*
2 * Copyright 2007 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.mockftpserver.stub;
17
18import java.io.IOException;
19import java.net.ServerSocket;
20import java.net.Socket;
21import java.net.SocketTimeoutException;
22import java.util.HashMap;
23import java.util.Iterator;
24import java.util.Map;
25import java.util.ResourceBundle;
26
27import org.apache.log4j.Logger;
28import org.mockftpserver.core.MockFtpServerException;
29import org.mockftpserver.core.command.Command;
30import org.mockftpserver.core.command.CommandHandler;
31import org.mockftpserver.core.command.CommandNames;
32import org.mockftpserver.core.command.ReplyTextBundleAware;
33import org.mockftpserver.core.command.ReplyTextBundleUtil;
34import org.mockftpserver.core.session.DefaultSession;
35import org.mockftpserver.core.session.Session;
36import org.mockftpserver.core.socket.DefaultServerSocketFactory;
37import org.mockftpserver.core.socket.ServerSocketFactory;
38import org.mockftpserver.core.util.Assert;
39import org.mockftpserver.core.util.AssertFailedException;
40import org.mockftpserver.stub.command.AborCommandHandler;
41import org.mockftpserver.stub.command.AcctCommandHandler;
42import org.mockftpserver.stub.command.AlloCommandHandler;
43import org.mockftpserver.stub.command.AppeCommandHandler;
44import org.mockftpserver.stub.command.CdupCommandHandler;
45import org.mockftpserver.stub.command.ConnectCommandHandler;
46import org.mockftpserver.stub.command.CwdCommandHandler;
47import org.mockftpserver.stub.command.DeleCommandHandler;
48import org.mockftpserver.stub.command.HelpCommandHandler;
49import org.mockftpserver.stub.command.ListCommandHandler;
50import org.mockftpserver.stub.command.MkdCommandHandler;
51import org.mockftpserver.stub.command.ModeCommandHandler;
52import org.mockftpserver.stub.command.NlstCommandHandler;
53import org.mockftpserver.stub.command.NoopCommandHandler;
54import org.mockftpserver.stub.command.PassCommandHandler;
55import org.mockftpserver.stub.command.PasvCommandHandler;
56import org.mockftpserver.stub.command.PortCommandHandler;
57import org.mockftpserver.stub.command.PwdCommandHandler;
58import org.mockftpserver.stub.command.QuitCommandHandler;
59import org.mockftpserver.stub.command.ReinCommandHandler;
60import org.mockftpserver.stub.command.RestCommandHandler;
61import org.mockftpserver.stub.command.RetrCommandHandler;
62import org.mockftpserver.stub.command.RmdCommandHandler;
63import org.mockftpserver.stub.command.RnfrCommandHandler;
64import org.mockftpserver.stub.command.RntoCommandHandler;
65import org.mockftpserver.stub.command.SiteCommandHandler;
66import org.mockftpserver.stub.command.SmntCommandHandler;
67import org.mockftpserver.stub.command.StatCommandHandler;
68import org.mockftpserver.stub.command.StorCommandHandler;
69import org.mockftpserver.stub.command.StouCommandHandler;
70import org.mockftpserver.stub.command.StruCommandHandler;
71import org.mockftpserver.stub.command.SystCommandHandler;
72import org.mockftpserver.stub.command.TypeCommandHandler;
73import org.mockftpserver.stub.command.UserCommandHandler;
74
75/**
76 * <b>StubFtpServer</b> is the top-level class for a "stub" implementation of an FTP Server,
77 * suitable for testing FTP client code or standing in for a live FTP server. It supports
78 * the main FTP commands by defining handlers for each of the corresponding low-level FTP
79 * server commands (e.g. RETR, DELE, LIST). These handlers implement the {@link CommandHandler}
80 * interface.
81 * <p>
82 * <b>StubFtpServer</b> works out of the box with default command handlers that return
83 * success reply codes and empty data (for retrieved files, directory listings, etc.).
84 * The command handler for any command can be easily configured to return custom data
85 * or reply codes. Or it can be replaced with a custom {@link CommandHandler}
86 * implementation. This allows simulation of a complete range of both success and
87 * failure scenarios. The command handlers can also be interrogated to verify command
88 * invocation data such as command parameters and timestamps.
89 * <p>
90 * <b>StubFtpServer</b> can be fully configured programmatically or within a Spring Framework
91 * ({@link http://www.springframework.org/}) or similar container.
92 * <p>
93 * <h4>Starting the StubFtpServer</h4>
94 * Here is how to start the <b>StubFtpServer</b> with the default configuration.
95 * <pre><code>
96 * StubFtpServer stubFtpServer = new StubFtpServer();
97 * stubFtpServer.start();
98 * </code></pre>
99 * <p>
100 * <h4>Retrieving Command Handlers</h4>
101 * You can retrieve the existing {@link CommandHandler} defined for an FTP server command
102 * by calling the {@link #getCommandHandler(String)} method, passing in the FTP server
103 * command name. For example:
104 * <pre><code>
105 * PwdCommandHandler pwdCommandHandler = (PwdCommandHandler) stubFtpServer.getCommandHandler("PWD");
106 * </code></pre>
107 * <p>
108 * <h4>Replacing Command Handlers</h4>
109 * You can replace the existing {@link CommandHandler} defined for an FTP server command
110 * by calling the {@link #setCommandHandler(String, CommandHandler)} method, passing
111 * in the FTP server command name and {@link CommandHandler} instance. For example:
112 * <pre><code>
113 * PwdCommandHandler pwdCommandHandler = new PwdCommandHandler();
114 * pwdCommandHandler.setDirectory("some/dir");
115 * stubFtpServer.setCommandHandler("PWD", pwdCommandHandler);
116 * </code></pre>
117 * You can also replace multiple command handlers at once by using the {@link #setCommandHandlers(Map)}
118 * method. That is especially useful when configuring the server through the <b>Spring Framework</b>.
119 * <h4>FTP Command Reply Text ResourceBundle</h4>
120 * <p>
121 * The default text asociated with each FTP command reply code is contained within the
122 * "ReplyText.properties" ResourceBundle file. You can customize these messages by providing a
123 * locale-specific ResourceBundle file on the CLASSPATH, according to the normal lookup rules of
124 * the ResourceBundle class (e.g., "ReplyText_de.properties"). Alternatively, you can
125 * completely replace the ResourceBundle file by calling the calling the
126 * {@link #setReplyTextBaseName(String)} method.
127 *
128 * @version $Revision$ - $Date$
129 *
130 * @author Chris Mair
131 */
132public final class StubFtpServer implements Runnable {
133
134    /** Default basename for reply text ResourceBundle */
135    public static final String REPLY_TEXT_BASENAME = "ReplyText";
136    private static final int DEFAULT_SERVER_CONTROL_PORT = 21;
137
138    private static Logger LOG = Logger.getLogger(StubFtpServer.class);
139
140    // Simple value object that holds the socket and thread for a single session
141    private static class SessionInfo {
142        private Socket socket;
143        private Thread thread;
144    }
145
146    private ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory();
147    private ServerSocket serverSocket = null;
148    ResourceBundle replyTextBundle;             // non-private for testing only
149    private volatile boolean terminate = false;
150    private Map commandHandlers;
151    private Thread serverThread;
152    private int serverControlPort = DEFAULT_SERVER_CONTROL_PORT;
153
154    // Map of Session -> SessionInfo
155    private Map sessions = new HashMap();
156
157    /**
158     * Create a new instance. Initialize the default command handlers and
159     * reply text ResourceBundle.
160     */
161    public StubFtpServer() {
162        replyTextBundle = ResourceBundle.getBundle(REPLY_TEXT_BASENAME);
163
164        commandHandlers = new HashMap();
165
166        PwdCommandHandler pwdCommandHandler = new PwdCommandHandler();
167
168        // Initialize the default CommandHandler mappings
169        setCommandHandler(CommandNames.ABOR, new AborCommandHandler());
170        setCommandHandler(CommandNames.ACCT, new AcctCommandHandler());
171        setCommandHandler(CommandNames.ALLO, new AlloCommandHandler());
172        setCommandHandler(CommandNames.APPE, new AppeCommandHandler());
173        setCommandHandler(CommandNames.PWD, pwdCommandHandler);            // same as XPWD
174        setCommandHandler(CommandNames.CONNECT, new ConnectCommandHandler());
175        setCommandHandler(CommandNames.CWD, new CwdCommandHandler());
176        setCommandHandler(CommandNames.CDUP, new CdupCommandHandler());
177        setCommandHandler(CommandNames.DELE, new DeleCommandHandler());
178        setCommandHandler(CommandNames.HELP, new HelpCommandHandler());
179        setCommandHandler(CommandNames.LIST, new ListCommandHandler());
180        setCommandHandler(CommandNames.MKD, new MkdCommandHandler());
181        setCommandHandler(CommandNames.MODE, new ModeCommandHandler());
182        setCommandHandler(CommandNames.NOOP, new NoopCommandHandler());
183        setCommandHandler(CommandNames.NLST, new NlstCommandHandler());
184        setCommandHandler(CommandNames.PASS, new PassCommandHandler());
185        setCommandHandler(CommandNames.PASV, new PasvCommandHandler());
186        setCommandHandler(CommandNames.PORT, new PortCommandHandler());
187        setCommandHandler(CommandNames.RETR, new RetrCommandHandler());
188        setCommandHandler(CommandNames.QUIT, new QuitCommandHandler());
189        setCommandHandler(CommandNames.REIN, new ReinCommandHandler());
190        setCommandHandler(CommandNames.REST, new RestCommandHandler());
191        setCommandHandler(CommandNames.RMD, new RmdCommandHandler());
192        setCommandHandler(CommandNames.RNFR, new RnfrCommandHandler());
193        setCommandHandler(CommandNames.RNTO, new RntoCommandHandler());
194        setCommandHandler(CommandNames.SITE, new SiteCommandHandler());
195        setCommandHandler(CommandNames.SMNT, new SmntCommandHandler());
196        setCommandHandler(CommandNames.STAT, new StatCommandHandler());
197        setCommandHandler(CommandNames.STOR, new StorCommandHandler());
198        setCommandHandler(CommandNames.STOU, new StouCommandHandler());
199        setCommandHandler(CommandNames.STRU, new StruCommandHandler());
200        setCommandHandler(CommandNames.SYST, new SystCommandHandler());
201        setCommandHandler(CommandNames.TYPE, new TypeCommandHandler());
202        setCommandHandler(CommandNames.USER, new UserCommandHandler());
203        setCommandHandler(CommandNames.XPWD, pwdCommandHandler);           // same as PWD
204    }
205
206    /**
207     * Start a new Thread for this server instance
208     */
209    public void start() {
210        serverThread = new Thread(this);
211        serverThread.start();
212    }
213
214    /**
215     * The logic for the server thread
216     * @see java.lang.Runnable#run()
217     */
218    public void run() {
219        try {
220            LOG.info("Starting the server on port " + serverControlPort);
221            serverSocket = serverSocketFactory.createServerSocket(serverControlPort);
222
223            serverSocket.setSoTimeout(500);
224            while(!terminate) {
225                try {
226                    Socket clientSocket = serverSocket.accept();
227                    LOG.info("Connection accepted from host " + clientSocket.getInetAddress());
228
229                    DefaultSession session = new DefaultSession(clientSocket, commandHandlers);
230                    Thread sessionThread = new Thread(session);
231                    sessionThread.start();
232
233                    SessionInfo sessionInfo = new SessionInfo();
234                    sessionInfo.socket = clientSocket;
235                    sessionInfo.thread = sessionThread;
236                    sessions.put(session, sessionInfo);
237                }
238                catch(SocketTimeoutException socketTimeoutException) {
239                    LOG.trace("Socket accept() timeout");
240                }
241            }
242        }
243        catch (IOException e) {
244            LOG.error("Error", e);
245        }
246        finally {
247
248            LOG.debug("Cleaning up server...");
249
250            try {
251                serverSocket.close();
252
253                for (Iterator iter = sessions.keySet().iterator(); iter.hasNext();) {
254                    Session session = (Session) iter.next();
255                    SessionInfo sessionInfo = (SessionInfo) sessions.get(session);
256                    session.close();
257                    sessionInfo.thread.join(500L);
258                    Socket sessionSocket = (Socket) sessionInfo.socket;
259                    if (sessionSocket != null) {
260                        sessionSocket.close();
261                    }
262                }
263            }
264            catch (IOException e) {
265                e.printStackTrace();
266                throw new MockFtpServerException(e);
267            }
268            catch (InterruptedException e) {
269                e.printStackTrace();
270                throw new MockFtpServerException(e);
271            }
272            LOG.info("Server stopped.");
273        }
274    }
275
276    /**
277     * Stop this server instance and wait for it to terminate.
278     */
279    public void stop() {
280
281        LOG.trace("Stopping the server...");
282        terminate = true;
283
284        try {
285            serverThread.join();
286        }
287        catch (InterruptedException e) {
288            e.printStackTrace();
289            throw new MockFtpServerException(e);
290        }
291    }
292
293    /**
294     * Return the CommandHandler defined for the specified command name
295     * @param name - the command name
296     * @return the CommandHandler defined for name
297     */
298    public CommandHandler getCommandHandler(String name) {
299        return (CommandHandler) commandHandlers.get(Command.normalizeName(name));
300    }
301
302    /**
303     * Override the default CommandHandlers with those in the specified Map of
304     * commandName>>CommandHandler. This will only override the default CommandHandlers
305     * for the keys in <code>commandHandlerMapping</code>. All other default CommandHandler
306     * mappings remain unchanged.
307     *
308     * @param commandHandlers - the Map of commandName->CommandHandler; these override the defaults
309     *
310     * @throws AssertFailedException - if the commandHandlerMapping is null
311     */
312    public void setCommandHandlers(Map commandHandlerMapping) {
313        Assert.notNull(commandHandlerMapping, "commandHandlers");
314        for (Iterator iter = commandHandlerMapping.keySet().iterator(); iter.hasNext();) {
315            String commandName = (String) iter.next();
316            setCommandHandler(commandName, (CommandHandler) commandHandlerMapping.get(commandName));
317        }
318    }
319
320    /**
321     * Set the CommandHandler for the specified command name. If the CommandHandler implements
322     * the {@link ReplyTextBundleAware} interface and its <code>replyTextBundle</code> attribute
323     * is null, then set its <code>replyTextBundle</code> to the <code>replyTextBundle</code> of
324     * this StubFtpServer.
325     *
326     * @param commandName - the command name to which the CommandHandler will be associated
327     * @param commandHandler - the CommandHandler
328     *
329     * @throws AssertFailedException - if the commandName or commandHandler is null
330     */
331    public void setCommandHandler(String commandName, CommandHandler commandHandler) {
332        Assert.notNull(commandName, "commandName");
333        Assert.notNull(commandHandler, "commandHandler");
334        commandHandlers.put(Command.normalizeName(commandName), commandHandler);
335        ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, replyTextBundle);
336    }
337
338    /**
339     * Set the reply text ResourceBundle to a new ResourceBundle with the specified base name,
340     * accessible on the CLASSPATH. See {@link ResourceBundle#getBundle(String)}.
341     * @param baseName - the base name of the resource bundle, a fully qualified class name
342     */
343    public void setReplyTextBaseName(String baseName) {
344        replyTextBundle = ResourceBundle.getBundle(baseName);
345    }
346
347    /**
348     * Set the port number to which the server control connection socket will bind. The default value is 21.
349     * @param serverControlPort - the port number for the server control connection ServerSocket
350     */
351    public void setServerControlPort(int serverControlPort) {
352        this.serverControlPort = serverControlPort;
353    }
354
355    //-------------------------------------------------------------------------
356    // Internal Helper Methods
357    //-------------------------------------------------------------------------
358
359    /**
360     * Return true if this server is fully shutdown -- i.e., there is no active (alive) threads and
361     * all sockets are closed. This method is intended for testing only.
362     * @return true if this server is fully shutdown
363     */
364    boolean isShutdown() {
365        boolean shutdown = !serverThread.isAlive() && serverSocket.isClosed();
366
367        for (Iterator iter = sessions.keySet().iterator(); iter.hasNext();) {
368            SessionInfo sessionInfo = (SessionInfo) iter.next();
369            shutdown = shutdown && sessionInfo.socket.isClosed() && !sessionInfo.thread.isAlive();
370        }
371        return shutdown;
372    }
373
374    /**
375     * Return true if this server has started -- i.e., there is an active (alive) server threads
376     * and non-null server socket. This method is intended for testing only.
377     * @return true if this server has started
378     */
379    boolean isStarted() {
380        return serverThread != null && serverThread.isAlive() && serverSocket != null;
381    }
382
383}