/*
 * Copyright (c) 2009 The openGion Project.
 *
 * 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.opengion.fukurou.util;

import java.io.File;
import java.io.IOException;

/**
 * AbstractConnect.java は、共通的に使用される ファイル伝送関連の基本機能を実装した、Abstractクラスです。
 *
 * -host=サーバー -user=ユーザー -passwd=パスワード -remoteFile=接続先のファイル名 を必須設定します。
 * -localFile=ローカルのファイル名は、必須ではありませんが、-command=DEL の場合にのみ不要であり、
 * それ以外の command の場合は、必要です。
 *
 * -command=[GET/PUT/DEL/GETDIR/PUTDIR/DELDIR] は、サーバーに対しての処理の方法を指定します。
 *   GET:サーバーからローカルにファイル転送します(初期値)
 *   PUT:ローカルファイルをサーバーに PUT(STORE、SAVE、UPLOAD、などと同意語)します。
 *   DEL:サーバーの指定のファイルを削除します。この場合のみ、-localFile 属性の指定は不要です。
 *   GETDIR,PUTDIR,DELDIR:指定のフォルダ以下のファイルを処理します。
 *
 * -mkdirs=[true/false] は、受け側のファイル(GET時:LOCAL、PUT時:サーバー)に取り込むファイルのディレクトリが
 * 存在しない場合に、作成するかどうかを指定します(初期値:true)
 * 通常、サーバーに、フォルダ階層を作成してPUTする場合、動的にフォルダ階層を作成したいケースで便利です。
 * 逆に、フォルダは確定しており、指定フォルダ以外に PUT するのはバグっていると事が分かっている場合には
 * false に設定して、存在しないフォルダにPUT しようとすると、エラーになるようにします。
 *
 * 引数文字列中に空白を含む場合は、ダブルコーテーション("") で括って下さい。
 * 引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に
 * 繋げてください。
 *
 * @og.formSample
 *  XXXConnect -host=サーバー -user=ユーザー -passwd=パスワード -remoteFile=接続先のファイル名 [-localFile=ローカルのファイル名]
 *                   [-command=[GET/PUT/DEL/GETDIR/PUTDIR/DELDIR] ] [-display=[true/false] ] ・・・・
 *
 *    -host=サーバー                    ：接続先のサーバーのアドレスまたは、サーバー名
 *    -user=ユーザー                    ：接続するユーザー名
 *    -passwd=パスワード                ：接続するユーザーのパスワード
 *    -remoteFile=接続先のファイル名    ：接続先のサーバー側のファイル名。PUT,GET 関係なくFTP側として指定します。
 *   [-localFile=ローカルのファイル名]  ：ローカルのファイル名。PUT,GET 関係なくローカルファイルを指定します。
 *   [-port=ポート ]                    ：接続するサーバーのポートを指定します。
 *   [-command=[GET/PUT/DEL] ]          ：サーバー側での処理の方法を指定します。
 *             [GETDIR/PUTDIR/DELDIR]]          GET:FTP⇒LOCAL、PUT:LOCAL⇒FTP への転送です(初期値:GET)
 *                                              DEL:FTPファイルを削除します。
 *                                              GETDIR,PUTDIR,DELDIR 指定のフォルダ以下のファイルを処理します。
 *   [-mkdirs=[true/false]  ]           ：受け側ファイル(GET時:LOCAL、PUT時:サーバー)にディレクトリを作成するかどうか(初期値:true)
 *                                              (false:ディレクトリが無ければ、エラーにします。)
 *   [-encode=エンコード名 ]            ：日本語ファイル名などのエンコード名を指定します(初期値:Windows-31J)
 *   [-timeout=タイムアウト[秒] ]       ：Dataタイムアウト(初期値:600 [秒])
 *   [-display=[false/true] ]           ：trueは、検索状況を表示します(初期値:false)
 *   [-debug=[false|true]   ]           ：デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *
 * @og.rev 5.1.6.0 (2010/05/01) 新規追加
 *
 * @version  5.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public abstract class AbstractConnect implements ConnectIF {
	public static final String CR = System.getProperty("line.separator");
	private final StringBuilder errMsg = new StringBuilder( 200 );

	/** 正常フラグ  {@value} */
	public static final boolean FLAG_OK = true;
	/** 異常フラグ  {@value} */
	public static final boolean FLAG_NG = false;
	/** Dataタイムアウト(初期値:{@value} [秒]) */
	public static final int		TIMEOUT = 600 ;

	/** サーバー */
	protected String	host 		= null;			// サーバー
	/** ユーザー */
	protected String	user 		= null;			// ユーザー
	/** パスワード */
	protected String	passwd 		= null;			// パスワード
	/** ポート */
	protected String	port 		= null;			// ポート

	/** ディレクトリを作成するかどうか */
	protected boolean	isMkdirs	= true;			// 受け側ファイルにディレクトリを作成するかどうか。true:作成する / false:無ければエラー
	/** Dataタイムアウト  */
	protected int		timeout		= TIMEOUT;		// Dataタイムアウト(初期値:600 [秒])
	/** 検索状況を表示するかどうか  */
	protected boolean	isDisplay	= false;		// trueは、検索状況を表示します(初期値:false)
	/** デバッグ情報を表示するかどうか  */
	protected boolean	isDebug		= false;		// デバッグ情報を標準出力に表示する(true)かしない(false)か

	/**
	 * サーバーへの接続、ログインを行います。
	 */
	@Override
	public abstract void connect() ;

	/**
	 * command , localFile , remoteFile を元に、FTP処理を行います。
	 *
	 * このメソッドは、connect( String , String , String )メソッド、および、
	 * paramInit() 実行後に、呼び出す必要があります。
	 *
	 * @param	command	GET:HOST⇒LOCAL 、PUT:LOCAL⇒HOST 、DEL:HOSTファイルを削除
	 * @param	localFile 	ローカルのファイル名
	 * @param	remoteFile	HOST接続先のファイル名
	 * @throws RuntimeException
	 */
	@Override
	public void action( final String command, final String localFile, final String remoteFile ) {
		String rmtFile = remoteFile.replace( '\\','/' );
		if( isDisplay ) {
			System.out.println( "ACTION: command=" + command + ",localFile=" + localFile + ",remoteFile=" + rmtFile );
		}

		try {
			// 実際の処理を行います。(GET/PUT/DEL)
			if( "GET".equals( command ) ) {
				actionGET( localFile, rmtFile );
			}
			else if( "PUT".equals( command ) ) {
				actionPUT( localFile, rmtFile );
			}
			else if( "DEL".equals( command ) ) {
				actionDEL( rmtFile );
			}
			else if( "GETDIR".equals( command ) ) {
				actionGETdir( localFile, rmtFile );
			}
			else if( "PUTDIR".equals( command ) ) {
				actionPUTdir( localFile, rmtFile );
			}
			else if( "DELDIR".equals( command ) ) {
				actionDELdir( rmtFile );
			}
			else {
				errAppend( "commandは、GET/PUT/DEL/GETDIR/PUTDIR/DELDIR から指定ください。" );
				errAppend( "   command    = [" , command , "]" );
				throw new RuntimeException(  getErrMsg() );
			}
		}
		catch( Exception ex ) {
			errAppend( "Server action Error." );
			errAppend( "   command    = [" , command	, "]" );
			errAppend( "   localFile  = [" , localFile	, "]" );
			errAppend( "   remoteFile = [" , remoteFile , "]" );
			errAppend( ex.getMessage() );
			if( isDebug ) { ex.printStackTrace(); }
			throw new RuntimeException( getErrMsg(),ex );
		}
	}

	/**
	 * サーバーとの接続をクローズします。
	 *
	 * ログインされている場合は、ログアウトも行います。
	 * コネクトされている場合は、ディスコネクトします。
	 */
	@Override
	public abstract void disconnect();

	/**
	 * command="GET" が指定されたときの処理を行います。
	 *
	 * 接続先のサーバー側のファイル名をローカルにダウンロードします。
	 *
	 * @param	localFile 	ローカルのファイル名
	 * @param	remoteFile	接続先のファイル名
	 * @throws Exception
	 */
	protected abstract void actionGET( final String localFile, final String remoteFile ) throws Exception ;

	/**
	 * command="GETDIR" が指定されたときの処理を行います。
	 *
	 * 接続先のサーバー側のディレクトリ以下をローカルディレクトリに階層構造のままダウンロードします。
	 *
	 * @param	localDir 	ローカルのディレクトリ名
	 * @param	remoteDir	接続先のディレクトリ名
	 * @throws IOException
	 */
	protected abstract void actionGETdir( final String localDir, final String remoteDir ) throws Exception ;

	/**
	 * command="PUT" が指定されたときの処理を行います。
	 *
	 * ローカルファイルを、接続先のサーバー側にアップロードします。
	 *
	 * @param	localFile 	ローカルのファイル名
	 * @param	remoteFile	接続先のファイル名
	 * @throws Exception
	 */
	protected abstract void actionPUT( final String localFile, final String remoteFile ) throws Exception ;

	/**
	 * command="PUTDIR" が指定されたときの処理を行います。
	 *
	 * ローカルファイルのディレクトリ以下を、接続先のサーバー側のディレクトリに階層構造のままアップロードします。
	 *
	 * @og.rev 5.3.7.0 (2011/07/01) フォルダにアクセスできない場合は、エラーを返します。
	 *
	 * @param	localDir 	ローカルのディレクトリ名
	 * @param	remoteDir	接続先のディレクトリ名
	 * @throws Exception
	 */
	protected void actionPUTdir( final String localDir, final String remoteDir ) throws Exception {
		File[] lclFiles = new File( localDir ).listFiles();

		// 5.3.7.0 (2011/07/01) フォルダにアクセスできない場合は、エラーを返します。
		if( lclFiles == null ) {
			errAppend( "指定のディレクトリは、アクセスできません。" );
			errAppend( "   localDir   = [" , localDir	, "]" );
			throw new RuntimeException( getErrMsg() );
		}

		for( int i=0; i<lclFiles.length; i++ ) {
			String lcl = lclFiles[i].getName();
			if( lclFiles[i].isDirectory() ) {
				actionPUTdir( addFile( localDir,lcl ),addFile( remoteDir,lcl ) );
			}
			else {
				actionPUT( addFile( localDir,lcl ),addFile( remoteDir,lcl ) );
			}
		}
	}

	/**
	 * command="DEL" が指定されたときの処理を行います。
	 *
	 * 接続先のサーバー側のファイル名を削除します。
	 *
	 * @param	remoteFile	接続先のファイル名
	 * @throws Exception
	 */
	protected abstract void actionDEL( final String remoteFile ) throws Exception ;

	/**
	 * command="DELDIR" が指定されたときの処理を行います。
	 *
	 * 接続先のサーバー側のディレクトリ名を削除します。
	 *
	 * @param	remoteDir	接続先のディレクトリ名
	 * @throws Exception
	 */
	protected abstract void actionDELdir( final String remoteDir ) throws Exception ;

	/**
	 * ローカルファイルのディレクトリを作成します。
	 *
	 * 引数のファイル名は、ファイル名です。作成するディレクトリは、そのファイルオブジェクトの
	 * getParentFile() で取得されるディレクトリまでを作成します。
	 *
	 * @param	localFile 	ローカルのファイル名
	 * @throws IOException , RuntimeException
	 */
	protected void makeLocalDir( final String localFile ) throws IOException {
		File fileDir = new File( localFile ).getCanonicalFile().getParentFile();
		if( !fileDir.exists() ) {
			if( ! fileDir.mkdirs() ) {
				errAppend( "ローカルファイルのディレクトリの作成に失敗しました。" );
				errAppend( "   localFile   = [" , localFile	, "]" );
				throw new RuntimeException( getErrMsg() );
			}
		}
	}

	/**
	 * ディレクトリとファイル名を合成します。
	 *
	 * 単純に、ディレクトリの最後と、ファイルの最初の、"/" をチェックし、
	 * 存在すれば、そのまま、結合し、存在しなければ、"/" を追加します。
	 * 両方に存在する場合は、片方をはずします。
	 *
	 * @param	dir  	ディレクトリ名
	 * @param	file 	ファイル名
	 *
	 * @return	合成されたファイル名
	 */
	protected String addFile( final String dir,final String file ) {
		final String filepath ;

		char ch1 = dir.charAt( dir.length()-1 ) ;	// ディレクトリの最後の文字
		char ch2 = file.charAt( 0 ) ;				// ファイルの最初の文字

		if( ch1 == '/' || ch1 == '\\' ) {
			if( ch2 == '/' || ch2 == '\\' ) {	// 両方とも存在する。
				filepath = dir + file.substring(1);
			}
			else {
				filepath = dir + file;
			}
		}
		else {
			if( ch2 == '/' || ch2 == '\\' ) {	// 片方のみ存在する。
				filepath = dir + file;
			}
			else {
				filepath = dir + "/" + file;
			}
		}

		return filepath ;
	}

	/**
	 * サーバーの、ホスト、ユーザー、パスワードを設定します。
	 *
	 * @param	host	サーバー
	 * @param	user	ユーザー
	 * @param	passwd	パスワード
	 */
	@Override
	public void setHostUserPass( final String host , final String user , final String passwd ) {
		this.host = host;
		this.user = user;
		this.passwd = passwd;
	}

	/**
	 * サーバー名を取得します。
	 *
	 * @return	サーバー
	 */
