blob: 149d65246afbad9e37512c88289dca713a5c9413 [file] [log] [blame]
/*
* Copyright 2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mockftpserver.core.server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.mockftpserver.core.MockFtpServerException;
import org.mockftpserver.core.command.Command;
import org.mockftpserver.core.command.CommandHandler;
import org.mockftpserver.core.session.DefaultSession;
import org.mockftpserver.core.session.Session;
import org.mockftpserver.core.socket.DefaultServerSocketFactory;
import org.mockftpserver.core.socket.ServerSocketFactory;
import org.mockftpserver.core.util.Assert;
import java.io.IOException;
import java.net.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.ResourceBundle;
/**
* This is the abstract superclass for "mock" implementations of an FTP Server,
* suitable for testing FTP client code or standing in for a live FTP server. It supports
* the main FTP commands by defining handlers for each of the corresponding low-level FTP
* server commands (e.g. RETR, DELE, LIST). These handlers implement the {@link org.mockftpserver.core.command.CommandHandler}
* interface.
* <p/>
* By default, mock FTP Servers bind to the server control port of 21. You can use a different server control
* port by setting the <code>serverControlPort</code> property. If you specify a value of <code>0</code>,
* then a free port number will be chosen automatically; call <code>getServerControlPort()</code> AFTER
* <code>start()</code> has been called to determine the actual port number being used. Using a non-default
* port number is usually necessary when running on Unix or some other system where that port number is
* already in use or cannot be bound from a user process.
* <p/>
* <h4>Command Handlers</h4>
* You can set the existing {@link CommandHandler} defined for an FTP server command
* by calling the {@link #setCommandHandler(String, CommandHandler)} method, passing
* in the FTP server command name and {@link CommandHandler} instance.
* You can also replace multiple command handlers at once by using the {@link #setCommandHandlers(Map)}
* method. That is especially useful when configuring the server through the <b>Spring Framework</b>.
* <p/>
* You can retrieve the existing {@link CommandHandler} defined for an FTP server command by
* calling the {@link #getCommandHandler(String)} method, passing in the FTP server command name.
* <p/>
* <h4>FTP Command Reply Text ResourceBundle</h4>
* The default text asociated with each FTP command reply code is contained within the
* "ReplyText.properties" ResourceBundle file. You can customize these messages by providing a
* locale-specific ResourceBundle file on the CLASSPATH, according to the normal lookup rules of
* the ResourceBundle class (e.g., "ReplyText_de.properties"). Alternatively, you can
* completely replace the ResourceBundle file by calling the calling the
* {@link #setReplyTextBaseName(String)} method.
*
* @author Chris Mair
* @version $Revision$ - $Date$
* @see org.mockftpserver.fake.FakeFtpServer
* @see org.mockftpserver.stub.StubFtpServer
*/
public abstract class AbstractFtpServer implements Runnable {
/**
* Default basename for reply text ResourceBundle
*/
public static final String REPLY_TEXT_BASENAME = "ReplyText";
private static final int DEFAULT_SERVER_CONTROL_PORT = 21;
protected Logger LOG = LoggerFactory.getLogger(getClass());
// Simple value object that holds the socket and thread for a single session
private static class SessionInfo {
Socket socket;
Thread thread;
}
protected ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory();
private ServerSocket serverSocket = null;
private ResourceBundle replyTextBundle;
private volatile boolean terminate = false;
private Map commandHandlers;
private Thread serverThread;
private int serverControlPort = DEFAULT_SERVER_CONTROL_PORT;
private final Object startLock = new Object();
// Map of Session -> SessionInfo
private Map sessions = new HashMap();
/**
* Create a new instance. Initialize the default command handlers and
* reply text ResourceBundle.
*/
public AbstractFtpServer() {
replyTextBundle = ResourceBundle.getBundle(REPLY_TEXT_BASENAME);
commandHandlers = new HashMap();
}
/**
* Start a new Thread for this server instance
*/
public void start() {
serverThread = new Thread(this);
synchronized (startLock) {
try {
// Start here in case server thread runs faster than main thread.
// See https://sourceforge.net/tracker/?func=detail&atid=1006533&aid=1925590&group_id=208647
serverThread.start();
// Wait until the server thread is initialized
startLock.wait();
}
catch (InterruptedException e) {
e.printStackTrace();
throw new MockFtpServerException(e);
}
}
}
/**
* The logic for the server thread
*
* @see Runnable#run()
*/
public void run() {
try {
LOG.info("Starting the server on port " + serverControlPort);
serverSocket = serverSocketFactory.createServerSocket(serverControlPort);
if (serverControlPort == 0) {
this.serverControlPort = serverSocket.getLocalPort();
LOG.info("Actual server port is " + this.serverControlPort);
}
// Notify to allow the start() method to finish and return
synchronized (startLock) {
startLock.notify();
}
while (!terminate) {
try {
Socket clientSocket = serverSocket.accept();
LOG.info("Connection accepted from host " + clientSocket.getInetAddress());
Session session = createSession(clientSocket);
Thread sessionThread = new Thread(session);
sessionThread.start();
SessionInfo sessionInfo = new SessionInfo();
sessionInfo.socket = clientSocket;
sessionInfo.thread = sessionThread;
sessions.put(session, sessionInfo);
}
catch (SocketException e) {
LOG.trace("Socket exception: " + e.toString());
}
}
}
catch (IOException e) {
LOG.error("Error", e);
}
finally {
LOG.debug("Cleaning up server...");
// Ensure that the start() method is not still blocked
synchronized (startLock) {
startLock.notifyAll();
}
try {
if (serverSocket != null) {
serverSocket.close();
}
closeSessions();
}
catch (IOException e) {
LOG.error("Error cleaning up server", e);
}
catch (InterruptedException e) {
LOG.error("Error cleaning up server", e);
}
LOG.info("Server stopped.");
terminate = false;
}
}
/**
* Stop this server instance and wait for it to terminate.
*/
public void stop() {
LOG.trace("Stopping the server...");
terminate = true;
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
throw new MockFtpServerException(e);
}
}
try {
if (serverThread != null) {
serverThread.join();
}
}
catch (InterruptedException e) {
e.printStackTrace();
throw new MockFtpServerException(e);
}
}
/**
* Return the CommandHandler defined for the specified command name
*
* @param name - the command name
* @return the CommandHandler defined for name
*/
public CommandHandler getCommandHandler(String name) {
return (CommandHandler) commandHandlers.get(Command.normalizeName(name));
}
/**
* Override the default CommandHandlers with those in the specified Map of
* commandName>>CommandHandler. This will only override the default CommandHandlers
* for the keys in <code>commandHandlerMapping</code>. All other default CommandHandler
* mappings remain unchanged.
*
* @param commandHandlerMapping - the Map of commandName->CommandHandler; these override the defaults
* @throws org.mockftpserver.core.util.AssertFailedException
* - if the commandHandlerMapping is null
*/
public void setCommandHandlers(Map commandHandlerMapping) {
Assert.notNull(commandHandlerMapping, "commandHandlers");
for (Iterator iter = commandHandlerMapping.keySet().iterator(); iter.hasNext();) {
String commandName = (String) iter.next();
setCommandHandler(commandName, (CommandHandler) commandHandlerMapping.get(commandName));
}
}
/**
* Set the CommandHandler for the specified command name. If the CommandHandler implements
* the {@link org.mockftpserver.core.command.ReplyTextBundleAware} interface and its <code>replyTextBundle</code> attribute
* is null, then set its <code>replyTextBundle</code> to the <code>replyTextBundle</code> of
* this StubFtpServer.
*
* @param commandName - the command name to which the CommandHandler will be associated
* @param commandHandler - the CommandHandler
* @throws org.mockftpserver.core.util.AssertFailedException
* - if the commandName or commandHandler is null
*/
public void setCommandHandler(String commandName, CommandHandler commandHandler) {
Assert.notNull(commandName, "commandName");
Assert.notNull(commandHandler, "commandHandler");
commandHandlers.put(Command.normalizeName(commandName), commandHandler);
initializeCommandHandler(commandHandler);
}
/**
* Set the reply text ResourceBundle to a new ResourceBundle with the specified base name,
* accessible on the CLASSPATH. See {@link java.util.ResourceBundle#getBundle(String)}.
*
* @param baseName - the base name of the resource bundle, a fully qualified class name
*/
public void setReplyTextBaseName(String baseName) {
replyTextBundle = ResourceBundle.getBundle(baseName);
}
/**
* Return the ReplyText ResourceBundle. Set the bundle through the {@link #setReplyTextBaseName(String)} method.
*
* @return the reply text ResourceBundle
*/
public ResourceBundle getReplyTextBundle() {
return replyTextBundle;
}
/**
* Set the port number to which the server control connection socket will bind. The default value is 21.
*
* @param serverControlPort - the port number for the server control connection ServerSocket
*/
public void setServerControlPort(int serverControlPort) {
this.serverControlPort = serverControlPort;
}
/**
* Return the port number to which the server control connection socket will bind. The default value is 21.
*
* @return the port number for the server control connection ServerSocket
*/
public int getServerControlPort() {
return serverControlPort;
}
/**
* Return true if this server is fully shutdown -- i.e., there is no active (alive) threads and
* all sockets are closed. This method is intended for testing only.
*
* @return true if this server is fully shutdown
*/
public boolean isShutdown() {
boolean shutdown = !serverThread.isAlive() && serverSocket.isClosed();
for (Iterator iter = sessions.values().iterator(); iter.hasNext();) {
SessionInfo sessionInfo = (SessionInfo) iter.next();
shutdown = shutdown && sessionInfo.socket.isClosed() && !sessionInfo.thread.isAlive();
}
return shutdown;
}
/**
* Return true if this server has started -- i.e., there is an active (alive) server threads
* and non-null server socket. This method is intended for testing only.
*
* @return true if this server has started
*/
public boolean isStarted() {
return serverThread != null && serverThread.isAlive() && serverSocket != null;
}
//-------------------------------------------------------------------------
// Internal Helper Methods
//-------------------------------------------------------------------------
/**
* Create a new Session instance for the specified client Socket
*
* @param clientSocket - the Socket associated with the client
* @return a Session
*/
protected Session createSession(Socket clientSocket) {
return new DefaultSession(clientSocket, commandHandlers);
}
private void closeSessions() throws InterruptedException, IOException {
for (Iterator iter = sessions.entrySet().iterator(); iter.hasNext();) {
Map.Entry entry = (Map.Entry) iter.next();
Session session = (Session) entry.getKey();
SessionInfo sessionInfo = (SessionInfo) entry.getValue();
session.close();
sessionInfo.thread.join(500L);
Socket sessionSocket = sessionInfo.socket;
if (sessionSocket != null) {
sessionSocket.close();
}
}
}
//------------------------------------------------------------------------------------
// Abstract method declarations
//------------------------------------------------------------------------------------
/**
* Initialize a CommandHandler that has been registered to this server. What "initialization"
* means is dependent on the subclass implementation.
*
* @param commandHandler - the CommandHandler to initialize
*/
protected abstract void initializeCommandHandler(CommandHandler commandHandler);
}