/*
 * $Id: CmsFtpClient.java,v 0.0 2009/01/21 00:00:00 t-imamura Exp $
 *
 * Copyright (c) 2009 t-imamura, All rights reserved.
 */
package cms3.actions;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * FTP簡易クライアント.
 * <br>
 * FTPの制御ソケットを作成し、直接コマンドを送信し操作を行います。
 * PORTモード・PASVモード両方のモードにも対応しています。
 * ファイルの送受信以外に、ディレクトリ操作なども行えます。
 *
 * <pre>
 * CmsFtpClient ftp = new CmsFtpClient("ftp.example.com", false);
 * // 接続
 * ftp.connect();
 * // ログイン
 * ftp.logon("user", "pass");
 *
 * // ファイル送信
 * byte[] data = 転送データ;
 * ftp.put("/home/user/ftptest.txt", data);
 *
 * // ファイル受信
 * OutputStream os = new OutputStream();
 * ftp.get("/home/user/ftptest.txt", baos);
 *
 * // ファイル削除
 * ftp.dele("/home/user/ftptest.txt");
 *
 * // 切断
 * ftp.disconnect();
 * </pre>
 */
public class CmsFtpClient {

    /** FTP制御ポート番号 */
    private static final int FTP_PORT_NO = 21;
    /** アノニマスログインID */
    private static final String ANONYMOUS_USERID = "anonymous";
    /** PASVモード時の接続先を取得する為の正規表現 */
    private static final Pattern PASV_REPLY_MESSAGE = Pattern.compile(
        "227 .*\\(([0-9]{1,3}),([0-9]{1,3}),([0-9]{1,3}),([0-9]{1,3}),([0-9]{1,3}),([0-9]{1,3})\\).*");
    /** カレントディレクトリを取得する為の正規表現 */
    private static final Pattern PWD_REPLY_MESSAGE = Pattern.compile("257 .*\"(.*)\".*");

    /** IPアドレス */
    private final InetAddress address;
    /** ポート番号 */
    private final int portNo;

    /** FTP制御ソケット */
    private Socket ftpCtrl;
    /** FTP制御入力(OutputStream) */
    private BufferedWriter ctrlOut;
    /** FTP制御出力(InputStream) */
    private BufferedReader ctrlIn;

    /** パッシブモードか */
    private boolean passive = false;
    /** デバッグモード */
    public boolean debug = false;

    /**
     * FTP簡易クライアントオブジェクトを構築します。
     *
     * @param address IPアドレス or ホスト名
     * @param passive パッシブモードか
     * @throws UnknownHostException <code>address</code>のIPアドレスが見つからなかった場合
     */
    public CmsFtpClient(String address, boolean passive) throws UnknownHostException {
        this(InetAddress.getByName(address), FTP_PORT_NO, passive);
    }

    /**
     * FTP簡易クライアントオブジェクトを構築します。
     *
     * @param address IPアドレス
     * @param passive パッシブモードか
     */
    public CmsFtpClient(InetAddress address, boolean passive) {
        this(address, FTP_PORT_NO, passive);
    }

    /**
     * FTP簡易クライアントオブジェクトを構築します。
     *
     * @param address IPアドレス
     * @param portNo  制御ポート番号
     * @param passive パッシブモードか
     */
    public CmsFtpClient(InetAddress address, int portNo, boolean passive) {
        this.address = address;
        this.portNo = portNo;
        this.passive = passive;
    }

    @Override
    protected void finalize() throws Throwable {
        disconnect();
        super.finalize();
    }

    /**
     * パッシブモードかを設定します。
     *
     * @param passive パッシブモードか
     */
    public void setPassive(boolean passive) {
        this.passive = passive;
    }

