org.waarp.ftp.core.data.FtpTransferControl.java Source code

Java tutorial

Introduction

Here is the source code for org.waarp.ftp.core.data.FtpTransferControl.java

Source

/**
 * This file is part of Waarp Project.
 * 
 * Copyright 2009, Frederic Bregier, and individual contributors by the @author tags. See the
 * COPYRIGHT.txt in the distribution for a full listing of individual contributors.
 * 
 * All Waarp Project is free software: you can redistribute it and/or modify it under the terms of
 * the GNU General Public License as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 * 
 * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with Waarp . If not, see
 * <http://www.gnu.org/licenses/>.
 */
package org.waarp.ftp.core.data;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;

import org.waarp.common.command.ReplyCode;
import org.waarp.common.command.exception.CommandAbstractException;
import org.waarp.common.command.exception.Reply425Exception;
import org.waarp.common.crypto.ssl.WaarpSslUtility;
import org.waarp.common.future.WaarpChannelFuture;
import org.waarp.common.future.WaarpFuture;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;
import org.waarp.common.utility.WaarpThreadFactory;
import org.waarp.ftp.core.command.FtpCommandCode;
import org.waarp.ftp.core.command.service.ABOR;
import org.waarp.ftp.core.config.FtpConfiguration;
import org.waarp.ftp.core.config.FtpInternalConfiguration;
import org.waarp.ftp.core.control.NetworkHandler;
import org.waarp.ftp.core.data.handler.DataNetworkHandler;
import org.waarp.ftp.core.exception.FtpNoConnectionException;
import org.waarp.ftp.core.exception.FtpNoFileException;
import org.waarp.ftp.core.exception.FtpNoTransferException;
import org.waarp.ftp.core.file.FtpFile;
import org.waarp.ftp.core.session.FtpSession;

/**
 * Main class that handles transfers and their execution
 * 
 * @author Frederic Bregier
 * 
 */
public class FtpTransferControl {
    /**
     * Internal Logger
     */
    private static final WaarpLogger logger = WaarpLoggerFactory.getLogger(FtpTransferControl.class);

    /**
     * SessionInterface
     */
    private final FtpSession session;

    /**
     * Step in order to wait that the DataNetworkHandler is ready
     */
    private volatile boolean isDataNetworkHandlerReady = false;

    /**
     * The associated DataChannel
     */
    private volatile Channel dataChannel = null;

    /**
     * Waiter for the dataChannel to be opened
     */
    private volatile WaarpChannelFuture waitForOpenedDataChannel = new WaarpChannelFuture(true);

    /**
     * Is the current Command Finished (or previously current command)
     */
    private volatile boolean isExecutingCommandFinished = true;
    /**
     * Waiter for the Command finishing
     */
    private volatile WaarpFuture commandFinishing = null;

    /**
     * Current command executed
     */
    private volatile FtpTransfer executingCommand = null;

    /**
     * Thread pool for execution of transfer command
     */
    private ExecutorService executorService = null;
    /**
     * Thread pool for execution of transfer command
     */
    private static final ScheduledExecutorService scheduleService = Executors.newScheduledThreadPool(100,
            new WaarpThreadFactory("FinalizeCommand"));

    /**
     * Blocking step for the Executor in order to wait for the end of the command (internal wait,
     * not to be used outside).
     */
    private volatile WaarpFuture endOfCommand = null;

    /**
     * A boolean to know if Check was called once
     */
    private volatile boolean isCheckAlreadyCalled = false;

    /**
     * 
     * @param session
     */
    public FtpTransferControl(FtpSession session) {
        this.session = session;
        endOfCommand = null;
    }

    // XXX DataNetworkHandler functions
    /**
     * The DataNetworkHandler is ready (from setNewFtpExecuteTransfer)
     * 
     */
    private void setDataNetworkHandlerReady() {
        isCheckAlreadyCalled = false;
        isDataNetworkHandlerReady = true;
    }

