Coverage Report - org.mockftpserver.core.server.AbstractFtpServer
 
Classes in this File Line Coverage Branch Coverage Complexity
AbstractFtpServer
0%
0/113
0%
0/34
2.5
AbstractFtpServer$1
N/A
N/A
2.5
AbstractFtpServer$SessionInfo
100%
1/1
N/A
2.5
 
 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: 264 $ - $Date: 2012-07-17 21:19:23 -0400 (Tue, 17 Jul 2012) $
 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  0
     protected Logger LOG = LoggerFactory.getLogger(getClass());
 82  
 
 83  
     // Simple value object that holds the socket and thread for a single session
 84  180
     private static class SessionInfo {
 85  
         Socket socket;
 86  
         Thread thread;
 87  
     }
 88  
 
 89  0
     protected ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory();
 90  0
     private ServerSocket serverSocket = null;
 91  
     private ResourceBundle replyTextBundle;
 92  0
     private volatile boolean terminate = false;
 93  
     private Map commandHandlers;
 94  
     private Thread serverThread;
 95  0
     private int serverControlPort = DEFAULT_SERVER_CONTROL_PORT;
 96  0
     private final Object startLock = new Object();
 97  
 
 98  
     // Map of Session -> SessionInfo
 99  0
     private Map sessions = new HashMap();
 100  
 
 101  
     /**
 102  
      * Create a new instance. Initialize the default command handlers and
 103  
      * reply text ResourceBundle.
 104  
      */
 105  0
     public AbstractFtpServer() {
 106  0
         replyTextBundle = ResourceBundle.getBundle(REPLY_TEXT_BASENAME);
 107  0
         commandHandlers = new HashMap();
 108  0
     }
 109  
 
 110  
     /**
 111  
      * Start a new Thread for this server instance
 112  
      */
 113  
     public void start() {
 114  0
         serverThread = new Thread(this);
 115  
 
 116  0
         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  0
                 serverThread.start();
 121  
 
 122  
                 // Wait until the server thread is initialized
 123  0
                 startLock.wait();
 124  
             }
 125  0
             catch (InterruptedException e) {
 126  0
                 e.printStackTrace();
 127  0
                 throw new MockFtpServerException(e);
 128  0
             }
 129  0
         }
 130  0
     }
 131  
 
 132  
     /**
 133  
      * The logic for the server thread
 134  
      *
 135  
      * @see Runnable#run()
 136  
      */
 137  
     public void run() {
 138  
         try {
 139  0
             LOG.info("Starting the server on port " + serverControlPort);
 140  0
             serverSocket = serverSocketFactory.createServerSocket(serverControlPort);
 141  0
             if (serverControlPort == 0) {
 142  0
                 this.serverControlPort = serverSocket.getLocalPort();
 143  0
                 LOG.info("Actual server port is " + this.serverControlPort);
 144  
             }
 145  
 
 146  
             // Notify to allow the start() method to finish and return
 147  0
             synchronized (startLock) {
 148  0
                 startLock.notify();
 149  0
             }
 150  
 
 151  0
             while (!terminate) {
 152  
                 try {
 153  0
                     Socket clientSocket = serverSocket.accept();
 154  0
                     LOG.info("Connection accepted from host " + clientSocket.getInetAddress());
 155  
 
 156  0
                     Session session = createSession(clientSocket);
 157  0
                     Thread sessionThread = new Thread(session);
 158  0
                     sessionThread.start();
 159  
 
 160  0
                     SessionInfo sessionInfo = new SessionInfo();
 161  0
                     sessionInfo.socket = clientSocket;
 162  0
                     sessionInfo.thread = sessionThread;
 163  0
                     sessions.put(session, sessionInfo);
 164  
                 }
 165  0
                 catch (SocketException e) {
 166  0
                     LOG.trace("Socket exception: " + e.toString());
 167  0
                 }
 168  
             }
 169  0
         }
 170  0
         catch (IOException e) {
 171  0
             LOG.error("Error", e);
 172  0
         }
 173  
         finally {
 174  
 
 175  0
             LOG.debug("Cleaning up server...");
 176  
 
 177  
             // Ensure that the start() method is not still blocked
 178  0
             synchronized (startLock) {
 179  0
                 startLock.notifyAll();
 180  0
             }
 181  
 
 182  
             try {
 183  0
                 if (serverSocket != null) {
 184  0
                     serverSocket.close();
 185  
                 }
 186  0
                 closeSessions();
 187  
             }
 188  0
             catch (IOException e) {
 189  0
                 LOG.error("Error cleaning up server", e);
 190  
             }
 191  0
             catch (InterruptedException e) {
 192  0
                 LOG.error("Error cleaning up server", e);
 193  0
             }
 194  0
             LOG.info("Server stopped.");
 195  0
             terminate = false;
 196  0
         }
 197  0
     }
 198  
 
 199  
     /**
 200  
      * Stop this server instance and wait for it to terminate.
 201  
      */
 202  
     public void stop() {
 203  
 
 204  0
         LOG.trace("Stopping the server...");
 205  0
         terminate = true;
 206  
 
 207  0
         if (serverSocket != null) {
 208  
             try {
 209  0
                 serverSocket.close();
 210  0
             } catch (IOException e) {
 211  0
                 throw new MockFtpServerException(e);
 212  0
             }
 213  
         }
 214  
 
 215  
         try {
 216  0
             if (serverThread != null) {
 217  0
                 serverThread.join();
 218  
             }
 219  
         }
 220  0
         catch (InterruptedException e) {
 221  0
             e.printStackTrace();
 222  0
             throw new MockFtpServerException(e);
 223  0
         }
 224  0
     }
 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  0
         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  0
         Assert.notNull(commandHandlerMapping, "commandHandlers");
 248  0
         for (Iterator iter = commandHandlerMapping.keySet().iterator(); iter.hasNext();) {
 249  0
             String commandName = (String) iter.next();
 250  0
             setCommandHandler(commandName, (CommandHandler) commandHandlerMapping.get(commandName));
 251  0
         }
 252  0
     }
 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  0
         Assert.notNull(commandName, "commandName");
 267  0
         Assert.notNull(commandHandler, "commandHandler");
 268  0
         commandHandlers.put(Command.normalizeName(commandName), commandHandler);
 269  0
         initializeCommandHandler(commandHandler);
 270  0
     }
 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  0
         replyTextBundle = ResourceBundle.getBundle(baseName);
 280  0
     }
 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  0
         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  0
         this.serverControlPort = serverControlPort;
 298  0
     }
 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  0
         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  0
         boolean shutdown = !serverThread.isAlive() && serverSocket.isClosed();
 317  
 
 318  0
         for (Iterator iter = sessions.values().iterator(); iter.hasNext();) {
 319  0
             SessionInfo sessionInfo = (SessionInfo) iter.next();
 320  0
             shutdown = shutdown && sessionInfo.socket.isClosed() && !sessionInfo.thread.isAlive();
 321  0
         }
 322  0
         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  0
         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  0
         return new DefaultSession(clientSocket, commandHandlers);
 347  
     }
 348  
 
 349  
     private void closeSessions() throws InterruptedException, IOException {
 350  0
         for (Iterator iter = sessions.entrySet().iterator(); iter.hasNext();) {
 351  0
             Map.Entry entry = (Map.Entry) iter.next();
 352  0
             Session session = (Session) entry.getKey();
 353  0
             SessionInfo sessionInfo = (SessionInfo) entry.getValue();
 354  0
             session.close();
 355  0
             sessionInfo.thread.join(500L);
 356  0
             Socket sessionSocket = sessionInfo.socket;
 357  0
             if (sessionSocket != null) {
 358  0
                 sessionSocket.close();
 359  
             }
 360  0
         }
 361  0
     }
 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  
 }