AbstractFtpServer.java revision b5c534dd0ab41438502ee18fa6f88c3f8dbbe314
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.core.server;
17
18import org.apache.log4j.Logger;
19import org.mockftpserver.core.MockFtpServerException;
20import org.mockftpserver.core.command.Command;
21import org.mockftpserver.core.command.CommandHandler;
22import org.mockftpserver.core.session.DefaultSession;
23import org.mockftpserver.core.session.Session;
24import org.mockftpserver.core.socket.DefaultServerSocketFactory;
25import org.mockftpserver.core.socket.ServerSocketFactory;
26import org.mockftpserver.core.util.Assert;
27
28import java.io.IOException;
29import java.net.*;
30import java.util.HashMap;
31import java.util.Iterator;
32import java.util.Map;
33import java.util.ResourceBundle;
34
35/**
36 * This is the abstract superclass for "mock" implementations of an FTP Server,
37 * suitable for testing FTP client code or standing in for a live FTP server. It supports
38 * the main FTP commands by defining handlers for each of the corresponding low-level FTP
39 * server commands (e.g. RETR, DELE, LIST). These handlers implement the {@link org.mockftpserver.core.command.CommandHandler}
40 * interface.
41 * <p/>
42 * By default, mock FTP Servers bind to the server control port of 21. You can use a different server
43 * control port by setting the the <code>serverControlPort</code> property. This is usually necessary
44 * when running on Unix or some other system where that port number is already in use or cannot be bound
45 * from a user process.
46 * <p/>
47 * <h4>Command Handlers</h4>
48 * You can set the existing {@link CommandHandler} defined for an FTP server command
49 * by calling the {@link #setCommandHandler(String, CommandHandler)} method, passing
50 * in the FTP server command name and {@link CommandHandler} instance.
51 * You can also replace multiple command handlers at once by using the {@link #setCommandHandlers(Map)}
52 * method. That is especially useful when configuring the server through the <b>Spring Framework</b>.
53 * <p/>
54 * You can retrieve the existing {@link CommandHandler} defined for an FTP server command by
55 * calling the {@link #getCommandHandler(String)} method, passing in the FTP server command name.
56 * <p/>
57 * <h4>FTP Command Reply Text ResourceBundle</h4>
58 * The default text asociated with each FTP command reply code is contained within the
59 * "ReplyText.properties" ResourceBundle file. You can customize these messages by providing a
60 * locale-specific ResourceBundle file on the CLASSPATH, according to the normal lookup rules of
61 * the ResourceBundle class (e.g., "ReplyText_de.properties"). Alternatively, you can
62 * completely replace the ResourceBundle file by calling the calling the
63 * {@link #setReplyTextBaseName(String)} method.
64 *
65 * @author Chris Mair
66 * @version $Revision$ - $Date$
67 * @see org.mockftpserver.fake.FakeFtpServer
68 * @see org.mockftpserver.stub.StubFtpServer
69 */
70public abstract class AbstractFtpServer implements Runnable {
71
72    /**
73     * Default basename for reply text ResourceBundle
74     */
75    public static final String REPLY_TEXT_BASENAME = "ReplyText";
76    private static final int DEFAULT_SERVER_CONTROL_PORT = 21;
77
78    protected Logger LOG = Logger.getLogger(getClass());
79
80    // Simple value object that holds the socket and thread for a single session
81    private static class SessionInfo {
82        Socket socket;
83        Thread thread;
84    }
85
86    protected ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory();
87    private ServerSocket serverSocket = null;
88    private ResourceBundle replyTextBundle;
89    private volatile boolean terminate = false;
90    private Map commandHandlers;
91    private Thread serverThread;
92    private int serverControlPort = DEFAULT_SERVER_CONTROL_PORT;
93    private final Object startLock = new Object();
94
95    // Map of Session -> SessionInfo
96    private Map sessions = new HashMap();
97
98    /**
99     * Create a new instance. Initialize the default command handlers and
100     * reply text ResourceBundle.
101     */
102    public AbstractFtpServer() {
103        replyTextBundle = ResourceBundle.getBundle(REPLY_TEXT_BASENAME);
104        commandHandlers = new HashMap();
105    }
106
107    /**
108     * Start a new Thread for this server instance
109     */
110    public void start() {
111        serverThread = new Thread(this);
112
113        synchronized (startLock) {
114            try {
115                // Start here in case server thread runs faster than main thread.
116                // See https://sourceforge.net/tracker/?func=detail&atid=1006533&aid=1925590&group_id=208647
117                serverThread.start();
118
119                // Wait until the server thread is initialized
120                startLock.wait();
121            }
122            catch (InterruptedException e) {
123                e.printStackTrace();
124                throw new MockFtpServerException(e);
125            }
126        }
127    }
128
129    /**
130     * The logic for the server thread
131     *
132     * @see Runnable#run()
133     */
134    public void run() {
135        try {
136            LOG.info("Starting the server on port " + serverControlPort);
137            serverSocket = serverSocketFactory.createServerSocket(serverControlPort);
138
139            // Notify to allow the start() method to finish and return
140            synchronized (startLock) {
141                startLock.notify();
142            }
143
144            while (!terminate) {
145                try {
146                    Socket clientSocket = serverSocket.accept();
147                    LOG.info("Connection accepted from host " + clientSocket.getInetAddress());
148
149                    Session session = createSession(clientSocket);
150                    Thread sessionThread = new Thread(session);
151                    sessionThread.start();
152
153                    SessionInfo sessionInfo = new SessionInfo();
154                    sessionInfo.socket = clientSocket;
155                    sessionInfo.thread = sessionThread;
156                    sessions.put(session, sessionInfo);
157                }
158                catch (SocketException e) {
159                    LOG.trace("Socket exception: " + e.toString());
160                }
161            }
162        }
163        catch (IOException e) {
164            LOG.error("Error", e);
165        }
166        finally {
167
168            LOG.debug("Cleaning up server...");
169
170            // Ensure that the start() method is not still blocked
171            synchronized (startLock) {
172                startLock.notifyAll();
173            }
174
175            try {
176                if (serverSocket != null) {
177                    serverSocket.close();
178                }
179                closeSessions();
180            }
181            catch (IOException e) {
182                LOG.error("Error cleaning up server", e);
183            }
184            catch (InterruptedException e) {
185                LOG.error("Error cleaning up server", e);
186            }
187            LOG.info("Server stopped.");
188            terminate = false;
189        }
190    }
191
192    /**
193     * Stop this server instance and wait for it to terminate.
194     */
195    public void stop() {
196
197        LOG.trace("Stopping the server...");
198        terminate = true;
199
200        if (serverSocket != null) {
201            try {
202                serverSocket.close();
203            } catch (IOException e) {
204                throw new MockFtpServerException(e);
205            }
206        }
207
208        try {
209            if (serverThread != null) {
210                serverThread.join();
211            }
212        }
213        catch (InterruptedException e) {
214            e.printStackTrace();
215            throw new MockFtpServerException(e);
216        }
217    }
218
219    /**
220     * Return the CommandHandler defined for the specified command name
221     *
222     * @param name - the command name
223     * @return the CommandHandler defined for name
224     */
225    public CommandHandler getCommandHandler(String name) {
226        return (CommandHandler) commandHandlers.get(Command.normalizeName(name));
227    }
228
229    /**
230     * Override the default CommandHandlers with those in the specified Map of
231     * commandName>>CommandHandler. This will only override the default CommandHandlers
232     * for the keys in <code>commandHandlerMapping</code>. All other default CommandHandler
233     * mappings remain unchanged.
234     *
235     * @param commandHandlerMapping - the Map of commandName->CommandHandler; these override the defaults
236     * @throws org.mockftpserver.core.util.AssertFailedException
237     *          - if the commandHandlerMapping is null
238     */
239    public void setCommandHandlers(Map commandHandlerMapping) {
240        Assert.notNull(commandHandlerMapping, "commandHandlers");
241        for (Iterator iter = commandHandlerMapping.keySet().iterator(); iter.hasNext();) {
242            String commandName = (String) iter.next();
243            setCommandHandler(commandName, (CommandHandler) commandHandlerMapping.get(commandName));
244        }
245    }
246
247    /**
248     * Set the CommandHandler for the specified command name. If the CommandHandler implements
249     * the {@link org.mockftpserver.core.command.ReplyTextBundleAware} interface and its <code>replyTextBundle</code> attribute
250     * is null, then set its <code>replyTextBundle</code> to the <code>replyTextBundle</code> of
251     * this StubFtpServer.
252     *
253     * @param commandName    - the command name to which the CommandHandler will be associated
254     * @param commandHandler - the CommandHandler
255     * @throws org.mockftpserver.core.util.AssertFailedException
256     *          - if the commandName or commandHandler is null
257     */
258    public void setCommandHandler(String commandName, CommandHandler commandHandler) {
259        Assert.notNull(commandName, "commandName");
260        Assert.notNull(commandHandler, "commandHandler");
261        commandHandlers.put(Command.normalizeName(commandName), commandHandler);
262        initializeCommandHandler(commandHandler);
263    }
264
265    /**
266     * Set the reply text ResourceBundle to a new ResourceBundle with the specified base name,
267     * accessible on the CLASSPATH. See {@link java.util.ResourceBundle#getBundle(String)}.
268     *
269     * @param baseName - the base name of the resource bundle, a fully qualified class name
270     */
271    public void setReplyTextBaseName(String baseName) {
272        replyTextBundle = ResourceBundle.getBundle(baseName);
273    }
274
275    /**
276     * Return the ReplyText ResourceBundle. Set the bundle through the  {@link #setReplyTextBaseName(String)}  method.
277     *
278     * @return the reply text ResourceBundle
279     */
280    public ResourceBundle getReplyTextBundle() {
281        return replyTextBundle;
282    }
283
284    /**
285     * Set the port number to which the server control connection socket will bind. The default value is 21.
286     *
287     * @param serverControlPort - the port number for the server control connection ServerSocket
288     */
289    public void setServerControlPort(int serverControlPort) {
290        this.serverControlPort = serverControlPort;
291    }
292
293    /**
294     * Return the port number to which the server control connection socket will bind. The default value is 21.
295     *
296     * @return the port number for the server control connection ServerSocket
297     */
298    public int getServerControlPort() {
299        return serverControlPort;
300    }
301
302    /**
303     * Return true if this server is fully shutdown -- i.e., there is no active (alive) threads and
304     * all sockets are closed. This method is intended for testing only.
305     *
306     * @return true if this server is fully shutdown
307     */
308    public boolean isShutdown() {
309        boolean shutdown = !serverThread.isAlive() && serverSocket.isClosed();
310
311        for (Iterator iter = sessions.values().iterator(); iter.hasNext();) {
312            SessionInfo sessionInfo = (SessionInfo) iter.next();
313            shutdown = shutdown && sessionInfo.socket.isClosed() && !sessionInfo.thread.isAlive();
314        }
315        return shutdown;
316    }
317
318    /**
319     * Return true if this server has started -- i.e., there is an active (alive) server threads
320     * and non-null server socket. This method is intended for testing only.
321     *
322     * @return true if this server has started
323     */
324    public boolean isStarted() {
325        return serverThread != null && serverThread.isAlive() && serverSocket != null;
326    }
327
328    //-------------------------------------------------------------------------
329    // Internal Helper Methods
330    //-------------------------------------------------------------------------
331
332    /**
333     * Create a new Session instance for the specified client Socket
334     *
335     * @param clientSocket - the Socket associated with the client
336     * @return a Session
337     */
338    protected Session createSession(Socket clientSocket) {
339        return new DefaultSession(clientSocket, commandHandlers);
340    }
341
342    private void closeSessions() throws InterruptedException, IOException {
343        for (Iterator iter = sessions.entrySet().iterator(); iter.hasNext();) {
344            Map.Entry entry = (Map.Entry) iter.next();
345            Session session = (Session) entry.getKey();
346            SessionInfo sessionInfo = (SessionInfo) entry.getValue();
347            session.close();
348            sessionInfo.thread.join(500L);
349            Socket sessionSocket = sessionInfo.socket;
350            if (sessionSocket != null) {
351                sessionSocket.close();
352            }
353        }
354    }
355
356    //------------------------------------------------------------------------------------
357    // Abstract method declarations
358    //------------------------------------------------------------------------------------
359
360    /**
361     * Initialize a CommandHandler that has been registered to this server. What "initialization"
362     * means is dependent on the subclass implementation.
363     *
364     * @param commandHandler - the CommandHandler to initialize
365     */
366    protected abstract void initializeCommandHandler(CommandHandler commandHandler);
367
368}