    /**
     * Wait for the DataNetworkHandler to be ready (from trueRetrieve of {@link FtpFile})
     * 
     * @throws InterruptedException
     * 
     */
    public void waitForDataNetworkHandlerReady() throws InterruptedException {
        if (!isDataNetworkHandlerReady) {
            Thread.sleep(10);
            if (!isDataNetworkHandlerReady) {
                // logger.debug("Wait for DataNetwork Ready over {}");
                throw new InterruptedException("Bad initialization");
            }
        }
    }

    /**
     * Set the new opened Channel (from channelConnected of {@link DataNetworkHandler})
     * 
     * @param channel
     * @param dataNetworkHandler
     */
    public void setOpenedDataChannel(Channel channel, DataNetworkHandler dataNetworkHandler) {
        logger.debug("SetOpenedDataChannel: " + (channel != null ? channel.remoteAddress() : "no channel"));
        if (channel != null) {
            session.getDataConn().setDataNetworkHandler(dataNetworkHandler);
            waitForOpenedDataChannel.setChannel(channel);
            waitForOpenedDataChannel.setSuccess();
        } else {
            waitForOpenedDataChannel.cancel();
        }
    }

    /**
     * Wait that the new opened connection is ready (same method in {@link FtpDataAsyncConn} from
     * openConnection)
     * 
     * @return the new opened Channel
     * @throws InterruptedException
     */
    public Channel waitForOpenedDataChannel() throws InterruptedException {
        Channel channel = null;
        if (waitForOpenedDataChannel.await(session.getConfiguration().TIMEOUTCON + 1000, TimeUnit.MILLISECONDS)) {
            if (waitForOpenedDataChannel.isSuccess()) {
                channel = waitForOpenedDataChannel.channel();
            } else {
                logger.warn("data connection is in error");
            }
        } else {
            logger.warn("Timeout occurs during data connection");
        }
        waitForOpenedDataChannel = new WaarpChannelFuture(true);
        return channel;
    }

    /**
     * Allow to reset the waitForOpenedDataChannel
     */
    public void resetWaitForOpenedDataChannel() {
        if (waitForOpenedDataChannel != null) {
            waitForOpenedDataChannel.cancel();
        }
        waitForOpenedDataChannel = new WaarpChannelFuture(true);
    }