    /**
     * デバッグモードかを設定します。
     *
     * @param debug デバッグモードか
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * FTPコマンドを送信します。
     * <br>
     * 受信したリプライメッセージが必要な場合は、内容を入れる {@link StringBuffer} を
     * <code>replyMessage</code> に指定します。
     *
     * @param command FTPコマンド
     * @param replyMessage リプライメッセージ
     * @return リプライコード
     */
    private int sendCommand(String command, StringBuffer replyMessage) {
        int replyCode = 0;

        try {
            // コマンド送信
            if (command != null) {
                debug("FTP > %s", command);

                ctrlOut.write(command);
                ctrlOut.write("\r\n");
                ctrlOut.flush();
            }

            // レスポンス受信
            String reply = null;
            while ((reply = ctrlIn.readLine()) != null) {
                debug("FTP < %s", reply);
                if (replyMessage != null)
                    replyMessage.append(reply);

                // リプライコード
                replyCode  = (reply.charAt(0) - '0') * 100;
                replyCode += (reply.charAt(1) - '0') * 10;
                replyCode += (reply.charAt(2) - '0') * 1;

                // 続かない場合は、終わり
                if (reply.charAt(3) == ' ') break;

                replyCode = 0;
                if (replyMessage != null)
                    replyMessage.append("\n");
            }
        } catch (IOException e) {
            replyCode = 0;
        }

        return replyCode;
    }

    /**
     * FTPコマンドを送信し、データコネクションを作成します。
     *
     * @param command FTPコマンド
     * @return データコネクション
     * @throws IOException 入出力エラーが発生した場合
     */
    private Socket creatDataConnection(String command) throws IOException {
        return passive ? creatDataConnectionPasv(command) : creatDataConnectionPort(command);
    }

    /**
     * FTPコマンドを送信し、PORTモードでデータコネクションを作成します。
     *
     * @param command FTPコマンド
     * @return データコネクション
     * @throws IOException 入出力エラーが発生した場合
     */
    private Socket creatDataConnectionPort(String command) throws IOException {
        Socket ftpData = null;
        ServerSocket serverSocket = null;

        try {
            serverSocket = new ServerSocket(0, 1, ftpCtrl.getLocalAddress());

            byte[] ip = serverSocket.getInetAddress().getAddress();
            int port = serverSocket.getLocalPort();
            debug("INFO: Open data connection. (%d.%d.%d.%d:%d)",
                    (ip[0] & 0xFF), (ip[1] & 0xFF), (ip[2] & 0xFF), (ip[3] & 0xFF), port);

            String cmdPort = "PORT "
                    + (ip[0] & 0xff) + "," + (ip[1] & 0xff) + ","
                    + (ip[2] & 0xff) + "," + (ip[3] & 0xff) + ","
                    + ((port / 256) & 0xff) + "," + (port & 0xff);
System.out.printf("createDataCo 5 - %s",cmdPort);
            if (sendCommand(cmdPort, null) == 200) {
                if (sendCommand(command, null) == 150) {
                    ftpData = serverSocket.accept();
                }
            }

        } catch (IOException e) {
            // Bug: nullであることが明らかな参照ftpDataを無駄にnullチェックしています
            // if (ftpData != null) ftpData.close();
            // ftpData = null;
            throw e;
        } finally {
            if (serverSocket != null)
                serverSocket.close();
        }

        return ftpData;
    }

    /**
     * FTPコマンドを送信し、PASVモードでデータコネクションを作成します。
     *
     * @param command FTPコマンド
     * @return データコネクション
     * @throws IOException 入出力エラーが発生した場合
     */
    private Socket creatDataConnectionPasv(String command) throws IOException {
        Socket ftpData = null;

        try {
            StringBuffer msg = new StringBuffer();
            if (sendCommand("PASV", msg) != 227) {
                return null;
            }

            Matcher match = PASV_REPLY_MESSAGE.matcher(msg.toString());
            if (!match.find() || match.groupCount() != 6) {
                // データコネクションの情報が取れない。
                return null;
            }

            String address = match.group(1) + "." + match.group(2) + "."
                    + match.group(3) + "." + match.group(4);
            int portNo = Integer.parseInt(match.group(5)) * 256
                    + Integer.parseInt(match.group(6));
            debug("INFO: Connect data connection. (%s:%d)", address, portNo);

            ftpData = new Socket(address, portNo);

            if (sendCommand(command, null) != 150) {
                ftpData.close();
                ftpData = null;
            }
        } catch (NumberFormatException e) {
            ftpData = null;
        } catch (IOException e) {
            if (ftpData != null)
                ftpData.close();
            ftpData = null;
            throw e;
        }

        return ftpData;
    }