//	protected String getHost() { return host; }

	/**
	 * ユーザー名を取得します。
	 *
	 * @return	ユーザー
	 */
//	protected String getUser() { return user; }

	/**
	 * パスワードを取得します。
	 *
	 * @return	パスワード
	 */
//	protected String getPassword() { return passwd; }

	/**
	 * 接続に利用するポート番号を設定します。
	 *
	 * @param	port	接続に利用するポート番号
	 */
	@Override
	public void setPort( final String port ) {
		if( port != null ) {
			this.port = port ;
		}
	}

	/**
	 * ポートを取得します。
	 * 設定されている生のport属性(nullもありうる)を返します。
	 *
	 * @return	ポート
	 */
	protected String getPort() { return port; }

	/**
	 * ポートを取得します。
	 * 設定されているport属性が、nullの場合は、defPortを返します。
	 *
	 * @param	defPort	port が null の場合の初期値
	 *
	 * @return	ポート
	 */
	protected int getPort( final int defPort) {
		return ( port == null ) ? defPort : Integer.parseInt( port );
	}

	/**
	 * それぞれの受け側ファイルにディレクトリを作成するかどうか(初期値:true:作成する)。
	 *
	 * -mkdirs=[true/false] は、受け側のファイル(GET時:LOCAL、PUT時:サーバー)に取り込むファイルのディレクトリが
	 * 存在しない場合に、作成するかどうかを指定します(初期値:true)
	 * 通常、サーバーに、フォルダ階層を作成してPUTする場合、動的にフォルダ階層を作成したいケースで便利です。
	 * 逆に、フォルダは確定しており、指定フォルダ以外に PUT するのはバグっていると事が分かっている場合には
	 * false に設定して、存在しないフォルダにPUT しようとすると、エラーになるようにします。
	 *
	 * @param	isMkdirs	受け側ファイルにディレクトリを作成するかどうか。true:作成する
	 */
	@Override
	public void setMkdirs( final boolean isMkdirs ) {
		this.isMkdirs = isMkdirs ;
	}

	/**
	 * 受け側ファイルにディレクトリを作成するかどうかを取得します。
	 *
	 * @return	受け側ファイルにディレクトリを作成するかどうか。true:作成する
	 */