    /**
     * Wait for the client to be connected (Passive) or Wait for the server to be connected to the
     * client (Active)
     * 
     * @return True if the connection is OK
     * @throws Reply425Exception
     */
    public synchronized boolean openDataConnection() throws Reply425Exception {
        // Prepare this Data channel to be closed ;-)
        // In fact, prepare the future close op which should occur since it is
        // now opened
        FtpDataAsyncConn dataAsyncConn = session.getDataConn();
        if (!dataAsyncConn.isStreamFile()) {
            // FIXME isActive or isDNHReady ?
            if (dataAsyncConn.isActive()) {
                // Already connected
                logger.debug("Connection already open");
                session.setReplyCode(ReplyCode.REPLY_125_DATA_CONNECTION_ALREADY_OPEN,
                        dataAsyncConn.getType().name() + " mode data connection already open");
                return true;
            }
        } else {
            // Stream, Data Connection should not be opened
            if (dataAsyncConn.isActive()) {
                logger.error("Connection already open but should not since in Stream mode");
                setTransferAbortedFromInternal(false);
                throw new Reply425Exception("Connection already open but should not since in Stream mode");
            }
        }
        // Need to open connection
        session.setReplyCode(ReplyCode.REPLY_150_FILE_STATUS_OKAY,
                "Opening " + dataAsyncConn.getType().name() + " mode data connection");
        if (dataAsyncConn.isPassiveMode()) {
            if (!dataAsyncConn.isBind()) {
                // No passive connection prepared
                throw new Reply425Exception("No passive data connection prepared");
            }
            // Wait for the connection to be done by the client
            logger.debug("Passive mode standby");
            try {
                dataChannel = waitForOpenedDataChannel();
                dataAsyncConn.setNewOpenedDataChannel(dataChannel);
            } catch (InterruptedException e) {
                logger.warn("Connection abort in passive mode", e);
                // Cannot open connection
                throw new Reply425Exception("Cannot open passive data connection");
            }
            logger.debug("Passive mode connected");
        } else {
            // Wait for the server to be connected to the client
            InetAddress inetAddress = dataAsyncConn.getLocalAddress().getAddress();
            InetSocketAddress inetSocketAddress = dataAsyncConn.getRemoteAddress();
            if (session.getConfiguration().getFtpInternalConfiguration().hasFtpSession(inetAddress,
                    inetSocketAddress)) {
                throw new Reply425Exception(
                        "Cannot open active data connection since remote address is already in use: "
                                + inetSocketAddress);
            }
            logger.debug("Active mode standby");
            Bootstrap bootstrap = session.getConfiguration().getFtpInternalConfiguration()
                    .getActiveBootstrap(session.isDataSsl());
            session.getConfiguration().setNewFtpSession(inetAddress, inetSocketAddress, session);
            // Set the session for the future dataChannel
            String mylog = session.toString();
            logger.debug("DataConn for: " + session.getCurrentCommand().getCommand() + " to "
                    + inetSocketAddress.toString());
            ChannelFuture future = bootstrap.connect(inetSocketAddress, dataAsyncConn.getLocalAddress());
            try {
                future.await();
            } catch (InterruptedException e1) {
            }
            if (!future.isSuccess()) {
                logger.warn("Connection abort in active mode from future while session: " + session.toString()
                        + "\nTrying connect to: " + inetSocketAddress.toString() + " From: "
                        + dataAsyncConn.getLocalAddress() + "\nWas: " + mylog, future.cause());
                // Cannot open connection
                session.getConfiguration().delFtpSession(inetAddress, inetSocketAddress);
                throw new Reply425Exception("Cannot open active data connection");
            }
            try {
                dataChannel = waitForOpenedDataChannel();
                dataAsyncConn.setNewOpenedDataChannel(dataChannel);
            } catch (InterruptedException e) {
                logger.warn("Connection abort in active mode", e);
                // Cannot open connection
                session.getConfiguration().delFtpSession(inetAddress, inetSocketAddress);
                throw new Reply425Exception("Cannot open active data connection");
            }
            // logger.debug("Active mode connected");
        }
        if (dataChannel == null) {
            // Cannot have a new Data connection since shutdown
            if (!dataAsyncConn.isPassiveMode()) {
                session.getConfiguration().getFtpInternalConfiguration().delFtpSession(
                        dataAsyncConn.getLocalAddress().getAddress(), dataAsyncConn.getRemoteAddress());
            }
            throw new Reply425Exception("Cannot open data connection, shuting down");
        }
        return true;
    }

    // XXX FtpTransfer functions
    /**
     * Run the command from an executor
     */
    private void runExecutor() {
        final WaarpFuture toFinish = commandFinishing;
        endOfCommand = new WaarpFuture(true);
        final WaarpFuture toCommand = endOfCommand;
        try {
            session.getDataConn().getCurrentDataChannel().closeFuture()
                    .addListener(new GenericFutureListener<Future<? super Void>>() {
                        public void operationComplete(Future<? super Void> future) throws Exception {
                            if (!toFinish.isDone() || !toCommand.isDone()) {
                                logger.debug("Schedule to finish command: " + session + ":" + toFinish.isDone()
                                        + ":" + toCommand.isDone());
                                scheduleService.schedule(new Runnable() {
                                    public void run() {
                                        if (!toFinish.isDone() || !toCommand.isDone()) {
                                            logger.warn("Will try to finish command: " + session
                                                    + " CommandFinishing:" + toFinish.isDone() + " EndOfCommand:"
                                                    + toCommand.isDone());
                                            toFinish.cancel();
                                        }
                                    }
                                }, FtpConfiguration.DATATIMEOUTCON * 2, TimeUnit.MILLISECONDS);
                            }
                        }
                    });
        } catch (FtpNoConnectionException e1) {
            //e1.printStackTrace();
        }
        // Run the command
        if (executorService == null) {
            executorService = Executors.newSingleThreadExecutor();
        }
        executorService.execute(new FtpTransferExecutor(session, executingCommand));
        try {
            commandFinishing.await();
            if (commandFinishing.isFailed()) {
                endOfCommand.cancel();
            }
        } catch (InterruptedException e) {
        }
    }

