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