//	protected boolean isMkdirs() { return isMkdirs; }

	/**
	 * タイムアウトを秒で指定します(初期値:600 [秒])。
	 *
	 * オリジナルの FTPClient#setDataTimeout( int ) は、ミリ秒でセット
	 * しますが、ここのメソッドでは、秒でセットします。
	 *
	 * @param	timeout	タイムアウト[秒]
	 * @throws RuntimeException タイムアウトの指定が大きすぎた場合
	 */
	@Override
	public void setTimeout( final int timeout ) {
		if( Integer.MAX_VALUE / 1000 < timeout ) {
			errAppend( "タイムアウトの指定が大きすぎます。" );
			errAppend( "   timeout   = [" , timeout	, "]" );
			throw new RuntimeException( getErrMsg() );
		}

		this.timeout = timeout ;
	}

	/**
	 *指定されたタイムアウトを秒で取得します(初期値:600 [秒])。
	 *
	 * @return	タイムアウト[秒]
	 */
//	protected int getTimeout() { return timeout; }

	/**
	 * 実行状況の表示可否 を設定します(初期値:false:表示しない)。
	 *
	 * @param	isDisplay	実行状況の表示可否
	 */
	@Override
	public void setDisplay( final boolean isDisplay ) {
		this.isDisplay = isDisplay ;
	}

	/**
	 * 実行状況の表示可否を取得します。
	 *
	 * @return	実行状況の表示可否
	 */