    /**
     * Add a new transfer to be executed. This is to be called from Command after connection is
     * opened and before answering to the client that command is ready to be executed (for Store or
     * Retrieve like operations).
     * 
     * @param command
     * @param file
     */
    public void setNewFtpTransfer(FtpCommandCode command, FtpFile file) {
        isExecutingCommandFinished = false;
        commandFinishing = new WaarpFuture(true);
        // logger.debug("setNewCommand: {}", command);
        setDataNetworkHandlerReady();
        executingCommand = new FtpTransfer(command, file);
        runExecutor();
        commandFinishing = null;
    }

    /**
     * Add a new transfer to be executed. This is to be called from Command after connection is
     * opened and before answering to the client that command is ready to be executed (for List like
     * operations).
     * 
     * @param command
     * @param list
     * @param path
     *            as Original Path
     */
    public void setNewFtpTransfer(FtpCommandCode command, List<String> list, String path) {
        isExecutingCommandFinished = false;
        commandFinishing = new WaarpFuture(true);
        logger.debug("setNewCommand: {}", command);
        setDataNetworkHandlerReady();
        executingCommand = new FtpTransfer(command, list, path);
        runExecutor();
        commandFinishing = null;
    }

    public boolean waitFtpTransferExecuting() {
        boolean notFinished = true;
        for (int i = 0; i < FtpInternalConfiguration.RETRYNB * 100; i++) {
            if (isExecutingCommandFinished || commandFinishing == null || session.isCurrentCommandFinished()
                    || (commandFinishing != null
                            && commandFinishing.awaitUninterruptibly(FtpInternalConfiguration.RETRYINMS))) {
                notFinished = false;
                break;
            }
        }
        return notFinished;
    }

    /**
     * Is a command currently executing (called from {@link NetworkHandler} when a message is
     * received to see if another transfer command is already in execution, which is not allowed)
     * 
     * @return True if a command is currently executing
     */
    public boolean isFtpTransferExecuting() {
        return !isExecutingCommandFinished;
    }

    /**
     * 
     * @return the current executing FtpTransfer
     * @throws FtpNoTransferException
     */
    public FtpTransfer getExecutingFtpTransfer() throws FtpNoTransferException {
        if (executingCommand != null) {
            return executingCommand;
        }
        throw new FtpNoTransferException("No Command currently running");
    }

    /**
     * 
     * @return True if the current FtpTransfer is a Retrieve like transfer
     * @throws FtpNoTransferException
     * @throws CommandAbstractException
     * @throws FtpNoFileException
     */
    private boolean isExecutingRetrLikeTransfer()
            throws FtpNoTransferException, CommandAbstractException, FtpNoFileException {
        return !session.isCurrentCommandFinished()
                && FtpCommandCode.isRetrLikeCommand(getExecutingFtpTransfer().getCommand())
                && getExecutingFtpTransfer().getFtpFile().isInReading();
    }

    /**
     * Run the retrieve operation if necessary (called from channelInterestChanged in {@link DataNetworkHandler})
     */
    public void runTrueRetrieve() {
        try {
            if (isExecutingRetrLikeTransfer()) {
                getExecutingFtpTransfer().getFtpFile().trueRetrieve();
            }
        } catch (CommandAbstractException e) {
        } catch (FtpNoTransferException e) {
        } catch (FtpNoFileException e) {
        }
    }