    /**
     * 接続します。
     *
     * @return 成功したか
     */
    public boolean connect() {
        if (ftpCtrl != null) return false;

        try {
            ftpCtrl = new Socket(address, portNo);
            ctrlOut = new BufferedWriter(new OutputStreamWriter(ftpCtrl.getOutputStream()));
            ctrlIn  = new BufferedReader(new InputStreamReader(ftpCtrl.getInputStream()));

            if (sendCommand(null, null) != 220) {
                disconnect();
                return false;
            }
        } catch (IOException e) {
            try {
                if (ctrlIn != null)
                    ctrlIn.close();
                if (ctrlOut != null)
                    ctrlOut.close();
                if (ftpCtrl != null)
                    ftpCtrl.close();
            } catch (IOException ex) {
                // クローズの例外は無視
            }
            ftpCtrl = null;
            ctrlOut = null;
            ctrlIn  = null;
            return false;
        }
        return true;
    }

    /**
     * 切断します。
     *
     * @return 成功したか
     */
    public boolean disconnect() {
        if (ftpCtrl == null) return false;
        boolean result = true;

        // 終了
        if (sendCommand("QUIT", null) != 221) {
            result = false;
        }

        try {
            if (ctrlIn != null)
                ctrlIn.close();
        } catch (IOException e) {
            result = false;
        } finally {
            ctrlIn = null;
        }
        try {
            if (ctrlOut != null)
                ctrlOut.close();
        } catch (IOException e) {
            result = false;
        } finally {
            ctrlOut = null;
        }
        try {
            if (ftpCtrl != null)
                ftpCtrl.close();
        } catch (IOException e) {
            result = false;
        } finally {
            ftpCtrl = null;
        }

        return result;
    }

    /**
     * アノニマスログインします。
     *
     * @return 成功したか
     */
    public boolean anonymousLogon() {
        return logon(ANONYMOUS_USERID, ANONYMOUS_USERID);
    }

    /**
     * アノニマスログインします。
     *
     * @param email メールアドレス
     * @return 成功したか
     */
    public boolean anonymousLogon(String email) {
        return logon(ANONYMOUS_USERID, email);
    }

    /**
     * ログインします。
     *
     * @param user ユーザ名
     * @param pass パスワード
     * @return 成功したか
     */
    public boolean logon(String user, String pass) {
        if (user == null) return false;
        if (pass == null) return false;
        if (ftpCtrl == null) connect();
        boolean result = true;

        // ユーザ
        if (sendCommand("USER " + user, null) != 331) {
            result = false;
        }

        // パスワード
        if (sendCommand("PASS " + pass, null) != 230) {
            result = false;
        }

        return result;
    }

    /**
     * 転送データの形式を設定します。
     *
     * @param binary バイナリデータか
     * @return 成功したか
     */
    public boolean type(boolean binary) {
        if (ftpCtrl == null) return false;
        boolean result = true;

        if (sendCommand(binary ? "TYPE I" : "TYPE A", null) != 250) {
            result = false;
        }

        return result;
    }