//	protected boolean isDisplay() { return isDisplay; }

	/**
	 * デバッグ情報の表示可否 を設定します(初期値:false:表示しない)。
	 *
	 * @param	isDebug デバッグ情報の表示可否
	 */
	@Override
	public void setDebug( final boolean isDebug ) {
		this.isDebug = isDebug ;
	}

	/**
	 * デバッグ情報の表示可否 を取得します。
	 *
	 * @return	デバッグ情報の表示可否
	 */
//	protected boolean isDebug() { return isDebug; }

	/**
	 * 処理中に発生したエラーメッセージをセットします。
	 *
	 * @param	msg  メッセージ化したいオブジェクト
	 */
	protected void errAppend( final Object msg ) {
		errMsg.append( String.valueOf(msg) ).append( CR );
	}

	/**
	 * 処理中に発生したエラーメッセージをセットします。
	 *
	 * @param	msgs  Object...
	 */
	protected void errAppend( final Object... msgs ) {
		for( int i=0; i<msgs.length; i++ ) {
			errMsg.append( String.valueOf(msgs[i]) );
		}

		errMsg.append( CR );
	}

	/**
	 * 処理中に発生したエラーメッセージを取り出します。
	 *
	 * @return	エラーメッセージ
	 */
	@Override
	public String getErrMsg() {
		return errMsg.toString();
	}
}