    /**
     * Called when a transfer is finished from setEndOfTransfer
     * 
     * @return True if it was already called before
     * @throws FtpNoTransferException
     */
    private boolean checkFtpTransferStatus() throws FtpNoTransferException {
        if (isCheckAlreadyCalled) {
            logger.warn("Check: ALREADY CALLED");
            return true;
        }
        if (isExecutingCommandFinished) {
            // already done
            logger.warn("Check: already Finished");
            if (commandFinishing != null) {
                commandFinishing.cancel();
            }
            throw new FtpNoTransferException("No transfer running");
        }
        if (!isDataNetworkHandlerReady) {
            // already done
            logger.warn("Check: already DNH not ready");
            throw new FtpNoTransferException("No connection");
        }
        isCheckAlreadyCalled = true;
        FtpTransfer executedTransfer = getExecutingFtpTransfer();
        logger.debug("Check: command {}", executedTransfer.getCommand());
        // DNH is ready and Transfer is running
        if (FtpCommandCode.isListLikeCommand(executedTransfer.getCommand())) {
            if (executedTransfer.getStatus()) {
                // Special status for List Like command
                logger.debug("Check: List OK");
                closeTransfer();
                return false;
            }
            logger.debug("Check: List Ko");
            abortTransfer();
            return false;
        } else if (FtpCommandCode.isRetrLikeCommand(executedTransfer.getCommand())) {
            FtpFile file = null;
            try {
                file = executedTransfer.getFtpFile();
            } catch (FtpNoFileException e) {
                logger.debug("Check: Retr no FtpFile for Retr");
                abortTransfer();
                return false;
            }
            try {
                if (file.isInReading()) {
                    logger.debug("Check: Retr FtpFile still in reading KO");
                    abortTransfer();
                } else {
                    logger.debug("Check: Retr FtpFile no more in reading OK");
                    closeTransfer();
                }
            } catch (CommandAbstractException e) {
                logger.warn("Retr Test is in Reading problem", e);
                closeTransfer();
            }
            return false;
        } else if (FtpCommandCode.isStoreLikeCommand(executedTransfer.getCommand())) {
            // logger.debug("Check: Store OK");
            closeTransfer();
            return false;
        } else {
            logger.warn("Check: Unknown command");
            abortTransfer();
        }
        return false;
    }

    /**
     * Abort the current transfer
     */
    private void abortTransfer() {
        logger.debug("Will abort transfer and write: ", new Exception("trace only"));
        FtpFile file = null;
        FtpTransfer current = null;
        try {
            current = getExecutingFtpTransfer();
            file = current.getFtpFile();
            file.abortFile();
        } catch (FtpNoTransferException e) {
            logger.warn("Abort problem", e);
        } catch (FtpNoFileException e) {
        } catch (CommandAbstractException e) {
            logger.warn("Abort problem", e);
        }
        if (current != null) {
            current.setStatus(false);
        }
        endDataConnection();
        session.setReplyCode(ReplyCode.REPLY_426_CONNECTION_CLOSED_TRANSFER_ABORTED,
                "Transfer aborted for " + (current == null ? "Unknown command" : current.toString()));
        if (current != null) {
            if (!FtpCommandCode.isListLikeCommand(current.getCommand())) {
                try {
                    session.getBusinessHandler().afterTransferDoneBeforeAnswer(current);
                } catch (CommandAbstractException e) {
                    session.setReplyCode(e);
                }
            }
        }
        finalizeExecution();
    }