    /**
     * データを送信します。
     *
     * @param name ファイル名
     * @param data データ
     * @return 成功したか
     */
    public boolean put(String name, byte[] data) {
        if (ftpCtrl == null) return false;
        if (name == null || name.isEmpty()) return false;
        if (data == null) return false;
        boolean result = true;

        Socket ftpData = null;
        OutputStream dataOut = null;
        InputStream dataIn = null;

        try {
            ftpData = creatDataConnection("STOR " + name);
            dataOut = ftpData.getOutputStream();
            dataIn  = ftpData.getInputStream();

            // データ送信
            dataOut.write(data);
            dataOut.close();
            dataOut = null;
            if (sendCommand(null, null) != 226) {
                result = false;
            }

        } catch (IOException e) {
            result = false;
        } finally {
            try {
                if (dataIn != null)
                    dataIn.close();
            } catch (IOException e) {
                result = false;
            } finally {
                dataIn = null;
            }
            try {
                if (dataOut != null)
                    dataOut.close();
            } catch (IOException e) {
                result = false;
            } finally {
                dataOut = null;
            }
            try {
                if (ftpData != null)
                    ftpData.close();
            } catch (IOException e) {
                result = false;
            } finally {
                ftpData = null;
            }
        }

        return result;
    }

    /**
     * データを受信します。
     *
     * @param name ファイル名
     * @param data データ
     * @return 成功したか
     */
    public boolean get(String name, OutputStream data) {
        if (ftpCtrl == null) return false;
        if (name == null || name.isEmpty()) return false;
        if (data == null) return false;
        boolean result = true;

        Socket ftpData = null;
        OutputStream dataOut = null;
        InputStream dataIn = null;

        try {
            ftpData = creatDataConnection("RETR " + name);
            //2011.11.12 ファイル名を間違えた場合、IOException発生時に異常終了するバグ修正（S.Ono）
            if(ftpData == null)
            	return false;
            dataOut = ftpData.getOutputStream();
            dataIn = ftpData.getInputStream();

            byte[] buf = new byte[1024];
            int len = 0;

            // データ受信
            while ((len = dataIn.read(buf)) != -1) {
                data.write(buf, 0, len);
            }
            dataIn.close();
            dataIn = null;
            if (sendCommand(null, null) != 226) {
                result = false;
            }

        } catch (IOException e) {
            result = false;
        } finally {
            try {
                if (dataIn != null)
                    dataIn.close();
            } catch (IOException e) {
                result = false;
            } finally {
                dataIn = null;
            }
            try {
                if (dataOut != null)
                    dataOut.close();
            } catch (IOException e) {
                result = false;
            } finally {
                dataOut = null;
            }
            try {
                if (ftpData != null)
                    ftpData.close();
            } catch (IOException e) {
                result = false;
            } finally {
                ftpData = null;
            }
        }

        return result;
    }

    /**
     * ファイルを削除します。
     *
     * @param name ファイル名
     * @return 成功したか
     */
    public boolean dele(String name) {
        if (ftpCtrl == null) return false;
        if (name == null || name.isEmpty()) return false;
        boolean result = true;

        if (sendCommand("DELE " + name, null) != 250) {
            result = false;
        }

        return result;
    }

    /**
     * 1つ上位のディレクトリにカレントディレクトリを移動します。
     *
     * @return 成功したか
     */
    public boolean cdup() {
        if (ftpCtrl == null) return false;
        boolean result = true;

        if (sendCommand("CDUP", null) != 250) {
            result = false;
        }

        return result;
    }

    /**
     * カレントディレクトリを移動します。
     * ディレクトリパスは、絶対パスもしくは相対パスで指定します。
     *
     * @param path ディレクトリパス
     * @return 成功したか
     */
    public boolean cwd(String path) {
        if (ftpCtrl == null) return false;
        if (path == null || path.isEmpty()) return false;
        boolean result = true;

        if (sendCommand("CWD " + path, null) != 250) {
            result = false;
        }

        return result;
    }

    /**
     * 指定したディレクトリを削除します。
     * ディレクトリパスは、絶対パスもしくは相対パスで指定します。
     *
     * @param path ディレクトリパス
     * @return 成功したか
     */
    public boolean rmd(String path) {
        if (ftpCtrl == null) return false;
        if (path == null || path.isEmpty()) return false;
        boolean result = true;

        if (sendCommand("RMD " + path, null) != 250) {
            result = false;
        }

        return result;
    }