    /**
     * Finish correctly a transfer
     * 
     */
    private void closeTransfer() {
        // logger.debug("Will close transfer and write: {}", write);
        FtpFile file = null;
        FtpTransfer current = null;
        try {
            current = getExecutingFtpTransfer();
            file = current.getFtpFile();
            file.closeFile();
        } catch (FtpNoTransferException e) {
            logger.warn("Close problem", e);
        } catch (FtpNoFileException e) {
        } catch (CommandAbstractException e) {
            logger.warn("Close problem", e);
        }
        if (current != null) {
            current.setStatus(true);
        }
        if (session.getDataConn().isStreamFile()) {
            endDataConnection();
        }
        session.setReplyCode(ReplyCode.REPLY_226_CLOSING_DATA_CONNECTION,
                "Transfer complete for " + (current == null ? "Unknown command" : current.toString()));
        if (current != null) {
            if (!FtpCommandCode.isListLikeCommand(current.getCommand())) {
                try {
                    session.getBusinessHandler().afterTransferDoneBeforeAnswer(current);
                } catch (CommandAbstractException e) {
                    session.setReplyCode(e);
                }
            } else {
                // Special wait to prevent fast LIST following by STOR or RETR command
                try {
                    Thread.sleep(FtpInternalConfiguration.RETRYINMS);
                } catch (InterruptedException e) {
                }
            }
        }
        finalizeExecution();
    }

    /**
     * Set the current transfer as finished. Called from {@link FtpTransferExecutor} when a transfer
     * is over.
     * 
     */
    public void setEndOfTransfer() {
        try {
            checkFtpTransferStatus();
        } catch (FtpNoTransferException e) {
            return;
        }
    }

    /**
     * To enable abort from internal error
     * 
     * @param write
     *            True means the message is write back to the control command, false it is only
     *            prepared
     */
    public void setTransferAbortedFromInternal(boolean write) {
        logger.debug("Set transfer aborted internal {}", write);
        abortTransfer();
        if (write) {
            session.getNetworkHandler().writeIntermediateAnswer();
        }
        if (endOfCommand != null) {
            endOfCommand.cancel();
        }
    }

    /**
     * Called by messageReceived, channelClosed (from {@link DataNetworkHandler} ) and trueRetrieve
     * (from {@link FtpFile}) when the transfer is over or by channelClosed
     */
    public void setPreEndOfTransfer() {
        if (endOfCommand != null) {
            endOfCommand.setSuccess();
        }
    }

    /**
     * Wait for the current transfer to finish, called from {@link FtpTransferExecutor}
     * 
     * @throws InterruptedException
     */
    public void waitForEndOfTransfer() throws InterruptedException {
        if (endOfCommand != null) {
            endOfCommand.await();
            if (endOfCommand.isFailed()) {
                throw new InterruptedException("Transfer aborted");
            }
        }
        // logger.debug("waitEndOfCommand over");
    }

    // XXX ExecutorHandler functions
    /**
     * Finalize execution
     * 
     */
    private void finalizeExecution() {
        // logger.debug("Finalize execution");
        if (commandFinishing != null) {
            commandFinishing.setSuccess();
        }
        isExecutingCommandFinished = true;
        executingCommand = null;
    }

    // XXX Finalize of Transfer
    /**
     * End the data connection if any
     */
    private synchronized void endDataConnection() {
        // logger.debug("End Data connection");
        if (isDataNetworkHandlerReady && dataChannel != null) {
            try {
                WaarpSslUtility.closingSslChannel(dataChannel).await(FtpConfiguration.DATATIMEOUTCON,
                        TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
            }
            isDataNetworkHandlerReady = false;
            // logger.debug("waitForClosedDataChannel over");
            dataChannel = null;
        }
    }

    /**
     * Clear the FtpTransferControl (called when the data connection must be over like from clear of {@link FtpDataAsyncConn},
     * abort from {@link ABOR} or ending control connection from {@link NetworkHandler}.
     * 
     */
    public void clear() {
        // logger.debug("Clear Ftp Transfer Control");
        endDataConnection();
        finalizeExecution();
        if (endOfCommand != null) {
            endOfCommand.cancel();
        }
        if (waitForOpenedDataChannel != null) {
            waitForOpenedDataChannel.cancel();
        }
        if (executorService != null) {
            executorService.shutdownNow();
            executorService = null;
        }
    }
}