    /**
     * 指定したディレクトリを作成します。
     * ディレクトリパスは、絶対パスもしくは相対パスで指定します。
     *
     * @param path ディレクトリパス
     * @return 成功したか
     */
    public boolean mkd(String path) {
        if (ftpCtrl == null) return false;
        if (path == null || path.isEmpty()) return false;
        boolean result = true;

        if (sendCommand("MKD " + path, null) != 250) {
            result = false;
        }

        return result;
    }

    /**
     * 指定したディレクトリを調査します。
     * ディレクトリパスは、絶対パスもしくは相対パスで指定します。
     *
     * @param path ディレクトリパス
     * @return 0:ディレクトリは存在する。 2:ディレクトリは存在しない。 1:その他
     */
    public int stat(String path) {
    	int stat = 0;
        if (ftpCtrl == null) return -1;
        if (path == null || path.isEmpty()) return -1;
        int result = 1;

        stat = sendCommand("STAT " + path, null);
        switch(stat)
        {
        case 450:
            result = 2;
            break;
        case 211:
            result = 0;
            break;
        default:
            result = 1;
        	break;
        }

        return result;
    }
     
    /**
     * カレントディレクトリのファイル一覧を取得します。
     *
     * @return ファイル一覧
     */
    public String list() {
        return list(null);
    }

    /**
     * ファイル一覧を取得します。
     *
     * @param path ディレクトリパス
     * @return ファイル一覧
     */
    public String list(String path) {
        if (ftpCtrl == null) return null;

        Socket ftpData = null;
        OutputStream dataOut = null;
        InputStream dataIn = null;
        ByteArrayOutputStream result = new ByteArrayOutputStream();

        String command = "LIST";
        if(path != null) command += " " + path;

        try {
            ftpData = creatDataConnection(command);
            dataOut = ftpData.getOutputStream();
            dataIn = ftpData.getInputStream();

            byte[] buf = new byte[1024];
            int len = 0;

            // データ受信
            while ((len = dataIn.read(buf)) != -1) {
                result.write(buf, 0, len);
            }
            result.flush();
            dataIn.close();
            dataIn = null;
            if (sendCommand(null, null) != 226) {
                result.reset();
            }

        } catch (IOException e) {
            result.reset();
        } finally {
            try {
                if (dataIn != null)
                    dataIn.close();
            } catch (IOException e) {
                result.reset();
            } finally {
                dataIn = null;
            }
            try {
                if (dataOut != null)
                    dataOut.close();
            } catch (IOException e) {
                result.reset();
            } finally {
                dataOut = null;
            }
            try {
                if (ftpData != null)
                    ftpData.close();
            } catch (IOException e) {
                result.reset();
            } finally {
                ftpData = null;
            }
        }

        return result.toString();
    }

    /**
     * カレントディレクトリを取得します。
     *
     * @return ディレクトリパス
     */
    public String pwd() {
        if (ftpCtrl == null) return null;

        StringBuffer reply = new StringBuffer();
        if (sendCommand("PWD", reply) != 257) {
            return null;
        }

        Matcher match = PWD_REPLY_MESSAGE.matcher(reply.toString());
        if (!match.find() || match.groupCount() != 1) {
            return null;
        }

        return match.group(1);
    }

    /**
     * 指定された書式の文字列と引数を使って、書式付き文字列をコンソールに出力します。
     * <code>debug == false</code> の場合は、出力されません。
     *
     * @param format 書式文字列
     * @param args 書式文字列内の書式指示子により参照される引数
     */
    private void debug(String format, Object... args) {
        if (!debug) return;
        System.out.println(String.format(format, args));
    }

	public void FtpMain() {
		// TODO 自動生成されたメソッド・スタブ
		
	}

}

